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.
The homepage, content records and delivery process can now change independently. The homepage selects four cases; the complete project index exposes every case record.
- Custom homepage
- Hardcoded content
- Direct page edits
- Presentation owns structure
- Source and published output are nearly the same thing
- 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:
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.
The system can be understood as a sequence of transformations:
Authoring
Astro pages, Markdown, HTML/CSS/JSSource of truth
Git repository, GitHubCI
GitHub Actions, Node 24, npm ciBuild
Astro, npm run buildArtifact
dist/Hosting
GitHub PagesEdge
CloudflareVisitor
BrowserJavaScript handles the terminal animation, navigation, project carousel and optional motion. Article content is present in the initial HTML response.
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() 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:
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.
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
- Persistent Story Rail
- Horizontal Architecture Flow
- Wide Comparison Layouts
- Hover-aware Enhancements
- Persistent Context
- 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
Challenge 02 — Changing the Deployment Boundary
dist/ the explicit deployable artifact. Never deploy source directly.Challenge 03 — Build Systems Expose Parser Boundaries
npm run build as part of development, not as the final publishing button.Challenge 04 — Mobile Is Not Desktop at a Smaller Size
Migration log, July 2026
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.
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:
- contents: read
- pages: write
- id-token: write
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.
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.