Skip to content

Blog

The blog uses Astro’s content collections - Markdown files in src/content/blog/, typed with a Zod schema.


import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
---
title: "Why local businesses need a website in 2025"
description: "Most small businesses still rely on word of mouth. Here's why that's leaving money on the table."
pubDate: 2025-06-01
tags: ["local-business", "website", "seo"]
---
Post content starts here...

The filename becomes the URL slug:

src/content/blog/why-local-business-needs-a-website.md
-> /blog/why-local-business-needs-a-website/

Use kebab-case, no spaces, no uppercase.


Blog index page (src/pages/blog/index.astro)

Section titled “Blog index page (src/pages/blog/index.astro)”
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---

Sort by pubDate descending (newest first).


Blog post page (src/pages/blog/[slug].astro)

Section titled “Blog post page (src/pages/blog/[slug].astro)”
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(p => ({ params: { slug: p.slug }, props: { post: p } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<Layout title={post.data.title} description={post.data.description}>
<article class="prose prose-invert max-w-none">
<Content />
</article>
</Layout>

Tag pages live at src/pages/tags/[tag].astro. Each tag becomes a browsable archive page.


src/pages/rss.xml.ts generates an RSS feed at /rss.xml. The blog index links to it. This is already set up in the Juju Alpha template.


  • Each post gets its own title and description via frontmatter
  • The [slug].astro page passes these to <Layout>
  • Add FAQPage or HowTo JSON-LD to posts where relevant (pass via <slot name="head">)
  • Blog posts are included in the sitemap automatically via @astrojs/sitemap

  • Write for the client’s target customer, not for other developers
  • Target one keyword per post (include in title, first paragraph, at least one H2)
  • 800-1500 words is the sweet spot for service business blogs
  • Link internally to service pages where relevant
  • End every post with a CTA linking to /contact