Data Interface
Blog layouts receive different data depending on the view type. IndexLayout gets minimal props and loads posts itself, while PostLayout receives the full processed post.
What Layouts Receive
| Layout | Data Type | Received As | Layout Responsibility |
|---|---|---|---|
| IndexLayout | Post listing | Path only (dataPath) |
Load all posts, render cards |
| PostLayout | Single post | Rendered HTML (content) |
Just display it |
Route Handler ([...slug].astro)
─────────────────────────────────
For /blog (index):
• Passes dataPath only
• Layout loads and renders posts
For /blog/hello-world (post):
• Loads markdown, runs parser → HTML
• Passes content as rendered HTML
│
▼
IndexLayout PostLayout
─────────── ──────────
• Receives: dataPath • Receives: content (HTML)
• Must: loadContent(dataPath) • Just: set:html={content}
• Must: Render post cards • Already processed
Key point: IndexLayout must load posts itself. PostLayout receives pre-rendered HTML.
IndexLayout Props
The index layout receives minimal props and loads content internally:
interface IndexLayoutProps {
dataPath: string; // Path to blog folder
postsPerPage?: number; // Optional pagination limit
}
Loading Posts
IndexLayout loads posts using the data loader:
import { loadContent } from '@loaders/data';
const { dataPath, postsPerPage = 10 } = Astro.props;
const posts = await loadContent(dataPath, 'blog', {
pattern: '*.{md,mdx}',
sort: 'date',
order: 'desc',
});
// posts is an array of LoadedContent (includes headings)
Post Array Structure
Each post in the array has this shape:
interface LoadedContent {
id: string; // Unique identifier
slug: string; // URL slug (e.g., "hello-world")
content: string; // Rendered HTML (full post)
headings: Heading[]; // Extracted headings (for TOC on long posts)
data: {
title: string; // Post title
description?: string; // Excerpt/summary
date?: string; // Publication date
author?: string; // Author name
tags?: string[]; // Tag array
image?: string; // Featured image
draft?: boolean; // Draft status
};
filePath: string; // Absolute file path
relativePath: string; // Relative path
}
interface Heading {
depth: number; // 1-6 (h1-h6)
slug: string; // URL-safe ID
text: string; // Heading text
}
Example IndexLayout
---
import PostCard from '../../components/cards/default/PostCard.astro';
import { loadContent } from '@loaders/data';
interface Props {
dataPath: string;
postsPerPage?: number;
}
const { dataPath, postsPerPage = 10 } = Astro.props;
const posts = await loadContent(dataPath, 'blog', {
pattern: '*.{md,mdx}',
sort: 'date',
order: 'desc',
});
---
<div class="blog-index">
<h1>Blog</h1>
<div class="post-grid">
{posts.slice(0, postsPerPage).map(post => (
<PostCard
title={post.data.title}
description={post.data.description}
date={post.data.date}
author={post.data.author}
tags={post.data.tags}
href={`/blog/${post.slug}`}
image={post.data.image}
/>
))}
</div>
</div>
PostLayout Props
Post layouts receive the full processed post data:
interface PostLayoutProps {
// Content metadata (from frontmatter)
title: string; // Required
description?: string; // Optional summary
// Post-specific fields
date?: string; // Publication date
author?: string; // Author name
tags?: string[]; // Tag array
// Rendered content
content: string; // HTML string (processed markdown)
}
Content is Pre-Processed
Like docs, the content prop is fully processed HTML:
Raw Markdown content prop (HTML)
───────────── ───────────────────
# Introduction <h1>Introduction</h1>
Some **bold** text. → <p>Some <strong>bold</strong> text.</p>
```javascript <pre><code class="language-javascript">
console.log('hi'); console.log('hi');
``` </code></pre>
Example PostLayout
---
interface Props {
title: string;
description?: string;
date?: string;
author?: string;
tags?: string[];
content: string;
}
const { title, description, date, author, tags, content } = Astro.props;
// Format date for display
const formattedDate = date
? new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: null;
---
<article class="blog-post">
<header>
<h1>{title}</h1>
<div class="meta">
{author && <span class="author">By {author}</span>}
{formattedDate && <time datetime={date}>{formattedDate}</time>}
</div>
{description && <p class="summary">{description}</p>}
</header>
<div class="content">
<Fragment set:html={content} />
</div>
{tags && tags.length > 0 && (
<footer class="tags">
{tags.map(tag => (
<span class="tag">{tag}</span>
))}
</footer>
)}
</article>
Frontmatter to Props Mapping
| Frontmatter | Props | Type | Notes |
|---|---|---|---|
title |
title |
string | Required |
description |
description |
string | Optional |
date |
date |
string | ISO format or filename date |
author |
author |
string | Optional |
tags |
tags |
string[] | Optional array |
image |
(via data) | string | For IndexLayout cards |
draft |
(filtered) | boolean | Hidden in production |
Date Handling
Dates can come from two sources:
1. Filename (Default)
2024-01-15-hello-world.md → date: "2024-01-15"
2. Frontmatter (Override)
---
title: Hello World
date: 2024-01-20 # Overrides filename date
---
Formatting Dates
// In your layout
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
// "January 15, 2024"
Working with Tags
Displaying Tags
{tags && tags.length > 0 && (
<div class="tags">
{tags.map(tag => (
<a href={`/blog/tags/${tag}`} class="tag">
{tag}
</a>
))}
</div>
)}
Getting All Tags (IndexLayout)
// Collect all unique tags from posts
const allTags = [...new Set(
posts.flatMap(post => post.data.tags || [])
)].sort();
Featured Images
In Post Data
---
title: My Post
image: ./assets/cover.jpg
---
Displaying in Card
<PostCard
title={post.data.title}
image={post.data.image} <!-- Passed to card -->
href={`/blog/${post.slug}`}
/>
In PostCard Component
---
interface Props {
title: string;
image?: string;
href: string;
// ...
}
const { title, image, href } = Astro.props;
---
<a href={href} class="post-card">
{image && (
<div class="card-image">
<img src={image} alt="" loading="lazy" />
</div>
)}
<h3>{title}</h3>
</a>
Type Definitions
For TypeScript, import types:
import type { LoadedContent, Heading } from '@loaders/data';
// Then use in your components
const posts: LoadedContent[] = await loadContent(dataPath, 'blog', options);
// Access headings for TOC (on long posts)
const headings: Heading[] = post.headings;