Skip to content

SEO meta (articleHead)

articleHead(article, opts?) turns an Article into a useHead() input — the <title>, meta description, Open Graph + Twitter cards, the canonical <link>, and schema.org Article JSON-LD — all driven by the feed’s guarantees. It’s a pure function: it returns plain head data and you call useHead yourself, so you stay in control of when and how the head is set. See SEO & JSON-LD for the cross-SDK model.

It lives at the core entry:

import { articleHead } from "@ghostwritr/vue";

In Nuxt, useHead is built in (auto-imported). In plain Vue (Vite SSR/SPA), install @unhead/vue and import useHead from it. The articleHead call is identical either way.

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

ArticleHeadOptions is optional — every field within it is too.

OptionTypeEffect
urlstringAbsolute URL of this page. Sets <link rel="canonical"> and og:url. Falls back to article.canonicalUrl. Pass your own when self-hosting under a different domain.
siteNamestringSets og:site_name and becomes the JSON-LD publisher (an Organization).
twitterSitestringSets twitter:site (your default @handle).

From a typical article, articleHead returns an ArticleHead with title, a meta array, an optional canonical link, and a JSON-LD script:

  • title = article.seoTitle — the <title>. seoTitle always falls back to title upstream, so it’s never empty.
  • { name: "description", content: article.seoDescription } — only when seoDescription is set.
  • og:type = "article", og:title = seoTitle, and og:description (when present).
  • og:url (when a canonical resolves), og:site_name (when siteName is given), og:image (when the article has an image).
  • article:published_time = publishedAt, article:modified_time = updatedAt, and one article:tag per entry in tags.
  • twitter:card = "summary_large_image" when the article has an image, otherwise "summary"; plus twitter:site when twitterSite is given.
  • link = [{ rel: "canonical", href }] — only when a canonical resolves.
  • script = [{ type: "application/ld+json", innerHTML }] — the schema.org Article JSON-LD (below).

The injected Article JSON-LD carries headline (= article.title), description (when set), url + mainEntityOfPage (when a canonical resolves), image (when present), datePublished, dateModified, inLanguage, the author, and publisher (when siteName is given). The innerHTML is pre-serialized with < escaped, so a stray </script> inside a field can’t break out of the element.

The author shape follows author.kind:

  • "real" → a Person with name, plus jobTitle and sameAs when present.
  • "persona" → an Organization byline (just name). Ghost Writr never fabricates a human for a brand voice — see SEO & JSON-LD.