Fusite High-Level Design
Design Documents
Last updated August 23, 2025
Fusite High-Level Design
1) URL & Route Map (hardcoded)
/ -> Home (hand-authored template + curated content)
/about -> Static page (code)
/fuschronicles -> Listing page with filters & latest posts
/fuschronicles/{slug} -> Individual article (from Notion “fuschronicle” DB)
/projects -> Listing page (public projects only)
/projects/{slug} -> Project main page (from Notion “project” DB)
/projects/{project}/{doc_slug} -> Specific doc for a project (requirements, design, etc.)
All routes are defined in code. Notion provides content and metadata only.
2) Notion Data Model → Site Mapping
A) about me
(single page)
- Content: page blocks
- Used by:
/about
B) project
Properties (required unless marked optional):
name
(title) → Display nameslug
(text) →/projects/{slug}
description
(rich/text) → meta/OG + project card snippetstatus
(select) → “Active”, “Paused”, etc.tags
(multi-select) → listing technologies used or other keywordslogo
(file/url) → shown on cards + OGprod url
(url) → external link on project pageis_public
(checkbox) → filter for listings and sitemap- Page content → Project main page body
C) project task
(linked to project)
Properties:
task
(title)status
(select)date
(date)estimated time (min)
(number)actual time (min)
(number)link to project
(relation →project
)- Page content → Task description
- Use cases:
/projects/{slug}/backlog
(public read-only)
D) fuschronicle
Properties:
title
(title) → H1 + meta/OGdate
(date) → publish date + sorting + JSON‑LDtype
(select:change log
|work log
|other
)projects
(relation, multi) → used for/projects/{slug}/timeline
, internal linksslug
(text) →/fuschronicles/{slug}
(fallback:yyyy-mm-dd-title
)- Page content → Article body
Validation rules (build-time):
project.is_public === true
to appear under/projects
+ sitemap.fuschronicle.date
required- Ensure slugs are stable and unique; maintain a redirect map if a slug changes.
3) Build Pipeline (Bun)
Repo layout
/site
bunfig.toml
/src
/notion
client.ts # SDK init + helpers
project.ts # getters for project DB
task.ts # getters for project task DB
chronicle.ts # getters for fuschronicle DB
/render
templates.ts # HTML templates (home, listing, project, chronicle)
seo.ts # title/meta/canonical/OG/JSON-LD helpers
routes.ts # hardcoded route table + slug helpers
images.ts # rehost/optimize (to S3)
/build
build-all.ts # full site build
build-one.ts # build one route by slug & type
sitemap.ts # sitemap.xml generation
robots.ts # robots.txt
feed.ts # optional RSS for fuschronicles
deploy.ts # S3 upload + (optional) CF invalidation
/build # output (gitignored)
Steps (full build)
- Load config (env: S3 bucket, CF dist, Notion secrets, site URL).
- Fetch data:
about me
: one page.project
: all rows (filteris_public
).fuschronicle
: all rows (no draft concept—use aPublished
type or date presence).project task
: by project (only if backlog page enabled).
- Normalize:
- Compute
slug
s (prefer notion prop; else break build and throw error). - Rehost images to
s3://your-site/assets/...
and rewrite URLs.
- Compute
- Render pages:
- Static routes:
/
,/about
,/projects
,/fuschronicles
- Dynamic routes:
/projects/{slug}
/projects/{slug}/timeline
(chronicles filtered by relation)/projects/{slug}/backlog
(optional, fromproject task
)/fuschronicles/{slug}
- Static routes:
- Generate:
sitemap.xml
(only public pages)robots.txt
- Write to S3:
index.html
,/projects/*/index.html
, etc.
- Invalidate:
- Invalidate CloudFront cache
Single‑page regeneration (webhook path)
- Input:
{ kind: "project"|"chronicle"|"task"|"about", id: notionId }
- Lookup: resolve
slug
& dependencies:- If
project
changed → rebuild:/projects/{slug}
, listing/projects
,/sitemap.xml
,/feed.xml
(if needed)- If “timeline/backlog” exist → rebuild those too
- If
chronicle
changed → rebuild its page,/fuschronicles
listing, any related project/timeline
- If
task
changed → rebuild that project page +/backlog
, plus possibly/projects
(if list shows metrics) - If
about
→ rebuild/about
(and maybe home if it surfaces “about” content)
- If
- After writing outputs to S3, optionally invalidate the changed paths only.
4) Templates & Page Contracts
Common head (all pages)
<title>
(≤60 chars),<meta name="description">
(≤160 chars)<link rel="canonical" href="https://fuscripts.com/...">
- Open Graph/Twitter (title, description,
og:image
pointing to rehosted image) - Structured data:
- Home:
WebSite
- About:
Person
- Project:
CreativeWork
(withurl
,image
,inLanguage
,about
,keywords
) - Chronicle:
BlogPosting
(withdatePublished
,dateModified
,author
)
- Home:
- Preload critical CSS (inline small critical subset; rest defer).
Listings
- /projects: grid of public projects (logo, name, short description, tags, status), filters by tag/status.
- /fuschronicles: grouped by type; pagination by date; quick filters for
change logs
,work logs
,other
.
Project page
- Hero: logo, name, tags, status, prod URL.
- Body: Notion content rendered to HTML.
- “Related posts”: latest
fuschronicle
entries linked to this project. - “Metrics” (if tasks exist): total tasks, total actual time, first/last date, etc.
- Links:
/timeline
,/backlog
if enabled.
Chronicle page
- Title, date, type, related projects chips.
- Body: Notion content.
- Prev/next (by date within same type).
5) Data Access (Notion queries)
- Pagination: pull all records with cursor; store a local JSON cache keyed by Notion page id +
last_edited_time
to avoid re‑rendering unchanged pages during full builds. - Filtering:
- Projects:
is_public == true
- Chronicles: ensure
date
exists;type
∈ allowed set - Project tasks: fetch by project relation when needed
- Projects:
- Rate limits: Apply minimal concurrency.
6) Webhook & Regeneration Wiring
- API Gateway (HTTP API) → Lambda (Node):
- Validate secret/signature.
- Translate incoming Notion event to
{kind, notionId}
. - Option A: enqueue SQS
{kind, notionId}
and let a Bun worker Lambda (container) runbun run src/build-one.ts --kind=... --id=...
. - Option B: call the worker Lambda directly (no SQS) if volume is tiny.
- build-one.ts determines the impact set and renders only those pages + derived listings/sitemap/feed that changed.
7) Hosting & Caching
S3 object headers
- HTML (
.html
):Content-Type: text/html; charset=utf-8
Cache-Control: public, s-maxage=300, stale-while-revalidate=86400
- Fingerprinted CSS/JS/images:
Cache-Control: public, max-age=31536000, immutable
CloudFront
- Origin: S3 bucket (not website endpoint)
- Default root:
index.html
- Custom error pages: 404 →
/404.html
- Optional path invalidations on regen for sub‑minute freshness; otherwise rely on short TTLs.
8) Sitemap, Robots, Feed
sitemap.xml:
- Include:
/
,/about
,/projects
, each/projects/{slug}
,/fuschronicles
, each/fuschronicles/{slug}
, and optionally/projects/{slug}/timeline
&/backlog
if public. lastmod
from sourcelast_edited_time
(or render time).
- Include:
robots.txt:
User-agent: * Allow: / Sitemap: https://fuscripts.com/sitemap.xml
feed.xml (optional): most recent
fuschronicle
entries.
9) Assets & OG Images
- Rehost all Notion images to
s3://your-site/assets/...
(avoid expiring URLs). - Optional: auto-generate OG cards per page (Bun + Satori or HTML→PNG renderer); write to
s3://your-site/og/{path}.png
and reference in OG/Twitter tags.
Implementation Notes (Bun)
- Use AWS SDK v3 (
@aws-sdk/client-s3
,@aws-sdk/client-cloudfront
) and Notion SDK (@notionhq/client
). - Prefer pure ESM; Bun’s
fetch
is great for image downloads.
“What regenerates when X changes?”
- Project row: its page,
/projects
listing,/sitemap.xml
,/feed.xml
(if project posts in feed), plus/timeline
&/backlog
if enabled. - Project task: the parent project page (metrics),
/backlog
,/projects
(if card shows metrics). - Chronicle entry: its page,
/fuschronicles
listing, related project/timeline
,/sitemap.xml
,/feed.xml
. - About page:
/about
(and/
if it surfaces about content).