Skip to content

Build a blog end-to-end

This guide takes an empty app to a live blog that reads your published articles, ranks, and updates itself when you publish. It uses Next.js as the worked example and points to the Astro and React Router equivalents at each step. It orchestrates the per-SDK references rather than repeating them — follow the links for the full surface.

The whole thing reads from your keyless static feed: your siteId is the only key, there is no API key, and reads come straight from feeds.ghostwritr.io/{siteId}/.

  1. Install the SDK and the renderer.

    npm install @ghostwritr/next @ghostwritr/react

    @ghostwritr/next is the server-only data client; @ghostwritr/react gives you <ArticleContent> to render the Markdown body.

  2. Get your site ID. It’s the unguessable read capability for your feed — there’s nothing else to provision. See Your site ID. Put it in .env.local:

    .env.local
    GHOSTWRITR_SITE_ID=your-site-id

    In Astro this is GHOSTWRITR_SITE_ID read via import.meta.env; in React Router it’s process.env.GHOSTWRITR_SITE_ID inside your server-side loaders.

  3. Create the data client as a module singleton in a server-only file. createGhostwritr lives at the /server subpath:

    lib/ghostwritr.ts
    import "server-only";
    import { createGhostwritr } from "@ghostwritr/next/server";
    export const gw = createGhostwritr({
    siteId: process.env.GHOSTWRITR_SITE_ID!, // public read key — no API key
    revalidate: 3600, // ISR window in seconds (default 3600)
    });

    The Astro equivalent is a Content Layer loader in src/content.config.ts (ghostwritr({ siteId })); React Router has no client object — you call fetchArticles/fetchArticle directly inside a route loader.

  4. Build the index, newest first. getArticles() returns every published article, already sorted:

    app/blog/page.tsx
    import Link from "next/link";
    import { gw } from "@/lib/ghostwritr";
    export default async function Blog() {
    const articles = await gw.getArticles();
    return (
    <ul>
    {articles.map((a) => (
    <li key={a.id}>
    <Link href={`/blog/${a.slug}`}>{a.title}</Link>
    </li>
    ))}
    </ul>
    );
    }

    Each item is a full Articletitle, slug, image, publishedAt, author, and the rest are right there if you want a richer card.

  5. Build the article page. gw.generateStaticParams is ready to export as-is, so every slug is prerendered. Render the Markdown with <ArticleContent>:

    app/blog/[slug]/page.tsx
    import { notFound } from "next/navigation";
    import { ArticleContent } from "@ghostwritr/react";
    import { gw } from "@/lib/ghostwritr";
    export const generateStaticParams = gw.generateStaticParams;
    export default async function Page({
    params,
    }: {
    params: Promise<{ slug: string }>;
    }) {
    const { slug } = await params;
    const article = await gw.getArticle(slug);
    if (!article) notFound();
    return (
    <article>
    <h1>{article.title}</h1>
    <ArticleContent markdown={article.markdown} className="prose" />
    </article>
    );
    }
  6. Drive SEO from the article fields. Export generateMetadata on the article route and feed <title>, the canonical URL, Open Graph, and JSON-LD from seoTitle, seoDescription, canonicalUrl, image, and author. The full Next recipe (plus app/sitemap.ts) is in SEO metadata & sitemap; the cross-framework reasoning is in SEO & structured data. React Router users get this for free from articleMeta.

  7. Make publishing instant. ISR refreshes on the revalidate clock; to make a just-published article appear in seconds, wire the signed feed.updated webhook with createRevalidateHandler from @ghostwritr/next/revalidate. See Instant publishing and the concept page Instant updates.

  8. Deploy. Run next dev, open /blog, then ship to your host of choice. The feed is static and cached at the edge, so reads stay cheap at any traffic. If your blog lives on its own domain, decide the canonical policy up front — see Custom domain & canonical.

That’s a complete, statically-generated, self-updating blog reading from the keyless feed.