Blog
The blog uses Astro’s content collections - Markdown files in src/content/blog/, typed with a Zod schema.
src/content.config.ts
Section titled “src/content.config.ts”import { defineCollection, z } from 'astro:content';import { glob } from 'astro/loaders';
const blog = defineCollection({ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }), schema: z.object({ title: z.string(), description: z.string(), pubDate: z.coerce.date(), updatedDate: z.union([z.coerce.date(), z.literal(''), z.null()]).optional(), heroImage: z.string().optional(), tags: z.array(z.string()).default([]), author: z.string().default('Juju Alpha'), draft: z.boolean().default(false), faqs: z.array(z.object({ q: z.string(), a: z.string() })).optional(), }),});
export const collections = { blog };updatedDate uses z.union instead of z.coerce.date() alone. Sveltia CMS writes an empty string "" when the field is left blank, and z.coerce.date("") produces an invalid Date that crashes the build.
Post frontmatter
Section titled “Post frontmatter”---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-01updatedDate: ""tags: ["local-business", "website", "seo"]author: "Juju Alpha"draft: falsefaqs: - q: How long does a website take to build? a: Most projects go live within 10-14 days of receiving all content. - q: Do I need a website if I'm already on JustDial? a: Yes. JustDial controls your listing and can change ranking or pricing at any time. Your own website is the only digital asset you fully own.---
Post content starts here...File naming
Section titled “File naming”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)”Always filter for draft: false and pubDate <= now — hides both drafts and scheduled future posts at build time:
---import { getCollection } from 'astro:content';
const now = new Date();const posts = (await getCollection('blog', ({ data }) => !data.draft && data.pubDate <= now)).sort( (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());---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 now = new Date(); const posts = await getCollection('blog', ({ data }) => !data.draft && data.pubDate <= now); return posts.map(p => ({ params: { slug: p.id }, props: { post: p } }));}
const { post } = Astro.props;const { Content } = await render(post);
const faqs = post.data.faqs ?? [];const faqSchema = faqs.length ? { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqs.map((f: { q: string; a: string }) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a }, })),} : null;---
<Layout title={post.data.title} description={post.data.description}> <Fragment slot="head"> {faqSchema && <script is:inline type="application/ld+json" set:html={JSON.stringify(faqSchema)} />} </Fragment> <article class="prose prose-invert max-w-none"> <Content /> </article></Layout>The faqs type annotation (f: { q: string; a: string }) is required in strict TypeScript mode — omitting it causes an implicit any error. Use const faqs = post.data.faqs ?? [] before checking .length, otherwise TypeScript may narrow to never.
Tags page (src/pages/tags/[tag].astro)
Section titled “Tags page (src/pages/tags/[tag].astro)”Apply the same filter:
const now = new Date();const posts = await getCollection('blog', ({ data }) => !data.draft && data.pubDate <= now);RSS feed (src/pages/rss.xml.ts)
Section titled “RSS feed (src/pages/rss.xml.ts)”Apply the same filter:
const now = new Date();const posts = (await getCollection('blog', ({ data }) => !data.draft && data.pubDate <= now)).sort(...);Post scheduling
Section titled “Post scheduling”Static Astro sites only update on rebuild. To make future-dated posts go live automatically without manual deploys:
- Set
pubDateto a future date in the CMS, leavedraft: false - The filter
data.pubDate <= nowhides it until a build runs on or after that date - Add a daily GitHub Actions cron to trigger Cloudflare Pages rebuilds
.github/workflows/scheduled-build.yml
Section titled “.github/workflows/scheduled-build.yml”name: Scheduled Buildon: schedule: - cron: "30 3 * * *" # 3:30 UTC = 9:00 IST daily workflow_dispatch: # allows manual trigger from GitHub UI
jobs: trigger-deploy: runs-on: ubuntu-latest steps: - name: Trigger Cloudflare Pages Deploy run: curl -s -X POST "${{ secrets.CF_PAGES_DEPLOY_HOOK }}"Setup: Cloudflare Pages → project → Settings → Builds & deployments → Deploy hooks → create a hook → copy the URL → add it as CF_PAGES_DEPLOY_HOOK in the GitHub repo’s Secrets (Settings → Secrets and variables → Actions).
VS Code false-positive errors after changing content.config.ts
Section titled “VS Code false-positive errors after changing content.config.ts”After modifying content.config.ts (adding fields, changing types), VS Code may show cascade errors in all files that call getCollection:
Property 'data' does not exist on type 'never'These are stale .astro/types.d.ts cache errors from the Astro language server. npm run build is the source of truth. If it passes cleanly, the errors are false positives. Restart the TypeScript server (Ctrl+Shift+P → “TypeScript: Restart TS Server”) to clear them.
SEO for blog posts
Section titled “SEO for blog posts”- Each post gets its own
titleanddescriptionvia frontmatter - The
[slug].astropage passes these to<Layout> - Add
faqsto frontmatter to get FAQPage JSON-LD and Google FAQ accordion rich results - Blog posts are included in the sitemap automatically via
@astrojs/sitemap - After each post goes live: submit in Google Search Console → URL Inspection → Request Indexing
Content tips
Section titled “Content tips”- 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)
- 1,500-2,500 words is the sweet spot for service business blogs targeting India SMB queries
- Include 4-5
faqsper post for FAQ rich results - Link internally to service pages where relevant
- End every post with a CTA linking to
/contact