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:
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
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
}));
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:
Where [slug] is the filename without extension:
File Path URL src/content/blog/guide-to-geometry-dash.md/blog/guide-to-geometry-dashsrc/content/blog/my-first-post.md/blog/my-first-postsrc/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:
Calls getStaticPaths() to discover all posts
Generates a static HTML file for each post
Optimizes images and assets
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
Add Related Posts
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