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

@bliztek/seo

v2.1.1

Published

Zero-dependency SEO metadata, JSON-LD structured data, sitemap/robots helpers, validation, and LLM/AI search optimization for Next.js

Readme

@bliztek/seo

Zero-dependency SEO metadata, JSON-LD structured data, sitemap/robots helpers, validation, and LLM/AI search optimization for Next.js.

Installation

pnpm add @bliztek/seo

React is an optional peer dependency — only required if you use the JSON-LD components.

Entry Points

| Import | What you get | |---|---| | @bliztek/seo | Metadata utilities, sitemap/robots helpers, validation, feed adapter, types | | @bliztek/seo/json-ld | React JSON-LD components (26 components) | | @bliztek/seo/ai | LLM/AI search optimization — llms.txt, AI crawlers, AI robots, JsonLdGraph |

Quick Start

Site Config

Create a central config object for your site:

import type { SiteConfig } from "@bliztek/seo";

export const siteConfig: SiteConfig = {
  url: "https://example.com",
  titleSuffix: "My Site",
  defaultImage: "/assets/cover.png",
  defaultAuthor: "Jane Doe",
  blogPathPrefix: "/blog/post",
  siteName: "My Site",           // → og:site_name
  twitterSite: "@mysite",        // → twitter:site
  icons: {
    icon: "/favicon.ico",
  },
};

Generate Metadata

import { generateSEO } from "@bliztek/seo";
import { siteConfig } from "./seo-config";

// Basic page metadata
export const metadata = generateSEO(
  {
    title: "About Us",
    description: "Learn more about our team.",
    url: "/about",
  },
  siteConfig
);

// Article with full OG support
export const articleMetadata = generateSEO(
  {
    title: "How We Built Our Platform",
    description: "A deep dive into our architecture.",
    url: "/blog/how-we-built",
    ogType: "article",
    twitterCreator: "@janedoe",
    imageAlt: "Architecture diagram",
    article: {
      publishedTime: "2026-01-15T00:00:00Z",
      modifiedTime: "2026-02-01T00:00:00Z",
      author: "Jane Doe",
      section: "Technology",
      tags: ["architecture", "nextjs"],
    },
    alternateLanguages: {
      es: "/es/blog/como-construimos",
      fr: "/fr/blog/comment-nous-avons-construit",
    },
  },
  siteConfig
);

Blog Post Metadata

blogPostToSEOProps automatically sets ogType: "article" and populates article OG tags from your post metadata:

import { blogPostToSEOProps, generateSEO } from "@bliztek/seo";
import { siteConfig } from "./seo-config";

const seoProps = blogPostToSEOProps(slug, postMetadata, siteConfig);
export const metadata = generateSEO(seoProps, siteConfig);

Composable SEO Objects

import { createSEO } from "@bliztek/seo";

const baseSEO = {
  website: "https://example.com",
  url: "https://example.com",
  title: "My Site",
  description: "Default description",
  image: "/cover.png",
};

const aboutSEO = createSEO(baseSEO, "/about", {
  title: "About Us",
  description: "Learn more about our team.",
});

Note: generateSEO is the recommended API for most use cases. createSEO is a lightweight alternative for building composable SEO objects.

URL Handling

All URL-shaped props throughout the library (logo, contactUrl, image, defaultImage, favicon, siteUrl, url, feedLinks.*, etc.) accept either:

  • A path relative to the site URL (e.g. /logo.png) — joined to the configured site URL.
  • An already-absolute URL (e.g. https://cdn.example.com/logo.png) — passed through unchanged.
  • A protocol-relative URL (e.g. //cdn.example.com/logo.png) — passed through unchanged.

Trailing slashes on the site URL and missing leading slashes on paths are normalized automatically.

If a URL-shaped prop is undefined or empty, the helper returns the bare base URL rather than throwing — so accidentally passing undefined to a non-optional prop (e.g. via // @ts-ignore) will produce a bare-origin URL like https://example.com in the rendered JSON-LD, not a runtime error. TypeScript types enforce non-optional props at compile time; this is a fallback contract for the rare cases TypeScript is bypassed.

OG Image URLs

import { buildOgImageUrl } from "@bliztek/seo";

const ogUrl = buildOgImageUrl("My Page Title", {
  subtitle: "Category",
  type: "blog",
});
// → "/api/og?title=My+Page+Title&subtitle=Category&type=blog"

JSON-LD Components

All 26 components render a <script type="application/ld+json"> tag with XSS-safe serialization.

BreadcrumbJsonLd

import { BreadcrumbJsonLd } from "@bliztek/seo/json-ld";

<BreadcrumbJsonLd
  items={[
    { name: "Home", url: "https://example.com" },
    { name: "Blog", url: "https://example.com/blog" },
  ]}
/>;

BlogPostingJsonLd

import { BlogPostingJsonLd } from "@bliztek/seo/json-ld";

<BlogPostingJsonLd
  title="My Post"
  description="Post description"
  date="2025-01-15"
  slug="my-post"
  siteUrl="https://example.com"
  blogPathPrefix="/blog/post"
  author={{
    name: "Jane Doe",
    url: "https://janedoe.com",
    jobTitle: "Engineer",
  }}
  publisher={{
    name: "My Site",
    url: "https://example.com",
    logoUrl: "https://example.com/logo.png",
  }}
/>;

ArticleJsonLd

import { ArticleJsonLd } from "@bliztek/seo/json-ld";

<ArticleJsonLd
  headline="How to Build a Blog"
  description="A comprehensive guide"
  datePublished="2026-01-15"
  dateModified="2026-02-01"
  url="https://example.com/articles/build-blog"
  image="https://example.com/article-img.jpg"
  articleSection="Technology"
  wordCount={2500}
  author={{ name: "Jane Doe", url: "https://janedoe.com" }}
  publisher={{
    name: "My Site",
    url: "https://example.com",
    logoUrl: "https://example.com/logo.png",
  }}
/>;

NewsArticleJsonLd

import { NewsArticleJsonLd } from "@bliztek/seo/json-ld";

<NewsArticleJsonLd
  headline="Breaking: New Discovery"
  description="Scientists announce breakthrough"
  datePublished="2026-01-15"
  url="https://news.com/discovery"
  author={[{ name: "Alice" }, { name: "Bob" }]}
  publisher={{
    name: "News Corp",
    url: "https://news.com",
    logoUrl: "https://news.com/logo.png",
  }}
  articleSection="Science"
  keywords={["science", "discovery"]}
/>;

FAQPageJsonLd

import { FAQPageJsonLd } from "@bliztek/seo/json-ld";

<FAQPageJsonLd
  faqs={[
    { question: "What is this?", answer: "A great package." },
  ]}
/>;

ProductJsonLd

import { ProductJsonLd } from "@bliztek/seo/json-ld";

<ProductJsonLd
  name="Premium Widget"
  description="The best widget money can buy"
  image="https://example.com/widget.jpg"
  brand="WidgetCo"
  sku="WIDGET-001"
  offers={{
    price: 29.99,
    priceCurrency: "USD",
    availability: "InStock",
    url: "https://example.com/buy",
  }}
  aggregateRating={{ ratingValue: 4.5, reviewCount: 100 }}
/>;

EventJsonLd

import { EventJsonLd } from "@bliztek/seo/json-ld";

<EventJsonLd
  name="Tech Conference 2026"
  startDate="2026-06-01T09:00:00"
  endDate="2026-06-03T17:00:00"
  description="The premier tech event"
  location={{ name: "Convention Center", address: "123 Main St, Springfield" }}
  offers={{ price: 199, priceCurrency: "USD", availability: "InStock" }}
  organizer={{ name: "TechEvents Inc", url: "https://techevents.com" }}
  eventStatus="EventScheduled"
  eventAttendanceMode="MixedEventAttendanceMode"
/>;

VideoJsonLd

import { VideoJsonLd } from "@bliztek/seo/json-ld";

<VideoJsonLd
  name="Product Demo"
  description="See our product in action"
  thumbnailUrl="https://example.com/thumb.jpg"
  uploadDate="2026-01-15"
  duration="PT5M30S"
  contentUrl="https://example.com/video.mp4"
  embedUrl="https://example.com/embed/video"
/>;

WebSiteJsonLd

Enables the sitelinks search box in Google:

import { WebSiteJsonLd } from "@bliztek/seo/json-ld";

<WebSiteJsonLd
  name="My Site"
  url="https://example.com"
  searchAction={{
    target: "https://example.com/search?q={search_term_string}",
  }}
/>;

HowToJsonLd

import { HowToJsonLd } from "@bliztek/seo/json-ld";

<HowToJsonLd
  name="How to Make Coffee"
  description="A simple guide to brewing coffee"
  totalTime="PT10M"
  steps={[
    { name: "Boil water", text: "Heat water to 200°F" },
    { name: "Add grounds", text: "Put coffee grounds in filter" },
    { name: "Pour and wait", text: "Pour water over grounds, wait 4 minutes" },
  ]}
  supply={["Coffee grounds", "Water", "Filter"]}
  tool={["Coffee maker", "Kettle"]}
/>;

ReviewJsonLd

import { ReviewJsonLd } from "@bliztek/seo/json-ld";

<ReviewJsonLd
  itemReviewed={{ type: "Product", name: "Widget Pro" }}
  author={{ name: "John Doe" }}
  reviewRating={{ ratingValue: 5, bestRating: 5 }}
  reviewBody="Excellent product, highly recommended!"
  datePublished="2026-01-15"
/>;

AggregateRatingJsonLd

import { AggregateRatingJsonLd } from "@bliztek/seo/json-ld";

<AggregateRatingJsonLd
  itemReviewed={{ type: "Restaurant", name: "Burger Place" }}
  ratingValue={4.2}
  reviewCount={250}
  bestRating={5}
/>;

PersonJsonLd

import { PersonJsonLd } from "@bliztek/seo/json-ld";

<PersonJsonLd
  name="Jane Doe"
  url="https://janedoe.com"
  image="https://janedoe.com/photo.jpg"
  jobTitle="Software Engineer"
  worksFor={{ name: "Tech Co", url: "https://techco.com" }}
  sameAs={["https://github.com/janedoe", "https://twitter.com/janedoe"]}
/>;

ProfilePageJsonLd

import { ProfilePageJsonLd } from "@bliztek/seo/json-ld";

<ProfilePageJsonLd
  name="Jane's Profile"
  url="https://example.com/team/jane"
  mainEntity={{
    name: "Jane Doe",
    jobTitle: "Lead Engineer",
    sameAs: ["https://github.com/janedoe"],
  }}
/>;

CourseJsonLd

import { CourseJsonLd } from "@bliztek/seo/json-ld";

<CourseJsonLd
  name="Introduction to Machine Learning"
  description="Learn the fundamentals of ML"
  provider={{ name: "Tech University", url: "https://techuni.edu" }}
  courseCode="ML101"
  hasCourseInstance={{
    courseMode: "online",
    startDate: "2026-09-01",
  }}
/>;

JobPostingJsonLd

import { JobPostingJsonLd } from "@bliztek/seo/json-ld";

<JobPostingJsonLd
  title="Senior Software Engineer"
  description="Build scalable web applications"
  datePosted="2026-01-15"
  validThrough="2026-03-15"
  hiringOrganization={{ name: "Tech Co", url: "https://techco.com" }}
  employmentType="FULL_TIME"
  remote={true}
  baseSalary={{
    currency: "USD",
    value: { minValue: 120000, maxValue: 180000 },
    unitText: "YEAR",
  }}
/>;

SoftwareAppJsonLd

import { SoftwareAppJsonLd } from "@bliztek/seo/json-ld";

<SoftwareAppJsonLd
  name="MyApp"
  operatingSystem="Windows, macOS, Linux"
  applicationCategory="DeveloperApplication"
  offers={{ price: 0, priceCurrency: "USD" }}
  aggregateRating={{ ratingValue: 4.8, reviewCount: 500 }}
/>;

ServiceJsonLd

import { ServiceJsonLd } from "@bliztek/seo/json-ld";

<ServiceJsonLd
  name="Web Development"
  description="Custom web applications"
  url="https://example.com/services/web"
  provider="My Company"
  providerUrl="https://example.com"
/>;

OrganizationJsonLd

import { OrganizationJsonLd } from "@bliztek/seo/json-ld";

<OrganizationJsonLd
  name="My Company"
  url="https://example.com"
  logo="/logo.png"
  description="We build software."
  sameAs={["https://twitter.com/mycompany"]}
/>;

LocalBusinessJsonLd

import { LocalBusinessJsonLd } from "@bliztek/seo/json-ld";

<LocalBusinessJsonLd
  name="My Company"
  url="https://example.com"
  logo="/logo.png"
  description="Local software consultancy."
  telephone="+1-555-555-5555"
  email="[email protected]"
  address={{
    locality: "Orlando",
    region: "FL",
    country: "US",
  }}
  areaServed={[
    { type: "City", name: "Orlando", containedIn: "Florida" },
  ]}
  sameAs={["https://twitter.com/mycompany"]}
/>;

ItemListJsonLd

For carousels and collection pages:

import { ItemListJsonLd } from "@bliztek/seo/json-ld";

<ItemListJsonLd
  items={[
    { url: "https://example.com/item-1", name: "Item 1" },
    { url: "https://example.com/item-2", name: "Item 2" },
    { url: "https://example.com/item-3", name: "Item 3" },
  ]}
  itemListOrder="ascending"
/>;

GenericJsonLd

Escape hatch for any Schema.org type not covered by the typed components:

import { GenericJsonLd } from "@bliztek/seo/json-ld";

<GenericJsonLd
  type="MedicalCondition"
  data={{
    name: "Common Cold",
    description: "A viral infectious disease of the upper respiratory tract",
    possibleTreatment: { "@type": "MedicalTherapy", name: "Rest and fluids" },
  }}
/>;

Sitemap Helpers

Utilities that return objects compatible with Next.js sitemap.ts:

// app/sitemap.ts
import { generateSitemapEntry, generateSitemap } from "@bliztek/seo";
import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return generateSitemap([
    { url: "https://example.com", options: { priority: 1.0, changeFrequency: "daily" } },
    { url: "https://example.com/about", options: { priority: 0.8 } },
    { url: "https://example.com/blog", options: { changeFrequency: "weekly" } },
  ]);
}

Priority is validated to be between 0 and 1. lastModified accepts both Date objects and ISO date strings.

Robots.txt Helpers

Generate a robots.txt config compatible with Next.js robots.ts:

// app/robots.ts
import { generateRobots } from "@bliztek/seo";
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return generateRobots({
    siteUrl: "https://example.com",
    sitemapUrl: "/sitemap.xml",
    rules: [
      { userAgent: "*", allow: "/", disallow: "/admin" },
      { userAgent: "Googlebot", allow: "/" },
    ],
  });
}

// For staging/preview environments:
export default function robots(): MetadataRoute.Robots {
  return generateRobots({
    siteUrl: "https://staging.example.com",
    disallowAll: true,
  });
}

SEO Validation

validateSEO is a standalone, pure function that returns warnings without side effects. Use it in dev mode, tests, or CI:

import { validateSEO } from "@bliztek/seo";

const warnings = validateSEO(
  {
    title: "This is a very long title that exceeds sixty characters and will be truncated in search results",
    description: "Short",
    url: "/page",
  },
  siteConfig
);

// warnings = [
//   { field: "title", severity: "warning", message: "Title is 93 characters (recommended: 60 or fewer)", value: 93 },
//   { field: "description", severity: "warning", message: "Description is 5 characters (recommended: 50 or more)", value: 5 },
// ]

// Use in tests:
import { expect, it } from "vitest";

it("has valid SEO metadata", () => {
  const warnings = validateSEO(seoProps, siteConfig);
  const errors = warnings.filter((w) => w.severity === "error");
  expect(errors).toHaveLength(0);
});

Validation rules:

| Rule | Severity | Condition | |---|---|---| | Title empty | error | No title | | Title too long | warning | > 60 characters | | Description empty | error | No description | | Description too long | warning | > 160 characters | | Description too short | warning | < 50 characters | | Missing OG image | warning | No image and no default | | No URL or canonical | info | Both empty | | URL missing slash | warning | Doesn't start with / | | Too many keywords | info | > 10 keywords |

LLM / AI Search Optimization

The @bliztek/seo/ai entry point provides tools for optimizing your site for AI-powered search engines (ChatGPT, Perplexity, Google AI Overviews, etc.).

llms.txt Generation

Generate a llms.txt file that helps LLMs understand your site:

// app/llms.txt/route.ts
import { generateLlmsTxt } from "@bliztek/seo/ai";

export function GET() {
  const content = generateLlmsTxt({
    name: "My Company",
    summary: "We build developer tools for the modern web.",
    sections: [
      {
        heading: "Documentation",
        links: [
          { name: "Getting Started", url: "/docs/start", description: "Quick start guide" },
          { name: "API Reference", url: "/docs/api", description: "Full API documentation" },
        ],
      },
      {
        heading: "Products",
        links: [
          { name: "Widget Pro", url: "/products/pro", description: "Our flagship product" },
        ],
      },
    ],
    optional: [
      { name: "Blog", url: "/blog", description: "Company blog" },
      { name: "Careers", url: "/careers" },
    ],
  });

  return new Response(content, {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

AI Crawler Management

A registry of 28+ known AI crawler user agents with metadata (company, purpose):

import {
  AI_CRAWLERS,
  getTrainingCrawlers,
  getSearchCrawlers,
  getCrawlerUserAgents,
} from "@bliztek/seo/ai";

// All crawlers with metadata
console.log(AI_CRAWLERS);
// [{ userAgent: "GPTBot", company: "OpenAI", purpose: "training" }, ...]

// Just training crawlers (GPTBot, ClaudeBot, CCBot, etc.)
const training = getTrainingCrawlers();

// Just search/citation crawlers (OAI-SearchBot, PerplexityBot, etc.)
const search = getSearchCrawlers();

// Get user-agent strings only (useful for custom robots.txt logic)
const allAgents = getCrawlerUserAgents("all");
const trainingAgents = getCrawlerUserAgents("training");
const searchAgents = getCrawlerUserAgents("search");

AI-Enhanced Robots.txt

Generate robots.txt with AI-specific presets:

// app/robots.ts
import { generateAIRobots } from "@bliztek/seo/ai";

export default function robots() {
  return generateAIRobots({
    siteUrl: "https://example.com",
    blockAITraining: true,    // Block GPTBot, ClaudeBot, CCBot, etc.
    allowAISearch: true,      // Allow OAI-SearchBot, PerplexityBot, etc.
    sitemapUrl: "/sitemap.xml",
    additionalRules: [
      { userAgent: "*", allow: "/", disallow: "/admin" },
    ],
  });
}

// Block ALL AI crawlers (training + search):
generateAIRobots({ siteUrl: "https://example.com", blockAllAI: true });

JSON-LD Knowledge Graph

Compose multiple schema types into a connected @graph with @id references. This is the #1 signal AI search engines use for entity disambiguation:

import { JsonLdGraph } from "@bliztek/seo/ai";

<JsonLdGraph items={[
  {
    "@type": "Organization",
    "@id": "#org",
    name: "My Company",
    url: "https://mycompany.com",
    knowsAbout: ["Web Development", "AI", "SEO"],
  },
  {
    "@type": "WebSite",
    "@id": "#site",
    name: "My Company Blog",
    url: "https://mycompany.com",
    publisher: { "@id": "#org" },
  },
  {
    "@type": "Article",
    headline: "How to Optimize for AI Search",
    author: { "@id": "#author" },
    publisher: { "@id": "#org" },
  },
]} />;

Speakable Markup

Add speakable to Article, BlogPosting, NewsArticle, and TechArticle to flag the most citable passages for AI search:

import { ArticleJsonLd } from "@bliztek/seo/json-ld";

<ArticleJsonLd
  headline="How to Optimize for AI Search"
  description="A guide to getting cited by ChatGPT and Perplexity"
  datePublished="2026-01-15"
  url="https://example.com/ai-seo"
  author={{ name: "Jane Doe" }}
  publisher={{ name: "My Co", url: "https://myco.com", logoUrl: "/logo.png" }}
  speakable={[".article-summary", ".key-takeaways"]}
  mainEntityOfPage="https://example.com/ai-seo"
/>;

AI-Optimized JSON-LD Components

New components specifically valuable for AI search:

  • ClaimReviewJsonLd — fact-checking schema; AI overviews treat these as high-trust sources
  • DatasetJsonLd — original research/data that LLMs reference
  • DiscussionForumPostingJsonLd — community content (Perplexity cites forums heavily)
  • TechArticleJsonLd — technical documentation with proficiency level and speakable
import {
  ClaimReviewJsonLd,
  DatasetJsonLd,
  DiscussionForumPostingJsonLd,
  TechArticleJsonLd,
} from "@bliztek/seo/json-ld";

<ClaimReviewJsonLd
  url="https://factcheck.com/review/123"
  claimReviewed="The earth is flat"
  author={{ name: "Flat Earth Society", type: "Organization" }}
  reviewRating={{ ratingValue: 1, bestRating: 5, alternateName: "False" }}
  publisher={{ name: "FactCheck.com", url: "https://factcheck.com" }}
/>;

<DatasetJsonLd
  name="Global Temperature Data"
  description="Monthly global temperature anomalies"
  creator={{ name: "Climate Institute", type: "Organization" }}
  license="https://creativecommons.org/licenses/by/4.0/"
  temporalCoverage="1880/2026"
  distribution={[
    { contentUrl: "https://data.climate.org/temps.csv", encodingFormat: "text/csv" },
  ]}
/>;

<DiscussionForumPostingJsonLd
  headline="How to optimize for AI search?"
  text="I'm looking for tips on getting cited by ChatGPT and Perplexity..."
  url="https://forum.com/post/123"
  author={{ name: "Alice", url: "https://forum.com/u/alice" }}
  datePublished="2026-01-15"
  commentCount={12}
  upvoteCount={42}
  comment={[
    { text: "Great question! Use structured data.", author: { name: "Bob" }, datePublished: "2026-01-16" },
  ]}
/>;

<TechArticleJsonLd
  headline="React Server Components: A Deep Dive"
  description="Everything you need to know about RSC architecture"
  datePublished="2026-01-15"
  url="https://dev.co/rsc-guide"
  author={{ name: "Jane Doe", url: "https://janedoe.com" }}
  publisher={{ name: "Dev Co", url: "https://dev.co", logoUrl: "https://dev.co/logo.png" }}
  proficiencyLevel="Expert"
  dependencies="React 19, Next.js 15"
  speakable={[".article-intro", ".key-points"]}
  mainEntityOfPage="https://dev.co/rsc-guide"
/>;

knowsAbout (Topical Authority)

Add knowsAbout to Person and Organization schemas to signal topical expertise to AI search:

<PersonJsonLd
  name="Jane Doe"
  jobTitle="Senior Engineer"
  knowsAbout={["Machine Learning", "Next.js", "SEO Optimization"]}
  sameAs={["https://github.com/janedoe", "https://linkedin.com/in/janedoe"]}
/>;

<OrganizationJsonLd
  name="Tech Co"
  url="https://techco.com"
  logo="/logo.png"
  description="We build AI tools"
  knowsAbout={["Artificial Intelligence", "Cloud Infrastructure", "Developer Tools"]}
  sameAs={["https://en.wikipedia.org/wiki/TechCo", "https://www.crunchbase.com/organization/techco"]}
/>;

Feed Generator Integration

Bridge your PostMetadata to @bliztek/feed-generator with zero manual mapping:

pnpm add @bliztek/feed-generator

Convert posts to feed items

import { postMetadataToFeedItem } from "@bliztek/seo";
import { siteConfig } from "./seo-config";

const feedItems = posts.map(({ slug, metadata }) =>
  postMetadataToFeedItem(slug, metadata, siteConfig)
);

postMetadataToFeedItem maps PostMetadata fields to feed-generator's FeedItem shape:

  • descriptionsummary
  • author (string) → authors (object array)
  • category + keywordscategories (deduplicated)
  • dateModifiedupdated
  • Constructs link from siteConfig.url + blogPathPrefix + slug (absolute link overrides via options.link are passed through)

Build a complete feed

import { postMetadataToFeedItem, buildFeed } from "@bliztek/seo";
import { generateFeeds } from "@bliztek/feed-generator";
import { siteConfig } from "./seo-config";

const items = posts.map(({ slug, metadata }) =>
  postMetadataToFeedItem(slug, metadata, siteConfig)
);

const feed = buildFeed(siteConfig, items, {
  description: "Latest posts from our blog",
  language: "en",
  copyright: `© ${new Date().getFullYear()} My Company`,
  favicon: "/favicon.ico",
  feedLinks: {
    rss: "/feed.xml",
    atom: "/atom.xml",
    json: "/feed.json",
  },
});

const { rss, atom, json } = generateFeeds(feed);

buildFeed derives feed metadata from your existing SiteConfig:

  • siteName → feed title
  • defaultAuthor → feed author
  • defaultImage → feed image (joined to site URL; absolute URLs pass through unchanged)
  • Auto-sets updated to the most recent item date

Types

From @bliztek/seo:

  • SiteConfig — Site-wide configuration (URL, title suffix, site name, Twitter handle, icons)
  • SEOProps — Input props for generateSEO() (title, description, OG type, article tags, i18n, etc.)
  • SEO — Shape used by createSEO()
  • FAQ — Question/answer pair for FAQPageJsonLd
  • PostMetadata — Blog post metadata for blogPostToSEOProps()
  • BlogPostingAuthor — Author object for blog/article components
  • BlogPostingPublisher — Publisher object for blog/article components
  • ArticleOG — Article-specific Open Graph metadata
  • OGType — Open Graph type union
  • SEOWarning — Validation warning returned by validateSEO()
  • SitemapEntryOptions — Options for generateSitemapEntry()
  • RobotsConfig — Configuration for generateRobots()
  • RobotsRule — Individual rule for robots.txt
  • FeedItemOutput — Feed item shape compatible with @bliztek/feed-generator
  • FeedItemOptions — Options for postMetadataToFeedItem()
  • FeedOutput — Complete feed shape compatible with @bliztek/feed-generator
  • BuildFeedOptions — Options for buildFeed()

From @bliztek/seo/ai:

  • AICrawler — AI crawler entry with userAgent, company, and purpose
  • CrawlerPurpose"training" | "search" | "both"
  • LlmsTxtConfig — Configuration for generateLlmsTxt()
  • LlmsTxtSection — Section with heading and links for llms.txt
  • LlmsTxtLink — Link entry for llms.txt (name, url, description)
  • AIRobotsConfig — Configuration for generateAIRobots()

Security

All JSON-LD components use safeJsonLd() to prevent XSS via </script> injection in structured data. The < character is escaped to \u003c in all serialized output.

Migration

From 2.1.0 to 2.1.1

Bug-fix release. No breaking type changes. Two behavioral notes:

  • OrganizationJsonLd / LocalBusinessJsonLd: relative contactUrl is now joined to url. Previously a relative contactUrl like /help was emitted verbatim into the schema (Schema.org-invalid); it now resolves to ${url}/help. Absolute contactUrl values are unchanged.
  • All URL-shaped props across the public API now accept absolute, protocol-relative, or relative inputs interchangeably. Spot-check any of these if you pass them: OrganizationJsonLd.{logo,contactUrl}, LocalBusinessJsonLd.{logo,contactUrl}, BlogPostingJsonLd (built from siteUrl + blogPathPrefix + slug), generateSEO (og:url, canonical), buildFeed (image, favicon, feedLinks), postMetadataToFeedItem (link). OG/Twitter images and icons are also handled correctly — via the metadataBase fix above rather than joinUrl directly: generateSEO passes image / defaultImage / icons verbatim to Next.js, which resolves relative paths against metadataBase (now pinned to site origin) at SSR. If you were already passing relative paths (the documented usage), output is identical. Consumers who were working around the old double-prefix bug by hand-stripping siteConfig.url from their inputs should remove that workaround.

See the CHANGELOG for the full bug-fix list.

From v1 to v2

v2 is backward compatible with one behavioral change:

  • blogPostToSEOProps now sets ogType: "article" — Blog posts will output og:type=article instead of og:type=website. This is the correct behavior per the Open Graph spec. If you need the old behavior, pass { ogType: "website" } in the defaults parameter.

All new features use optional fields — existing code compiles and runs without changes.

License

MIT