mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-21 14:50:57 +00:00
33ea221a12
Octocat banner in the upper right links to the .md source file for each page. Directory indexes link to index.md, regular pages to their .md, and the homepage falls back to the repo root. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
719 lines
27 KiB
Python
719 lines
27 KiB
Python
"""Content blueprint for serving markdown files and static content."""
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from flask import (
|
|
Blueprint,
|
|
abort,
|
|
jsonify,
|
|
redirect,
|
|
render_template,
|
|
send_from_directory,
|
|
)
|
|
|
|
content_bp = Blueprint("content", __name__)
|
|
|
|
DATA_DIR = Path("data")
|
|
STATIC_DIR = Path("static")
|
|
|
|
|
|
def extract_exif_data(image_path):
|
|
"""Extract EXIF data from an image file."""
|
|
try:
|
|
from PIL import Image
|
|
from PIL.ExifTags import TAGS
|
|
|
|
img = Image.open(image_path)
|
|
exif_data = {}
|
|
|
|
# Get basic image info
|
|
exif_data['width'] = img.width
|
|
exif_data['height'] = img.height
|
|
exif_data['format'] = img.format
|
|
|
|
# Get EXIF data if available
|
|
if hasattr(img, '_getexif') and img._getexif() is not None:
|
|
exif = img._getexif()
|
|
for tag_id, value in exif.items():
|
|
tag = TAGS.get(tag_id, tag_id)
|
|
|
|
# Extract commonly useful fields
|
|
if tag == 'Make':
|
|
exif_data['camera_make'] = str(value).strip()
|
|
elif tag == 'Model':
|
|
exif_data['camera_model'] = str(value).strip()
|
|
elif tag == 'DateTime':
|
|
exif_data['date_taken'] = str(value)
|
|
elif tag == 'DateTimeOriginal':
|
|
exif_data['date_original'] = str(value)
|
|
elif tag == 'LensModel':
|
|
exif_data['lens'] = str(value).strip()
|
|
elif tag == 'FocalLength':
|
|
# Convert to readable format
|
|
if isinstance(value, tuple):
|
|
exif_data['focal_length'] = f"{value[0]/value[1]:.0f}mm"
|
|
else:
|
|
exif_data['focal_length'] = f"{value}mm"
|
|
elif tag == 'FNumber':
|
|
# Convert to readable format
|
|
if isinstance(value, tuple):
|
|
f_num = value[0]/value[1]
|
|
# Round to 1 decimal place, but show as integer if whole number
|
|
if f_num == int(f_num):
|
|
exif_data['aperture'] = f"f/{int(f_num)}"
|
|
else:
|
|
exif_data['aperture'] = f"f/{f_num:.1f}"
|
|
else:
|
|
exif_data['aperture'] = f"f/{value}"
|
|
elif tag == 'ISOSpeedRatings':
|
|
exif_data['iso'] = f"ISO {value}"
|
|
elif tag == 'ExposureTime':
|
|
# Convert to readable format as fraction
|
|
from fractions import Fraction
|
|
|
|
if isinstance(value, tuple):
|
|
frac = Fraction(value[0], value[1])
|
|
else:
|
|
# Convert float to fraction
|
|
frac = Fraction(value).limit_denominator(8000)
|
|
|
|
if frac >= 1:
|
|
# For exposures >= 1 second, show as decimal
|
|
exif_data['shutter_speed'] = f"{float(frac):.1f}s"
|
|
else:
|
|
# For fast shutter speeds, show as fraction
|
|
exif_data['shutter_speed'] = f"1/{int(1/frac)}s"
|
|
|
|
return exif_data
|
|
except Exception as e:
|
|
print(f"Error extracting EXIF from {image_path}: {e}")
|
|
return {}
|
|
|
|
|
|
@content_bp.route("/static/")
|
|
@content_bp.route("/static/<path:path>/")
|
|
def browse_static_directory(path=""):
|
|
"""Browse static directory contents."""
|
|
from datetime import datetime
|
|
from flask import request
|
|
|
|
static_path = STATIC_DIR / path
|
|
|
|
if not static_path.exists() or not static_path.is_dir():
|
|
abort(404)
|
|
|
|
items = []
|
|
for item in sorted(static_path.iterdir()):
|
|
if item.name.startswith("."):
|
|
continue
|
|
|
|
item_url = f"/static/{path}/{item.name}" if path else f"/static/{item.name}"
|
|
if item.is_dir():
|
|
item_url += "/"
|
|
|
|
items.append(
|
|
{
|
|
"name": item.name,
|
|
"url": item_url,
|
|
"is_dir": item.is_dir(),
|
|
"size": item.stat().st_size if item.is_file() else None,
|
|
"modified": datetime.fromtimestamp(item.stat().st_mtime),
|
|
}
|
|
)
|
|
|
|
# Check if JSON is requested
|
|
if request.headers.get("Accept") == "application/json":
|
|
return jsonify(
|
|
{"path": f"/static/{path}" if path else "/static", "items": items}
|
|
)
|
|
|
|
# Return HTML directory listing
|
|
breadcrumb = []
|
|
if path:
|
|
parts = path.split("/")
|
|
current = "/static"
|
|
for part in parts:
|
|
current = f"{current}/{part}"
|
|
breadcrumb.append({"name": part, "path": current})
|
|
|
|
return render_template(
|
|
"directory.html",
|
|
items=items,
|
|
current_path=f"/static/{path}" if path else "/static",
|
|
breadcrumb=breadcrumb,
|
|
current_year=datetime.now().year,
|
|
title=f"Static Directory: {path}" if path else "Static Directory",
|
|
)
|
|
|
|
|
|
@content_bp.route("/data/<path:path>")
|
|
def serve_data_file(path):
|
|
"""Serve static files from the data directory."""
|
|
from flask import make_response
|
|
|
|
file_path = DATA_DIR / path
|
|
|
|
if not file_path.exists() or not file_path.is_file():
|
|
abort(404)
|
|
|
|
# Serve the file with proper caching headers
|
|
response = make_response(send_from_directory(DATA_DIR, path))
|
|
|
|
# Add caching headers for static assets
|
|
if file_path.suffix.lower() in [
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".webp",
|
|
".svg",
|
|
".ico",
|
|
]:
|
|
# Cache images for 7 days
|
|
response.headers["Cache-Control"] = "public, max-age=604800"
|
|
else:
|
|
# Cache other static files for 1 hour
|
|
response.headers["Cache-Control"] = "public, max-age=3600"
|
|
|
|
return response
|
|
|
|
|
|
@content_bp.route("/<path:path>.md")
|
|
def serve_content_markdown(path):
|
|
"""Serve the raw markdown source of the content."""
|
|
from flask import make_response
|
|
|
|
# Remove trailing slash for consistency
|
|
path = path.rstrip("/")
|
|
|
|
# Check for the markdown file
|
|
file_path = DATA_DIR / f"{path}.md"
|
|
|
|
if not file_path.exists():
|
|
abort(404)
|
|
|
|
# Read the raw markdown content
|
|
markdown_content = file_path.read_text()
|
|
|
|
# Create response with proper headers
|
|
response = make_response(markdown_content)
|
|
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
|
response.headers['Content-Disposition'] = f'inline; filename="{path.split("/")[-1]}.md"'
|
|
|
|
return response
|
|
|
|
|
|
@content_bp.route("/<path:path>.pdf")
|
|
def serve_content_pdf(path):
|
|
"""Generate and serve a PDF version of the content."""
|
|
from ..core.markdown import render_markdown_file
|
|
from flask import make_response, request
|
|
import io
|
|
|
|
# Check if WeasyPrint is available
|
|
try:
|
|
from weasyprint import HTML
|
|
from weasyprint.text.fonts import FontConfiguration
|
|
WEASYPRINT_AVAILABLE = True
|
|
except (ImportError, OSError) as e:
|
|
# WeasyPrint or its system dependencies are not available
|
|
print(f"WeasyPrint not available: {e}")
|
|
WEASYPRINT_AVAILABLE = False
|
|
|
|
# Remove trailing slash for consistency
|
|
path = path.rstrip("/")
|
|
|
|
# Check for the markdown file
|
|
file_path = DATA_DIR / f"{path}.md"
|
|
|
|
if not file_path.exists():
|
|
abort(404)
|
|
|
|
# If WeasyPrint is not available, return a helpful error page
|
|
if not WEASYPRINT_AVAILABLE:
|
|
from datetime import datetime
|
|
error_html = render_template(
|
|
"error.html",
|
|
title="PDF Generation Unavailable",
|
|
message="PDF generation requires WeasyPrint and system dependencies to be installed.",
|
|
details="Please see the README for installation instructions, or use the browser's print dialog (Cmd+P / Ctrl+P) to save as PDF.",
|
|
current_year=datetime.now().year,
|
|
)
|
|
response = make_response(error_html, 503)
|
|
return response
|
|
|
|
# Render the markdown content
|
|
content_data = render_markdown_file(file_path)
|
|
|
|
# Calculate themes for this article
|
|
article_themes = []
|
|
try:
|
|
import re
|
|
raw_content = file_path.read_text()
|
|
content_lower = raw_content.lower()
|
|
|
|
theme_patterns = {
|
|
"consciousness": "Exploring the nature of awareness, identity, and the recursive loop between code and mind.",
|
|
"technology": "Human-first approaches to building tools that serve rather than exploit.",
|
|
"mental health": "Reality-checking, debugging consciousness, and navigating neurodivergence.",
|
|
"programming": "Code as meditation, API design as compassion, and software as spiritual practice.",
|
|
"AI": "Human-AI collaboration as partnership, not replacement—augmenting consciousness through digital minds.",
|
|
"human centered": "Designing systems that adapt to human mental models rather than forcing humans to adapt.",
|
|
"recursive": "The feedback loops between programmer consciousness, code patterns, and collective impact.",
|
|
"spiritual": "Technical work as contemplative practice—finding transcendence in systematicity.",
|
|
"mindful": "Bringing awareness and intentionality to the craft of building software.",
|
|
"contemplative": "Reflective approaches to technology, blending Eastern wisdom with Western pragmatism.",
|
|
}
|
|
|
|
for theme_name, description in theme_patterns.items():
|
|
regex_pattern = theme_name.replace(" ", r"[- ]")
|
|
if re.search(regex_pattern, content_lower):
|
|
article_themes.append({
|
|
"name": theme_name,
|
|
"description": description
|
|
})
|
|
except Exception as e:
|
|
print(f"Error calculating themes: {e}")
|
|
|
|
# Render HTML for PDF
|
|
from datetime import datetime
|
|
html_content = render_template(
|
|
"pdf.html",
|
|
content=content_data["content"],
|
|
title=content_data["title"],
|
|
metadata=content_data["metadata"],
|
|
current_year=datetime.now().year,
|
|
reading_time=content_data.get("reading_time"),
|
|
word_count=content_data.get("word_count"),
|
|
article_themes=article_themes,
|
|
unique_icon=content_data.get("unique_icon"),
|
|
)
|
|
|
|
# Generate PDF
|
|
font_config = FontConfiguration()
|
|
|
|
# Get the base URL for resolving relative URLs in CSS/images
|
|
base_url = request.url_root
|
|
|
|
pdf_file = io.BytesIO()
|
|
HTML(string=html_content, base_url=base_url).write_pdf(
|
|
pdf_file,
|
|
font_config=font_config
|
|
)
|
|
pdf_file.seek(0)
|
|
|
|
# Create response
|
|
response = make_response(pdf_file.read())
|
|
response.headers['Content-Type'] = 'application/pdf'
|
|
response.headers['Content-Disposition'] = f'attachment; filename="{path.split("/")[-1]}.pdf"'
|
|
|
|
return response
|
|
|
|
|
|
@content_bp.route("/<path:path>")
|
|
def serve_content(path):
|
|
"""Serve content from markdown files or directories."""
|
|
from ..core.markdown import render_markdown_file
|
|
from ..utils.content import get_directory_structure
|
|
from datetime import datetime
|
|
|
|
# Remove trailing slash for consistency
|
|
path = path.rstrip("/")
|
|
|
|
# Check if this is an image file request
|
|
raw_file_path = DATA_DIR / path
|
|
if raw_file_path.exists() and raw_file_path.is_file():
|
|
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico'}
|
|
suffix = raw_file_path.suffix.lower()
|
|
if suffix in image_extensions:
|
|
# Serve the image file directly
|
|
from flask import make_response
|
|
# Use absolute path for send_from_directory
|
|
abs_data_dir = DATA_DIR.resolve()
|
|
response = make_response(send_from_directory(abs_data_dir, path))
|
|
response.headers["Cache-Control"] = "public, max-age=604800"
|
|
return response
|
|
|
|
# Check for various file patterns
|
|
file_path = DATA_DIR / f"{path}.md"
|
|
dir_path = DATA_DIR / path
|
|
index_path = dir_path / "index.md"
|
|
|
|
# Handle directories
|
|
if dir_path.is_dir():
|
|
# Check for index.md in the directory
|
|
if index_path.exists():
|
|
content_data = render_markdown_file(index_path)
|
|
|
|
# Get items in the directory for display
|
|
items = get_directory_structure(dir_path)
|
|
|
|
# Detect if this is an image gallery
|
|
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'}
|
|
image_items = []
|
|
for item in items:
|
|
if not item['is_dir']:
|
|
item_path = DATA_DIR / item['url_path'].lstrip('/')
|
|
if item_path.suffix.lower() in image_extensions:
|
|
item['is_image'] = True
|
|
item['static_path'] = item['url_path']
|
|
item['is_data_file'] = True
|
|
if item_path.suffix.lower() in {'.jpg', '.jpeg'}:
|
|
item['exif'] = extract_exif_data(item_path)
|
|
else:
|
|
item['exif'] = {}
|
|
image_items.append(item)
|
|
else:
|
|
item['is_image'] = False
|
|
else:
|
|
item['is_image'] = False
|
|
|
|
is_image_gallery = len(image_items) > 0
|
|
|
|
# Calculate themes for this article
|
|
article_themes = []
|
|
try:
|
|
import re
|
|
raw_content = index_path.read_text()
|
|
content_lower = raw_content.lower()
|
|
|
|
theme_patterns = {
|
|
"consciousness": "Exploring the nature of awareness, identity, and the recursive loop between code and mind.",
|
|
"technology": "Human-first approaches to building tools that serve rather than exploit.",
|
|
"mental health": "Reality-checking, debugging consciousness, and navigating neurodivergence.",
|
|
"programming": "Code as meditation, API design as compassion, and software as spiritual practice.",
|
|
"AI": "Human-AI collaboration as partnership, not replacement—augmenting consciousness through digital minds.",
|
|
"human centered": "Designing systems that adapt to human mental models rather than forcing humans to adapt.",
|
|
"recursive": "The feedback loops between programmer consciousness, code patterns, and collective impact.",
|
|
"spiritual": "Technical work as contemplative practice—finding transcendence in systematicity.",
|
|
"mindful": "Bringing awareness and intentionality to the craft of building software.",
|
|
"contemplative": "Reflective approaches to technology, blending Eastern wisdom with Western pragmatism.",
|
|
}
|
|
|
|
for theme_name, description in theme_patterns.items():
|
|
regex_pattern = theme_name.replace(" ", r"[- ]")
|
|
if re.search(regex_pattern, content_lower):
|
|
article_themes.append({
|
|
"name": theme_name,
|
|
"description": description
|
|
})
|
|
except Exception as e:
|
|
print(f"Error calculating themes: {e}")
|
|
|
|
from flask import make_response
|
|
|
|
# If this directory is an image gallery, use directory template
|
|
# so images render as a gallery instead of links
|
|
if is_image_gallery:
|
|
parts = path.split("/")
|
|
breadcrumb = []
|
|
current = ""
|
|
for part in parts:
|
|
current = f"{current}/{part}" if current else part
|
|
breadcrumb.append({
|
|
"name": part.replace("-", " ").replace("_", " ").title(),
|
|
"path": f"/{current}",
|
|
})
|
|
|
|
from ..utils.content import generate_folder_icon
|
|
folder_icon = generate_folder_icon(content_data["title"], size=32)
|
|
|
|
response = make_response(render_template(
|
|
"directory.html",
|
|
items=items,
|
|
image_items=image_items,
|
|
is_image_gallery=True,
|
|
title=content_data["title"],
|
|
current_path=f"/{path}",
|
|
breadcrumb=breadcrumb,
|
|
current_year=datetime.now().year,
|
|
folder_icon=folder_icon,
|
|
index_content={"content": content_data["content"]},
|
|
content_position="top",
|
|
))
|
|
else:
|
|
response = make_response(render_template(
|
|
"post.html",
|
|
content=content_data["content"],
|
|
title=content_data["title"],
|
|
metadata=content_data["metadata"],
|
|
items=items,
|
|
current_path=f"/{path}",
|
|
github_file=f"data/{path}/index.md",
|
|
current_year=datetime.now().year,
|
|
reading_time=content_data.get("reading_time"),
|
|
word_count=content_data.get("word_count"),
|
|
tags=content_data.get("tags", []),
|
|
series_posts=content_data.get("series_posts", []),
|
|
series_name=content_data.get("series_name"),
|
|
unique_icon=content_data.get("unique_icon"),
|
|
article_themes=article_themes,
|
|
))
|
|
|
|
# Add Link headers for alternate formats
|
|
response.headers.add('Link', f'</{path}.pdf>; rel="alternate"; type="application/pdf"')
|
|
response.headers.add('Link', f'</{path}.md>; rel="alternate"; type="text/plain"')
|
|
|
|
return response
|
|
else:
|
|
# Show directory listing
|
|
items = get_directory_structure(dir_path)
|
|
|
|
# Detect if this is an image gallery
|
|
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'}
|
|
image_items = []
|
|
for item in items:
|
|
if not item['is_dir']:
|
|
item_path = DATA_DIR / item['url_path'].lstrip('/')
|
|
if item_path.suffix.lower() in image_extensions:
|
|
# Mark as image and create path that will be caught by serve_data_file
|
|
item['is_image'] = True
|
|
# The path is the URL path which will be handled by the catch-all route
|
|
# We need to mark these to be served as files
|
|
item['static_path'] = item['url_path']
|
|
item['is_data_file'] = True
|
|
|
|
# Extract EXIF data for JPEGs
|
|
if item_path.suffix.lower() in {'.jpg', '.jpeg'}:
|
|
item['exif'] = extract_exif_data(item_path)
|
|
else:
|
|
item['exif'] = {}
|
|
|
|
image_items.append(item)
|
|
else:
|
|
item['is_image'] = False
|
|
else:
|
|
item['is_image'] = False
|
|
|
|
is_image_gallery = len(image_items) > 0
|
|
|
|
# Create breadcrumb navigation
|
|
parts = path.split("/")
|
|
breadcrumb = []
|
|
current = ""
|
|
for part in parts:
|
|
current = f"{current}/{part}" if current else part
|
|
breadcrumb.append(
|
|
{
|
|
"name": part.replace("-", " ").replace("_", " ").title(),
|
|
"path": f"/{current}",
|
|
}
|
|
)
|
|
|
|
# Get title from directory name
|
|
title = path.split("/")[-1].replace("-", " ").replace("_", " ").title()
|
|
|
|
# Generate folder icon for this directory
|
|
from ..utils.content import generate_folder_icon
|
|
folder_icon = generate_folder_icon(title, size=32)
|
|
|
|
return render_template(
|
|
"directory.html",
|
|
items=items,
|
|
current_path=f"/{path}",
|
|
breadcrumb=breadcrumb,
|
|
current_year=datetime.now().year,
|
|
title=title,
|
|
is_image_gallery=is_image_gallery,
|
|
image_items=image_items,
|
|
folder_icon=folder_icon,
|
|
)
|
|
|
|
# Handle markdown files
|
|
elif file_path.exists():
|
|
content_data = render_markdown_file(file_path)
|
|
|
|
# Check if this is a special page
|
|
is_special_page = any(
|
|
path.startswith(prefix)
|
|
for prefix in [
|
|
"essays/",
|
|
"software/",
|
|
"poetry/",
|
|
"personality/",
|
|
"reactions/",
|
|
]
|
|
)
|
|
|
|
# Get related posts if it's an essay
|
|
related_posts = []
|
|
adjacent_posts = {"prev": None, "next": None}
|
|
if "essays/" in path:
|
|
from ..utils.content import find_related_posts, find_adjacent_posts
|
|
|
|
related_posts = find_related_posts(file_path)
|
|
adjacent_posts = find_adjacent_posts(file_path)
|
|
|
|
# Calculate themes for this article
|
|
article_themes = []
|
|
try:
|
|
import re
|
|
raw_content = file_path.read_text()
|
|
content_lower = raw_content.lower()
|
|
|
|
theme_patterns = {
|
|
"consciousness": "Exploring the nature of awareness, identity, and the recursive loop between code and mind.",
|
|
"technology": "Human-first approaches to building tools that serve rather than exploit.",
|
|
"mental health": "Reality-checking, debugging consciousness, and navigating neurodivergence.",
|
|
"programming": "Code as meditation, API design as compassion, and software as spiritual practice.",
|
|
"AI": "Human-AI collaboration as partnership, not replacement—augmenting consciousness through digital minds.",
|
|
"human centered": "Designing systems that adapt to human mental models rather than forcing humans to adapt.",
|
|
"recursive": "The feedback loops between programmer consciousness, code patterns, and collective impact.",
|
|
"spiritual": "Technical work as contemplative practice—finding transcendence in systematicity.",
|
|
"mindful": "Bringing awareness and intentionality to the craft of building software.",
|
|
"contemplative": "Reflective approaches to technology, blending Eastern wisdom with Western pragmatism.",
|
|
}
|
|
|
|
for theme_name, description in theme_patterns.items():
|
|
regex_pattern = theme_name.replace(" ", r"[- ]")
|
|
if re.search(regex_pattern, content_lower):
|
|
article_themes.append({
|
|
"name": theme_name,
|
|
"description": description
|
|
})
|
|
except Exception as e:
|
|
print(f"Error calculating themes: {e}")
|
|
|
|
from flask import make_response
|
|
response = make_response(render_template(
|
|
"post.html",
|
|
content=content_data["content"],
|
|
title=content_data["title"],
|
|
metadata=content_data["metadata"],
|
|
current_path=f"/{path}",
|
|
github_file=f"data/{path}.md",
|
|
current_year=datetime.now().year,
|
|
reading_time=content_data.get("reading_time"),
|
|
word_count=content_data.get("word_count"),
|
|
tags=content_data.get("tags", []),
|
|
series_posts=content_data.get("series_posts", []),
|
|
series_name=content_data.get("series_name"),
|
|
unique_icon=content_data.get("unique_icon"),
|
|
related_posts=related_posts,
|
|
adjacent_posts=adjacent_posts,
|
|
article_themes=article_themes,
|
|
))
|
|
|
|
# Add Link headers for alternate formats
|
|
response.headers.add('Link', f'</{path}.pdf>; rel="alternate"; type="application/pdf"')
|
|
response.headers.add('Link', f'</{path}.md>; rel="alternate"; type="text/plain"')
|
|
|
|
return response
|
|
|
|
# Check for redirects or alternative paths
|
|
# For example, handle /archive/2025 -> show year archive
|
|
if path.startswith("archive/"):
|
|
parts = path.split("/")
|
|
if len(parts) == 2 and parts[1].isdigit():
|
|
year = int(parts[1])
|
|
return archive_year(year)
|
|
elif len(parts) == 3 and parts[1].isdigit() and parts[2].isdigit():
|
|
year = int(parts[1])
|
|
month = int(parts[2])
|
|
return archive_month(year, month)
|
|
|
|
# Handle random redirects
|
|
if path == "random":
|
|
return random_post()
|
|
elif path.startswith("random/"):
|
|
collection = path.split("/", 1)[1]
|
|
return random_from_collection(collection)
|
|
|
|
# File not found
|
|
abort(404)
|
|
|
|
|
|
def archive_year(year):
|
|
"""Display archive for a specific year."""
|
|
from ..core.cache import get_blog_cache
|
|
from datetime import datetime
|
|
|
|
blog_data = get_blog_cache()
|
|
posts = blog_data.get("posts", [])
|
|
|
|
# Filter posts by year
|
|
year_posts = [p for p in posts if p["pub_date"].year == year]
|
|
|
|
return render_template(
|
|
"archive.html",
|
|
posts=year_posts,
|
|
year=year,
|
|
current_year=datetime.now().year,
|
|
title=f"Archive - {year}",
|
|
)
|
|
|
|
|
|
def archive_month(year, month):
|
|
"""Display archive for a specific month."""
|
|
from ..core.cache import get_blog_cache
|
|
from datetime import datetime
|
|
import calendar
|
|
|
|
blog_data = get_blog_cache()
|
|
posts = blog_data.get("posts", [])
|
|
|
|
# Filter posts by year and month
|
|
month_posts = [
|
|
p for p in posts if p["pub_date"].year == year and p["pub_date"].month == month
|
|
]
|
|
|
|
month_name = calendar.month_name[month]
|
|
|
|
return render_template(
|
|
"archive.html",
|
|
posts=month_posts,
|
|
year=year,
|
|
month=month,
|
|
month_name=month_name,
|
|
current_year=datetime.now().year,
|
|
title=f"Archive - {month_name} {year}",
|
|
)
|
|
|
|
|
|
def random_post():
|
|
"""Redirect to a random post."""
|
|
from ..core.cache import get_blog_cache
|
|
import random
|
|
|
|
blog_data = get_blog_cache()
|
|
posts = blog_data.get("posts", [])
|
|
|
|
if posts:
|
|
chosen = random.choice(posts)
|
|
response = redirect(chosen["url"])
|
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
return response
|
|
else:
|
|
abort(404)
|
|
|
|
|
|
def random_from_collection(collection):
|
|
"""Redirect to a random post from a specific collection."""
|
|
from pathlib import Path
|
|
import random
|
|
|
|
# Handle special case for personality
|
|
if collection in ["personality", "personality/"]:
|
|
collection_path = DATA_DIR / "personality"
|
|
else:
|
|
collection_path = DATA_DIR / collection
|
|
|
|
if not collection_path.exists() or not collection_path.is_dir():
|
|
abort(404)
|
|
|
|
# Get all markdown files in the collection
|
|
md_files = list(collection_path.glob("**/*.md"))
|
|
md_files = [f for f in md_files if f.name != "index.md"]
|
|
|
|
if md_files:
|
|
random_file = random.choice(md_files)
|
|
# Create URL path
|
|
relative_path = random_file.relative_to(DATA_DIR)
|
|
url_path = "/" + str(relative_path.with_suffix(""))
|
|
response = redirect(url_path)
|
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
return response
|
|
else:
|
|
abort(404)
|