Getting Started with Web Development

Published on June 27, 2025

Web development doesn't have to be complicated. In this article, I'll share my approach to building simple, effective websites without overwhelming complexity.

A Simple Approach

I wanted to get a simple website going and I don't mind manually editing the HTML. To make it easier to maintain, I am using a simple template system which uses a python script to compile the development files with placeholders into the final HTML. No javascript is needed.

Template Processor Script

import os
import shutil
import re
from pathlib import Path
from datetime import datetime
import html

class TemplateProcessor:
    def __init__(self, dev_dir='dev', deploy_dir='deploy', templates_dir='dev/templates'):
        self.dev_dir = Path(dev_dir)
        self.deploy_dir = Path(deploy_dir)
        self.templates_dir = Path(templates_dir)
        self.templates = {}
        self.content_metadata = {}
        
    def load_templates(self):
        """Load all template files from the templates directory"""
        print("Loading templates...")
        
        if not self.templates_dir.exists():
            print(f"Templates directory {self.templates_dir} not found!")
            return
            
        for template_file in self.templates_dir.glob('*.html'):
            template_name = template_file.stem
            with open(template_file, 'r', encoding='utf-8') as f:
                template_content = f.read()
            
            self.templates[template_name] = template_content
            print(f"  Loaded template: {template_name}")
    
    def extract_first_image_from_content(self, file_path):
        """Extract the first image from HTML content and convert to root-relative path"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Look for images in the content
            img_match = re.search(r']+src=["\']([^"\']+)["\'][^>]*>', content, re.IGNORECASE)
            if img_match:
                img_src = img_match.group(1)
                
                # Convert to root-relative path for consistency
                if img_src.startswith('../'):
                    # Remove the ../ prefix to make it root-relative
                    return img_src[3:]  # Remove '../'
                elif img_src.startswith('assets/'):
                    # Already root-relative
                    return img_src
                elif not img_src.startswith(('http://', 'https://', '//')):
                    # Assume it's a relative path from the content file
                    # Convert to root-relative
                    return f"assets/{img_src}"
                else:
                    # External URL - return as-is
                    return img_src
            
            return ""
        except Exception as e:
            print(f"  Warning: Could not extract image from {file_path}: {e}")
            return ""
    
    def extract_metadata_from_file(self, file_path):
        """Extract title, description, and image from HTML file"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Extract title from h1 tag
            title_match = re.search(r']*>(.*?)', content, re.IGNORECASE | re.DOTALL)
            title = title_match.group(1).strip() if title_match else file_path.stem.replace('-', ' ').title()
            
            # Extract description from first paragraph after h1
            desc_match = re.search(r']*>.*?.*?]*>(.*?)

', content, re.IGNORECASE | re.DOTALL) description = desc_match.group(1).strip() if desc_match else "No description available." # Clean HTML tags from description description = re.sub(r'<[^>]+>', '', description) if len(description) > 150: description = description[:150] + "..." # Extract first image image_path = self.extract_first_image_from_content(file_path) # Get file modification time mod_time = datetime.fromtimestamp(file_path.stat().st_mtime) return { 'title': html.escape(title), 'description': html.escape(description), 'image': image_path, 'modified': mod_time, 'file_path': file_path } except Exception as e: print(f" Warning: Could not extract metadata from {file_path}: {e}") return None def scan_content_files(self): """Scan for blog posts and projects, extract metadata""" print("Scanning content files...") # Scan blog posts blog_dir = self.dev_dir / 'blog' if blog_dir.exists(): blog_posts = [] for html_file in blog_dir.glob('*.html'): if html_file.name != 'index.html': metadata = self.extract_metadata_from_file(html_file) if metadata: metadata['type'] = 'blog' metadata['relative_link'] = f"blog/{html_file.name}" blog_posts.append(metadata) print(f" Found blog post: {metadata['title']}") # Sort by modification date (newest first) blog_posts.sort(key=lambda x: x['modified'], reverse=True) self.content_metadata['blog_posts'] = blog_posts # Scan projects projects_dir = self.dev_dir / 'projects' if projects_dir.exists(): projects = [] for html_file in projects_dir.glob('*.html'): if html_file.name != 'index.html': metadata = self.extract_metadata_from_file(html_file) if metadata: metadata['type'] = 'project' metadata['relative_link'] = f"projects/{html_file.name}" projects.append(metadata) print(f" Found project: {metadata['title']}") # Sort by modification date (newest first) projects.sort(key=lambda x: x['modified'], reverse=True) self.content_metadata['projects'] = projects def generate_project_dropdown_links(self, relative_path=""): """Generate dropdown menu links for projects""" if 'projects' not in self.content_metadata: return "" dropdown_links = [] for project in self.content_metadata['projects']: link_html = f'{project["title"]}' dropdown_links.append(link_html) return '\n '.join(dropdown_links) def generate_card(self, metadata, relative_path="", button_text="Read More"): """Generate a card HTML from metadata""" if 'card' not in self.templates: print("Warning: Card template not found!") return "" card_html = self.templates['card'] # Format date date_str = metadata['modified'].strftime("%B %d, %Y") meta_text = f"{'Posted' if metadata['type'] == 'blog' else 'Updated'} on {date_str}" # Handle image path - convert root-relative to page-relative image_path = metadata.get('image', '') if image_path and not image_path.startswith(('http://', 'https://', '//')): # Convert root-relative path to page-relative path image_path = relative_path + image_path # Replace placeholders card_html = card_html.replace('{{CARD_TITLE}}', metadata['title']) card_html = card_html.replace('{{CARD_META}}', meta_text) card_html = card_html.replace('{{CARD_DESCRIPTION}}', metadata['description']) card_html = card_html.replace('{{CARD_LINK}}', relative_path + metadata['relative_link']) card_html = card_html.replace('{{CARD_BUTTON_TEXT}}', button_text) card_html = card_html.replace('{{CARD_IMAGE}}', image_path) return card_html return card_html def generate_card_collections(self, relative_path=""): """Generate card collections for different contexts""" cards = {} # Recent blog cards (for home page - limit to 2) if 'blog_posts' in self.content_metadata: recent_blogs = self.content_metadata['blog_posts'][:2] cards['recent_blog_cards'] = '\n'.join([ self.generate_card(post, relative_path, "Read More") for post in recent_blogs ]) # All blog cards (for blog index) cards['blog_cards'] = '\n'.join([ self.generate_card(post, relative_path, "Read More") for post in self.content_metadata['blog_posts'] ]) # Recent project cards (for home page - limit to 2) if 'projects' in self.content_metadata: recent_projects = self.content_metadata['projects'][:2] cards['recent_project_cards'] = '\n'.join([ self.generate_card(project, relative_path, "View Project") for project in recent_projects ]) # All project cards (for projects index) cards['project_cards'] = '\n'.join([ self.generate_card(project, relative_path, "View Project") for project in self.content_metadata['projects'] ]) return cards def calculate_relative_path(self, file_path): """Calculate relative path based on directory depth""" relative_parts = file_path.relative_to(self.dev_dir).parts depth = len(relative_parts) - 1 if depth == 0: return "" else: return "../" * depth def process_file(self, source_file, target_file): """Process a single HTML file, replacing placeholders with templates""" with open(source_file, 'r', encoding='utf-8') as f: content = f.read() # Calculate relative path for this file relative_path = self.calculate_relative_path(source_file) # Extract metadata for current page current_page_metadata = self.extract_metadata_from_file(source_file) page_meta = "" if current_page_metadata: date_str = current_page_metadata['modified'].strftime("%B %d, %Y") # Determine if it's a blog or project based on path if 'blog' in source_file.parts and source_file.name != 'index.html': page_meta = f"Published on {date_str}" elif 'projects' in source_file.parts and source_file.name != 'index.html': page_meta = f"Last Updated on {date_str}" # Generate card collections for this file's context card_collections = self.generate_card_collections(relative_path) # Generate project dropdown links project_dropdown_links = self.generate_project_dropdown_links(relative_path) # Combine all replacements all_replacements = { **self.templates, **card_collections, 'project_dropdown_links': project_dropdown_links, 'page_meta': page_meta } # Replace template placeholders for template_name, template_content in all_replacements.items(): placeholder = f"{{{{ {template_name} }}}}" if placeholder in content: # Replace RELATIVE_PATH in template with actual relative path processed_template = template_content.replace('{{RELATIVE_PATH}}', relative_path) content = content.replace(placeholder, processed_template) # Ensure target directory exists target_file.parent.mkdir(parents=True, exist_ok=True) # Write processed content with open(target_file, 'w', encoding='utf-8') as f: f.write(content) def copy_assets(self): """Copy non-HTML files (CSS, images, etc.) to deploy directory""" print("Copying assets...") for root, dirs, files in os.walk(self.dev_dir): # Skip templates directory if 'templates' in Path(root).parts: continue for file in files: if not file.endswith('.html'): source_path = Path(root) / file relative_path = source_path.relative_to(self.dev_dir) target_path = self.deploy_dir / relative_path # Ensure target directory exists target_path.parent.mkdir(parents=True, exist_ok=True) # Copy file shutil.copy2(source_path, target_path) print(f" Copied: {relative_path}") def process_html_files(self): """Process all HTML files in the dev directory""" print("Processing HTML files...") for html_file in self.dev_dir.rglob('*.html'): # Skip template files if 'templates' in html_file.parts: continue # Calculate target path relative_path = html_file.relative_to(self.dev_dir) target_path = self.deploy_dir / relative_path # Process the file self.process_file(html_file, target_path) print(f" Processed: {relative_path}") def clean_deploy_directory(self): """Clean the deploy directory before building""" if self.deploy_dir.exists(): shutil.rmtree(self.deploy_dir) self.deploy_dir.mkdir(parents=True, exist_ok=True) print(f"Cleaned deploy directory: {self.deploy_dir}") def build(self): """Run the complete build process""" print("=== Starting Build Process ===") self.clean_deploy_directory() self.load_templates() self.scan_content_files() self.copy_assets() self.process_html_files() print("=== Build Complete ===") print(f"Deployment files ready in: {self.deploy_dir}") if __name__ == "__main__": processor = TemplateProcessor() processor.build()