Blog
Bye Bye Jekyll, Hello Astro
I’ve been running tecnobrat.com for a long time, and the site has worn a lot of hats: Octopress, Jekyll, a theme, custom plugins, and enough “just make it work” glue to qualify as archaeology.
It worked — until it didn’t.
This post is the write-up of migrating the site from the old Jekyll site to the current Astro version you’re reading now.
What I was running before
The old site was a Jekyll build with:
- A date-based permalink structure (
/:year/:month/:day/:title.html) - The Minimal Mistakes theme
- Taxonomy pages (categories/tags)
- A comment system configured via Disqus
- A custom photography pipeline powered by a Ruby plugin and ImageMagick
The photography piece is worth calling out because it’s a great example of why I moved on: the Jekyll repo includes a bespoke gallery generator (_plugins/jekyll-art-gallery-generator.rb) that:
- Walks a
_photography/folder - Reads config from
_data/gallery.yml - Generates thumbnails and “best image” variants
- Strips EXIF (and optionally watermarks/resize-limits) using
rmagick
That’s a lot of power… and a lot of native dependency surface area.
The Gemfile also tells a story: it pins Jekyll ~> 4.4.0, pulls in rmagick, and even pins google-protobuf because newer builds wouldn’t compile cleanly on Fedora at the time. That’s exactly the kind of “works on my machine, please don’t touch it” friction I was ready to retire.
My constraints for the migration
I didn’t want a “new app”. I wanted the same site, with a better build.
- Static-only output (no server runtime)
- S3 + CloudFront hosting stays the same
- Keep URLs working (old blog permalinks should redirect)
- Keep content simple (Markdown-first, with the option of MDX when it’s useful)
- Prefer strict typing for site data and metadata
The big shift: themes → typed content + components
Jekyll is a wonderful tool, but it encourages an ecosystem of theme conventions, Liquid templates, and plugin hooks.
With Astro, the “unit of composition” becomes components + data, with static rendering by default. Instead of discovering behavior via theme docs, I wanted a repo where the behavior is visible in TypeScript and .astro files.
In this codebase:
- Content lives in collections under
src/content/ - Layouts/components are explicit (
src/layouts/BaseLayout.astro,src/components/*) - Metadata is typed with Zod in
src/content/config.ts
That last point is a quiet win: in the Astro site, blog and page entries share a common shape (title, optional description, optional header), and blog posts add date + tags. That makes it hard to accidentally ship malformed frontmatter.
Blog: keep Markdown, modernize the scaffolding
The blog is still Markdown/MDX — but the scaffolding is now “Astro-native”:
src/pages/blog/[slug].astrorenders a post from theblogcontent collection- Dates are formatted via
src/utils/dateFormat.ts - Tags are displayed inline on the post page
- Blog listing and pagination are handled by the Astro pages under
src/pages/blog/
I also added a nice little quality-of-life improvement: excerpts.
There’s a remark plugin (src/utils/remark-excerpt.ts) that generates a ~200 character excerpt from paragraph text (skipping code blocks and MDX imports/JSX). In the UI, excerpt is used for blog previews (and social/SEO description text); if it isn’t provided, a reasonable default is auto-generated (src/utils/excerpts.ts).
In Jekyll, I used an explicit excerpt_separator (<!--more-->). In Astro, I like having both options:
- Write an explicit
excerptwhen I care about it - Otherwise let the build generate a reasonable default
Separately, description is reserved for the hero/header copy on the post itself.
Photography: Ruby pipeline → Astro assets
This was the most satisfying refactor.
In the Jekyll version, the gallery generator did a ton of image work at build time, and the source of truth was _data/gallery.yml plus filesystem discovery.
In the Astro version, the approach is:
- Put photos in
src/assets/photos/<gallery>/<file>.jpg - Define the “gallery index” metadata in the Photography page content
- Use
import.meta.glob()to load the photos for a gallery page - Use
astro:assetsto optimize images for display, and generate full-size versions for the lightbox
The key bit is in src/pages/photography/[slug].astro: it globs /src/assets/photos/*/*.{png,jpg,jpeg,webp} and filters by gallery slug.
Instead of pre-generating thumbnails on disk, Astro’s pipeline handles the resized images and caching as part of the build.
Net effect:
- No Ruby native deps
- No custom plugin execution
- Much simpler mental model (files + a small amount of typed metadata)
CI/CD: PR previews and static deploys
The new repo also formalizes how the site is shipped.
There are three workflows:
-
PR preview deploy (
.github/workflows/ci.yml):- Runs
npm ci - Runs
npm run check(type-check + lint + formatting check) - Builds the site with a PR-scoped base path
- Syncs
dist/to a PR-scoped prefix in the same S3 bucket - Invalidates CloudFront for just that preview prefix
- Runs
-
Production deploy (
.github/workflows/deploy.yml):- On push to
main, builds and deploysdist/to the bucket root - Excludes the preview namespace so it doesn’t get wiped
- Invalidates CloudFront for
/*
- On push to
-
PR preview cleanup (
.github/workflows/cleanup.yml):- When a PR closes, removes the preview prefix from the bucket
- Invalidates CloudFront for that preview prefix
- Removes the GitHub deployment environment
The important part is what it enables: reviewing a static site change as a real deployed site before it hits production, without standing up any extra infrastructure.
Keeping old links alive (redirects)
One migration footgun is URLs.
The old site had dated permalinks like:
/2012/03/15/octopress-up-and-running.html
The new site uses blog slugs like:
/blog/2012-03-15-octopress-up-and-running/
To preserve inbound links (and my own muscle memory), I added explicit S3 website routing rules in Terraform (terraform/tecnobrat.com.tf) to redirect the known old post paths to the new ones with 301s.
I also included a couple of “broad strokes” redirects:
/categories/…→/blog/(302)/photography/main/→/photography/(302)
Not everything needs to be permanent — but the old blog post URLs absolutely do.
What I like better now
This migration wasn’t about chasing shiny things. It was about removing friction.
- Build is Node-based (no Ruby toolchain, no native gems)
- Site metadata is typed
- Layouts and components are local and explicit
- Images are handled by the framework
- Deploys are automated and reviewable
What’s next
I intentionally kept the migration lean: get the new generator in place, keep the look/feel, keep content working, preserve URLs.
Future improvements I’m considering:
- A dedicated tags page (and/or tag archives)
- Search (client-side) if it ever feels necessary
- Comments again — but only if they’re low-noise and low-maintenance
For now: the site is static, fast, and a lot easier to live with.
Leave a comment
Your comment will be pending admin review. If you're not logged in, we'll email you to confirm first.
Comments
Loading…