/** * Slug and materialised-path handling. * * This is the piece most flat-page CMSes skip, so it is worth being explicit about the model: * * - Every content item stores a denormalised `path` (its ancestors' slugs plus its own), indexed * and unique, so the public catch-all route resolves a request in exactly one lookup. * - Slugs are unique **among siblings**, not site-wide. That is what lets `/admissions/apply` and * `/financial-aid/apply` coexist. * - Renaming or re-parenting a node must rewrite every descendant's path. That cascade is the * reason this feature usually gets special-cased away; here it is implemented properly, reading * the subtree with one recursive CTE and writing the result as one atomic batch. */ export declare const PATH_SEPARATOR = "/"; /** * Convert arbitrary text into a URL-safe slug. * * Unicode is normalised and stripped of combining marks first, so "Résumé" becomes "resume" * rather than being mangled or percent-escaped in the URL. */ export declare function slugify(input: string): string; /** True when a slug is safe to place in a path. */ export declare function isValidSlug(slug: string): boolean; /** * Build a child's path from its parent's path and its own slug. * * A null parent path yields a root-level path. The result never has a trailing slash, so the * stored value and an incoming request path compare directly after normalisation. */ export declare function buildPath(parentPath: string | null, slug: string): string; /** * Build the path for a `collection`-kind type, which is flat and type-prefixed * (`/events/spring-open-house`) rather than nested under a parent. */ export declare function buildCollectionPath(urlPrefix: string | null, slug: string): string; /** * Normalise an incoming request path for lookup: leading slash, no trailing slash, no duplicate * separators. The site root normalises to `/`. */ export declare function normalizePath(path: string): string; /** Depth of a path: 0 for a root-level item. */ export declare function pathDepth(path: string): number; /** Split a path into the ancestor paths leading to it, useful for breadcrumbs. */ export declare function ancestorPaths(path: string): string[]; /** * The bounds that select every descendant of a path, as a range rather than a prefix match. * * The predicate is `path > start and path < end`, and it has to be a range because **`like` cannot * use the index**. SQLite's LIKE optimisation only fires when the indexed column has `NOCASE` * collation or `case_sensitive_like` is on; `content_items_path_unique` is a plain BINARY index and * **D1 refuses PRAGMA**, so neither escape is available. Measured on the real index: * `like '/catalog/2026-27/%'` plans as `SCAN content_items`, this plans as `SEARCH … USING INDEX`. * Same lesson as `0020_perf_indexes` — a query that looks correct is not evidence the scan is gone. * * `end` is the prefix with its trailing separator replaced by the next codepoint: `/` is 0x2F and * `0` is 0x30, so `/catalog/` becomes `/catalog0` and everything under `/catalog/` sorts between * them. Nothing else can: a sibling named `/catalog-archive` sorts *below* `/catalog/` because `-` * is 0x2D, and `/catalog0` is the first thing above the branch. * * **Descendants only, never the root itself.** Including it would need an `or`, and this repo has * already paid for one: indexing both sides of `purgeStaleResetTokens`' `or` changed its plan by * nothing at all and the delete had to be split in two to spend the indexes. Every caller wants * descendants anyway — `resolveDelivery` fetches the root separately. `>` rather than `>=` so a * branch rooted at `/` excludes the home page; no other root can equal its own prefix, which * carries a trailing separator no stored path has. */ export declare function descendantPathRange(rootPath: string): { start: string; end: string; }; export interface SubtreeNode { id: string; path: string; depth: number; } export interface PathRewrite { id: string; oldPath: string; newPath: string; depth: number; } /** * Given a subtree and the node's new path, compute every descendant's new path. * * Pure and synchronous by design: the caller reads the subtree with one recursive CTE, calls this * to work out the whole rewrite in memory, then submits the updates as a single atomic batch. * That ordering is what makes the operation portable — D1 has no interactive transactions, so * "read, then compute, then write once" is the only shape that works everywhere. * * Throws if a descendant does not actually sit under the root, which would mean the caller passed * a mismatched subtree and is about to corrupt paths. */ export declare function computeSubtreeRewrite(subtree: SubtreeNode[], rootId: string, newRootPath: string): PathRewrite[]; /** True when `candidate` sits strictly below `ancestor` in the tree. */ export declare function isDescendantPath(candidate: string, ancestor: string): boolean; /** * Guard against re-parenting a node underneath itself. * * Without this check the subtree read would recurse forever and the node plus everything under it * would be detached from the tree entirely. */ export declare function wouldCreateCycle(subtree: SubtreeNode[], newParentId: string | null): boolean; /** * Pick a slug that does not collide with its siblings, appending `-2`, `-3`, and so on. * * Used when an author names two sibling pages the same thing — better to disambiguate silently at * creation than to reject the save and make them invent a slug by hand. */ export declare function uniqueSlug(desired: string, takenSlugs: Iterable): string;