Getting Started with Web Development
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()