Back to writing

learning note

What I learnt migrating my portfolio from Gatsby to Astro

Notes from rebuilding my portfolio on Astro: why static-first won, what content collections replace, and the traps I hit along the way.

My portfolio started life as a Gatsby site built from a template. It served me well for years, but every time I came back to it the gap between what the site needed and what the stack demanded had widened: a React runtime shipped to render what is essentially a text document, a GraphQL layer between me and my own markdown, and a plugin ecosystem that seemed to need care and feeding every time I ran npm install.

Rebuilding on Astro taught me a few things I wish I had known earlier.

The site is a document, not an app

The single biggest mental shift was admitting that my portfolio has almost no interactivity. A contact form, a scroll-reveal animation — that is the whole JavaScript budget. Astro’s model of shipping zero JavaScript by default, and opting in per component only where interactivity lives, matches that reality exactly. Gatsby’s model — hydrate a React tree everywhere and work backwards — never did.

The numbers make the point better than the argument does. The rebuilt site ships HTML, CSS, a font subset, and one small script for the reveal animations. There is no framework runtime at all.

You do not need a data layer for six markdown files

Gatsby routes everything through GraphQL, which is impressive engineering and complete overkill for a personal site. Astro replaces it with typed data: plain TypeScript objects for structured content, and content collections with a zod schema for markdown. If I typo a date in frontmatter, the build fails with an error that points at the exact file and field. That is the entire feature, and it is the right size.

const writing = defineCollection({
  loader: glob({ pattern: "**/[^_]*.md", base: "./src/content/writing" }),
  schema: z.object({
    title: z.string().max(80),
    publishedDate: z.coerce.date(),
    draft: z.boolean().default(false),
  }),
});

Templates age faster than sites

The old site was a customised template, and the template’s decisions outlived their welcome: gradient buttons, clip-path section dividers, centred text everywhere. Rebuilding forced me to make my own decisions — a token system with CSS custom properties, a real dark scheme driven by prefers-color-scheme, and typography chosen on purpose rather than inherited.

The lesson is not “never use a template”. It is that a template is a loan, and you repay it the first time you want the site to say something specific about you.

Small traps I hit

  • Node versions matter. Astro pins a minimum Node version and will refuse to start on anything older. Worth encoding in engines so the failure is loud and early.
  • Image optimisation is built in, but only if you use it. astro:assets wants images imported from src/, not referenced from public/. The difference is responsive srcset and modern formats for free.
  • Forced dark mode is real. If a site does not declare color-scheme, Chrome may invert it — and a design that was never tested dark can end up with black text on black inputs. Declaring the scheme and designing both modes fixed a bug I did not know I had.

Was it worth it?

Yes — but not mainly for the performance, which was the headline reason I started. The real value was that the rebuild made the site cheap to change. Adding this writing section was a schema, two pages, and an RSS endpoint, because the foundations are plain markdown, typed data, and HTML. That is the property I want in every codebase I touch, and it took rebuilding my own site to see it clearly.