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
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.yml→astro.config.mjs_layouts/*.html→src/layouts/*.astro_includes/*.html→src/components/*.astro_plugins/*.rb→ integrated intosrc/pages/[param].astrofiles- Content stays in place, frontmatter mostly compatible
- Initialize Astro project alongside Jekyll (can run both during migration)
- Configure
astro.config.mjs:- Set output to
static - Configure markdown processing (remark/rehype)
- Set up Netlify adapter if needed
- Set output to
- Define content collection schemas in
src/content/config.ts:posts- blog posts with date, title, tags, categoriesdevlog- version, title, date, tagsnotes- title, date, tagslinks- date, tags, title
Convert layouts to Astro components:
_layouts/default.html→src/layouts/Layout.astro_layouts/post.html→src/layouts/PostLayout.astro_layouts/devlog.html→src/layouts/DevlogLayout.astro_layouts/note.html→src/pages/notes/[tag].astro(dynamic)_layouts/link.html→src/layouts/LinkLayout.astro_layouts/links.html→src/pages/links/[date].astro(dynamic)
Convert includes to reusable components:
_includes/head.html→ part ofLayout.astro_includes/header.html→src/components/Header.astro_includes/footer.html→src/components/Footer.astro_includes/link.html→src/components/LinkCard.astro_includes/share.html→src/components/ShareButtons.astro- etc.
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
- Move content directories:
_posts/→src/content/posts/_devlog/→src/content/devlog/_notes/→src/content/notes/_links/→src/content/links/
- Validate frontmatter against schemas
- Test markdown rendering (kramdown → remark)
- Handle any kramdown-specific syntax differences
Migrate root pages:
index.html→src/pages/index.astroabout.md→src/pages/about.astro(or keep as .md)apps.html→src/pages/apps.astroblog/index.html→src/pages/blog/index.astro- etc.
- Move SCSS:
_sass/main.scss→src/styles/main.scss - Move static assets:
assets/→public/assets/ - Move JS files:
js/→public/js/or convert to modules - Update paths in templates
- Update build script:
- Remove Docker/Ruby dependency
npm run buildproducesdist/folder
- Update
.github/workflows/netlify.yml:- Remove Ruby setup
- Build with Node.js only
- Update
netlify.toml:- Change publish directory to
dist - Keep redirects and headers
- Change publish directory to
- Compare output URLs between Jekyll and Astro
- Verify all dynamic pages generate correctly
- Test tag filtering, archives, cross-collection links
- Check SEO: meta tags, OG tags, Twitter cards
- Validate RSS feed if applicable
- Test image generation pipeline integration
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
| 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 |
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>