mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-21 14:50:57 +00:00
e9e7ad0dbd
- Render image gallery below index.md content for directories - Add top-12 and nsfw gallery index text - Add tattoos photo collection - Rename sarah.jpg to sort last in gallery Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
717 lines
27 KiB
Python
717 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}",
|
|
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}",
|
|
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)
|