Skip to main content

Overview

The blog post page (src/pages/blog/[slug].astro) uses dynamic routing to generate individual pages for each blog post in the content collection.

Source Code

src/pages/blog/[slug].astro
---
import { getCollection, type CollectionEntry } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

type Props = {
  post: CollectionEntry<'blog'>;
};

const { post } = Astro.props;
const { Content } = await post.render();
---

<PostLayout {...post.data}>
  <Content />
</PostLayout>

Dynamic Routing

The [slug].astro filename creates a dynamic route:
1

Generate Paths

getStaticPaths() fetches all blog posts and creates a path for each:
// Creates routes like:
// /blog/guide-to-geometry-dash
// /blog/my-second-post
// /blog/latest-update
2

Map to Props

Each post is passed as a prop to the page:
return posts.map(post => ({
  params: { slug: post.slug },  // URL parameter
  props: { post },              // Post data
}));
3

Render Content

The post markdown is rendered into HTML:
const { Content } = await post.render();

Post Layout

Posts use the PostLayout component which provides:
  • SEO meta tags from frontmatter
  • Author information and avatar
  • Post creation/update dates
  • Tag badges
  • Breadcrumb navigation
  • Markdown content rendering
<PostLayout {...post.data}>
  <Content />
</PostLayout>
All frontmatter fields are spread into the layout as props.

URL Structure

Blog posts follow this URL pattern:
/blog/[slug]
Where [slug] is the filename without extension:
File PathURL
src/content/blog/guide-to-geometry-dash.md/blog/guide-to-geometry-dash
src/content/blog/my-first-post.md/blog/my-first-post
src/content/blog/2024/year-review.md/blog/2024/year-review

Type Safety

The page uses TypeScript for type-safe post handling:
type Props = {
  post: CollectionEntry<'blog'>;
};
This ensures the post object has all required fields from the blog schema.

Content Rendering

The Content component renders the markdown body:
const { Content } = await post.render();

<PostLayout>
  <Content />  <!-- Renders all markdown content -->
</PostLayout>
This supports:
  • Headings
  • Paragraphs
  • Lists
  • Code blocks
  • Images
  • Links
  • Blockquotes

Frontmatter Data

The post page has access to all frontmatter fields:
post.data.title        // Post title
post.data.description  // Post description
post.data.author       // Author name
post.data.createdAt    // Creation date
post.data.updatedAt    // Update date (optional)
post.data.tags         // Array of tags
post.data.image        // Feature image URL
post.data.authorImage  // Author avatar URL
post.data.publish      // Published status

Example Post

Here’s an example of a rendered post URL: File: src/content/blog/guide-to-geometry-dash.md URL: https://yourdomain.com/blog/guide-to-geometry-dash Frontmatter:
---
title: "Beginner's Guide to Geometry Dash"
author: "Ivanciooo"
createdAt: "2025-08-12T04:17:35.590Z"
image: "https://gepig.com/game_cover_bg_1190w/2680.jpg"
description: "Learn the basic concepts of the game"
publish: true
tags: ["Guide", "Beginners", "Tutorials"]
---

Build Process

During npm run build, Astro:
  1. Calls getStaticPaths() to discover all posts
  2. Generates a static HTML file for each post
  3. Optimizes images and assets
  4. Creates the final site in dist/blog/
Only posts with publish: true are included in getStaticPaths(), but you can modify this to include draft posts during development.

Customization

Show related posts at the bottom:
src/pages/blog/[slug].astro
---
const relatedPosts = posts
  .filter(p => p.slug !== post.slug)
  .filter(p => p.data.tags?.some(tag => post.data.tags?.includes(tag)))
  .slice(0, 3);
---

<PostLayout {...post.data}>
  <Content />
  
  <div class="related-posts">
    <h2>Related Posts</h2>
    {relatedPosts.map(p => (
      <a href={`/blog/${p.slug}`}>{p.data.title}</a>
    ))}
  </div>
</PostLayout>

Add Reading Time

Calculate and display reading time:
---
const wordsPerMinute = 200;
const words = post.body.split(/\s+/).length;
const readingTime = Math.ceil(words / wordsPerMinute);
---

<PostLayout {...post.data} readingTime={readingTime}>
  <Content />
</PostLayout>

Add Table of Contents

Generate a table of contents from headings:
---
const headings = await post.render().then(r => r.headings);
---

<PostLayout {...post.data}>
  <aside class="toc">
    <h2>Table of Contents</h2>
    <ul>
      {headings.map(h => (
        <li><a href={`#${h.slug}`}>{h.text}</a></li>
      ))}
    </ul>
  </aside>
  <Content />
</PostLayout>

Post Layout

Learn about the PostLayout component

Creating Posts

How to create new blog posts

Content Collections

Understand the blog collection schema

Blog Index

See the blog listing page

Build docs developers (and LLMs) love