Portfolio & Blog — Astro Redesign
A complete rewrite of my personal portfolio and blog using Astro 5, replacing the original Hugo site with a modern component architecture, TypeScript data files, View Transitions, and Shiki syntax highlighting.
Why Rewrite?
The original site was built with Hugo — fast and flexible, but template logic lives in .html files with Go syntax, styles were loaded via CDN Tailwind, and content structure was managed through YAML data files. The Astro rewrite replaces all of that with a proper component model, first-class TypeScript, and a significantly improved developer experience.
Stack
- Astro 5 — islands architecture, content collections, static output
- Tailwind CSS v4 — CSS-first configuration via
@themedirectives, notailwind.config.*file - Shiki — dual-theme syntax highlighting (
github-light/github-dark) switching with the theme toggle - astro-icon — Lucide icon set, tree-shaken at build time
- Geist Sans + Geist Mono — variable fonts via
@fontsource(self-hosted, no Google Fonts) - View Transitions — client-side navigation with the browser’s native View Transitions API
@astrojs/sitemap+@astrojs/rss— sitemap and RSS feed generated at build
Architecture
Content Collections
Blog posts and project pages live in src/content/ as Markdown files with Zod-validated frontmatter. The schema is defined in src/content.config.ts and enforced at build time — missing or wrongly typed fields fail the build rather than silently producing broken pages.
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
description: z.string(),
categories: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
Two Data Systems
Structured CV and project data lives in src/data/ as TypeScript files (experience.ts, skills.ts, projects.ts, etc.) rather than YAML. This gives full type safety and IDE autocomplete, and removes a build step — TypeScript is the source of truth.
The projects listing page merges both systems: src/data/projects.ts provides the card grid data, while getCollection('projects') is checked to see which entries have full detail pages.
Component Architecture
Every UI section is a .astro single-file component. Page-level scripts use <script is:inline> with a named init function registered for both immediate execution and astro:after-swap to survive View Transitions:
function initFeature() { /* ... */ }
initFeature();
document.addEventListener('astro:after-swap', initFeature);
Theming
Dark mode is the default (class="dark" on <html>). Light mode is applied by swapping to class="light". Color tokens are CSS custom properties under @theme in global.css, overridden in the .light selector. A flash-prevention inline script in <head> reads localStorage and sets the class before paint.
Key Features
Blog — Post listing with client-side search and category filtering. Individual post pages include a sidebar with recent posts, highlighted posts, and related posts; a copy-code button is injected into every code block via BlogPostLayout.astro.
CV — Tabbed sections (About, Experience, Education, Skills, Projects) driven by TypeScript data files. Print styles expand all tabs so the full CV is visible when printing or saving as PDF.
Projects — Card grid from src/data/projects.ts with optional detail pages from content collections. Cards show tech stack badges and link to GitHub/live site or an internal detail page.
SEO — JSON-LD structured data for Person, WebSite, and BlogPosting schemas. OpenGraph, Twitter Card, and geo meta tags via BaseLayout.astro. RSS feed at /rss.xml and sitemap at /sitemap-index.xml.
OG Image Generation — Each blog post gets a unique Open Graph image generated at build time via Satori + Sharp. A src/pages/og/[slug].png.ts static endpoint renders a 1200×630 branded card per post — matching the site’s dark color palette, using the self-hosted Geist Sans font — and outputs it as a PNG. No external services, no runtime dependencies.
Deployment
GitHub Actions builds on push to develop with npm run build and deploys dist/ to the gh-pages branch via peaceiris/actions-gh-pages. No server — fully static output to GitHub Pages.