Site Architecture: from Rust WASM to Astro Full-Stack
This post is also available in Chinese — Read the 中文 version
Overview
This site is a fully static-generated (SSG) personal portal with essays, tools, and an about page. The tech stack:
| Layer | Choice |
|---|---|
| Framework | Astro 7 (static generation) |
| Interactivity | SolidJS (client hydration) |
| Styling | Tailwind CSS 4 + semantic tokens |
| Runtime | Bun (build, package management, scripting) |
| Compute | Rust → WASM |
| Type safety | TypeScript + Zod |
Monorepo Layered Architecture
Bun workspaces manage three internal packages + one app:
packages/
ui/ → design system primitives (Button, Icon, Prose...)
content/ → Zod schemas + domain types
wasm/ → Rust-compiled WASM module
apps/
i/ → Astro site
Dependencies flow strictly downward:
pages(thin routing)
└── features/*(home / essays / tools / about)
└── shared / i18n / config / lib(cross-feature)
└── packages/ui | content | wasm
Features never import from each other. All feature access goes through barrel exports (index.ts), keeping internal structure invisible to consumers.
Static Generation + i18n Routing
Astro is configured with output: "static". Every page is prerendered to pure HTML at build time. i18n uses file-based routing:
pages/
index.astro → / (default zh)
en/ → /en/ prefix
index.astro → /en/
Each page file is a 3-15 line proxy; all logic lives in features/:
---
import { AboutPage } from "@/features/about";
import { asLocale } from "@/i18n";
---
<AboutPage locale={asLocale(Astro.currentLocale)} />
The i18n dictionary uses a type-safe pattern: zh.ts defines the Messages type, which en.ts must satisfy. Missing translations are compile-time errors.
SolidJS + client:load Hydration
Ten interactive tools (JSON formatter, Base64 codec, hash calculator, QR generator, etc.) are SolidJS components hydrated via Astro’s client:load directive.
One important detail: Astro’s hydration requires literal JSX tags (<JsonFormatter client:load />); components cannot be dynamically referenced through variables or object maps. ToolPage.astro therefore keeps static branches for each tool, synchronized with the toolIds array in registry.ts.
Shared tool components (InputCard, OutputCard, CopyButton) live in features/tools/shared/, following the rule that only components used in ≥2 features graduate to packages/ui.
Rust → WASM: Hash Calculator
The hash calculator backend (SHA-256, SHA-512, MD5, SHA-1, BLAKE3) is Rust compiled to WASM.
packages/wasm/native/src/crypto.rs uses the sha2, md-5, blake3, and sha1 crates to implement five hash functions, exported via #[no_mangle]:
#[no_mangle]
pub extern "C" fn sha256(ptr: *const u8, len: usize, out: *mut u8) {
let data = unsafe { std::slice::from_raw_parts(ptr, len) };
let hash = Sha256::digest(data);
unsafe { std::ptr::copy_nonoverlapping(hash.as_ptr(), out, 32) };
}
The TypeScript side loads and wraps the WASM module via packages/wasm/src/loader.ts, exposing a type-safe WasmTools interface. The build script bun scripts/build.ts runs cargo build --target wasm32-unknown-unknown and copies the output to dist/tools.wasm.
Tailwind CSS + Semantic Tokens
Instead of dark: overrides everywhere, the site uses CSS custom property semantic tokens. All color definitions live in packages/ui/src/styles/theme.css:
:root {
--color-bg: #fff;
--color-ink: #111;
--color-accent: #2563eb;
}
.dark {
--color-bg: #111;
--color-ink: #eee;
--color-accent: #60a5fa;
}
Components use only semantic classes (bg-surface, text-ink, border-line), never raw utility colors. Theming the entire site means changing one file.
Dark Mode
Dark mode is applied via an inline <script is:inline> in <head>, preventing FOUC (flash of unstyled content):
const stored = localStorage.getItem("theme");
const dark = stored
? stored === "dark"
: matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", dark);
Theme toggling is handled by ThemeToggle.tsx (a SolidJS island), which syncs localStorage and the DOM class. The script also listens for astro:after-swap to support Astro view transitions.
Content Management
Essays use Astro Content Collections with Zod schema validation:
const postSchema = z.object({
lang: z.enum(["zh", "en"]),
title: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
sameAs: z.string().optional(),
});
All data access goes through features/essays/data.ts. Switching CMS would involve changing a single file. Card rendering, RSS feeds, and tag aggregation all call the same data functions.
Build & Deployment
bun run build # astro build → dist/ (40 pages, ~1.5s)
The output is pure static files with zero server-side JS dependency. The @/ path alias is supported by both tsconfig.json paths and Astro’s Vite alias, eliminating all ../../../../ deep relative imports.
Summary
This site may be small, but it touches several key areas of modern web development: SSG architecture, i18n routing, client hydration, WASM compute, design token systems, and content schema validation. Every layer follows a “capable but not over-engineered” philosophy, with no premature abstractions for hypothetical future needs.
Future Work
The site is currently fully static, with all content managed as Markdown files. Several key evolutions are planned for the next phase:
Database integration. Migrate essays, tags, and translation mappings from the filesystem to a database (SQLite or PostgreSQL), enabling more flexible querying and relationships. User preferences for tools (favorites, history) also require persistent storage.
Dashboard admin panel. Build a management backend with a visual essay editor, tag manager, and translation linking interface. A RESTful API layer will serve both the frontend and admin panel from a shared data layer.
Online editing and publishing workflow. Implement Markdown live preview, draft/published state toggling, and scheduled publishing within the dashboard.
Automated deployment. A webhook-based CI pipeline will automatically pull and deploy the latest frontend and backend code on every Git push.
These directions will be adopted incrementally based on actual needs, preserving architectural evolvability without premature complexity.
Series · site-build