npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jorgejhms/astro-seo-meta-tags

v0.4.0

Published

A drop-in Astro component for SEO meta tags: Open Graph, Twitter Cards, canonical URLs and JSON-LD structured data (Product schema).

Readme

@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.