v0.3.1
svelte-selfheal
/the-article-title-1 looks right in Google. Your load still fetches article 1. Break the slug and you get a 301 to whatever title you have now.
Try it
Wrong slugs 301 to /the-article-title-1. Missing IDs 404. Click one and watch the address bar.
Setup
Define healArticle once. run() or stack() in load parses the param, fetches your row, compares the slug. Canonical URL? One fetch, typed Article (or a tuple for nested routes) in result.resources. Off slug? redirect(301) before the page renders. You call error() and redirect().
import { selfheal } from 'svelte-selfheal';import { getArticle } from '$lib/db/articles.js';export const healer = selfheal();export const healArticle = healer.layer({ fetch: getArticle, segment: (article) => ({ identifier: article.id, slug: article.title })});import { selfheal } from 'svelte-selfheal';import { getArticle } from '$lib/db/articles.js';import { getAuthor } from '$lib/db/authors.js';export const healer = selfheal();export const healArticle = healer.layer({ fetch: getArticle, segment: (article) => ({ identifier: article.id, slug: article.title })});export const healAuthor = healer.layer({ fetch: getAuthor, segment: (author) => ({ identifier: author.id, slug: author.name })});import { healArticle, healer } from '$lib/healer.js';import { error, redirect } from '@sveltejs/kit';export const load = async ({ params, url }) => { const result = await healer.run(healArticle(params.id), url.searchParams); if (result.notFound) error(404, 'Article not found'); if (result.redirect) redirect(301, result.redirect); const [article] = result.resources; return { article };};import { healArticle, healAuthor, healer } from '$lib/healer.js';import { error, redirect } from '@sveltejs/kit';export const load = async ({ params, url }) => { const result = await healer.stack( [healArticle(params.id), 'details', healAuthor(params.innerId)], url.searchParams ); if (result.notFound) error(404, 'Not found'); if (result.redirect) redirect(301, result.redirect); const [article, author] = result.resources; return { article, author };};How it works
parseId() from the param, fetch by ID, build the canonical slug, compare. Mismatch sends a 301.
Page still loads from the ID — then one 301 to the single canonical path.
Where this comes from
Aaron Francis walked through self-healing URLs on Laravel — pretty slug, stable ID at the end, 301 when the slug rots.
Self-healing URLs in Laravel Watch Self-healing URLs in Laravel on YouTube (opens in new tab) Also, at this point: shoutout Aaron for all the cool shit he's put out. This repo wouldn't exist without that video.
Defaults
Hyphen IDs and kebab slugs out of the box. Pass identifier and sanitize to selfheal() when your routes use something else.
Parse ID
identifier.separate(param) in parseId()
HyphenIdentifierHandlermy-article-title-1 → id 1TildeIdentifierHandlermy-article~a1b2-c3d4 → id a1b2-c3d4
Build canonical
sanitize(slug) + identifier.join() in createUrl()
KebabSlugSanitizer"My Fancy Title" → my-fancy-titleSnakeSlugSanitizer"My Fancy Title" → my_fancy_title