Vue — overview
@ghostwritr/vue pulls your published articles into Vue 3 and Nuxt 3 — keyless feed fetchers, a pure articleHead helper for SEO + JSON-LD, and an optional ArticleContent Markdown renderer. It works in Nuxt (server routes, useAsyncData, SSG) and in plain Vue/Vite SSR. Peer dependency: vue >=3.
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.
npm install @ghostwritr/vue pnpm add @ghostwritr/vue yarn add @ghostwritr/vue bun add @ghostwritr/vue The three pieces
Section titled “The three pieces”- Keyless fetchers —
fetchArticles/fetchArticle, re-exported from the shared@ghostwritr/feedcore, so the contract is identical across the Next / Astro / React Router / Vue SDKs.fetchArticlesreturns every published Article newest-first;fetchArticlereturns one by slug, ornull. Call them server-side — insideuseAsyncData, a Nuxt server route, or at build time. articleHead(article, opts?)— a pure function that returns auseHead()input:<title>(=seoTitle), description, Open Graph + Twitter cards,<link rel="canonical">, and schema.orgArticleJSON-LD. It returns plain head data; you decide when to calluseHead. See SEO & JSON-LD.ArticleContent— at the@ghostwritr/vue/article-contentsubpath, a render-function component that rendersarticle.markdownto sanitized HTML (GFM on, HTML sanitized — the same contract as@ghostwritr/react). It lives at a subpath so data-only consumers don’t pull in the Markdown deps. SSR-safe (synchronous).
The 30-second example
Section titled “The 30-second example”A blog index and an article page with SEO meta — both server-side in Nuxt:
<script setup lang="ts">import { fetchArticles } from "@ghostwritr/vue";
const { data: articles } = await useAsyncData("gw-articles", () => fetchArticles({ siteId: useRuntimeConfig().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><script setup lang="ts">import { fetchArticle, articleHead } from "@ghostwritr/vue";import { ArticleContent } from "@ghostwritr/vue/article-content";
const route = useRoute();const { data: article } = await useAsyncData(`gw-${route.params.slug}`, () => fetchArticle({ siteId: useRuntimeConfig().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 is a complete, server-rendered blog reading from the keyless feed.