I recently moved a friend's blog to Astro v5. The motivation behind selecting Astro was its first-class support for markdown content.
With just a minimal amount of boilerplate, Astro will validate markdown frontmatter, generate static pages for each post, and optimize all the images in your posts.
Here is the result of extracting these key features into a new blog starter. The repo does not include a lot of design - just the configuration and a minimal amount of code.
The project includes:
<Image>
component├── LICENSE
├── README.md
├── astro.config.mjs
├── package.json
├── public
│ ├── _headers
│ └── favicon.svg
├── src
│ ├── assets
│ │ └── astro.svg
│ ├── components
│ │ └── AstroLogo.astro
│ ├── content
│ │ ├── blog
│ │ │ ├── 2nd-post.md
│ │ │ └── first-post.md
│ │ └── images
│ │ ├── birch-trees.webp
│ │ └── sunset-cambridge.jpg
│ ├── content.config.ts
│ ├── layouts
│ │ └── Layout.astro
│ ├── pages
│ │ ├── 404.astro
│ │ ├── blog
│ │ │ └── [id].astro
│ │ └── index.astro
│ └── styles
│ └── global.css
├── tailwind.config.mjs
├── tsconfig.json
└── wrangler.toml
Markdown blog posts live in src/content/blog.
The schema for the blog
collection is defined in content.config.ts. This file also includes utility functions for sorting and filtering posts. E.g. if the draft
flag is set, a post will only be included during dev, but not published with the production build.
The home page is defined in src/pages/index.astro which lists posts in date order.
Posts are rendered by the dynamic route src/pages/blog/[id].astro. In order for Astro to pre-render static pages, dynamic routes export a getStaticPaths
function which returns a list of params and props for each rendered route.
---
import { Image } from 'astro:assets';
import { getCollection, render } from 'astro:content';
import { filterAllPosts } from '~/content.config';
import Layout from '~/layouts/Layout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog', filterAllPosts);
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<Layout title={post.data.title}>
<Image
src={post.data.image.src}
alt={post.data.image.alt || ''}
class="w-full h-60 object-cover object-bottom"
/>
<article class="prose mx-auto p-4">
<h1>{post.data.title}</h1>
<a href="/"><< Back</a>
<Content />
</article>
</Layout>
A src
path and alt
text can be declared for images in markdown frontmatter, or inline in markdown.
These are passed into the <Image>
component which inspects each image, and generates <img>
tags with width
and height
attributes, thereby reducing layout shifts in the browser.
Images are converted to webp format, and stored in dist/_astro
with unique (cacheable) names during the build process.
The @astrojs/cloudflare adapter is not needed for static sites.
Cloudflare Pages matches routes with .html
files. To avoid trailing slashes, configure the build to generate <route>.html
files instead of <route>/index.html
.
The _headers file adds cache control headers for immutable content.
Astro v5 is a great choice for a markdown driven blog, as long as you're fine with doing occasional maintenance to update dependencies.
Here are some ideas for future improvements to this starter:
I hope you find this starter useful. Please reach out on X if you have any feedback or suggestions.
{ "path": "/blog/astro-v5-blog-starter", "attrs": { "title": "Astro v5 Blog starter", "date": "2024-12-27", "layout": "BlogPostLayout", "splash": { "image": "/images/big-ben.webp" } }, "md": "# Astro v5 blog starter\n\nI recently moved a friend's blog to [Astro v5](https://astro.build/blog/astro-5/). The motivation behind selecting Astro was its first-class support for markdown content.\n\nWith just a minimal amount of boilerplate, Astro will validate markdown frontmatter, generate static pages for each post, and optimize all the images in your posts.\n\n> https://astro-v5-blog-starter.jldec.me/blog/first-post\n\n### Markdown\n![screenshot of blog post markdown](/images/astro-starter-markdown.webp)\n\n### HTML\n![screenshot of blog post HTML](/images/astro-starter-html.webp)\n\n## Starter Repo\nHere is the result of extracting these key features into a new blog starter. The repo does not include a lot of design - just the configuration and a minimal amount of code.\n\n> https://github.com/jldec/astro-v5-blog-starter\n\nThe project includes:\n- [Tailwind CSS](https://docs.astro.build/en/guides/integrations-guide/tailwind/) with [typography](https://docs.astro.build/en/recipes/tailwind-rendered-markdown/#setting-up-tailwindcsstypography)\n- Image optimization with the [`<Image>`](https://docs.astro.build/en/guides/images/#display-optimized-images-with-the-image--component) component\n- [Static](https://docs.astro.build/en/guides/content-collections/#building-for-static-output-default) builds (SSG) for Cloudflare Pages.\n\n## File Structure\n```\n├── LICENSE\n├── README.md\n├── astro.config.mjs\n├── package.json\n├── public\n│ ├── _headers\n│ └── favicon.svg\n├── src\n│ ├── assets\n│ │ └── astro.svg\n│ ├── components\n│ │ └── AstroLogo.astro\n│ ├── content\n│ │ ├── blog\n│ │ │ ├── 2nd-post.md\n│ │ │ └── first-post.md\n│ │ └── images\n│ │ ├── birch-trees.webp\n│ │ └── sunset-cambridge.jpg\n│ ├── content.config.ts\n│ ├── layouts\n│ │ └── Layout.astro\n│ ├── pages\n│ │ ├── 404.astro\n│ │ ├── blog\n│ │ │ └── [id].astro\n│ │ └── index.astro\n│ └── styles\n│ └── global.css\n├── tailwind.config.mjs\n├── tsconfig.json\n└── wrangler.toml\n```\n\n## Markdown driven blog\nMarkdown blog posts live in [src/content/blog](https://github.com/jldec/astro-v5-blog-starter/tree/main/src/content/blog).\n\nThe schema for the `blog` collection is defined in [content.config.ts](https://github.com/jldec/astro-v5-blog-starter/blob/main/src/content.config.ts). This file also includes utility functions for sorting and filtering posts. E.g. if the `draft` flag is set, a post will only be included during dev, but not published with the production build.\n\nThe home page is defined in [src/pages/index.astro](https://github.com/jldec/astro-v5-blog-starter/blob/main/src/pages/index.astro) which lists posts in date order.\n\nPosts are rendered by the dynamic route [src/pages/blog/[id].astro](https://github.com/jldec/astro-v5-blog-starter/blob/main/src/pages/blog/%5Bid%5D.astro). In order for Astro to pre-render static pages, dynamic routes export a `getStaticPaths` function which returns a list of params and props for each rendered route.\n\n### src/pages/blog/[id].astro\n```tsx\n---\nimport { Image } from 'astro:assets';\nimport { getCollection, render } from 'astro:content';\nimport { filterAllPosts } from '~/content.config';\nimport Layout from '~/layouts/Layout.astro';\n\nexport async function getStaticPaths() {\n const posts = await getCollection('blog', filterAllPosts);\n return posts.map((post) => ({\n params: { id: post.id },\n props: { post },\n }));\n}\n\nconst { post } = Astro.props;\nconst { Content } = await render(post);\n---\n\n<Layout title={post.data.title}>\n <Image\n src={post.data.image.src}\n alt={post.data.image.alt || ''}\n class=\"w-full h-60 object-cover object-bottom\"\n />\n <article class=\"prose mx-auto p-4\">\n <h1>{post.data.title}</h1>\n <a href=\"/\"><< Back</a>\n <Content />\n </article>\n</Layout>\n```\n\n## Image optimization\nA `src` path and `alt` text can be declared for images in markdown frontmatter, or inline in markdown.\n\nThese are passed into the `<Image>` component which inspects each image, and generates `<img>` tags with `width` and `height` attributes, thereby reducing layout shifts in the browser.\n\nImages are converted to webp format, and stored in `dist/_astro` with unique (cacheable) names during the build process.\n\n## Publishing on Cloudflare Pages\nThe [@astrojs/cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/) adapter is not needed for static sites.\n\nCloudflare Pages [matches routes](https://developers.cloudflare.com/pages/configuration/serving-pages/#route-matching) with `.html` files. To avoid trailing slashes, [configure](https://github.com/jldec/astro-v5-blog-starter/blob/main/astro.config.mjs#L8-L11) the build to generate `<route>.html` files instead of `<route>/index.html`.\n\nThe [_headers](https://github.com/jldec/astro-v5-blog-starter/blob/main/public/_headers) file adds cache control headers for immutable content.\n\n## Conclusion\nAstro v5 is a great choice for a markdown driven blog, as long as you're fine with doing occasional maintenance to update dependencies.\n\nHere are some ideas for future improvements to this starter:\n\n- Sitemap\n- Menu component for desktop and mobile\n- Nicer fonts\n- Icons for social links\n\nI hope you find this starter useful. Please reach out [on X](https://x.com/jldec) if you have any feedback or suggestions.", "html": "<h1>Astro v5 blog starter</h1>\n<p>I recently moved a friend's blog to <a href=\"https://astro.build/blog/astro-5/\">Astro v5</a>. The motivation behind selecting Astro was its first-class support for markdown content.</p>\n<p>With just a minimal amount of boilerplate, Astro will validate markdown frontmatter, generate static pages for each post, and optimize all the images in your posts.</p>\n<blockquote>\n<p><a href=\"https://astro-v5-blog-starter.jldec.me/blog/first-post\">https://astro-v5-blog-starter.jldec.me/blog/first-post</a></p>\n</blockquote>\n<h3>Markdown</h3>\n<p><img src=\"/images/astro-starter-markdown.webp\" alt=\"screenshot of blog post markdown\"></p>\n<h3>HTML</h3>\n<p><img src=\"/images/astro-starter-html.webp\" alt=\"screenshot of blog post HTML\"></p>\n<h2>Starter Repo</h2>\n<p>Here is the result of extracting these key features into a new blog starter. The repo does not include a lot of design - just the configuration and a minimal amount of code.</p>\n<blockquote>\n<p><a href=\"https://github.com/jldec/astro-v5-blog-starter\">https://github.com/jldec/astro-v5-blog-starter</a></p>\n</blockquote>\n<p>The project includes:</p>\n<ul>\n<li><a href=\"https://docs.astro.build/en/guides/integrations-guide/tailwind/\">Tailwind CSS</a> with <a href=\"https://docs.astro.build/en/recipes/tailwind-rendered-markdown/#setting-up-tailwindcsstypography\">typography</a></li>\n<li>Image optimization with the <a href=\"https://docs.astro.build/en/guides/images/#display-optimized-images-with-the-image--component\"><code><Image></code></a> component</li>\n<li><a href=\"https://docs.astro.build/en/guides/content-collections/#building-for-static-output-default\">Static</a> builds (SSG) for Cloudflare Pages.</li>\n</ul>\n<h2>File Structure</h2>\n<pre><code>├── LICENSE\n├── README.md\n├── astro.config.mjs\n├── package.json\n├── public\n│ ├── _headers\n│ └── favicon.svg\n├── src\n│ ├── assets\n│ │ └── astro.svg\n│ ├── components\n│ │ └── AstroLogo.astro\n│ ├── content\n│ │ ├── blog\n│ │ │ ├── 2nd-post.md\n│ │ │ └── first-post.md\n│ │ └── images\n│ │ ├── birch-trees.webp\n│ │ └── sunset-cambridge.jpg\n│ ├── content.config.ts\n│ ├── layouts\n│ │ └── Layout.astro\n│ ├── pages\n│ │ ├── 404.astro\n│ │ ├── blog\n│ │ │ └── [id].astro\n│ │ └── index.astro\n│ └── styles\n│ └── global.css\n├── tailwind.config.mjs\n├── tsconfig.json\n└── wrangler.toml\n</code></pre>\n<h2>Markdown driven blog</h2>\n<p>Markdown blog posts live in <a href=\"https://github.com/jldec/astro-v5-blog-starter/tree/main/src/content/blog\">src/content/blog</a>.</p>\n<p>The schema for the <code>blog</code> collection is defined in <a href=\"https://github.com/jldec/astro-v5-blog-starter/blob/main/src/content.config.ts\">content.config.ts</a>. This file also includes utility functions for sorting and filtering posts. E.g. if the <code>draft</code> flag is set, a post will only be included during dev, but not published with the production build.</p>\n<p>The home page is defined in <a href=\"https://github.com/jldec/astro-v5-blog-starter/blob/main/src/pages/index.astro\">src/pages/index.astro</a> which lists posts in date order.</p>\n<p>Posts are rendered by the dynamic route <a href=\"https://github.com/jldec/astro-v5-blog-starter/blob/main/src/pages/blog/%5Bid%5D.astro\">src/pages/blog/[id].astro</a>. In order for Astro to pre-render static pages, dynamic routes export a <code>getStaticPaths</code> function which returns a list of params and props for each rendered route.</p>\n<h3>src/pages/blog/[id].astro</h3>\n<pre><code class=\"language-tsx\">---\nimport { Image } from 'astro:assets';\nimport { getCollection, render } from 'astro:content';\nimport { filterAllPosts } from '~/content.config';\nimport Layout from '~/layouts/Layout.astro';\n\nexport async function getStaticPaths() {\n const posts = await getCollection('blog', filterAllPosts);\n return posts.map((post) => ({\n params: { id: post.id },\n props: { post },\n }));\n}\n\nconst { post } = Astro.props;\nconst { Content } = await render(post);\n---\n\n<Layout title={post.data.title}>\n <Image\n src={post.data.image.src}\n alt={post.data.image.alt || ''}\n class="w-full h-60 object-cover object-bottom"\n />\n <article class="prose mx-auto p-4">\n <h1>{post.data.title}</h1>\n <a href="/">&lt;&lt; Back</a>\n <Content />\n </article>\n</Layout>\n</code></pre>\n<h2>Image optimization</h2>\n<p>A <code>src</code> path and <code>alt</code> text can be declared for images in markdown frontmatter, or inline in markdown.</p>\n<p>These are passed into the <code><Image></code> component which inspects each image, and generates <code><img></code> tags with <code>width</code> and <code>height</code> attributes, thereby reducing layout shifts in the browser.</p>\n<p>Images are converted to webp format, and stored in <code>dist/_astro</code> with unique (cacheable) names during the build process.</p>\n<h2>Publishing on Cloudflare Pages</h2>\n<p>The <a href=\"https://docs.astro.build/en/guides/integrations-guide/cloudflare/\">@astrojs/cloudflare</a> adapter is not needed for static sites.</p>\n<p>Cloudflare Pages <a href=\"https://developers.cloudflare.com/pages/configuration/serving-pages/#route-matching\">matches routes</a> with <code>.html</code> files. To avoid trailing slashes, <a href=\"https://github.com/jldec/astro-v5-blog-starter/blob/main/astro.config.mjs#L8-L11\">configure</a> the build to generate <code><route>.html</code> files instead of <code><route>/index.html</code>.</p>\n<p>The <a href=\"https://github.com/jldec/astro-v5-blog-starter/blob/main/public/_headers\">_headers</a> file adds cache control headers for immutable content.</p>\n<h2>Conclusion</h2>\n<p>Astro v5 is a great choice for a markdown driven blog, as long as you're fine with doing occasional maintenance to update dependencies.</p>\n<p>Here are some ideas for future improvements to this starter:</p>\n<ul>\n<li>Sitemap</li>\n<li>Menu component for desktop and mobile</li>\n<li>Nicer fonts</li>\n<li>Icons for social links</li>\n</ul>\n<p>I hope you find this starter useful. Please reach out <a href=\"https://x.com/jldec\">on X</a> if you have any feedback or suggestions.</p>\n" }