· 1 min read · #astro #vue #migration

Migrating my portfolio to Astro

[Astro] [TypeScript]

The old version of this site was a Vue SPA: a single empty div, a router with one route, and a bundle of JavaScript whose only real job was to render text that never changes. It worked, but every visit paid for a framework it did not need, and publishing anything meant writing Vue components by hand.

What changed

The rebuild runs on Astro v7 and stays fully static:

  • the home page is the same design, ported to plain CSS without any client framework;
  • blog posts are plain markdown files in the repo;
  • astro build outputs HTML that any static host can serve as-is.

Astro ships zero JavaScript by default, which fits a portfolio perfectly. The only scripts left on the page are the ones used for design purposes: the cursor-following gradient and the fullscreen menu toggle.

The content pipeline

Posts live in src/content/blog/ with a YYYY-MM-DD-slug.md naming convention, so sorting by filename is sorting by date. Every file is validated at build time by a Zod schema:

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/[^_]*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    stack: z.array(z.string()).default([]),
    repo: z.string().url().optional(),
    demo: z.string().url().optional(),
    draft: z.boolean().default(false)
  })
});

export const collections = { blog };

Each post is automatically turned into a static page at build time using getStaticPaths. The date is removed from the URL to keep links clean, and code snippets are automatically highlighted with Shiki