@alufie/seo
v0.1.4
Published
Modular SEO helpers, schema builders, Svelte components, and Drizzle storage for SvelteKit.
Maintainers
Readme
@alufie/seo
SEO building blocks for SvelteKit.
@alufie/seo gives you:
- site and route SEO config
- final SEO resolution
<head>rendering- JSON-LD/schema helpers
- optional Drizzle storage
- optional editor and form helpers
The package is subpath-only.
Use explicit imports like:
@alufie/seo/defaults@alufie/seo/resolve@alufie/seo/schemas@alufie/seo/drizzle@alufie/seo/editor@alufie/seo/validate@alufie/seo/svelte@alufie/seo/types
Index
- Install
- Import Style
- Package Hierarchy
- Quick Start
- Files To Create In Your App
- Step 1: Create Site Defaults
- Step 2: Return SEO From Routes
- Step 3: Render SEO In Root Layout
- Optional: Store Route SEO In DB
- Optional: Use The Editor
- Functions And Components By Module
- Common App File Layout
- Publish And Security Notes
- Potential Todo
Install
pnpm add @alufie/seoor:
npm install @alufie/seoImport Style
Use explicit subpaths.
Example:
import { createSeoConfig, defineSeoSite } from '@alufie/seo/defaults';
import { resolveSeoPage } from '@alufie/seo/resolve';
import { buildBreadcrumbSchema, buildFaqSchema } from '@alufie/seo/schemas';
import { SeoHead } from '@alufie/seo/svelte';
import type { SeoInput } from '@alufie/seo/types';Why:
- avoids accidental client bundling of server-only code
- keeps each import’s intent obvious
- matches the package’s module boundaries
Package Hierarchy
Think of the package like this:
Subpath: @alufie/seo/defaults
Use this if you only want config and title/meta helpers.
Subpath: @alufie/seo/resolve
Use this if you only want resolution and final tag or JSON-LD output.
Subpath: @alufie/seo/schemas
Use this if you only want schema builders.
Subpath: @alufie/seo/drizzle
Use this if you only want DB table and store helpers.
Subpath: @alufie/seo/editor
Use this if you only want form parsing, editor loading, and save helpers.
Subpath: @alufie/seo/validate
Use this if you only want normalization and validation.
Subpath: @alufie/seo/svelte
Use this if you only want Svelte components.
Subpath: @alufie/seo/types
Use this for public shared types.
Quick Start
The normal setup is:
- create one SEO config file
- return
seofrom routes when needed - resolve and render in root layout
That is enough for a working setup.
Files To Create In Your App
Minimum setup:
src/lib/seo.tssite-wide SEO defaultssrc/routes/+layout.sveltefinal SEO resolution and rendering
Optional setup:
src/routes/**/+page.tsroute-level SEOsrc/routes/**/+layout.tssection-level SEOsrc/lib/server/seo-store.tsDB-backed route SEOsrc/routes/admin/...editor pages usingSeoEditoror form helpers
Step 1: Create Site Defaults
Create:
src/lib/seo.ts
import { createSeoConfig, defineSeoSite } from '@alufie/seo/defaults';
export const seoConfig = createSeoConfig({
site: defineSeoSite({
siteName: 'Example Site',
siteUrl: 'https://example.com',
description: 'Clear default description for the whole site.',
locale: 'en_US',
image: 'https://example.com/og-default.jpg',
titleJoin: ' | '
})
});This file gives you:
- fallback site title
- fallback description
- canonical base URL
- default OG image
- locale
Step 2: Return SEO From Routes
You can return SEO from:
+page.ts+layout.ts+page.server.ts+layout.server.ts
Example:
src/routes/guides/[slug]/+page.ts
import { defineSeoPage } from '@alufie/seo/defaults';
import { buildBreadcrumbSchema, buildFaqSchema } from '@alufie/seo/schemas';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, url }) => {
const canonical = new URL(url.pathname, url.origin).toString();
const title = `Guide: ${params.slug}`;
return {
seo: defineSeoPage({
title,
description: 'A practical guide page.',
canonical,
ogType: 'article',
publishedAt: '2026-03-25T00:00:00.000Z',
schemas: [
buildBreadcrumbSchema({
fallbackName: 'Guide',
itemListElement: [
{ name: 'Home', item: url.origin },
{ name: 'Guides', item: `${url.origin}/guides` },
{ name: params.slug, item: canonical, fallbackName: title }
]
}),
buildFaqSchema({
mainEntity: [
{
name: 'Who is this page for?',
acceptedAnswer: {
text: 'Anyone who needs a clear example.'
}
}
]
})
]
})
};
};Use +layout.ts the same way if you want section-wide defaults.
Schema validation and fallbacks
Schema builders default to safe mode because route data is often fed by a CMS. Safe mode trims text and URLs, omits empty optional fields, filters unusable FAQ/HowTo rows, and uses breadcrumb fallback labels instead of emitting empty names.
import { buildBreadcrumbSchema } from '@alufie/seo/schemas';
import { validateSchemas } from '@alufie/seo/validate';
const breadcrumb = buildBreadcrumbSchema({
fallbackName: 'Page',
itemListElement: [
{ name: 'Home', item: 'https://example.com' },
{ name: page.name, item: canonical, fallbackName: 'Article' }
]
});
const warnings = validateSchemas([breadcrumb]);Use strict mode in tests or CI when missing required fields should fail loudly:
buildBreadcrumbSchema(
{
itemListElement: [{ name: page.name, item: canonical }]
},
{ mode: 'strict' }
);buildTrailSchema() remains available for compatibility. Prefer buildBreadcrumbSchema() in new code because it matches Google’s breadcrumb structured data terminology.
Step 3: Render SEO In Root Layout
Create or update:
src/routes/+layout.svelte
<script lang="ts">
import { page } from '$app/state';
import { resolveSeoPage } from '@alufie/seo/resolve';
import { SeoHead } from '@alufie/seo/svelte';
import { seoConfig } from '$lib/seo';
let { children } = $props();
const seo = $derived(
resolveSeoPage({
site: seoConfig.site,
routePath: page.url.pathname,
pageUrl: page.url,
file: page.data.seo
})
);
</script>
<SeoHead seo={seo} />
{@render children?.()}Import map for this file:
- from your app:
seoConfig - from
@alufie/seo/resolve:resolveSeoPage - from
@alufie/seo/svelte:SeoHead - from SvelteKit:
page
Optional: Store Route SEO In DB
Use this if you want route-level SEO records in a database.
Install drizzle-orm in your app before using the Drizzle helpers:
pnpm add drizzle-ormCreate src/lib/server/seo-store.ts
import { createSeoStore } from '@alufie/seo/drizzle';
import { db } from '$lib/server/db';
export const seoStore = createSeoStore(db);createSeoStore(db) uses the SQLite schema by default. For PostgreSQL, pass dialect: 'pg':
export const seoStore = createSeoStore(db, {
dialect: 'pg'
});Migrations are app-owned. The package provides Drizzle table builders and store helpers; it does not generate or ship app migrations. Add a uniqueness constraint for (route_key, locale) in your app schema or migration because store lookups treat that pair as logically unique. Decide explicitly how your database should represent the default locale, especially when mixing locale = null with locale strings such as en or zh-TW.
Read a stored record in a route
import { defineSeoPage } from '@alufie/seo/defaults';
import { seoStore } from '$lib/server/seo-store';
const stored = await seoStore.findByRoute('/about');
return {
seo: defineSeoPage({
...stored?.input
})
};Merge DB SEO and route SEO
import { resolveSeoPage } from '@alufie/seo/resolve';
const seo = resolveSeoPage({
site: seoConfig.site,
db: stored?.input,
file: page.data.seo,
routePath: page.url.pathname,
pageUrl: page.url
});Use a custom table name
import { createSeoStore } from '@alufie/seo/drizzle';
export const seoStore = createSeoStore(db, {
tableName: 'route_seo',
routeKeyColumn: 'path_key'
});Optional: Use The Editor
Use this if you want a simple admin or CMS flow.
Render SeoEditor
<script lang="ts">
import { SeoEditor } from '@alufie/seo/svelte';
const value = {
routeKey: '/about',
enabled: true,
input: {
title: 'About',
description: 'About page'
}
};
</script>
<SeoEditor value={value} />Parse posted form data
import { parseSeoFormData } from '@alufie/seo/editor';
const formData = await request.formData();
const result = parseSeoFormData(formData);Save directly to the built-in store
import { saveSeoForm } from '@alufie/seo/editor';
const formData = await request.formData();
const saved = await saveSeoForm(db, seoTable, formData, {});Load editor data
import { loadSeoEditor } from '@alufie/seo/editor';
const editorValue = await loadSeoEditor(seoStore, '/about');Functions And Components By Module
@alufie/seo/defaults
Smaller config-only surface:
createSeoConfig()defineSeoSite()defineSeoPage()buildSeoTitle()buildSeoMeta()
@alufie/seo/resolve
Resolution-only surface:
resolveSeoPage()mergeSeoInput()buildSeoJsonLd()buildSeoTags()
@alufie/seo/schemas
Schema-builder-only surface:
- all
build*Schema()helpers buildBreadcrumbSchema()is the preferred breadcrumb helperbuildTrailSchema()is kept as a backwards-compatible alias
@alufie/seo/drizzle
DB-only surface:
createSeoTable()createSqliteSeoTable()createPgSeoTable()createSeoStore()defineSeoStore()createSeoQueries()findSeoByRoute()upsertSeoByRoute()listSeoRecords()deleteSeoByRoute()getSeoRecord()saveSeoRecord()
@alufie/seo/editor
Editor and form-only surface:
serializeSeoInput()parseSeoFormData()loadSeoEditor()saveSeoForm()saveSeoEditor()
@alufie/seo/validate
Validation-only surface:
normalizeSeoInput()validateSeoInput()validateSchemas()createSeoEditorValue()
@alufie/seo/svelte
Svelte-components-only surface:
SeoHeadSeoJsonLdSeoFormSeoEditorArticleSchemaFieldsFaqSchemaFieldsMedicalSchemaFieldsProductSchemaFieldsTrailSchemaFields
@alufie/seo/types
Shared public types:
SeoInputSeoPageSeoSiteSeoSiteProfileSeoRecordSeoResultSeoConfigSeoSchemaSeoEditorValue
Common App File Layout
src/
lib/
seo.ts
server/
seo-store.ts
routes/
+layout.svelte
about/
+page.ts
admin/
seo/
+page.server.ts
+page.sveltePublish And Security Notes
- package name:
@alufie/seo - publish access: public
- security policy: see
SECURITY.md - package contents are limited by the
fileslist inpackage.json
Potential Todo
- add an optional convenience facade like
createSeoKit()that wraps common flows without replacing the modular API
