@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
Maintainers
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/seoReact 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:
generateSEOis the recommended API for most use cases.createSEOis 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 sourcesDatasetJsonLd— original research/data that LLMs referenceDiscussionForumPostingJsonLd— 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-generatorConvert 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:
description→summaryauthor(string) →authors(object array)category+keywords→categories(deduplicated)dateModified→updated- Constructs
linkfromsiteConfig.url+blogPathPrefix+ slug (absolutelinkoverrides viaoptions.linkare 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 titledefaultAuthor→ feed authordefaultImage→ feed image (joined to site URL; absolute URLs pass through unchanged)- Auto-sets
updatedto the most recent item date
Types
From @bliztek/seo:
SiteConfig— Site-wide configuration (URL, title suffix, site name, Twitter handle, icons)SEOProps— Input props forgenerateSEO()(title, description, OG type, article tags, i18n, etc.)SEO— Shape used bycreateSEO()FAQ— Question/answer pair forFAQPageJsonLdPostMetadata— Blog post metadata forblogPostToSEOProps()BlogPostingAuthor— Author object for blog/article componentsBlogPostingPublisher— Publisher object for blog/article componentsArticleOG— Article-specific Open Graph metadataOGType— Open Graph type unionSEOWarning— Validation warning returned byvalidateSEO()SitemapEntryOptions— Options forgenerateSitemapEntry()RobotsConfig— Configuration forgenerateRobots()RobotsRule— Individual rule for robots.txtFeedItemOutput— Feed item shape compatible with@bliztek/feed-generatorFeedItemOptions— Options forpostMetadataToFeedItem()FeedOutput— Complete feed shape compatible with@bliztek/feed-generatorBuildFeedOptions— Options forbuildFeed()
From @bliztek/seo/ai:
AICrawler— AI crawler entry with userAgent, company, and purposeCrawlerPurpose—"training" | "search" | "both"LlmsTxtConfig— Configuration forgenerateLlmsTxt()LlmsTxtSection— Section with heading and links for llms.txtLlmsTxtLink— Link entry for llms.txt (name, url, description)AIRobotsConfig— Configuration forgenerateAIRobots()
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: relativecontactUrlis now joined tourl. Previously a relativecontactUrllike/helpwas emitted verbatim into the schema (Schema.org-invalid); it now resolves to${url}/help. AbsolutecontactUrlvalues 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 fromsiteUrl+blogPathPrefix+slug),generateSEO(og:url, canonical),buildFeed(image,favicon,feedLinks),postMetadataToFeedItem(link). OG/Twitter images and icons are also handled correctly — via themetadataBasefix above rather thanjoinUrldirectly:generateSEOpassesimage/defaultImage/iconsverbatim to Next.js, which resolves relative paths againstmetadataBase(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-strippingsiteConfig.urlfrom 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:
blogPostToSEOPropsnow setsogType: "article"— Blog posts will outputog:type=articleinstead ofog:type=website. This is the correct behavior per the Open Graph spec. If you need the old behavior, pass{ ogType: "website" }in thedefaultsparameter.
All new features use optional fields — existing code compiles and runs without changes.
License
MIT
