Skip to content

Vue quickstart

@ghostwritr/vue is a keyless feed client for Vue 3 and Nuxt 3. You call its fetchers server-side — inside useAsyncData, a Nuxt server route, or at build time — and articleHead builds your useHead() input for SEO + JSON-LD. No API key — your siteId is the only key. This quickstart wires a Nuxt /blog index and /blog/[slug] page end to end.

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

    npm install @ghostwritr/vue
  2. Add your site ID to Nuxt’s runtimeConfig.public so it’s readable in pages via useRuntimeConfig(). It’s the only key you need — see Your site ID.

    nuxt.config.ts
    export default defineNuxtConfig({
    runtimeConfig: {
    public: {
    // NUXT_PUBLIC_GHOSTWRITR_SITE_ID in the environment overrides this.
    ghostwritrSiteId: "",
    },
    },
    });
    .env
    NUXT_PUBLIC_GHOSTWRITR_SITE_ID=your-site-id
  3. Build the index page. useAsyncData runs the fetcher on the server (and dehydrates the result), so the feed read happens where it should:

    pages/blog/index.vue
    <script setup lang="ts">
    import { fetchArticles } from "@ghostwritr/vue";
    const config = useRuntimeConfig();
    const { data: articles } = await useAsyncData("gw-articles", () =>
    fetchArticles({ siteId: config.public.ghostwritrSiteId }),
    );
    </script>
    <template>
    <ul>
    <li v-for="a in articles" :key="a.id">
    <NuxtLink :to="`/blog/${a.slug}`">{{ a.title }}</NuxtLink>
    </li>
    </ul>
    </template>
  4. Build the article page. fetchArticle returns null when the slug isn’t in the feed — turn that into a real 404 with createError so Nuxt renders your error page:

    pages/blog/[slug].vue
    <script setup lang="ts">
    import { fetchArticle, articleHead } from "@ghostwritr/vue";
    import { ArticleContent } from "@ghostwritr/vue/article-content";
    const route = useRoute();
    const config = useRuntimeConfig();
    const { data: article } = await useAsyncData(`gw-${route.params.slug}`, () =>
    fetchArticle({
    siteId: config.public.ghostwritrSiteId,
    slug: String(route.params.slug),
    }),
    );
    if (!article.value) {
    throw createError({ statusCode: 404, statusMessage: "Not found" });
    }
    // <title>, description, Open Graph + Twitter, canonical, and schema.org Article JSON-LD.
    useHead(articleHead(article.value, { siteName: "Acme" }));
    </script>
    <template>
    <article v-if="article">
    <h1>{{ article.title }}</h1>
    <ArticleContent :markdown="article.markdown" class="prose" />
    </article>
    </template>

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

  • Data fetchinguseAsyncData, server routes, build time, reading siteId from runtimeConfig, and why there’s no shipped composable. See Data fetching.
  • SEO — everything articleHead emits and how to override the canonical URL. See articleHead and SEO & JSON-LD.
  • Publish instantly — verify the signed feed.updated webhook to purge a cache 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.