@alufie/seo
v0.2.2
Published
Headless SEO resolution, robots, validation, and JSON-LD helpers with optional Svelte head rendering.
Downloads
512
Maintainers
Readme
@alufie/seo
Headless SEO resolution, robots controls, validation, and JSON-LD helpers with an optional Svelte head-rendering adapter.
The package owns no database, CMS, editor, routes, hostname, or application-specific schema. Applications own their metadata sources and pass plain objects into deterministic functions.
Install
pnpm add @alufie/seoInstall Svelte only when using @alufie/seo/svelte:
pnpm add sveltePackage surface
The package is subpath-only:
@alufie/seo/defaults— typed site/page definitions and title composition@alufie/seo/resolve— ordered metadata composition, canonical resolution, tags, JSON-LD@alufie/seo/robots— robots meta,robots.txt, and purpose-specific crawler policies@alufie/seo/schemas— safe and strict schema.org builders@alufie/seo/validate— input normalization and validation@alufie/seo/types— shared public types@alufie/seo/svelte— optionalSeoHeadandSeoJsonLdrenderers
There is no root @alufie/seo barrel. Explicit subpaths keep runtime intent and dependency
boundaries visible.
Quick start
import { defineSeoPage, defineSeoSite } from '@alufie/seo/defaults';
import { resolveSeoPage } from '@alufie/seo/resolve';
import { buildArticleSchema, buildOrgSchema, buildSiteSchema } from '@alufie/seo/schemas';
const site = defineSeoSite({
siteName: 'Example',
siteUrl: 'https://example.com',
description: 'Example site',
schemas: [
buildOrgSchema({
name: 'Example',
url: 'https://example.com'
}),
buildSiteSchema({
name: 'Example',
url: 'https://example.com'
})
]
});
const routeSeo = defineSeoPage({
title: 'Guide',
description: 'A practical guide.',
schemas: [
buildArticleSchema({
headline: 'Guide',
description: 'A practical guide.',
url: 'https://example.com/guides/example'
})
]
});
const seo = resolveSeoPage({
site,
layers: [routeSeo],
routePath: '/guides/example',
pageUrl: 'https://www.example.com/guides/example?preview=1',
canonicalMode: 'site-origin'
});The result contains the final title, canonical, robots value, Open Graph and Twitter inputs, alternates, and composed schemas.
Ordered metadata layers
layers is an ordered list of plain SeoInput objects. Later values override earlier scalar
values. Schema arrays compose instead of replacing one another; a later schema with the same
@id replaces the earlier entity while unrelated entities remain.
Use image: null in a later layer to clear an inherited site or section image explicitly.
const seo = resolveSeoPage({
site,
layers: [sectionDefaults, generatedMetadata, routeOverrides],
pageUrl: url,
canonicalMode: 'site-origin'
});The package does not assign meaning to a layer. It can come from code, content, a CMS, a database, or any other application-owned source.
Canonical policies
resolveSeoPage() supports:
strip-query— default; removes query and fragment from the selected URLfull-url— keeps query and fragment and resolves relative values against a validsiteUrlpath-only— returns the path, query, and fragment without an originsite-origin— forces the pathname onto the configured HTTP(S)site.siteUrlorigin
site-origin accepts same-origin absolute canonicals and relative canonicals. Foreign or
malformed explicit values are ignored. If siteUrl is missing or invalid, resolution fails
closed to a relative current-page or route path rather than returning an unverified origin.
full-url emits only an absolute HTTP(S) canonical. When a selected value is relative, siteUrl
must provide a valid origin; otherwise the resolver omits the canonical.
This policy is generic: applications choose the canonical origin through siteUrl; the package
contains no consumer-specific hostname logic.
JSON-LD and schemas
buildSeoJsonLd():
- emits an empty string when there are no schemas
- removes structurally duplicate schemas regardless of object key order
- preserves composed entities
- serializes one entity as an object and multiple entities as an array
- escapes
<as\u003cfor safe embedding in a script element
Schema builders default to safe mode. Safe mode trims strings and URLs, removes empty optional values, filters unusable collection entries, and supplies a fallback for missing required names.
const breadcrumb = buildBreadcrumbSchema({
itemListElement: [
{ name: ' Home ', item: ' https://example.com ' },
{ name: page.title, item: canonical, fallbackName: 'Page' }
]
});Use strict mode in tests, CI, or authoring workflows where incomplete required data, malformed
HTTP(S) URL fields, empty entity references, or non-finite numbers must fail. Supply id to any
first-party builder when the entity needs a stable replacement key across metadata layers:
const article = buildArticleSchema(
{
headline: page.title,
url: canonical
},
{ mode: 'strict', id: '#article' }
);Product helpers cover identifiers, categories, aggregate ratings, offers, and variant relationships.
Use buildProductGroupSchema() with productGroupID, variesBy, and hasVariant; variants can
reference the group through isVariantOf or inProductGroupWithID.
buildCustomSchema() remains the explicit escape hatch for unsupported schema.org shapes.
validateSchemaStructure() reports structural errors, including missing @type, missing or invalid
@context, malformed nested URLs, non-finite numbers, and non-serializable values.
validateSchemas() returns those errors plus search-feature eligibility warnings.
FAQPage, HowTo, and SearchAction can remain valid schema.org markup even when a search engine no
longer displays their former rich result.
Robots controls
Build page-level directives:
import { buildRobotsMeta } from '@alufie/seo/robots';
const robots = buildRobotsMeta({
index: true,
follow: true,
maxImagePreview: 'large'
});Build robots.txt:
import { buildAiCrawlerRules, buildRobotsTxt } from '@alufie/seo/robots';
const body = buildRobotsTxt({
rules: [
{ userAgent: '*', allow: '/', disallow: ['/admin/'] },
...buildAiCrawlerRules({
openAi: { search: 'allow', training: 'disallow', userFetch: 'allow' },
anthropic: { search: 'allow', training: 'disallow', userFetch: 'allow' }
})
],
sitemaps: 'https://example.com/sitemap.xml'
});Search discovery, model training, and user-directed retrieval are separate controls. Configure
each purpose explicitly; do not treat one crawler token as a universal AI switch.
buildRobotsTxt() rejects CR/LF characters in user-agent, path, and sitemap values.
Validation
import { validateSeoInput } from '@alufie/seo/validate';
const result = validateSeoInput(routeSeo, {
canonicalMode: 'site-origin',
siteUrl: site.siteUrl
});
if (!result.valid) {
console.error(result.errors);
}
console.warn(result.warnings);Relative canonicals are accepted only when a valid site-origin policy supplies the resolution
origin. Images and alternate URLs remain absolute HTTP(S) URLs. Schema structure participates in
valid; search-feature eligibility remains available separately through warnings.
Optional Svelte rendering
<script lang="ts">
import { SeoHead } from '@alufie/seo/svelte';
import type { SeoResult } from '@alufie/seo/types';
interface Props {
seo: SeoResult;
}
let { seo }: Props = $props();
</script>
<SeoHead {seo} />SeoHead renders the title, canonical, alternates, robots, description, Open Graph, Twitter,
article metadata, and JSON-LD. It never emits the obsolete meta keywords tag.
Applications that use another framework can render SeoResult, buildSeoTags(), and
buildSeoJsonLd() directly without installing Svelte.
Migrating from 0.1
Version 0.2 is intentionally breaking:
- replace
dbandfileresolver properties with orderedlayers - replace
createSeoConfig({ site })withdefineSeoSite(site) - remove
@alufie/seo/drizzle,@alufie/seo/editor, editor components, and form presets - keep persistence and administrative metadata workflows in the consuming application
- install Svelte only when importing
@alufie/seo/svelte
Development
pnpm test
pnpm check
pnpm buildThe publish gate runs all three commands. The package contents are restricted by the files
list in package.json.
Primary references:
