diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c9f935 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +.PHONY: dev build deploy clean + +# Local development server +dev: + @echo "Starting dev server on :8080..." + @cd "$(CURDIR)" && python3 -m http.server 8080 + +# Build the static site (generate blog index + RSS from posts) +build: + @echo "Building the Tower..." + @python3 scripts/build.py + @echo "Done." + +# Deploy — configure DEPLOY_TARGET in environment or .env +deploy: build + @echo "Deploying..." + @if [ -z "$${DEPLOY_TARGET:-}" ]; then \ + echo "Error: Set DEPLOY_TARGET (e.g., user@host:/var/www/tower)"; \ + exit 1; \ + fi + rsync -avz --delete \ + --exclude '.git' \ + --exclude 'scripts/' \ + --exclude 'Makefile' \ + --exclude '*.md' \ + ./ "$${DEPLOY_TARGET}/" + @echo "Deployed to $${DEPLOY_TARGET}" + +# Clean generated files +clean: + @echo "Nothing to clean yet." diff --git a/README.md b/README.md index f5c92a4..aef5469 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# alexanderwhitestone.com +# The Wizard's Tower -Public-facing interface for Timmy — AlexanderWhitestone.com \ No newline at end of file +**AlexanderWhitestone.com** — two rooms, nothing else. + +## Rooms + +- **The Workshop** (`/world/`) — A 3D space where Timmy lives. Visitors enter and interact. +- **The Scrolls** (`/blog/`) — Alexander's words. Plain text, RSS, sovereign publishing. + +## Structure + +``` +index.html Entry hall — two doors +world/ The Workshop (3D scene, Timmy presence) +blog/ The Scrolls (posts, RSS feed) +scripts/ CLI tools (aw-post for quick publishing) +static/ Shared assets (fonts, favicon) +Makefile Build, dev, deploy +``` + +## Development + +```bash +make dev # Local dev server on :8080 +make build # Build static site +make deploy # Deploy (configure target in Makefile) +``` + +## Tech Decisions (Open) + +- [ ] 3D engine: Three.js vs Babylon.js +- [ ] Blog: Hugo vs hand-rolled static generator +- [ ] Hosting: self-hosted Nginx/Caddy vs static CDN +- [ ] Timmy's 3D character design + +## Philosophy + +Two doors. No navbar. No sidebar. No footer links. You walk in, you choose a room. +The Workshop is alive. The Scrolls are permanent. That's the Tower. diff --git a/blog/feed.xml b/blog/feed.xml new file mode 100644 index 0000000..1e43dfa --- /dev/null +++ b/blog/feed.xml @@ -0,0 +1,13 @@ + + + The Scrolls — Alexander Whitestone + + + https://alexanderwhitestone.com/blog/ + 2026-03-19T01:32:41Z + + Alexander Whitestone + + Words from the Wizard's Tower + + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 0000000..4967523 --- /dev/null +++ b/blog/index.html @@ -0,0 +1,40 @@ + + + + + + The Scrolls — The Wizard's Tower + + + + +
+

The Scrolls

+ +
+
+ +
+ + \ No newline at end of file diff --git a/blog/posts/.gitkeep b/blog/posts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..c7268f4 --- /dev/null +++ b/index.html @@ -0,0 +1,83 @@ + + + + + + The Wizard's Tower + + + +

The Wizard's Tower

+ + + diff --git a/scripts/aw-post b/scripts/aw-post new file mode 100755 index 0000000..6611123 --- /dev/null +++ b/scripts/aw-post @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# aw-post — Quick-post a scroll from the command line +# +# Usage: +# aw-post "Title of the Post" +# aw-post "Title" < body.md +# echo "Post body here" | aw-post "Title" +# +# Creates a new markdown file in blog/posts/ with frontmatter. +# Rebuilds the blog index and RSS feed. + +set -euo pipefail + +BLOG_DIR="$(cd "$(dirname "$0")/.." && pwd)/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) +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-') +FILENAME="${DATE}-${SLUG}.md" +FILEPATH="${BLOG_DIR}/${FILENAME}" + +if [ -f "$FILEPATH" ]; then + echo "Error: $FILEPATH already exists" + exit 1 +fi + +# Read body from stdin if available +BODY="" +if [ ! -t 0 ]; then + BODY=$(cat) +fi + +cat > "$FILEPATH" << EOF +--- +title: "${TITLE}" +date: ${DATE} +--- + +${BODY} +EOF + +echo "Created: ${FILEPATH}" +echo "Next: rebuild with 'make build'" diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 0000000..5b3ef0c --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Build script for The Scrolls — generates blog index and RSS feed from +markdown posts in blog/posts/. + +Each post is a markdown file with YAML frontmatter: + --- + title: "Post Title" + date: 2026-03-18 + --- + Body content here. + +Generates: + - blog/index.html with post listing + - blog/feed.xml Atom feed +""" + +import os +import re +from datetime import datetime, timezone +from pathlib import Path + +SITE_URL = "https://alexanderwhitestone.com" +BLOG_DIR = Path(__file__).parent.parent / "blog" +POSTS_DIR = BLOG_DIR / "posts" + + +def parse_frontmatter(text): + """Extract YAML frontmatter from markdown text.""" + match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", text, re.DOTALL) + if not match: + return {}, text + + meta = {} + for line in match.group(1).strip().split("\n"): + if ":" in line: + key, val = line.split(":", 1) + meta[key.strip()] = val.strip().strip('"').strip("'") + return meta, match.group(2).strip() + + +def load_posts(): + """Load and sort all posts by date (newest first).""" + posts = [] + if not POSTS_DIR.exists(): + return posts + + for path in sorted(POSTS_DIR.glob("*.md"), reverse=True): + text = path.read_text() + meta, body = parse_frontmatter(text) + if meta.get("title") and meta.get("date"): + posts.append( + { + "title": meta["title"], + "date": meta["date"], + "slug": path.stem, + "body": body, + "path": path, + } + ) + return posts + + +def generate_index(posts): + """Generate blog/index.html with post listing.""" + if not posts: + print(" No posts found. Keeping placeholder index.") + return + + items = [] + for p in posts: + items.append( + f'
  • \n' + f' {p["date"]}\n' + f' {p["title"]}\n' + f"
  • " + ) + + post_list = "\n".join(items) + + html = f""" + + + + + The Scrolls — The Wizard's Tower + + + + +
    +

    The Scrolls

    + +
    +
    + +
    + +""" + + (BLOG_DIR / "index.html").write_text(html) + print(f" Generated index with {len(posts)} post(s).") + + +def generate_feed(posts): + """Generate blog/feed.xml Atom feed.""" + now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + entries = [] + for p in posts[:20]: # Cap at 20 entries + # Simple HTML conversion: wrap paragraphs + html_body = "\n".join( + f"

    {para}

    " for para in p["body"].split("\n\n") if para.strip() + ) + entries.append( + f""" + {p["title"]} + + {SITE_URL}/blog/posts/{p["slug"]} + {p["date"]}T00:00:00Z + + """ + ) + + entry_block = "\n".join(entries) + + feed = f""" + + The Scrolls — Alexander Whitestone + + + {SITE_URL}/blog/ + {now} + + Alexander Whitestone + + Words from the Wizard's Tower +{entry_block} +""" + + (BLOG_DIR / "feed.xml").write_text(feed) + print(f" Generated feed with {len(entries)} entry/entries.") + + +def main(): + print("Building The Scrolls...") + posts = load_posts() + generate_index(posts) + generate_feed(posts) + print("Build complete.") + + +if __name__ == "__main__": + main() diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/world/assets/.gitkeep b/world/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/world/index.html b/world/index.html new file mode 100644 index 0000000..0428a42 --- /dev/null +++ b/world/index.html @@ -0,0 +1,37 @@ + + + + + + The Workshop — The Wizard's Tower + + + +
    +
    +

    The Workshop

    +

    Timmy's world is being built.

    +

    ← Back to the Tower

    +
    +
    + + + + diff --git a/world/main.js b/world/main.js new file mode 100644 index 0000000..ae518fd --- /dev/null +++ b/world/main.js @@ -0,0 +1,20 @@ +/** + * The Workshop — Three.js scene bootstrap + * + * This file will initialize the 3D world where Timmy lives. + * Currently a placeholder until tech decisions are made: + * - 3D engine confirmed (Three.js vs Babylon.js) + * - Character design direction chosen + * - WebSocket bridge to Timmy's soul designed (#243) + * + * See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema) + */ + +// Future: import * as THREE from 'three'; + +export function initWorkshop(container) { + // TODO: Initialize 3D scene + // TODO: Load wizard character model + // TODO: Connect to Timmy presence WebSocket + console.log('[Workshop] Scene container ready:', container.id); +}