splash image

December 27, 2024

Astro v5 blog starter

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.

https://astro-v5-blog-starter.jldec.me/blog/first-post

Markdown

screenshot of blog post markdown

HTML

screenshot of blog post HTML

Starter Repo

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.

https://github.com/jldec/astro-v5-blog-starter

The project includes:

File Structure

├── 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 driven blog

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.

src/pages/blog/[id].astro

---
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="/">&lt;&lt; Back</a>
    <Content />
  </article>
</Layout>

Image optimization

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.

Publishing on Cloudflare Pages

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.

Conclusion

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.

debug

user: anonymous

{
  "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=\"/\">&lt;&lt; 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>&lt;Image&gt;</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) =&gt; ({\n    params: { id: post.id },\n    props: { post },\n  }));\n}\n\nconst { post } = Astro.props;\nconst { Content } = await render(post);\n---\n\n&lt;Layout title={post.data.title}&gt;\n  &lt;Image\n    src={post.data.image.src}\n    alt={post.data.image.alt || ''}\n    class=&quot;w-full h-60 object-cover object-bottom&quot;\n  /&gt;\n  &lt;article class=&quot;prose mx-auto p-4&quot;&gt;\n    &lt;h1&gt;{post.data.title}&lt;/h1&gt;\n    &lt;a href=&quot;/&quot;&gt;&amp;lt;&amp;lt; Back&lt;/a&gt;\n    &lt;Content /&gt;\n  &lt;/article&gt;\n&lt;/Layout&gt;\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>&lt;Image&gt;</code> component which inspects each image, and generates <code>&lt;img&gt;</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>&lt;route&gt;.html</code> files instead of <code>&lt;route&gt;/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"
}