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 === trueto appear under/projects+ sitemap.fuschronicle.daterequired- 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 aPublishedtype or date presence).project task: by project (only if backlog page enabled).
- Normalize:
- Compute
slugs (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
projectchanged → rebuild:/projects/{slug}, listing/projects,/sitemap.xml,/feed.xml(if needed)- If “timeline/backlog” exist → rebuild those too
- If
chroniclechanged → rebuild its page,/fuschronicleslisting, any related project/timeline - If
taskchanged → 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:imagepointing 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
fuschronicleentries linked to this project. - “Metrics” (if tasks exist): total tasks, total actual time, first/last date, etc.
- Links:
/timeline,/backlogif 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_timeto avoid re‑rendering unchanged pages during full builds. - Filtering:
- Projects:
is_public == true - Chronicles: ensure
dateexists;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-8Cache-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&/backlogif public. lastmodfrom sourcelast_edited_time(or render time).
- Include:
robots.txt:
User-agent: * Allow: / Sitemap: https://fuscripts.com/sitemap.xmlfeed.xml (optional): most recent
fuschronicleentries.
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}.pngand 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
fetchis great for image downloads.
“What regenerates when X changes?”
- Project row: its page,
/projectslisting,/sitemap.xml,/feed.xml(if project posts in feed), plus/timeline&/backlogif enabled. - Project task: the parent project page (metrics),
/backlog,/projects(if card shows metrics). - Chronicle entry: its page,
/fuschronicleslisting, related project/timeline,/sitemap.xml,/feed.xml. - About page:
/about(and/if it surfaces about content).