npm.io
0.4.0 • Published yesterday

@jorgejhms/astro-seo-meta-tags

Licence
MPL-2.0
Version
0.4.0
Deps
0
Size
46 kB
Vulns
0
Weekly
0

@jorgejhms/astro-seo-meta-tags

A drop-in Astro component for SEO meta tags: Open Graph, Twitter Cards, canonical URLs and JSON-LD structured data — all from a single <MetaTags />.

  • One component, all the <head> tags you usually need.
  • Pass a single metadata object — ideal for data coming from a CMS, frontmatter or an API.
  • Open Graph + Twitter Card support out of the box (Twitter falls back to OG, so you only set what differs).
  • Canonical URL resolved automatically from Astro.site (or an explicit override), with query params (UTM, …) stripped by default.
  • Canonical omitted automatically when noindex is set (per Google's recommendation).
  • Robots enriched by default (max-snippet:-1, max-image-preview:large, max-video-preview:-1), fully overridable.
  • Generic JSON-LD via the jsonLd prop for any schema.org type (Product, Article, Organization, …). Relative image/logo/url values are resolved to absolute URLs against your site.
  • Safe JSON-LD embedding (escapes </script> breakouts).
  • Dev-only SEO warnings: console.warn when your <title> or meta description exceed Google's recommended length — caught while you write, stripped from production.
  • SSR & Edge-safe by design: no fs, no child_process, no build-time integration — renders identically in SSG, SSR (Node), and Edge runtimes.
  • Published as native .astro + .ts (no build step, per Astro's packaging guide).

Installation

npm install @jorgejhms/astro-seo-meta-tags
# or: pnpm add / yarn add / bun add

astro is a peer dependency (Astro 4, 5, 6, or 7).

Usage

Set it up in your base layout

The most common pattern: render <MetaTags /> once inside your site's base layout, then pass page-specific metadata to that layout from each page. The layout just forwards its metadata prop straight to the component — no duplication, no boilerplate per page.

1. Create a base layoutsrc/layouts/BaseLayout.astro:

---
import { MetaTags, type Metadata } from "@jorgejhms/astro-seo-meta-tags";

interface Props {
  metadata: Metadata;
}

const { metadata } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <MetaTags metadata={metadata} />
  </head>
  <body>
    <slot />
  </body>
</html>

2. Pass metadata from each pagesrc/pages/index.astro:

---
import BaseLayout from "../layouts/BaseLayout.astro";
---

<BaseLayout
  metadata={{
    title: "Kind of Blue — Miles Davis (180g Vinyl)",
    titleTemplate: "%s | Example Records",
    description:
      "1959 jazz masterpiece, 180g audiophile pressing. In stock, ships worldwide.",
    featuredImage: "https://example.com/img/kind-of-blue.jpg",
    ogImageWidth: 1200,
    ogImageHeight: 675,
    ogImageAlt: "Kind of Blue album cover",
    twitter: { card: "summary_large_image", site: "@examplerecords" },
    ogProperties: { locale: "en_US", site_name: "Example Records" },
  }}
>
  <!-- page content -->
</BaseLayout>

Set site in astro.config.mjs so canonical URLs — and relative JSON-LD images — resolve to absolute URLs automatically.

Pulling metadata from a CMS, frontmatter or an API? The metadata object is plain data, so you can fetch it anywhere and pass it straight through — including into jsonLd (see below).

Images

featuredImage (rendered as og:image) accepts the three image sources Astro documents and always resolves to an absolute URL (required by social crawlers):

Source What you pass Resolved to
Imported (src/) the ImageMetadata import image.src → absolute against site
public/ a root-relative path "/og.jpg" → absolute against site
External / CDN a full URL used as-is

Imported images are detected automatically; their built-in width/height also populate og:image:width / og:image:height unless you set them explicitly.

---
import BaseLayout from "../layouts/BaseLayout.astro";
import ogImage from "../assets/og.jpg"; // ImageMetadata
---

<BaseLayout
  metadata={{
    title: "Kind of Blue — Miles Davis",
    description: "1959 jazz masterpiece.",
    featuredImage: ogImage, // imported → .src resolved, dims auto-filled
    // featuredImage: "/og.jpg",                   // from public/
    // featuredImage: "https://cdn.example.com/og.jpg", // external
    ogImageAlt: "Kind of Blue album cover",
  }}
/>

og:image must be absolute. Set site in astro.config.mjs so imported and public/ paths resolve correctly.

Structured data (JSON-LD)

Pass any schema.org object — or an array of them — via jsonLd. Each is rendered in its own sanitized <script type="application/ld+json">. Relative URLs in the standard URL-bearing fields (image, logo, url) are resolved against your site, in any shape: a URL string, a list of strings, or an ImageObject.

---
import BaseLayout from "../layouts/BaseLayout.astro";

const name = "Kind of Blue (180g Vinyl)";
const description =
  "Miles Davis — 1959 jazz masterpiece, 180g audiophile pressing.";

const product = {
  "@context": "https://schema.org",
  "@type": "Product",
  name,
  description,
  image: "/img/kind-of-blue.jpg", // → https://example.com/img/kind-of-blue.jpg
  sku: "COL-CL-1355",
  offers: {
    "@type": "Offer",
    url: "/products/kind-of-blue", // → https://example.com/products/kind-of-blue
    price: "89.00",
    priceCurrency: "USD",
    availability: "https://schema.org/InStock",
  },
};
---

<BaseLayout
  metadata={{
    title: name,
    description,
    featuredImage: "https://example.com/img/kind-of-blue.jpg",
    jsonLd: product,
  }}
/>

jsonLd accepts any schema.org type — Article, WebPage, Organization, FAQPage, etc. Need several blocks (e.g. a Product and a BreadcrumbList)? Pass an array: jsonLd={[product, breadcrumbs]}.

You can also resolve/sanitize manually with the exported helpers (e.g. when generating JSON-LD in an API endpoint):

import {
  resolveJsonLd,
  sanitizeJsonLd,
} from "@jorgejhms/astro-seo-meta-tags/schema";

const resolved = resolveJsonLd(jsonLdObject, "https://example.com");
const safe = sanitizeJsonLd(JSON.stringify(resolved)); // for set:html
SSR & Edge compatibility

The component and its helpers contain no node:*, fs, child_process, or build-time hooks — only Astro.url / Astro.site, the URL constructor, and plain string ops. It therefore renders identically in:

  • Static generation (SSG) — the default.
  • SSR on Node (e.g. @astrojs/node).
  • SSR on the edge (Cloudflare Workers/Pages, Vercel Edge) where Node APIs are unavailable.
Dev-only SEO length warnings

Google truncates long titles and meta descriptions in search results (by display width, not a fixed character count). While you're developing, <MetaTags /> emits a console.warn when:

  • the rendered <title> exceeds 65 characters, or
  • the <meta name="description"> exceeds 155 characters.

These warnings are dev-only — guarded by import.meta.env.DEV, so Vite removes them from your production build (no runtime cost, no log noise).

Tune (or disable) the thresholds with environment variables in your .env:

# Defaults shown — set either to 0 to disable that check.
SEO_TITLE_MAX=65
SEO_DESCRIPTION_MAX=155

The numbers aren't Google-mandated: there's no official character limit, and truncation is based on pixel width. 65 / 155 are sensible, widely-used defaults that correspond roughly to what Google typically displays.

Props

<MetaTags metadata={...} />
Field Type Default Description
title string (req.) Page <title>; also og:title.
titleTemplate string Title template with a %s placeholder, e.g. "%s | My Site".
description string (req.) Meta description; also og:description.
keywords string Comma-separated meta keywords.
author string <meta name="author">.
copyright string <meta name="copyright">.
themeColor string <meta name="theme-color"> (e.g. "#ffffff").
featuredImage string | ImageMetadata og:image. Imported src/ image, public/ path, or full URL; resolved to an absolute URL. Imported images also fill ogImageWidth/ogImageHeight.
ogImageWidth number | string og:image:width (recommended for Google Discover). Auto-filled from imported images.
ogImageHeight number | string og:image:height. Auto-filled from imported images.
ogImageAlt string og:image:alt.
type string "website" Open Graph og:type.
canonicalUrl string current URL Canonical URL override.
siteUrl string Astro.site Base origin for canonical + JSON-LD URL resolution.
preserveQueryParams boolean false Keep query params on the canonical (default strips UTM, etc.).
twitter TwitterOverride Twitter Card config. Only set twitter:* tags are emitted.
twitterCard string (legacy alias for twitter.card).
twitterSite string (legacy alias for twitter.site).
ogProperties Record<string, string> Extra og:* properties (e.g. { locale, site_name }).
jsonLd JsonLd Arbitrary JSON-LD object(s); relative image/logo/url resolved against siteUrl, sanitized, rendered as ld+json.
robots string (enriched) Verbatim robots directive; overrides the computed default.
noindex boolean false Emit noindex, nofollow (also omits canonical). Ignored if robots is set.

License

Mozilla Public License Version 2.0 (MPL-2.0).

MPL-2.0 is a weak copyleft license: you can use, modify and distribute this component in projects of any license (including proprietary), but any modifications you make to these component files themselves must be redistributed under MPL-2.0 with source available.