Skip to content

Data fetching

fetchArticles and fetchArticle are the keyless feed fetchers, re-exported from the shared @ghostwritr/feed core. They read your published articles straight from the static feed at feeds.ghostwritr.io/{siteId}/. They are server-oriented — call them where server code runs, then dehydrate the result to the client.

import { fetchArticles, fetchArticle } from "@ghostwritr/vue";
// every published article, newest first
const articles = await fetchArticles({ siteId });
// one by slug — or null when it isn't in the feed
const article = await fetchArticle({ siteId, slug });

The common case. useAsyncData runs the fetcher on the server during SSR/SSG and ships the result to the client as dehydrated state, so the feed read stays server-side. Read siteId from runtimeConfig.public:

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>

For a single article, key the call by slug and turn a null result into a real 404 with createError:

pages/blog/[slug].vue
<script setup lang="ts">
import { fetchArticle } from "@ghostwritr/vue";
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" });
}
</script>

When you want the feed read to never touch the client bundle — or to add your own caching/headers — call the fetcher inside a server/ route handler and read siteId from the environment:

server/api/articles.get.ts
import { fetchArticles } from "@ghostwritr/vue";
export default defineEventHandler(() =>
fetchArticles({ siteId: useRuntimeConfig().public.ghostwritrSiteId }),
);
server/api/articles/[slug].get.ts
import { fetchArticle } from "@ghostwritr/vue";
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, "slug")!;
const article = await fetchArticle({
siteId: useRuntimeConfig().public.ghostwritrSiteId,
slug,
});
if (!article) throw createError({ statusCode: 404, statusMessage: "Not found" });
return article;
});

For pure SSG, the fetchers are just async functions — call them anywhere your build runs (a Nuxt route rules prerender, a nitro prerender hook, or your own generate script):

scripts/list-slugs.ts
import { fetchArticles } from "@ghostwritr/vue";
const articles = await fetchArticles({ siteId: process.env.GHOSTWRITR_SITE_ID! });
const routes = articles.map((a) => `/blog/${a.slug}`);

Outside Nuxt there’s no useAsyncDataawait the fetcher in your server entry (or data loader) and pass siteId from import.meta.env:

import { fetchArticle } from "@ghostwritr/vue";
const article = await fetchArticle({
siteId: import.meta.env.VITE_GHOSTWRITR_SITE_ID,
slug,
});
if (!article) {
// render your 404
}
ContextSource
Nuxt page / server routeuseRuntimeConfig().public.ghostwritrSiteId
Build script (Nuxt or plain)process.env.GHOSTWRITR_SITE_ID
Plain Viteimport.meta.env.VITE_GHOSTWRITR_SITE_ID

The siteId is a read capability, not a secret — it’s safe in public runtimeConfig. The fetchers still run server-side.

fetchArticle returns null for an unknown slug; that’s a routing miss, so convert it to a 404 with createError. Everything else — a missing feed (unseeded siteId), a network failure, a bad response — throws a typed GhostwritrError with a stable code (NOT_FOUND fails closed rather than returning an empty list). See Error handling and Error handling concepts.