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 %} \ No newline at end of file