import type { ContentTypeRow } from '../db/schema.js';
import type { ContentItem, SeoData } from './items.js';
/**
* SEO resolution: what a page actually claims to be, once fallbacks are applied.
*
* The point of putting this in core rather than in the template is that the admin's preview and
* the public page must agree. A preview that resolves its own fallbacks is a preview of something
* nobody will ever see — the failure mode is silent and only shows up in a social-media share
* weeks later. Both callers go through `resolveSeo`.
*/
/**
* Lengths at which the previews warn.
*
* These are guidance, not limits, and the UI must say so. Google truncates by *pixel width*, not
* character count, and the width depends on the glyphs — so no character count is ever exactly
* right, and enforcing one with `maxLength` would block a legitimate title that happens to be
* narrow. Nothing here is validated server-side for the same reason; over-length content is a
* quality warning, not an error worth refusing a save over.
*/
export declare const SEO_GUIDANCE: {
/** Roughly where a search result title starts being cut off. */
readonly titleChars: 60;
/** Roughly where a search result description starts being cut off. */
readonly descriptionChars: 160;
};
export interface ResolvedSeo {
/** What goes in `
` and the search-result heading. Never empty — falls back to the title. */
title: string;
/** Meta description, or null when the editor has written none. */
description: string | null;
/** Media id for the social card, after the item → content type fallback. */
ogImageId: string | null;
/** Where that image came from, so the editor can be told it is inheriting rather than unset. */
ogImageSource: 'item' | 'contentType' | 'site' | 'none';
/** Whether the page asks not to be indexed. */
noIndex: boolean;
}
/**
* The site-wide end of the fallback chain.
*
* Optional so every existing caller keeps working and resolves exactly as it did — a deployment that
* has never opened the site settings screen has an empty last link.
*/
export interface SeoSiteDefaults {
name?: string | null;
titleTemplate?: string | null;
seoDescription?: string | null;
seoOgImageId?: string | null;
seoNoIndex?: boolean;
}
export declare function resolveSeo(item: {
title: string;
seo: SeoData;
}, contentType?: Pick | null, site?: SeoSiteDefaults | null): ResolvedSeo;
/**
* Trim text the way a search result does, at a word boundary with an ellipsis.
*
* Used only by the preview. Nothing truncates what is stored — the editor's words are kept whole
* and the preview shows what a search engine is likely to do with them.
*/
export declare function truncateForPreview(text: string, limit: number): string;
/**
* The absolute URL a page will be shared and indexed under.
*
* Takes the site origin as an argument rather than reading configuration: core has no idea what
* host it is deployed behind, and a canonical URL guessed from a request header is how staging
* environments end up claiming to be production.
*/
export declare function canonicalUrl(origin: string, path: string): string;
/** Convenience for the common case of resolving straight off a hydrated item. */
export declare function resolveItemSeo(item: ContentItem, contentType?: Pick | null): ResolvedSeo;