diff --git a/Dockerfile b/Dockerfile
index 68f0e04..fbea9ff 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,6 +9,13 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy all application files
COPY . .
+# Build assets for production
+RUN python build_assets.py
+
+# Set production environment
+ENV FLASK_ENV=production
+ENV FLASK_DEBUG=0
+
# Ensure proper permissions
RUN chmod +x app.py
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..388a45b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,50 @@
+.PHONY: help build clean install run test deploy
+
+help: ## Show this help message
+ @echo "Available commands:"
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+install: ## Install Python dependencies
+ pip install -r requirements.txt
+
+build: ## Build minified assets with cache busting
+ python build_assets.py
+
+clean: ## Clean old cache-busted assets
+ python build_assets.py clean
+
+run: ## Run the Flask development server
+ export FLASK_ENV=development && export FLASK_DEBUG=1 && python app.py
+
+run-prod: ## Run the Flask production server
+ export FLASK_ENV=production && export FLASK_DEBUG=0 && python app.py
+
+test: ## Run tests (placeholder)
+ @echo "No tests configured yet"
+
+deploy: clean build ## Deploy: clean old assets and build new ones
+ @echo "Deployment completed!"
+
+docker-build: ## Build Docker image
+ docker build -t kobelly .
+
+docker-run: ## Run Docker container (production)
+ docker-compose up -d
+
+docker-run-dev: ## Run Docker container (development)
+ docker-compose -f docker-compose.dev.yml up -d
+
+docker-stop: ## Stop Docker container
+ docker-compose down
+
+docker-stop-dev: ## Stop Docker container (development)
+ docker-compose -f docker-compose.dev.yml down
+
+docker-logs: ## Show Docker logs
+ docker-compose logs -f
+
+docker-logs-dev: ## Show Docker logs (development)
+ docker-compose -f docker-compose.dev.yml logs -f
+
+dev-setup: install build ## Setup development environment
+ @echo "Development environment ready!"
\ No newline at end of file
diff --git a/README.md b/README.md
index 72cb958..1760ed1 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,9 @@ A professional Flask-based website for promoting small business web development
- ๐ฑ **Responsive Design**: Modern, mobile-friendly interface
- ๐จ **Professional UI**: Clean, modern design with Bootstrap 5
- ๐ณ **Docker Support**: Easy containerization and deployment
-- โก **Fast Performance**: Optimized for speed and performance
+- โก **Fast Performance**: Optimized for speed and performance with minified assets
- ๐ง **No Database Required**: Simple, lightweight setup
+- ๐ **Asset Optimization**: CSS/JS minification with cache busting
## Pages
@@ -27,16 +28,45 @@ A professional Flask-based website for promoting small business web development
pip install -r requirements.txt
```
-2. **Run the application:**
+2. **Build assets (optional, for production-like experience):**
+ ```bash
+ make build
+ ```
+
+3. **Run the application:**
```bash
python app.py
```
-3. **Access the website:**
+4. **Access the website:**
Open your browser and go to `http://localhost:5000`
+### Using Make Commands
+
+The project includes a Makefile for common tasks:
+
+```bash
+make help # Show all available commands
+make install # Install dependencies
+make build # Build minified assets
+make clean # Clean old assets
+make run # Run development server
+make run-prod # Run production server
+make deploy # Clean and build assets
+make dev-setup # Setup development environment
+
+# Docker commands
+make docker-run # Run production Docker container
+make docker-run-dev # Run development Docker container
+make docker-stop # Stop production container
+make docker-stop-dev # Stop development container
+make docker-logs # View production logs
+make docker-logs-dev # View development logs
+```
+
### Docker Deployment
+#### Production Mode (Default)
1. **Build and run with Docker Compose:**
```bash
docker-compose up --build
@@ -48,6 +78,25 @@ A professional Flask-based website for promoting small business web development
docker run -p 5000:5000 kobelly-website
```
+#### Development Mode
+For local development with Docker (with live reload and debug mode):
+
+1. **Build and run development container:**
+ ```bash
+ docker-compose -f docker-compose.dev.yml up --build
+ ```
+
+2. **Or use Make commands:**
+ ```bash
+ make docker-run-dev # Start development container
+ make docker-logs-dev # View development logs
+ make docker-stop-dev # Stop development container
+ ```
+
+**Development vs Production:**
+- **Development**: Debug mode enabled, live code reloading, unminified assets
+- **Production**: Debug disabled, minified assets, optimized performance
+
## Project Structure
```
@@ -56,7 +105,17 @@ Kobelly/
โโโ requirements.txt # Python dependencies
โโโ Dockerfile # Docker configuration
โโโ docker-compose.yml # Docker Compose configuration
+โโโ Makefile # Build and deployment commands
+โโโ build_assets.py # Asset minification script
โโโ babel.cfg # Babel configuration for translations
+โโโ static/ # Static assets
+โ โโโ css/ # CSS files
+โ โ โโโ main.css # Main stylesheet
+โ โ โโโ main.min.css # Minified CSS (generated)
+โ โโโ js/ # JavaScript files
+โ โ โโโ main.js # Main script
+โ โ โโโ main.min.js # Minified JS (generated)
+โ โโโ images/ # Images and icons
โโโ templates/ # HTML templates
โ โโโ base.html # Base template with navigation
โ โโโ index.html # Homepage
@@ -111,12 +170,62 @@ Users can switch languages using the language selector in the navigation bar. Th
pybabel compile -d translations
```
+## Asset Optimization
+
+The project includes automatic CSS and JavaScript minification with cache busting for optimal performance.
+
+### Asset Building
+
+**Development Mode:**
+- Uses unminified assets for easier debugging
+- Assets are served directly from source files
+
+**Production Mode:**
+- Automatically serves minified assets
+- Cache busting prevents browser caching issues
+- Assets are optimized for faster loading
+
+### Building Assets
+
+```bash
+# Build minified assets
+make build
+
+# Clean old cache-busted assets
+make clean
+
+# Full deployment (clean + build)
+make deploy
+```
+
+### Asset Files
+
+- **CSS**: `static/css/main.css` โ `static/css/main.min.css`
+- **JavaScript**: `static/js/main.js` โ `static/js/main.min.js`
+
+The build process creates cache-busted versions with MD5 hashes in the filename to ensure browsers always load the latest version.
+
## Configuration
### Environment Variables
- `SECRET_KEY`: Secret key for Flask sessions (default: 'your-secret-key-change-in-production')
- `FLASK_ENV`: Flask environment (development/production)
+- `FLASK_DEBUG`: Debug mode (0=disabled, 1=enabled, defaults to 1 in development)
+
+**Environment Modes:**
+
+**Development (Local):**
+```bash
+FLASK_ENV=development
+FLASK_DEBUG=1
+```
+
+**Production (Docker):**
+```bash
+FLASK_ENV=production
+FLASK_DEBUG=0
+```
### Production Deployment
@@ -135,6 +244,7 @@ For production deployment:
- **Internationalization**: Flask-Babel
- **Containerization**: Docker, Docker Compose
- **Styling**: Custom CSS with CSS variables
+- **Asset Management**: Flask-Assets with CSS/JS minification
## Browser Support
diff --git a/app.py b/app.py
index f13028c..26fa9d6 100644
--- a/app.py
+++ b/app.py
@@ -1,4 +1,5 @@
from flask import Flask, render_template, request, session, redirect, url_for, make_response
+from flask_assets import Environment, Bundle
from translation_manager import init_app, translate, get_current_language, create_language_selector, set_language
import os
from datetime import datetime
@@ -7,6 +8,29 @@ from xml.etree import ElementTree as ET
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'your-secret-key-change-in-production')
+# Initialize Flask-Assets
+assets = Environment(app)
+assets.url = app.static_url_path
+
+# Configure assets for production
+if not app.debug:
+ assets.auto_build = False
+ assets.cache = True
+ assets.manifest = 'file'
+
+# Register asset bundles
+css_bundle = assets.register('css_all', Bundle(
+ 'css/main.css',
+ filters='cssmin',
+ output='css/main.min.css'
+))
+
+js_bundle = assets.register('js_all', Bundle(
+ 'js/main.js',
+ filters='jsmin',
+ output='js/main.min.js'
+))
+
# Initialize the translation system
init_app(app)
@@ -90,5 +114,21 @@ def set_language_route(language):
return redirect(request.referrer or url_for('index'))
return redirect(url_for('index'))
+@app.route('/build-assets')
+def build_assets():
+ """Build minified assets with cache busting"""
+ if app.debug:
+ try:
+ # Build assets
+ css_bundle.build()
+ js_bundle.build()
+ return {'status': 'success', 'message': 'Assets built successfully'}
+ except Exception as e:
+ return {'status': 'error', 'message': str(e)}, 500
+ else:
+ return {'status': 'error', 'message': 'Asset building only available in debug mode'}, 403
+
if __name__ == '__main__':
- app.run(debug=True, host='0.0.0.0', port=5000)
\ No newline at end of file
+ # Check environment variables for debug mode
+ debug_mode = os.environ.get('FLASK_DEBUG', '1') == '1' and os.environ.get('FLASK_ENV') != 'production'
+ app.run(debug=debug_mode, host='0.0.0.0', port=5000)
\ No newline at end of file
diff --git a/build_assets.py b/build_assets.py
new file mode 100644
index 0000000..a14fb13
--- /dev/null
+++ b/build_assets.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""
+Asset build script for Kobelly Web Solutions
+Handles CSS and JS minification with cache busting
+"""
+
+import os
+import hashlib
+import shutil
+from datetime import datetime
+from cssmin import cssmin
+from jsmin import jsmin
+
+def get_file_hash(filepath):
+ """Generate MD5 hash of file content for cache busting"""
+ with open(filepath, 'rb') as f:
+ return hashlib.md5(f.read()).hexdigest()[:8]
+
+def minify_css(input_file, output_file):
+ """Minify CSS file"""
+ try:
+ with open(input_file, 'r', encoding='utf-8') as f:
+ css_content = f.read()
+
+ minified_css = cssmin(css_content)
+
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(minified_css)
+
+ print(f"โ CSS minified: {input_file} โ {output_file}")
+ return True
+ except Exception as e:
+ print(f"โ Error minifying CSS {input_file}: {e}")
+ return False
+
+def minify_js(input_file, output_file):
+ """Minify JavaScript file"""
+ try:
+ with open(input_file, 'r', encoding='utf-8') as f:
+ js_content = f.read()
+
+ minified_js = jsmin(js_content)
+
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(minified_js)
+
+ print(f"โ JS minified: {input_file} โ {output_file}")
+ return True
+ except Exception as e:
+ print(f"โ Error minifying JS {input_file}: {e}")
+ return False
+
+def create_cache_busted_assets():
+ """Create cache-busted versions of assets"""
+ static_dir = 'static'
+ css_dir = os.path.join(static_dir, 'css')
+ js_dir = os.path.join(static_dir, 'js')
+
+ # Ensure directories exist
+ os.makedirs(css_dir, exist_ok=True)
+ os.makedirs(js_dir, exist_ok=True)
+
+ # CSS files
+ css_files = [
+ ('main.css', 'main.min.css')
+ ]
+
+ # JS files
+ js_files = [
+ ('main.js', 'main.min.js')
+ ]
+
+ print("๐จ Building assets...")
+ print(f"๐
Build time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ print()
+
+ # Process CSS files
+ for input_file, output_file in css_files:
+ input_path = os.path.join(css_dir, input_file)
+ output_path = os.path.join(css_dir, output_file)
+
+ if os.path.exists(input_path):
+ if minify_css(input_path, output_path):
+ # Generate cache-busted filename
+ file_hash = get_file_hash(output_path)
+ cache_busted_name = f"main.{file_hash}.min.css"
+ cache_busted_path = os.path.join(css_dir, cache_busted_name)
+
+ # Copy minified file to cache-busted version
+ shutil.copy2(output_path, cache_busted_path)
+ print(f"โ Cache-busted CSS created: {cache_busted_name}")
+ else:
+ print(f"โ ๏ธ CSS file not found: {input_path}")
+
+ print()
+
+ # Process JS files
+ for input_file, output_file in js_files:
+ input_path = os.path.join(js_dir, input_file)
+ output_path = os.path.join(js_dir, output_file)
+
+ if os.path.exists(input_path):
+ if minify_js(input_path, output_path):
+ # Generate cache-busted filename
+ file_hash = get_file_hash(output_path)
+ cache_busted_name = f"main.{file_hash}.min.js"
+ cache_busted_path = os.path.join(js_dir, cache_busted_name)
+
+ # Copy minified file to cache-busted version
+ shutil.copy2(output_path, cache_busted_path)
+ print(f"โ Cache-busted JS created: {cache_busted_name}")
+ else:
+ print(f"โ ๏ธ JS file not found: {input_path}")
+
+ print()
+ print("๐ Asset build completed!")
+
+def clean_old_assets():
+ """Clean old cache-busted assets"""
+ static_dir = 'static'
+ css_dir = os.path.join(static_dir, 'css')
+ js_dir = os.path.join(static_dir, 'js')
+
+ # Remove old cache-busted files
+ for directory in [css_dir, js_dir]:
+ if os.path.exists(directory):
+ for filename in os.listdir(directory):
+ if filename.startswith('main.') and filename.endswith('.min.css') or filename.endswith('.min.js'):
+ if not filename in ['main.min.css', 'main.min.js']: # Keep the base minified files
+ filepath = os.path.join(directory, filename)
+ os.remove(filepath)
+ print(f"๐๏ธ Removed old asset: {filename}")
+
+if __name__ == '__main__':
+ import sys
+
+ if len(sys.argv) > 1 and sys.argv[1] == 'clean':
+ print("๐งน Cleaning old assets...")
+ clean_old_assets()
+ print("โ Cleanup completed!")
+ else:
+ clean_old_assets()
+ create_cache_busted_assets()
\ No newline at end of file
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..a983f1b
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,23 @@
+version: '3.8'
+
+services:
+ web:
+ build: .
+ ports:
+ - "10332:5000"
+ environment:
+ - FLASK_ENV=development
+ - FLASK_DEBUG=1
+ - SECRET_KEY=dev-secret-key-change-in-production
+ restart: unless-stopped
+ volumes:
+ - .:/app
+ - /app/static/css/main.min.css
+ - /app/static/js/main.min.js
+ networks:
+ - default
+ - nginx-proxy-manager_default
+
+networks:
+ nginx-proxy-manager_default:
+ external: true
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index d06ed0c..016010e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,8 +6,9 @@ services:
ports:
- "10332:5000"
environment:
- - FLASK_ENV=development
+ - FLASK_ENV=production
- SECRET_KEY=your-secret-key-change-in-production
+ - FLASK_DEBUG=0
restart: unless-stopped
networks:
- default
diff --git a/requirements.txt b/requirements.txt
index 1dd501a..35408cb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,8 @@
Flask==2.3.3
Werkzeug==2.3.7
+Flask-Assets==2.1.0
+cssmin==0.2.0
+jsmin==3.0.1
click==8.1.7
blinker==1.6.3
# Translation system - JSON-based, no compilation needed
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index da439e4..8f6a15d 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -37,7 +37,9 @@
-
+ {% assets "css_all" %}
+
+ {% endassets %}
-
+ {% assets "js_all" %}
+
+ {% endassets %}