Create a Blog
This guide covers creating a blog with EmDash, from defining the content type to displaying posts with categories and tags.
Prerequisites
Section titled “Prerequisites”- An EmDash site set up and running (see Getting Started)
- Basic familiarity with Astro components
Define the Posts Collection
Section titled “Define the Posts Collection”EmDash creates a default “posts” collection during setup. Customize it from the admin dashboard or the API.
The default posts collection includes:
title- Post titleslug- URL-friendly identifiercontent- Rich text bodyexcerpt- Short descriptionfeatured_image- Header image (optional)status- draft, published, or scheduledpublishedAt- Publication date (system field)
Create Your First Post
Section titled “Create Your First Post”-
Open the admin dashboard at
/_emdash/admin -
Click Posts in the sidebar
-
Click New Post
-
Enter a title and write your content using the rich text editor
-
Add categories and tags in the sidebar panel
-
Set the status to Published
-
Click Save
The post is now live and appears immediately.
Display Posts on Your Site
Section titled “Display Posts on Your Site”List All Posts
Section titled “List All Posts”The following page displays all published posts:
---import { getEmDashCollection } from "emdash";import Base from "../../layouts/Base.astro";
const { entries: posts } = await getEmDashCollection("posts", { status: "published",});
// Sort by publication date, newest firstconst sortedPosts = posts.sort( (a, b) => (b.data.publishedAt?.getTime() ?? 0) - (a.data.publishedAt?.getTime() ?? 0));---
<Base title="Blog"> <h1>Blog</h1> <ul> {sortedPosts.map((post) => ( <li> <a href={`/blog/${post.data.slug}`}> <h2>{post.data.title}</h2> <p>{post.data.excerpt}</p> <time datetime={post.data.publishedAt?.toISOString()}> {post.data.publishedAt?.toLocaleDateString()} </time> </a> </li> ))} </ul></Base>Display a Single Post
Section titled “Display a Single Post”The following dynamic route renders an individual post:
---import { getEmDashCollection, getEmDashEntry } from "emdash";import { PortableText } from "emdash/ui";import Base from "../../layouts/Base.astro";
export async function getStaticPaths() { const { entries: posts } = await getEmDashCollection("posts", { status: "published", });
return posts.map((post) => ({ params: { slug: post.data.slug }, }));}
const { slug } = Astro.params;const { entry: post } = await getEmDashEntry("posts", slug);
if (!post) { return Astro.redirect("/404");}---
<Base title={post.data.title}> <article> {post.data.featured_image && ( <img src={post.data.featured_image} alt="" /> )} <h1>{post.data.title}</h1> <time datetime={post.data.publishedAt?.toISOString()}> {post.data.publishedAt?.toLocaleDateString()} </time> <PortableText value={post.data.content} /> </article></Base>Add Categories and Tags
Section titled “Add Categories and Tags”EmDash includes built-in category and tag taxonomies. See Taxonomies for details on creating and managing terms.
Filter Posts by Category
Section titled “Filter Posts by Category”The following route lists posts in a single category:
---import { getEmDashCollection, getTerm, getTaxonomyTerms } from "emdash";import Base from "../../layouts/Base.astro";
export async function getStaticPaths() { const categories = await getTaxonomyTerms("category");
// Flatten hierarchical categories const flatten = (terms) => terms.flatMap((t) => [t, ...flatten(t.children)]);
return flatten(categories).map((cat) => ({ params: { slug: cat.slug }, props: { category: cat }, }));}
const { category } = Astro.props;
const { entries: posts } = await getEmDashCollection("posts", { status: "published", where: { category: category.slug },});---
<Base title={category.label}> <h1>{category.label}</h1> {category.description && <p>{category.description}</p>}
<ul> {posts.map((post) => ( <li> <a href={`/blog/${post.data.slug}`}>{post.data.title}</a> </li> ))} </ul></Base>Display Post Categories
Section titled “Display Post Categories”The following component shows the categories and tags assigned to a post:
---import { getEntryTerms } from "emdash";
interface Props { postId: string;}
const { postId } = Astro.props;const categories = await getEntryTerms("posts", postId, "category");const tags = await getEntryTerms("posts", postId, "tag");---
<div class="post-meta"> {categories.length > 0 && ( <div class="categories"> <span>Categories:</span> {categories.map((cat) => ( <a href={`/category/${cat.slug}`}>{cat.label}</a> ))} </div> )}
{tags.length > 0 && ( <div class="tags"> <span>Tags:</span> {tags.map((tag) => ( <a href={`/tag/${tag.slug}`}>{tag.label}</a> ))} </div> )}</div>Add Pagination
Section titled “Add Pagination”For blogs with many posts, the following route paginates the post list:
---import { getEmDashCollection } from "emdash";import Base from "../../../layouts/Base.astro";
const POSTS_PER_PAGE = 10;
export async function getStaticPaths() { const { entries: allPosts } = await getEmDashCollection("posts", { status: "published", });
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => ({ params: { page: String(i + 1) }, props: { currentPage: i + 1, totalPages }, }));}
const { currentPage, totalPages } = Astro.props;
const { entries: allPosts } = await getEmDashCollection("posts", { status: "published",});
const sortedPosts = allPosts.sort( (a, b) => (b.data.publishedAt?.getTime() ?? 0) - (a.data.publishedAt?.getTime() ?? 0));
const start = (currentPage - 1) * POSTS_PER_PAGE;const posts = sortedPosts.slice(start, start + POSTS_PER_PAGE);---
<Base title={`Blog - Page ${currentPage}`}> <h1>Blog</h1>
<ul> {posts.map((post) => ( <li> <a href={`/blog/${post.data.slug}`}>{post.data.title}</a> </li> ))} </ul>
<nav> {currentPage > 1 && ( <a href={`/blog/page/${currentPage - 1}`}>Previous</a> )} <span>Page {currentPage} of {totalPages}</span> {currentPage < totalPages && ( <a href={`/blog/page/${currentPage + 1}`}>Next</a> )} </nav></Base>Add an RSS Feed
Section titled “Add an RSS Feed”The following endpoint generates an RSS feed for the blog:
import rss from "@astrojs/rss";import { getEmDashCollection } from "emdash";
export async function GET(context) { const { entries: posts } = await getEmDashCollection("posts", { status: "published", });
return rss({ title: "My Blog", description: "A blog built with EmDash", site: context.site, items: posts.map((post) => ({ title: post.data.title, pubDate: post.data.publishedAt, description: post.data.excerpt, link: `/blog/${post.data.slug}`, })), });}The feed depends on the @astrojs/rss package. The following command installs it:
npm install @astrojs/rssNext Steps
Section titled “Next Steps”- Working with Content - Learn CRUD operations in the admin
- Media Library - Add images to your posts
- Taxonomies - Create custom classification systems