Skip to content

Svelte — overview

@ghostwritr/svelte pulls your published articles into Svelte 5 and SvelteKit — keyless feed fetchers, an <ArticleHead> component (plus a pure articleHead helper) for SEO + JSON-LD, and an optional <ArticleContent> Markdown renderer. It works in SvelteKit (server load, SSR, SSG/prerender) and in plain Svelte. Peer dependency: svelte >=5 — it uses Svelte 5 runes.

It is keyless: your siteId is the read capability, there is no API key and no authed fallback. Reads come straight from the static feed at feeds.ghostwritr.io/{siteId}/. A site with no published feed fails closed with a NOT_FOUND GhostwritrError rather than returning an empty list — so you can tell “nothing published yet” apart from a real failure. See Error handling.

npm install @ghostwritr/svelte
  • Keyless fetchersfetchArticles / fetchArticle, re-exported from the shared @ghostwritr/feed core, so the contract is identical across the Next / Astro / React Router / Vue / Svelte SDKs. fetchArticles returns every published Article newest-first; fetchArticle returns one by slug, or null. Call them inside a SvelteKit server load so the site id stays off the client.
  • <ArticleHead> — a Svelte component that renders <title> (= seoTitle), description, Open Graph + Twitter cards, <link rel="canonical">, and schema.org Article JSON-LD into <svelte:head>. Drop it into a page: <ArticleHead {article} siteName="Acme" />. Need a custom <svelte:head>? Import the pure articleHead(article, opts?) function instead and render the { title, meta, link, script } pieces yourself. See SEO & JSON-LD.
  • <ArticleContent> — at the @ghostwritr/svelte/article-content subpath, a component that renders article.markdown to sanitized HTML via {@html} (GFM on, HTML sanitized — the same contract as @ghostwritr/react / @ghostwritr/vue). It lives at a subpath so data-only consumers don’t pull in the Markdown deps. SSR-safe (synchronous).

A blog index and an article page with SEO meta — both fetched in a server load:

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 }) };
}
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>
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 };
}
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 is a complete, server-rendered blog reading from the keyless feed.