Skip to content

SEO meta (ArticleHead)

@ghostwritr/svelte ships SEO two ways. The <ArticleHead> component renders an Article’s <title>, Open Graph + Twitter card, the canonical <link>, and schema.org Article JSON-LD straight into <svelte:head>. Under it sits articleHead(article, opts?) — a pure function that returns plain head data so you can build your own <svelte:head>. Both are driven by the feed’s guarantees. See SEO & JSON-LD for the cross-SDK model.

Both live at the core entry:

import { ArticleHead, articleHead } from "@ghostwritr/svelte";
import type { ArticleHeadData, ArticleHeadOptions } from "@ghostwritr/svelte";

Drop <ArticleHead> into a page. It renders into <svelte:head> itself, so there’s nothing else to wire — no head manager, no useHead:

src/routes/blog/[slug]/+page.svelte
<script lang="ts">
import { ArticleHead } from "@ghostwritr/svelte";
let { data } = $props();
</script>
<!-- <title>, OG/Twitter, canonical, and schema.org Article JSON-LD into <svelte:head>. -->
<ArticleHead article={data.article} siteName="Acme" twitterSite="@acme" />
<article>
<h1>{data.article.title}</h1>
</article>

<ArticleHead> takes the article plus the three ArticleHeadOptions fields as props:

PropTypeEffect
articleArticleThe article to describe (required).
urlstringAbsolute URL of this page → <link rel="canonical"> + og:url. Falls back to article.canonicalUrl.
siteNamestringog:site_name and the JSON-LD publisher (an Organization).
twitterSitestringtwitter:site (your default @handle).

When you want a custom <svelte:head> — to merge in a robots tag, reorder, or drop a field — call articleHead(article, opts?) and render the pieces yourself. It returns an ArticleHeadData: a plain object that never touches the document, so it’s SSR-safe and side-effect-free.

src/routes/blog/[slug]/+page.svelte
<script lang="ts">
import { articleHead } from "@ghostwritr/svelte";
let { data } = $props();
const head = $derived(articleHead(data.article, { siteName: "Acme" }));
</script>
<svelte:head>
{#if head.title}<title>{head.title}</title>{/if}
{#each head.meta ?? [] as m}
<meta name={m.name} property={m.property} content={m.content} />
{/each}
{#each head.link ?? [] as l}
<link rel={l.rel} href={l.href} />
{/each}
{#each head.script ?? [] as s}
{@html `<script type="${s.type}">${s.innerHTML}<\/script>`}
{/each}
<meta name="robots" content="index,follow" />
</svelte:head>

ArticleHeadData is the shape <ArticleHead> renders for you:

FieldTypeNotes
titlestring (optional)The <title>article.seoTitle.
metaArray<{ name?, property?, content }>OG, Twitter, and article:* tags.
linkArray<{ rel: "canonical", href }>Present only when a canonical resolves.
scriptArray<{ type: "application/ld+json", innerHTML }>The Article JSON-LD. innerHTML is pre-serialized with < escaped, so a stray </script> in a field can’t break out of the element.

ArticleHeadOptions is optional — and so is every field within it.

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 ArticleHeadData with title, a meta array, an optional canonical link, and the JSON-LD script:

  • title = article.seoTitleseoTitle 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 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.