@dailyautomations/seo-platform
v0.1.3
Published
Shared SEO primitives for Daily Automations sites: sitemap, redirects, canonical, trailing-slash, slugify, security headers. Conforms to the locked /api/v1/seo/* v1 wire contract.
Readme
@dailyautomations/seo-platform
Shared SEO primitives for the dailyautomations portfolio (daily-seo, daily-coverage, fireside, dailylisten, and others). Eliminates per-site drift on canonical URLs, redirects, sitemaps, security headers, and slugs by providing a single, type-safe SDK backed by the daily-seo SEO API.
Install
npm i @dailyautomations/seo-platformEnvironment variables
| Variable | Fallback | Required |
|---|---|---|
| SEO_PLATFORM_API_URL | DAILY_SEO_API_URL | Yes |
| SEO_PLATFORM_API_KEY | DAILY_SEO_API_KEY | Yes |
Both env vars are read by loadConfigFromEnv. If neither is present, a SeoPlatformError with code CONFIG_MISSING is thrown at startup. Auth is sent as the X-API-Key request header on every API call.
Quick start
Sitemap (Next.js App Router)
// app/sitemap.xml/route.ts
import { loadConfigFromEnv, fetchSitemapEntries, renderSitemap } from '@dailyautomations/seo-platform/sitemap';
export async function GET() {
const config = loadConfigFromEnv({ siteId: process.env.SITE_ID!, domain: 'example.com' });
const entries = await fetchSitemapEntries(config);
return new Response(renderSitemap(entries, config), {
headers: { 'Content-Type': 'application/xml' },
});
}Redirect middleware (Next.js)
Import from /edge in middleware files. The edge subpath has zero node: imports and is safe for V8-isolate runtimes.
// middleware.ts
import { withRedirects, withTrailingSlash, loadConfigFromEnv } from '@dailyautomations/seo-platform/edge';
import type { NextRequest } from 'next/server';
const config = loadConfigFromEnv({ siteId: process.env.SITE_ID!, domain: 'example.com', trailingSlash: 'Always' });
export async function middleware(req: NextRequest) {
return withRedirects(config, req, (r) => withTrailingSlash(config, r));
}Security headers (next.config.js)
// next.config.js
const { withSeoPlatformHeaders } = require('@dailyautomations/seo-platform/headers');
module.exports = withSeoPlatformHeaders(
{ siteId: process.env.SITE_ID, domain: 'example.com' },
{ /* your existing next config */ }
);Slug rename
renameSlug POSTs to /api/v1/seo/slug-rename, is idempotent (a second call with the same from/to pair is a no-op), and records a 301 entry in the redirect index automatically.
import { renameSlug, loadConfigFromEnv } from '@dailyautomations/seo-platform';
const config = loadConfigFromEnv({ siteId: '...', domain: 'example.com' });
await renameSlug(config, { from: '/old-path', to: '/new-path' });Edge vs Node subpaths
| Subpath | Edge-safe | Node-only | Notes |
|---|---|---|---|
| . | No | Yes | Full SDK, includes Node fetch + fs utilities |
| ./edge | Yes | Yes | Pure subset, zero node: imports |
| ./sitemap | No | Yes | fetchSitemapEntries uses Node fetch; serializeSitemap/buildSitemapUrls are edge-safe individually |
| ./redirects | No | Yes | Includes getRedirectIndex (Node HTTP) |
| ./redirects/edge | Yes | Yes | lookupRedirect + withRedirects only |
| ./headers | No | Yes | withSeoPlatformHeaders wraps Next.js config |
| ./trailing-slash | Yes | Yes | Pure function, no I/O |
| ./canonical | Yes | Yes | Pure function, no I/O |
| ./slugs | Yes | Yes | slugify, normalize are pure |
| ./slug-rename | No | Yes | Posts to API via Node fetch |
| ./snapshot/node | No | Yes | TTL-cached redirect index for Node servers |
| ./snapshot/edge | Yes | Yes | hydrateFromSnapshot for edge hydration |
CLI: seo-platform-lint
Use as a pre-deploy CI gate. Exits 1 if any canonical overlap or redirect errors are detected.
npx seo-platform-lint \
--site-id <uuid> \
--domain example.com \
[--api-url https://daily-seo.fly.dev] \
[--api-key <key>] \
[--trailing-slash Always|Never|Preserve]Reads SEO_PLATFORM_API_URL / SEO_PLATFORM_API_KEY from env (fallback DAILY_SEO_*) when flags are omitted.
Example CI step (GitHub Actions):
- name: SEO lint
run: npx seo-platform-lint --site-id ${{ vars.SITE_ID }} --domain example.com
env:
SEO_PLATFORM_API_KEY: ${{ secrets.DAILY_SEO_API_KEY }}
SEO_PLATFORM_API_URL: ${{ vars.DAILY_SEO_API_URL }}Determinism guarantee
slugify and the sitemap serializer are golden-locked: their output is byte-identical to the Rust sibling crate seo-platform-rust. Any change to slugify output is a SemVer MAJOR bump. Do not alter slug normalization logic in a patch or minor release.
SEO rules enforced
- Canonical drift:
canonicalFornormalizes trailing-slash state to match thetrailingSlashconfig. A canonical at/pathserved at/path/causes Google to treat the page as non-canonical. - Slug renames need a redirect:
renameSlugrecords the 301 before returning. Never rename a slug without calling this function or manually inserting the redirect entry in the same deploy. - Sitemap domain must match the GSC property:
buildSitemapUrlsusesconfig.domainexclusively. Never hard-code a*.fly.devdomain in sitemap entries. - Auth must exempt public crawl targets:
/sitemap.xmland/robots.txtmust be excluded from any auth middleware.withRedirectsdoes not apply redirects to these paths.
