System design

Content records, frontend components and the static delivery path for tzheng.dev.

The homepage keeps its custom visual presentation. Articles and project cases use typed records and stable routes.

This reference covers the current implementation, its delivery boundaries and the earlier migration history.

Astro 7.3.5 TypeScript Markdown Git CI/CD Static Delivery

From Interface to System

The interface could grow. The content model could not.

tzheng.dev began as a highly customized personal homepage.

The original architecture was built around visual identity: custom HTML, CSS, JavaScript, motion, terminal-inspired interactions, project presentations, and a deliberately cinematic first impression.

Projects needed independent records. Articles needed stable routes, metadata, categories, and long-form layouts.

Search engines needed canonical URLs, structured identity signals, sitemap output, and pages that could exist independently from the homepage.

Separate summaries inside the homepage could fall behind their detail pages. Shared records remove that second place to maintain the same information.

Keep the expressive interface. Rebuild the system underneath it.

The homepage, content records and delivery process can now change independently. The homepage selects four cases; the complete project index exposes every case record.

Before
  • Custom homepage
  • Hardcoded content
  • Direct page edits
  • Presentation owns structure
  • Source and published output are nearly the same thing
→
After
  • Identity layer
  • Structured content
  • Generated routes
  • Validated content model
  • Explicit build system
  • Deployable artifact

The Architecture

Prebuilt pages for public content.

Most content on tzheng.dev is public and read-heavy.

Visitors do not need accounts to read a project. An article does not need a database query on every request. A portfolio does not need an authenticated application runtime merely because such a stack is available.

That made static generation the natural default. Astro owns the structural layer of the site.

Astro owns the structural layer:

routing content collections shared layouts metadata RSS generation sitemap generation production builds

Markdown stores the articles and project records. TypeScript checks the schemas and page code. Local CSS assets replace the browser-time utility CSS generator, and a bundled script handles homepage interactions.

Astro generates the pages; browser JavaScript handles the interactive parts.

The system can be understood as a sequence of transformations:

Authoring

Astro pages, Markdown, HTML/CSS/JS
→

Source of truth

Git repository, GitHub
→

CI

GitHub Actions, Node 24, npm ci
→

Build

Astro, npm run build
→

Artifact

dist/
→

Hosting

GitHub Pages
→

Edge

Cloudflare
→

Visitor

Browser

JavaScript handles the terminal animation, navigation, project carousel and optional motion. Article content is present in the initial HTML response.

Articles render as HTML. The homepage script adds motion and navigation.

Content as a System

One content record, several page views.

Homepage cards, indexes and detail routes read typed collection records.

Articles and projects are no longer treated as permanent fragments inside a large homepage. They exist as independent Markdown documents inside structured collections.

Directory structure

src/
├── content.config.ts
└── content/
    ├── articles/   (30 records)
    └── projects/   (11 records)

Each content type follows a strict Zod schema defined in src/content.config.ts.

Article schema

title: z.string()
slug: z.string().optional()
date: z.coerce.date()
updated: z.coerce.date().optional()
project: z.string().optional()
seriesMonth: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).optional()
homepageOrder: z.number().int().positive().optional()
excerpt: z.string()
tags: z.array(z.string()).default([])
category: z.string()
featured: z.boolean().default(false)
status: z.enum(["draft", "published", "archived"]).default("published")
readingTime: z.string().optional()  // estimated from the body when absent

Project schema

title: z.string()
slug: z.string().optional()
date: z.coerce.date()
status: z.enum(["active", "archived", "learning", "open-source", "security"])
type: z.string()
featured: z.boolean().default(false)
priority: z.number().default(99)
homepageOrder: z.number().int().positive().optional()
homepageFacts: z.array(z.object({
  label: z.string(), text: z.string()
})).default([])
stack: z.array(z.string()).default([])
github: z.url().optional()
demo: z.url().optional()
excerpt: z.string()
outcome: z.string().optional()
View content schema ↗

The content model creates a boundary between what exists and how it is presented. The entire transformation pipeline ensures content is validated before it ever reaches a layout:

Markdown FILE
↓
Schema validation
↓
Content collection
↓
Generated route
↓
Shared layout
↓
Static page

This makes content changes predictable. A malformed content record can be detected before publication instead of silently becoming part of a page. A new article does not require editing the homepage. A project can change its metadata without rewriting its layout.

The article archive groups original notes and companion guides into monthly reading series. Publication dates remain separate and are shared by detail pages, RSS and structured metadata. Search and category filtering run locally in the browser; reading the archive requires no JavaScript or database.

From Commit to Production

The repository is the source. dist/ is the product.

Every meaningful change begins in Git. Code, content, configuration, metadata, and interface changes enter the same version-controlled history. That makes changes reviewable, traceable, and reversible.

But moving from directly publishable files to Astro introduced an important new boundary. The repository itself is no longer the finished website. It is source code. The production website has to be built.

Push to main
↓
Github actions
↓
Checkout (actions/checkout@v4)
↓
Node 24 (actions/setup-node@v4)
↓
npm ci
↓
npm run check → npm run build → npm test
↓
dist/
↓
Upload pages artifact
↓
Deploy to github pages
↓
Cloudflare edge
↓
Visitor
View deployment workflow ↗

A commit can succeed while the build fails. A build can succeed while the wrong artifact is deployed. A deployment can succeed while a route or client interaction is still broken.

Treating source, build, artifact, and deployment as separate failure boundaries makes issues easier to locate. Type and content checks precede the build. Generated-page checks run before the artifact is uploaded.

The Interface Layer

Preserve identity without letting the interface own the architecture.

The website has two different jobs. The first is to create identity. The second is to carry information. Those jobs do not need the same visual intensity.

The homepage retains its dark palette, Earth background, terminal and custom motion. Its sections are native Astro components; project and article cards read the same records as their detail pages.

Articles, project case studies, and system pages have a different responsibility. They need to remain readable for longer periods. They need predictable spacing. They need stable typography. They need layouts that survive different amounts of content.

Identity layer

cinematic, interactive, motion-aware, brand-heavy, terminal-inspired

↕

Content layer

structured, readable, predictable, content-driven, schema-validated

Shared interface patterns create continuity between those layers: navigation, metadata, chips, typography hierarchy, spacing rules, cards, footer signals, and the interaction language.

The design system is deliberately lightweight. The project does not introduce an external UI framework simply to prove that one could be used. Custom CSS remains appropriate where the visual system is specific to the site. Astro provides structure where structure is needed.

Responsive transformation

Desktop and mobile are not scaled versions of the same composition.
Desktop
  • Persistent Story Rail
  • Horizontal Architecture Flow
  • Wide Comparison Layouts
  • Hover-aware Enhancements
  • Persistent Context
Mobile
  • Sticky Chapter Bar
  • Vertical System Flow
  • Stacked Comparisons
  • Touch-first Interaction
  • Prioritized Reading Order

Desktop layouts can use horizontal relationships, persistent navigation, architecture maps, and wider comparisons. Mobile layouts need a different reading order. Complex horizontal diagrams become vertical flows. Persistent side navigation becomes a compact chapter bar.

Hover states cannot carry essential information. Touch targets, readable measure, spacing, and content priority become more important than preserving the exact desktop composition. Responsive design is treated as a transformation problem, not a shrinking problem.

Motion follows the same logic. Animation can explain hierarchy, state, or movement through the system. It should not delay access to information. Where the operating system requests reduced motion, the interface respects that preference by disabling particle canvases, cursor tracking, and scroll-reveals.

What Broke Along the Way

Migration decisions and recorded commits.

The difficult part was not choosing a framework. It was deciding what to preserve, what to rebuild, and where the boundaries actually belonged.

Challenge 01 — Migrating Without Flattening the Interface

Problem
The content model needed to change without destroying the original visual identity.
Decision
Preserve the stable visual layer and rebuild the architecture underneath it.
Result
The site gained structured content and generated routes without becoming a generic framework rewrite.

Challenge 02 — Changing the Deployment Boundary

Problem
After the Astro migration, source files were no longer equivalent to the finished website.
Decision
Make dist/ the explicit deployable artifact. Never deploy source directly.
Result
Source, build, artifact, deployment, and delivery became separate failure boundaries.

Challenge 03 — Build Systems Expose Parser Boundaries

Problem
Source that looked harmless as page copy broke the Astro production build when unescaped syntax crossed parser boundaries.
Decision
Treat npm run build as part of development, not as the final publishing button.
Result
Production parser failures became pre-deployment feedback instead of deployment surprises.

Challenge 04 — Mobile Is Not Desktop at a Smaller Size

Problem
Desktop interactions did not always translate correctly to touch devices and narrower layouts.
Decision
Change interaction behavior where necessary instead of protecting implementation uniformity.
Result
Navigation, contact behavior, layout, and interactions became device-aware.

Migration log, July 2026

2026-07-03 9c1d3a2
Migrate site to Astro content system
Moved from static HTML to Astro content collections, Zod schemas, and generated routes.
2026-07-03 b806590
Add GitHub Pages deployment workflow
Established automated CI/CD pipeline with GitHub Actions.
2026-07-03 829e7b9
Restore premium homepage with content-system routes
Preserved the original cinematic homepage through the new Astro architecture.
2026-07-03 4eb4eff
Serve original premium homepage through Astro
Homepage served via Fragment set:html to maintain all original interactions.
2026-07-06 dd321af
fix: resolve astro build syntax errors
Build parser caught unescaped backticks that silently existed in source.
2026-07-06 440d761
feat: improve mobile button layout
Changed email button from mailto to clipboard copy to prevent browser freeze.
2026-07-06 caa226d
feat(system): overhaul architecture page
Ported cursor glow, particle canvas, 3D tilt, and reveal effects from homepage.

Security Through Less Surface

Security begins by removing infrastructure the system does not need.

Static architecture does not mean zero attack surface.

The system still depends on account security, repository permissions, the dependency supply chain, CI/CD permissions, build integrity, hosting configuration, DNS, edge infrastructure, and client-side code.

The security decision is narrower and more honest: Do not create a public runtime surface without a reason.

Static output reduces runtime responsibilities. Build and account security still matter.

Trust surface

Identity & access

  • GitHub Account
  • Repository Permissions
  • Branch / Deployment Permissions

Supply chain

  • package.json
  • package-lock.json
  • npm Dependencies
  • GitHub Actions
  • Third-party Actions

Build & deployment

  • GitHub Actions Workflow
  • Scoped Permissions
  • Build Artifact
  • GitHub Pages

Delivery

  • DNS Configuration
  • Cloudflare Edge
  • TLS / HTTPS

Client

  • Browser JavaScript
  • DOM Interactions
  • External Links

Ci/cd boundaries

The deployment workflow requests only the permissions required for its real job. It explicitly scopes the permissions required for GitHub Pages deployment instead of relying on unrestricted repository write access:

Source (Repository)
↓
Build identity (GitHub Actions)
↓
Scoped permissions
  • contents: read
  • pages: write
  • id-token: write
↓
Artifact (dist/)
↓
Deployment (GitHub Pages)
View deployment workflow ↗

What the system intentionally avoids

  • No runtime content database
  • No public admin login
  • No server-side user sessions
  • No public user account system
  • No comment backend
  • No intentional advertising trackers

The current reading and contact flows do not require a backend. A future authenticated feature would need a separate assessment of credentials, sessions and operational responsibility.

Residual risk

What static architecture does not solve

  • Account takeover
  • Repository compromise
  • Malicious dependency updates
  • CI/CD misconfiguration
  • DNS compromise
  • Edge configuration mistakes
  • Client-side vulnerabilities
  • Third-party resource risk

A mature security posture should describe its residual risk rather than hide it. A compromised source repository, a malicious dependency, an unsafe workflow change, a DNS mistake, or vulnerable client-side code remain meaningful risks. Security is not one feature. It is the collection of trust boundaries the system chooses to keep.

Trade-offs

What the system gains

  • Static delivery
  • Version-controlled content
  • Build-time validation
  • Reproducible deployment
  • Portable Markdown files
  • Simple rollback
  • Smaller public runtime surface

What the system gives up

  • No live database editor
  • No public user system
  • No server-side comments
  • A build step for every release
  • More Git-oriented publishing
  • Less runtime flexibility

Every simplification has a cost. The absence of a backend reduces the number of public systems that need to be operated. It also means publishing remains more closely tied to Git and the build pipeline. That trade-off is intentional today.

The hybrid homepage creates another trade-off. Keeping custom global interactions preserves the visual identity of the site. It also means that part of the frontend is less componentized than the newer content system. That is accepted.

A cleaner abstraction is not automatically a better engineering decision if creating it destroys something that already works.

Complexity has to earn its place.

tzheng.dev is not designed to use the largest possible stack. It is designed so that every major layer has a reason to exist.

Astro provides structure. Markdown provides portable content. TypeScript strengthens contracts around the system. Git provides history and rollback. Automation turns source into a reproducible artifact. GitHub Pages serves the generated site. The edge layer supports public delivery. Custom frontend code remains where expression matters.

Add structure where growth requires it.

Preserve what already works.

Make failure boundaries visible.

Add complexity only when the problem justifies it.

Haitao Zheng

EN DE