Skip to content

Svelte quickstart

@ghostwritr/svelte is a keyless feed client for Svelte 5 and SvelteKit. You call its fetchers in a server load (+page.server.ts), so your site id never reaches the client; <ArticleHead> writes your SEO + JSON-LD into <svelte:head>, and <ArticleContent> renders the body. No API key — your siteId is the only key. This quickstart wires a SvelteKit /blog index and /blog/[slug] route end to end.

  1. Install the SDK. The Markdown renderer ships in the same package, at the /article-content subpath:

    npm install @ghostwritr/svelte
  2. Add your site ID to .env. SvelteKit reads it server-side via $env/dynamic/private — it’s the only key you need, see Your site ID.

    .env
    GHOSTWRITR_SITE_ID=your-site-id
  3. Build the index load. A server load runs the fetcher during SSR (and prerender), then ships the result to the page:

    src/routes/blog/+page.server.ts
    import { fetchArticles } from "@ghostwritr/svelte";
    import { env } from "$env/dynamic/private";
    export async function load() {
    return { articles: await fetchArticles({ siteId: env.GHOSTWRITR_SITE_ID }) };
    }
  4. Render the index, newest first. data comes from the load above, via the Svelte 5 $props() rune:

    src/routes/blog/+page.svelte
    <script lang="ts">
    let { data } = $props();
    </script>
    <ul>
    {#each data.articles as a}
    <li><a href={`/blog/${a.slug}`}>{a.title}</a></li>
    {/each}
    </ul>
  5. Build the article load. fetchArticle returns null when the slug isn’t in the feed — turn that into a real 404 with SvelteKit’s error(404) helper so the route renders your error page:

    src/routes/blog/[slug]/+page.server.ts
    import { error } from "@sveltejs/kit";
    import { fetchArticle } from "@ghostwritr/svelte";
    import { env } from "$env/dynamic/private";
    export async function load({ params }) {
    const article = await fetchArticle({
    siteId: env.GHOSTWRITR_SITE_ID,
    slug: params.slug,
    });
    if (!article) throw error(404, "Not found");
    return { article };
    }
  6. Render the article page. <ArticleHead> (from @ghostwritr/svelte) writes the SEO meta into <svelte:head>; <ArticleContent> (from the @ghostwritr/svelte/article-content subpath) renders the body to sanitized HTML:

    src/routes/blog/[slug]/+page.svelte
    <script lang="ts">
    import { ArticleHead } from "@ghostwritr/svelte";
    import { ArticleContent } from "@ghostwritr/svelte/article-content";
    let { data } = $props();
    </script>
    <!-- <title>, OG/Twitter, canonical, and schema.org Article JSON-LD into <svelte:head> -->
    <ArticleHead article={data.article} siteName="Acme" />
    <article>
    <h1>{data.article.title}</h1>
    <ArticleContent markdown={data.article.markdown} class="prose" />
    </article>

That’s a complete, server-rendered blog reading from the keyless feed. Run vite dev and open /blog.

  • Data fetching — calling the keyless fetchers in a +page.server.ts load, reading siteId from $env, and why there’s no shipped load wrapper. See Data fetching.
  • SEO — everything <ArticleHead> emits and how to override the canonical URL, plus the articleHead pure function for a custom <svelte:head>. See ArticleHead and SEO & JSON-LD.
  • Publish instantly — verify the signed feed.updated webhook to revalidate the moment a new article lands. See Instant updates.
  • Error handlingnull vs. throw, and the typed GhostwritrError codes. See Error handling and Error handling concepts.