Compare commits
2 Commits
feat/scrol
...
feat/aw-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dc47845cf | ||
| f44bd3aa33 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Build artifacts — regenerated by `make build` / `python3 scripts/build.py`
|
||||||
|
# Source of truth is blog/posts/*.md
|
||||||
|
blog/posts/*.html
|
||||||
@@ -4,10 +4,22 @@
|
|||||||
<link href="https://alexanderwhitestone.com/blog/"/>
|
<link href="https://alexanderwhitestone.com/blog/"/>
|
||||||
<link rel="self" href="https://alexanderwhitestone.com/blog/feed.xml"/>
|
<link rel="self" href="https://alexanderwhitestone.com/blog/feed.xml"/>
|
||||||
<id>https://alexanderwhitestone.com/blog/</id>
|
<id>https://alexanderwhitestone.com/blog/</id>
|
||||||
<updated>2026-03-19T01:32:41Z</updated>
|
<updated>2026-03-19T01:45:33Z</updated>
|
||||||
<author>
|
<author>
|
||||||
<name>Alexander Whitestone</name>
|
<name>Alexander Whitestone</name>
|
||||||
</author>
|
</author>
|
||||||
<subtitle>Words from the Wizard's Tower</subtitle>
|
<subtitle>Words from the Wizard's Tower</subtitle>
|
||||||
|
<entry>
|
||||||
|
<title>Hello World</title>
|
||||||
|
<link href="https://alexanderwhitestone.com/blog/posts/2026-03-18-hello-world.html"/>
|
||||||
|
<id>https://alexanderwhitestone.com/blog/posts/2026-03-18-hello-world</id>
|
||||||
|
<updated>2026-03-18T00:00:00Z</updated>
|
||||||
|
<content type="html"><![CDATA[<p>The Tower stands. Two doors. You choose.</p>
|
||||||
|
<p><strong>The Workshop</strong> is where the wizard lives — a 3D world you walk into. Not a chatbot. A presence.</p>
|
||||||
|
<p><strong>The Scrolls</strong> is where the words live — plain text, RSS, no platform tax. You're reading one now.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>Sovereignty isn't a feature. It's the architecture.</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>This is the first scroll. More will follow.</p>]]></content>
|
||||||
|
</entry>
|
||||||
</feed>
|
</feed>
|
||||||
@@ -15,12 +15,24 @@
|
|||||||
header { margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }
|
header { margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }
|
||||||
header h1 { font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }
|
header h1 { font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }
|
||||||
header nav { margin-top: 0.5rem; font-size: 0.8rem; }
|
header nav { margin-top: 0.5rem; font-size: 0.8rem; }
|
||||||
header a { color: #8a7f6a; text-decoration: none; }
|
header a, a { color: #8a7f6a; text-decoration: none; }
|
||||||
header a:hover { color: #e0d8c8; }
|
header a:hover, a:hover { color: #e0d8c8; }
|
||||||
.posts { list-style: none; }
|
.posts { list-style: none; }
|
||||||
.posts li { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }
|
.posts li { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }
|
||||||
.posts .date { font-size: 0.8rem; color: #6a6050; display: block; }
|
.posts .date { font-size: 0.8rem; color: #6a6050; display: block; }
|
||||||
.posts .title { font-size: 1rem; }
|
.posts a { color: #e0d8c8; text-decoration: none; }
|
||||||
|
.posts a:hover { color: #fff; }
|
||||||
|
.post-date { font-size: 0.85rem; color: #6a6050; margin-bottom: 2rem; display: block; }
|
||||||
|
.post-body p { margin-bottom: 1.2rem; }
|
||||||
|
.post-body h2 { font-size: 1.1rem; margin: 2rem 0 1rem; color: #c0b8a8; }
|
||||||
|
.post-body h3 { font-size: 1rem; margin: 1.5rem 0 0.8rem; color: #a09888; }
|
||||||
|
.post-body blockquote { border-left: 2px solid #2a2520; padding-left: 1rem; color: #a09888; margin-bottom: 1.2rem; }
|
||||||
|
.post-body code { background: #1a1510; padding: 0.15em 0.4em; font-size: 0.9em; }
|
||||||
|
.post-body pre { background: #1a1510; padding: 1rem; overflow-x: auto; margin-bottom: 1.2rem; }
|
||||||
|
.post-body pre code { background: none; padding: 0; }
|
||||||
|
.post-body em { font-style: italic; }
|
||||||
|
.post-body strong { color: #fff; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -32,7 +44,7 @@
|
|||||||
<ul class="posts">
|
<ul class="posts">
|
||||||
<li>
|
<li>
|
||||||
<span class="date">2026-03-18</span>
|
<span class="date">2026-03-18</span>
|
||||||
<span class="title">Hello World</span>
|
<a href="/blog/posts/2026-03-18-hello-world.html">Hello World</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
14
blog/posts/2026-03-18-hello-world.md
Normal file
14
blog/posts/2026-03-18-hello-world.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: "Hello World"
|
||||||
|
date: 2026-03-18
|
||||||
|
---
|
||||||
|
|
||||||
|
The Tower stands. Two doors. You choose.
|
||||||
|
|
||||||
|
**The Workshop** is where the wizard lives — a 3D world you walk into. Not a chatbot. A presence.
|
||||||
|
|
||||||
|
**The Scrolls** is where the words live — plain text, RSS, no platform tax. You're reading one now.
|
||||||
|
|
||||||
|
> Sovereignty isn't a feature. It's the architecture.
|
||||||
|
|
||||||
|
This is the first scroll. More will follow.
|
||||||
118
scripts/aw-post
118
scripts/aw-post
@@ -2,48 +2,104 @@
|
|||||||
# aw-post — Quick-post a scroll from the command line
|
# aw-post — Quick-post a scroll from the command line
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# aw-post "Title of the Post"
|
# aw-post "Title" # Create empty post, open for editing
|
||||||
# aw-post "Title" < body.md
|
# aw-post "Title" < body.md # Pipe body from stdin
|
||||||
# echo "Post body here" | aw-post "Title"
|
# echo "Post body" | aw-post "Title" # Pipe body from stdin
|
||||||
|
# aw-post --file thoughts.md # Post from a file (title from first line)
|
||||||
|
# aw-post --from-x "Tweet text here" # Port an X/Twitter post
|
||||||
#
|
#
|
||||||
# Creates a new markdown file in blog/posts/ with frontmatter.
|
# Creates a markdown file in blog/posts/, rebuilds the blog.
|
||||||
# Rebuilds the blog index and RSS feed.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BLOG_DIR="$(cd "$(dirname "$0")/.." && pwd)/blog/posts"
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BLOG_DIR="${REPO_DIR}/blog/posts"
|
||||||
if [ $# -lt 1 ]; then
|
|
||||||
echo "Usage: aw-post \"Title of the Post\""
|
|
||||||
echo " Pipe or redirect body content via stdin."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TITLE="$1"
|
|
||||||
DATE=$(date +%Y-%m-%d)
|
DATE=$(date +%Y-%m-%d)
|
||||||
SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
|
|
||||||
FILENAME="${DATE}-${SLUG}.md"
|
|
||||||
FILEPATH="${BLOG_DIR}/${FILENAME}"
|
|
||||||
|
|
||||||
if [ -f "$FILEPATH" ]; then
|
slugify() {
|
||||||
echo "Error: $FILEPATH already exists"
|
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-' | head -c 60
|
||||||
exit 1
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
# Read body from stdin if available
|
create_post() {
|
||||||
BODY=""
|
local title="$1"
|
||||||
if [ ! -t 0 ]; then
|
local body="$2"
|
||||||
BODY=$(cat)
|
local slug
|
||||||
fi
|
slug=$(slugify "$title")
|
||||||
|
local filename="${DATE}-${slug}.md"
|
||||||
|
local filepath="${BLOG_DIR}/${filename}"
|
||||||
|
|
||||||
cat > "$FILEPATH" << EOF
|
if [ -f "$filepath" ]; then
|
||||||
|
echo "Error: $filepath already exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$filepath" << EOF
|
||||||
---
|
---
|
||||||
title: "${TITLE}"
|
title: "${title}"
|
||||||
date: ${DATE}
|
date: ${DATE}
|
||||||
---
|
---
|
||||||
|
|
||||||
${BODY}
|
${body}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Created: ${FILEPATH}"
|
echo "Created: ${filepath}"
|
||||||
echo "Next: rebuild with 'make build'"
|
python3 "${REPO_DIR}/scripts/build.py"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
case "${1:-}" in
|
||||||
|
--file)
|
||||||
|
# Post from a file — title from first non-empty line
|
||||||
|
if [ -z "${2:-}" ] || [ ! -f "${2:-}" ]; then
|
||||||
|
echo "Usage: aw-post --file <path-to-markdown>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BODY=$(cat "$2")
|
||||||
|
# Extract title: first line starting with # or first non-empty line
|
||||||
|
TITLE=$(echo "$BODY" | grep -m1 '^#' | sed 's/^#* *//' || echo "$BODY" | grep -m1 '.' || echo "Untitled")
|
||||||
|
if [ -z "$TITLE" ]; then
|
||||||
|
TITLE="Untitled"
|
||||||
|
fi
|
||||||
|
# Strip the title line from body if it was a markdown header
|
||||||
|
BODY=$(echo "$BODY" | sed '1{/^#/d;}')
|
||||||
|
create_post "$TITLE" "$BODY"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--from-x)
|
||||||
|
# Port an X/Twitter post — the text becomes both title and body
|
||||||
|
if [ -z "${2:-}" ]; then
|
||||||
|
echo "Usage: aw-post --from-x \"Tweet text here\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TEXT="$2"
|
||||||
|
# Title: first 60 chars, cleaned up
|
||||||
|
TITLE=$(echo "$TEXT" | head -1 | cut -c1-60 | sed 's/[[:space:]]*$//')
|
||||||
|
create_post "$TITLE" "$TEXT"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--help|-h)
|
||||||
|
echo "aw-post — Quick-post a scroll to The Scrolls"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " aw-post \"Title\" Create a post (body from stdin)"
|
||||||
|
echo " aw-post --file thoughts.md Post from a markdown file"
|
||||||
|
echo " aw-post --from-x \"Tweet text\" Port an X/Twitter post"
|
||||||
|
echo " aw-post --help Show this help"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"")
|
||||||
|
echo "Usage: aw-post \"Title of the Post\""
|
||||||
|
echo " See: aw-post --help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
# Standard mode: title as arg, body from stdin
|
||||||
|
TITLE="$1"
|
||||||
|
BODY=""
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
BODY=$(cat)
|
||||||
|
fi
|
||||||
|
create_post "$TITLE" "$BODY"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
214
scripts/build.py
214
scripts/build.py
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Build script for The Scrolls — generates blog index and RSS feed from
|
Build script for The Scrolls — generates blog pages and RSS feed from
|
||||||
markdown posts in blog/posts/.
|
markdown posts in blog/posts/.
|
||||||
|
|
||||||
Each post is a markdown file with YAML frontmatter:
|
Each post is a markdown file with YAML frontmatter:
|
||||||
@@ -11,10 +11,12 @@ Each post is a markdown file with YAML frontmatter:
|
|||||||
Body content here.
|
Body content here.
|
||||||
|
|
||||||
Generates:
|
Generates:
|
||||||
- blog/index.html with post listing
|
- blog/index.html with linked post listing
|
||||||
|
- blog/posts/<slug>.html for each post
|
||||||
- blog/feed.xml Atom feed
|
- blog/feed.xml Atom feed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import html
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -24,6 +26,35 @@ SITE_URL = "https://alexanderwhitestone.com"
|
|||||||
BLOG_DIR = Path(__file__).parent.parent / "blog"
|
BLOG_DIR = Path(__file__).parent.parent / "blog"
|
||||||
POSTS_DIR = BLOG_DIR / "posts"
|
POSTS_DIR = BLOG_DIR / "posts"
|
||||||
|
|
||||||
|
PAGE_STYLE = """\
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
max-width: 640px; margin: 0 auto; padding: 2rem 1rem;
|
||||||
|
background: #0a0a0f; color: #e0d8c8;
|
||||||
|
font-family: Georgia, serif; line-height: 1.6;
|
||||||
|
}
|
||||||
|
header { margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }
|
||||||
|
header h1 { font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }
|
||||||
|
header nav { margin-top: 0.5rem; font-size: 0.8rem; }
|
||||||
|
header a, a { color: #8a7f6a; text-decoration: none; }
|
||||||
|
header a:hover, a:hover { color: #e0d8c8; }
|
||||||
|
.posts { list-style: none; }
|
||||||
|
.posts li { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }
|
||||||
|
.posts .date { font-size: 0.8rem; color: #6a6050; display: block; }
|
||||||
|
.posts a { color: #e0d8c8; text-decoration: none; }
|
||||||
|
.posts a:hover { color: #fff; }
|
||||||
|
.post-date { font-size: 0.85rem; color: #6a6050; margin-bottom: 2rem; display: block; }
|
||||||
|
.post-body p { margin-bottom: 1.2rem; }
|
||||||
|
.post-body h2 { font-size: 1.1rem; margin: 2rem 0 1rem; color: #c0b8a8; }
|
||||||
|
.post-body h3 { font-size: 1rem; margin: 1.5rem 0 0.8rem; color: #a09888; }
|
||||||
|
.post-body blockquote { border-left: 2px solid #2a2520; padding-left: 1rem; color: #a09888; margin-bottom: 1.2rem; }
|
||||||
|
.post-body code { background: #1a1510; padding: 0.15em 0.4em; font-size: 0.9em; }
|
||||||
|
.post-body pre { background: #1a1510; padding: 1rem; overflow-x: auto; margin-bottom: 1.2rem; }
|
||||||
|
.post-body pre code { background: none; padding: 0; }
|
||||||
|
.post-body em { font-style: italic; }
|
||||||
|
.post-body strong { color: #fff; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def parse_frontmatter(text):
|
def parse_frontmatter(text):
|
||||||
"""Extract YAML frontmatter from markdown text."""
|
"""Extract YAML frontmatter from markdown text."""
|
||||||
@@ -39,6 +70,89 @@ def parse_frontmatter(text):
|
|||||||
return meta, match.group(2).strip()
|
return meta, match.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def md_to_html(text):
|
||||||
|
"""Minimal markdown to HTML — paragraphs, headers, bold, italic, code, links, blockquotes."""
|
||||||
|
lines = text.split("\n")
|
||||||
|
out = []
|
||||||
|
in_code_block = False
|
||||||
|
in_blockquote = False
|
||||||
|
paragraph = []
|
||||||
|
|
||||||
|
def flush_paragraph():
|
||||||
|
if paragraph:
|
||||||
|
content = "\n".join(paragraph)
|
||||||
|
content = inline_format(content)
|
||||||
|
out.append(f"<p>{content}</p>")
|
||||||
|
paragraph.clear()
|
||||||
|
|
||||||
|
def inline_format(s):
|
||||||
|
# Code spans first (protect from other formatting)
|
||||||
|
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
|
||||||
|
# Bold
|
||||||
|
s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
|
||||||
|
# Italic
|
||||||
|
s = re.sub(r"\*(.+?)\*", r"<em>\1</em>", s)
|
||||||
|
# Links
|
||||||
|
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# Fenced code blocks
|
||||||
|
if line.strip().startswith("```"):
|
||||||
|
if in_code_block:
|
||||||
|
out.append("</code></pre>")
|
||||||
|
in_code_block = False
|
||||||
|
else:
|
||||||
|
flush_paragraph()
|
||||||
|
out.append("<pre><code>")
|
||||||
|
in_code_block = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_code_block:
|
||||||
|
out.append(html.escape(line))
|
||||||
|
continue
|
||||||
|
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# Empty line — flush paragraph
|
||||||
|
if not stripped:
|
||||||
|
flush_paragraph()
|
||||||
|
if in_blockquote:
|
||||||
|
out.append("</blockquote>")
|
||||||
|
in_blockquote = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
header_match = re.match(r"^(#{1,3})\s+(.+)$", stripped)
|
||||||
|
if header_match:
|
||||||
|
flush_paragraph()
|
||||||
|
level = len(header_match.group(1)) + 1 # h2, h3, h4 (h1 is title)
|
||||||
|
content = inline_format(header_match.group(2))
|
||||||
|
out.append(f"<h{level}>{content}</h{level}>")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Blockquotes
|
||||||
|
if stripped.startswith("> "):
|
||||||
|
flush_paragraph()
|
||||||
|
content = inline_format(stripped[2:])
|
||||||
|
if not in_blockquote:
|
||||||
|
out.append("<blockquote>")
|
||||||
|
in_blockquote = True
|
||||||
|
out.append(f"<p>{content}</p>")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Regular text — accumulate paragraph
|
||||||
|
paragraph.append(stripped)
|
||||||
|
|
||||||
|
flush_paragraph()
|
||||||
|
if in_blockquote:
|
||||||
|
out.append("</blockquote>")
|
||||||
|
if in_code_block:
|
||||||
|
out.append("</code></pre>")
|
||||||
|
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
def load_posts():
|
def load_posts():
|
||||||
"""Load and sort all posts by date (newest first)."""
|
"""Load and sort all posts by date (newest first)."""
|
||||||
posts = []
|
posts = []
|
||||||
@@ -61,24 +175,57 @@ def load_posts():
|
|||||||
return posts
|
return posts
|
||||||
|
|
||||||
|
|
||||||
|
def generate_post_page(post):
|
||||||
|
"""Generate an individual post HTML page."""
|
||||||
|
body_html = md_to_html(post["body"])
|
||||||
|
|
||||||
|
page = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{html.escape(post["title"])} — The Scrolls</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
||||||
|
<style>
|
||||||
|
{PAGE_STYLE}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>{html.escape(post["title"])}</h1>
|
||||||
|
<nav><a href="/blog/">← The Scrolls</a> · <a href="/">The Tower</a> · <a href="/blog/feed.xml">RSS</a></nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<span class="post-date">{post["date"]}</span>
|
||||||
|
<article class="post-body">
|
||||||
|
{body_html}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
out_path = POSTS_DIR / f"{post['slug']}.html"
|
||||||
|
out_path.write_text(page)
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
def generate_index(posts):
|
def generate_index(posts):
|
||||||
"""Generate blog/index.html with post listing."""
|
"""Generate blog/index.html with linked post listing."""
|
||||||
if not posts:
|
if not posts:
|
||||||
print(" No posts found. Keeping placeholder index.")
|
items_html = ' <li><span class="date">—</span><span>No scrolls yet.</span></li>'
|
||||||
return
|
else:
|
||||||
|
items = []
|
||||||
|
for p in posts:
|
||||||
|
escaped_title = html.escape(p["title"])
|
||||||
|
items.append(
|
||||||
|
f' <li>\n'
|
||||||
|
f' <span class="date">{p["date"]}</span>\n'
|
||||||
|
f' <a href="/blog/posts/{p["slug"]}.html">{escaped_title}</a>\n'
|
||||||
|
f' </li>'
|
||||||
|
)
|
||||||
|
items_html = "\n".join(items)
|
||||||
|
|
||||||
items = []
|
page = f"""<!DOCTYPE html>
|
||||||
for p in posts:
|
|
||||||
items.append(
|
|
||||||
f' <li>\n'
|
|
||||||
f' <span class="date">{p["date"]}</span>\n'
|
|
||||||
f' <span class="title">{p["title"]}</span>\n'
|
|
||||||
f" </li>"
|
|
||||||
)
|
|
||||||
|
|
||||||
post_list = "\n".join(items)
|
|
||||||
|
|
||||||
html = f"""<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@@ -86,21 +233,7 @@ def generate_index(posts):
|
|||||||
<title>The Scrolls — The Wizard's Tower</title>
|
<title>The Scrolls — The Wizard's Tower</title>
|
||||||
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="The Scrolls" href="/blog/feed.xml">
|
||||||
<style>
|
<style>
|
||||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
{PAGE_STYLE}
|
||||||
body {{
|
|
||||||
max-width: 640px; margin: 0 auto; padding: 2rem 1rem;
|
|
||||||
background: #0a0a0f; color: #e0d8c8;
|
|
||||||
font-family: Georgia, serif; line-height: 1.6;
|
|
||||||
}}
|
|
||||||
header {{ margin-bottom: 3rem; border-bottom: 1px solid #2a2520; padding-bottom: 1rem; }}
|
|
||||||
header h1 {{ font-size: 1.2rem; font-weight: normal; letter-spacing: 0.1em; }}
|
|
||||||
header nav {{ margin-top: 0.5rem; font-size: 0.8rem; }}
|
|
||||||
header a {{ color: #8a7f6a; text-decoration: none; }}
|
|
||||||
header a:hover {{ color: #e0d8c8; }}
|
|
||||||
.posts {{ list-style: none; }}
|
|
||||||
.posts li {{ margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #1a1510; }}
|
|
||||||
.posts .date {{ font-size: 0.8rem; color: #6a6050; display: block; }}
|
|
||||||
.posts .title {{ font-size: 1rem; }}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -110,13 +243,13 @@ def generate_index(posts):
|
|||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<ul class="posts">
|
<ul class="posts">
|
||||||
{post_list}
|
{items_html}
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
(BLOG_DIR / "index.html").write_text(html)
|
(BLOG_DIR / "index.html").write_text(page)
|
||||||
print(f" Generated index with {len(posts)} post(s).")
|
print(f" Generated index with {len(posts)} post(s).")
|
||||||
|
|
||||||
|
|
||||||
@@ -126,17 +259,15 @@ def generate_feed(posts):
|
|||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for p in posts[:20]: # Cap at 20 entries
|
for p in posts[:20]: # Cap at 20 entries
|
||||||
# Simple HTML conversion: wrap paragraphs
|
body_html = md_to_html(p["body"])
|
||||||
html_body = "\n".join(
|
escaped_title = html.escape(p["title"])
|
||||||
f"<p>{para}</p>" for para in p["body"].split("\n\n") if para.strip()
|
|
||||||
)
|
|
||||||
entries.append(
|
entries.append(
|
||||||
f""" <entry>
|
f""" <entry>
|
||||||
<title>{p["title"]}</title>
|
<title>{escaped_title}</title>
|
||||||
<link href="{SITE_URL}/blog/posts/{p["slug"]}.html"/>
|
<link href="{SITE_URL}/blog/posts/{p["slug"]}.html"/>
|
||||||
<id>{SITE_URL}/blog/posts/{p["slug"]}</id>
|
<id>{SITE_URL}/blog/posts/{p["slug"]}</id>
|
||||||
<updated>{p["date"]}T00:00:00Z</updated>
|
<updated>{p["date"]}T00:00:00Z</updated>
|
||||||
<content type="html"><![CDATA[{html_body}]]></content>
|
<content type="html"><![CDATA[{body_html}]]></content>
|
||||||
</entry>"""
|
</entry>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,6 +294,9 @@ def generate_feed(posts):
|
|||||||
def main():
|
def main():
|
||||||
print("Building The Scrolls...")
|
print("Building The Scrolls...")
|
||||||
posts = load_posts()
|
posts = load_posts()
|
||||||
|
for p in posts:
|
||||||
|
out = generate_post_page(p)
|
||||||
|
print(f" Built: {out.relative_to(BLOG_DIR.parent)}")
|
||||||
generate_index(posts)
|
generate_index(posts)
|
||||||
generate_feed(posts)
|
generate_feed(posts)
|
||||||
print("Build complete.")
|
print("Build complete.")
|
||||||
|
|||||||
Reference in New Issue
Block a user