Skip to content

Instantly share code, notes, and snippets.

@staff0rd
Created December 21, 2025 07:39
Show Gist options
  • Select an option

  • Save staff0rd/cb82785470adde557179f5649e600979 to your computer and use it in GitHub Desktop.

Select an option

Save staff0rd/cb82785470adde557179f5649e600979 to your computer and use it in GitHub Desktop.

Astro Migration Plan

Decision: Migrate from Jekyll to Astro

Why Astro:

  • Modern Content Collections API maps to your 4 collection types
  • TypeScript-first with full type safety
  • Self-contained page files (logic + template together)
  • Static output by default (same deployment as Jekyll)
  • Active community and excellent documentation

Current State

Jekyll setup:

  • 4 content collections: posts (~150), devlog (~100), notes (253), links (174)
  • 4 custom Ruby plugins generating dynamic pages
  • Liquid templates (11 layouts, 24 includes)
  • Netlify deployment with image generation pipeline

Key files to migrate:

  • _config.ymlastro.config.mjs
  • _layouts/*.htmlsrc/layouts/*.astro
  • _includes/*.htmlsrc/components/*.astro
  • _plugins/*.rb → integrated into src/pages/[param].astro files
  • Content stays in place, frontmatter mostly compatible

Migration Plan

Phase 1: Project Setup

  1. Initialize Astro project alongside Jekyll (can run both during migration)
  2. Configure astro.config.mjs:
    • Set output to static
    • Configure markdown processing (remark/rehype)
    • Set up Netlify adapter if needed
  3. Define content collection schemas in src/content/config.ts:
    • posts - blog posts with date, title, tags, categories
    • devlog - version, title, date, tags
    • notes - title, date, tags
    • links - date, tags, title

Phase 2: Layout Migration

Convert layouts to Astro components:

  • _layouts/default.htmlsrc/layouts/Layout.astro
  • _layouts/post.htmlsrc/layouts/PostLayout.astro
  • _layouts/devlog.htmlsrc/layouts/DevlogLayout.astro
  • _layouts/note.htmlsrc/pages/notes/[tag].astro (dynamic)
  • _layouts/link.htmlsrc/layouts/LinkLayout.astro
  • _layouts/links.htmlsrc/pages/links/[date].astro (dynamic)

Phase 3: Includes → Components

Convert includes to reusable components:

  • _includes/head.html → part of Layout.astro
  • _includes/header.htmlsrc/components/Header.astro
  • _includes/footer.htmlsrc/components/Footer.astro
  • _includes/link.htmlsrc/components/LinkCard.astro
  • _includes/share.htmlsrc/components/ShareButtons.astro
  • etc.

Phase 4: Dynamic Page Generation

Replace Ruby plugins with Astro's getStaticPaths():

Note tag pages (_plugins/noteGenerator.rb):

src/pages/notes/[tag].astro

Devlog tag pages (_plugins/devlogTagGenerator.rb):

src/pages/devlog/[tag].astro

Monthly link archives (_plugins/linkGenerator.rb):

src/pages/links/[date].astro

Cross-collection tags (_plugins/combineTags.rb):

  • Utility function in src/utils/tags.ts
  • Used by pages that need aggregated tags

Phase 5: Content Migration

  1. Move content directories:
    • _posts/src/content/posts/
    • _devlog/src/content/devlog/
    • _notes/src/content/notes/
    • _links/src/content/links/
  2. Validate frontmatter against schemas
  3. Test markdown rendering (kramdown → remark)
  4. Handle any kramdown-specific syntax differences

Phase 6: Static Pages

Migrate root pages:

  • index.htmlsrc/pages/index.astro
  • about.mdsrc/pages/about.astro (or keep as .md)
  • apps.htmlsrc/pages/apps.astro
  • blog/index.htmlsrc/pages/blog/index.astro
  • etc.

Phase 7: Assets & Styling

  1. Move SCSS: _sass/main.scsssrc/styles/main.scss
  2. Move static assets: assets/public/assets/
  3. Move JS files: js/public/js/ or convert to modules
  4. Update paths in templates

Phase 8: Build & Deploy

  1. Update build script:
    • Remove Docker/Ruby dependency
    • npm run build produces dist/ folder
  2. Update .github/workflows/netlify.yml:
    • Remove Ruby setup
    • Build with Node.js only
  3. Update netlify.toml:
    • Change publish directory to dist
    • Keep redirects and headers

Phase 9: Testing & Validation

  1. Compare output URLs between Jekyll and Astro
  2. Verify all dynamic pages generate correctly
  3. Test tag filtering, archives, cross-collection links
  4. Check SEO: meta tags, OG tags, Twitter cards
  5. Validate RSS feed if applicable
  6. Test image generation pipeline integration

File Structure After Migration

blog/
├── astro.config.mjs
├── package.json
├── tsconfig.json
├── src/
│   ├── content/
│   │   ├── config.ts          # Collection schemas
│   │   ├── posts/             # Blog posts
│   │   ├── devlog/            # Dev logs
│   │   ├── notes/             # Notes
│   │   └── links/             # Links
│   ├── layouts/
│   │   ├── Layout.astro       # Base layout
│   │   ├── PostLayout.astro
│   │   ├── DevlogLayout.astro
│   │   └── LinkLayout.astro
│   ├── components/
│   │   ├── Header.astro
│   │   ├── Footer.astro
│   │   ├── LinkCard.astro
│   │   └── ShareButtons.astro
│   ├── pages/
│   │   ├── index.astro
│   │   ├── about.astro
│   │   ├── blog/
│   │   │   ├── index.astro
│   │   │   └── [...slug].astro
│   │   ├── devlog/
│   │   │   ├── index.astro
│   │   │   └── [tag]/[version].astro
│   │   ├── notes/
│   │   │   ├── index.astro
│   │   │   └── [tag].astro
│   │   └── links/
│   │       ├── index.astro
│   │       └── [date].astro
│   ├── styles/
│   │   └── main.scss
│   └── utils/
│       └── tags.ts
├── public/
│   ├── assets/
│   ├── js/
│   └── favicon.ico
└── dist/                      # Build output

Risks & Mitigations

Risk Mitigation
Markdown rendering differences Test early, use remark plugins for kramdown features
URL changes breaking links Verify all permalinks match, set up redirects
Complex Liquid logic JSX is more powerful, but requires rewriting
Image generation pipeline Keep as separate script, runs before or after Astro build

Example: Note Tag Page in Astro

Current Jekyll plugin + layout → Single Astro file:

// src/pages/notes/[tag].astro
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const notes = await getCollection('notes');
  const links = await getCollection('links');

  const tags = new Set<string>();
  notes.forEach(n => n.data.tags?.forEach(t => tags.add(t)));
  links.forEach(l => l.data.tags?.forEach(t => tags.add(t)));

  return [...tags].map(tag => ({
    params: { tag },
    props: {
      tag,
      notes: notes.filter(n => n.data.tags?.includes(tag)),
      links: links.filter(l => l.data.tags?.includes(tag)),
    }
  }));
}

const { tag, notes, links } = Astro.props;
const posts = await getCollection('posts', p => p.data.tags?.includes(tag));
---

<Layout title={tag}>
  <h1>{tag}</h1>

  {notes.map(note => (
    <div>
      <h2>{note.data.title}</h2>
      <note.Content />
    </div>
  ))}

  {posts.length > 0 && (
    <>
      <h2>Posts</h2>
      <ul>
        {posts.map(post => (
          <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
        ))}
      </ul>
    </>
  )}
</Layout>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment