01 — 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.
That model worked well while the website was primarily a portfolio. Then the site began to grow.
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.
The problem was no longer visual. The interface could continue growing, but the content model could not. Every new long-term feature increased the risk of turning one presentation layer into the source of truth for the entire site.
Keep the expressive interface. Rebuild the system underneath it.
The homepage remained the identity layer.
Articles and projects became structured content.
Astro became responsible for routing, content generation, metadata, and production builds.
Git became the history of the system.
The objective was not to replace the original site with a framework. It was to separate the parts that needed to evolve independently. The result is a hybrid system: an expressive shell on top of a structured core.
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
02 — The Architecture
Static by design, not by limitation.
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
MDX stores long-form articles and project records. TypeScript supports the configuration and content tooling around the build. The original homepage keeps the custom frontend behavior that gives it its identity.
This boundary is deliberate.
The framework does not need to own every interaction in order to own the architecture.
The system can be understood as a sequence of transformations:
AUTHORING
Astro pages · MDX · 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 This is not a zero-JavaScript website. JavaScript remains where interaction, navigation, motion, and interface behavior justify it. The distinction is more important:
JavaScript is a deliberate layer, not the default answer to every page.
The public system remains mostly static, while the expressive parts of the frontend are allowed to remain expressive.
03 — Content as a System
A new article should be a content file, not homepage surgery.
One of the most important architectural changes was separating content from presentation.
Articles and projects are no longer treated as permanent fragments inside a large homepage. They exist as independent Markdown and MDX documents inside structured collections.
DIRECTORY STRUCTURE
src/content/
├── config.ts
├── articles/ (8 files)
└── projects/ (4 files)
Each content type follows a strict Zod schema defined in config.ts.
ARTICLE SCHEMA
title: z.string()
slug: z.string().optional()
date: z.date()
updated: z.date().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()
PROJECT SCHEMA
title: z.string()
slug: z.string().optional()
date: z.date()
status: z.enum(["active", "archived", "learning", "open-source", "security"])
type: z.string()
featured: z.boolean().default(false)
priority: z.number().default(99)
stack: z.array(z.string()).default([])
github: z.string().url().optional()
demo: z.string().url().optional()
excerpt: z.string()
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:
MDX 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.
CONTENT decides what exists. LAYOUT decides how it is presented. BUILD decides how it becomes a page.
04 — 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 build
↓
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. The pipeline does not become more professional by having more stages. It becomes more useful when each stage has a clear purpose.
05 — 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 is intentionally expressive. It uses a dark visual system, terminal-inspired details, controlled motion, custom interactions, and a stronger sense of atmosphere. It is designed to be remembered.
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
It is not one interface repeated everywhere. It is one design language with different levels of expression.
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
The page architecture follows another important rule:
Desktop and mobile are not scaled versions of the same composition.
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.
06 — What Broke Along the Way
Real migration logs, not hypothetical.
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
Migrate site to Astro content system Moved from static HTML to Astro content collections, Zod schemas, and generated routes.
Add GitHub Pages deployment workflow Established automated CI/CD pipeline with GitHub Actions.
Restore premium homepage with content-system routes Preserved the original cinematic homepage through the new Astro architecture.
Serve original premium homepage through Astro Homepage served via Fragment set:html to maintain all original interactions.
fix: resolve astro build syntax errors Build parser caught unescaped backticks that silently existed in source.
feat: improve mobile button layout Changed email button from mailto to clipboard copy to prevent browser freeze.
feat(system): overhaul architecture page Ported cursor glow, particle canvas, 3D tilt, and reveal effects from homepage.
07 — 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.
The goal is not to claim that risk disappears. The goal is to avoid operating public systems that the site does not need.
Static-first is a reduction strategy, not a security guarantee.
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
These absences are not slogans. They are architectural decisions. Every missing runtime component is one less public system that needs credentials, patches, authentication logic, session handling, operational monitoring, and incident response.
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 MDX 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.
THE PRINCIPLE
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. MDX 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.