@lynkow/next 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/proxy/preview.ts","../../src/proxy/redirects.ts","../../src/proxy/lynkow-proxy.ts","../../src/proxy/markdown.ts"],"names":["NextResponse"],"mappings":"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","file":"index.js","sourcesContent":["import { NextResponse, type NextRequest, type NextMiddleware } from 'next/server'\nimport { DEFAULT_CMS_ORIGIN, PREVIEW_PARAM } from 'lynkow/visual-editor'\n\nconst CSP_HEADER = 'Content-Security-Policy'\n\ninterface LynkowPreviewOptions {\n /**\n * Origin allowed to frame this site, defaulting to {@link DEFAULT_CMS_ORIGIN}.\n *\n * Note the deliberate divergence from `VisualEditorConfig.cmsOrigin`: an EMPTY\n * value here takes the default too, because the safe direction for a CSP is a\n * header that names the real dashboard rather than one that names nothing.\n * The editor half instead refuses to start, since an editor pointed nowhere is\n * inert rather than unsafe.\n */\n cmsOrigin?: string\n /**\n * @deprecated Ignored. The middleware always looks for `lynkow-preview`.\n *\n * The Lynkow dashboard always opens the preview with `lynkow-preview=true` in\n * the URL, and the visual editor only starts when it sees that exact parameter.\n * Any other name would stop the dashboard from framing the page and leave the\n * editor inactive, so this option no longer has any effect. Remove it from your\n * configuration.\n */\n previewParam?: string\n}\n\n/**\n * Warns once, when the middleware is created, that a custom `previewParam` is\n * ignored. An empty value is treated like an omitted one.\n */\nfunction warnIgnoredPreviewParam(options?: LynkowPreviewOptions): void {\n const previewParam = options?.previewParam\n if (previewParam && previewParam !== PREVIEW_PARAM) {\n console.warn(\n `[lynkow] The previewParam option (\"${previewParam}\") is deprecated and ignored. ` +\n `The preview middleware always uses \"${PREVIEW_PARAM}\", the only parameter the ` +\n `Lynkow dashboard sends and the visual editor recognises. Remove the option.`\n )\n }\n}\n\n/**\n * Creates a Next.js middleware that lets the Lynkow dashboard frame the site in\n * preview mode. It is {@link withLynkowPreview} around a middleware that returns\n * nothing, so every response starts from `NextResponse.next()`.\n *\n * On a request carrying the `lynkow-preview` query parameter, the response gets\n * `Content-Security-Policy: frame-ancestors 'self' <cmsOrigin>` and loses any\n * `X-Frame-Options`. Every other response gets `frame-ancestors 'self'`.\n */\nexport function lynkowPreviewMiddleware(options?: LynkowPreviewOptions): NextMiddleware {\n return withLynkowPreview(() => undefined, options)\n}\n\nfunction isFrameAncestors(directive: string): boolean {\n const [name = ''] = directive.split(/\\s+/, 1)\n return name.toLowerCase() === 'frame-ancestors'\n}\n\n/**\n * Parses a `Content-Security-Policy` value into policies, each a list of\n * directives. The header may already hold several comma-separated policies, and\n * each policy is a list of `;`-separated directives. Empty entries are dropped.\n */\nfunction parseCspList(value: string | null): string[][] {\n if (!value) return []\n\n return value\n .split(',')\n .map((policy) =>\n policy\n .split(';')\n .map((directive) => directive.trim())\n .filter((directive) => directive.length > 0)\n )\n .filter((policy) => policy.length > 0)\n}\n\n/** Appends a directive to the first policy, creating it when there is none. */\nfunction appendToFirstPolicy(policies: string[][], directive: string): string[][] {\n const [first = [], ...rest] = policies\n return [[...first, directive], ...rest]\n}\n\nfunction serializeCspList(policies: string[][]): string {\n return policies\n .filter((policy) => policy.length > 0)\n .map((policy) => policy.join('; '))\n .join(', ')\n}\n\n/**\n * Merges the framing rule into whatever CSP the response already carries, at\n * the directive level, so a policy written by the wrapped middleware (a nonce\n * CSP, for instance) survives.\n *\n * `Headers.append` would not do: a comma-joined CSP is several policies that\n * are ALL enforced, so a relaxed `frame-ancestors` appended beside an existing\n * `frame-ancestors 'self'` would still block the dashboard. The value is\n * therefore rebuilt and written with a single `set`.\n *\n * - Preview: every existing `frame-ancestors` is replaced by one that allows\n * `cmsOrigin`, and `X-Frame-Options` is removed.\n * - Otherwise: an existing `frame-ancestors` is the site's own decision and is\n * left alone. Without one, `frame-ancestors 'self'` is added.\n *\n * `Content-Security-Policy-Report-Only` is never touched.\n */\nfunction addCspHeaders(\n response: NextResponse | Response,\n request: NextRequest,\n cmsOrigin: string\n): NextResponse | Response {\n const isPreview = request.nextUrl.searchParams.has(PREVIEW_PARAM)\n const policies = parseCspList(response.headers.get(CSP_HEADER))\n\n if (isPreview) {\n const withoutFraming = policies.map((policy) =>\n policy.filter((directive) => !isFrameAncestors(directive))\n )\n const merged = appendToFirstPolicy(withoutFraming, `frame-ancestors 'self' ${cmsOrigin}`)\n\n response.headers.set(CSP_HEADER, serializeCspList(merged))\n response.headers.delete('X-Frame-Options')\n } else if (!policies.some((policy) => policy.some(isFrameAncestors))) {\n const merged = appendToFirstPolicy(policies, \"frame-ancestors 'self'\")\n\n response.headers.set(CSP_HEADER, serializeCspList(merged))\n }\n\n return response\n}\n\n/**\n * Wraps an existing Next.js middleware so its responses let the Lynkow dashboard\n * frame the site in preview mode.\n *\n * The wrapped middleware's own `Content-Security-Policy` is kept and merged at\n * the directive level. In preview mode (the `lynkow-preview` query parameter),\n * its `frame-ancestors` is replaced by one allowing `cmsOrigin` and\n * `X-Frame-Options` is removed. Outside preview mode, a `frame-ancestors` it\n * already set is left as is, and `frame-ancestors 'self'` is added only when it\n * set none. A middleware returning nothing is treated as `NextResponse.next()`.\n */\nexport function withLynkowPreview(\n existingMiddleware: NextMiddleware,\n options?: LynkowPreviewOptions\n): NextMiddleware {\n const cmsOrigin = options?.cmsOrigin || DEFAULT_CMS_ORIGIN\n warnIgnoredPreviewParam(options)\n\n return function middleware(request: NextRequest, event: any) {\n const response = existingMiddleware(request, event)\n\n const finalResponse =\n response instanceof Promise\n ? response.then((res) => addCspHeaders(res || NextResponse.next(), request, cmsOrigin))\n : addCspHeaders(response || NextResponse.next(), request, cmsOrigin)\n\n return finalResponse\n }\n}\n","import type { Client, Redirect } from 'lynkow'\n\n/**\n * How long an answer from `/redirects/match` is trusted, in milliseconds.\n *\n * This is the only freshness control a proxy has, so it doubles as the delay\n * between creating a redirect in the admin and it taking effect. A minute is\n * short enough that nobody notices and long enough that a hot path costs one\n * round trip per minute rather than one per visitor.\n */\nconst TTL_MS = 60_000\n\n/**\n * Upper bound on remembered paths. A crawler walking an unbounded URL space\n * would otherwise grow the map until the process runs out of memory, and the\n * point of the cache is the small set of paths real traffic repeats.\n */\nconst MAX_ENTRIES = 500\n\ntype Entry = { value: Redirect | null; expiresAt: number }\n\n/** Returns the redirect rule matching a pathname, or null. */\nexport type RedirectMatcher = (pathname: string) => Promise<Redirect | null>\n\n/**\n * A redirect lookup with its own in-process cache.\n *\n * Why its own cache, when every other read hands caching to Next. Two facts,\n * both measured against Next 16.3.4 and the live API on 2026-09-08:\n *\n * 1. **Next's fetch cache does not reach the proxy.** `next: { revalidate,\n * tags }` is accepted and ignored there: five identical requests each\n * produced a real network call. So `cached()` would be decoration here,\n * and `revalidateTag()` from the revalidation webhook can never reach this\n * code, since the proxy is a separate bundle with its own module scope.\n * 2. **A miss is uncacheable at the edge.** A path with no redirect answers\n * 404 with `Cache-Control: no-store`, so a CDN bypasses it. Almost every\n * request is a miss, so an uncached lookup would send every page view of\n * the site to the API.\n *\n * The cache is per process, so a multi-instance deployment holds one copy per\n * instance, and nothing invalidates it early: `TTL_MS` is the whole contract.\n *\n * Concurrent lookups of one path share one request, so a link posted somewhere\n * busy costs one call rather than one per simultaneous visitor. A failed lookup\n * answers `null` and is never cached: a transient API error would otherwise\n * blind every redirect on the site for a full TTL, while answering \"no\n * redirect\" for one request gives the visitor exactly the page they would have\n * got without this module.\n *\n * `pathname` must carry no query string: the API compares the stored source\n * against it verbatim, so `/old?page=2` matches nothing.\n */\nexport function createRedirectMatcher(client: () => Client): RedirectMatcher {\n const cache = new Map<string, Entry>()\n const inFlight = new Map<string, Promise<Redirect | null>>()\n\n function remember(path: string, value: Redirect | null): void {\n // A Map iterates in insertion order, so the first key is the oldest write.\n if (cache.size >= MAX_ENTRIES && !cache.has(path)) {\n const oldest = cache.keys().next().value\n if (oldest !== undefined) cache.delete(oldest)\n }\n cache.set(path, { value, expiresAt: Date.now() + TTL_MS })\n }\n\n return async function matchRedirect(pathname) {\n const entry = cache.get(pathname)\n if (entry && entry.expiresAt > Date.now()) return entry.value\n if (entry) cache.delete(pathname)\n\n const pending = inFlight.get(pathname)\n if (pending) return pending\n\n const lookup = Promise.resolve()\n .then(() => client().paths.matchRedirect(pathname))\n .then((value) => {\n remember(pathname, value)\n return value\n })\n .catch((error: unknown) => {\n console.error(`[lynkow] redirect lookup failed for ${pathname}`, error)\n return null\n })\n .finally(() => {\n inFlight.delete(pathname)\n })\n\n inFlight.set(pathname, lookup)\n return lookup\n }\n}\n","import { NextResponse, type NextMiddleware, type NextRequest } from 'next/server'\nimport type { Client, Redirect } from 'lynkow'\nimport type { LynkowNextConfig } from '../config.js'\nimport { createLocalePaths } from '../locale-paths.js'\nimport { withLynkowPreview } from './preview.js'\nimport { createRedirectMatcher } from './redirects.js'\n\n/** The cookie remembering the locale a visitor was sent to. */\nconst LOCALE_COOKIE = 'NEXT_LOCALE'\n\n/** Options of {@link createLynkowProxy}. */\nexport interface LynkowProxyOptions {\n config: LynkowNextConfig\n /** The client of a locale, typically the function `createClients(config)` returns. */\n client: (locale: string) => Client\n /**\n * The origin allowed to frame the site on a preview request. Defaults to the\n * Lynkow dashboard. Give the visual editor the same value, or neither one.\n */\n cmsOrigin?: string\n}\n\n/**\n * The proxy of a Lynkow site: redirects, then locale routing, with the preview\n * headers the visual editor needs.\n *\n * 1. **Redirects**, first, on the path as the visitor typed it. The CMS stores\n * sources that way, a mix of `/old` and `/fr/blog/old` on a multilingual\n * site, so matching after a locale prefix is added would silently break\n * every unprefixed rule. The status code is the one the editor chose, which\n * `redirect()` from `next/navigation` could not give (307 or 308 only). A\n * target an editor wrote without a locale gets the visitor's, so the rule\n * and the locale step cost one hop rather than two.\n * 2. **Locale routing.** With `prefixDefaultLocale` on, a path without a locale\n * is sent (307) to the visitor's best match: the `NEXT_LOCALE` cookie, then\n * `Accept-Language`, then the default. With it off, a path without a locale\n * IS the default locale's page and is served from its `[locale]` route under\n * the same URL, while `/<default>/...` answers 308 to that URL. Either way a\n * locale segment written in another case answers 308 to its lowercase form,\n * the form the API writes, so each page has one URL.\n *\n * The application keeps its own `config.matcher`, which Next requires to be a\n * literal in `src/proxy.ts`. It must exclude everything that is not a page:\n * Next internals, API routes and files.\n *\n * @example\n * // src/proxy.ts\n * import { createClients } from '@lynkow/next'\n * import { createLynkowProxy } from '@lynkow/next/proxy'\n * import { config as site } from './lib/config'\n *\n * export default createLynkowProxy({ config: site, client: createClients(site) })\n *\n * export const config = {\n * matcher: ['/((?!_next/|api/|.*\\\\.[\\\\w]+$).*)'],\n * }\n */\nexport function createLynkowProxy({ config, client, cmsOrigin }: LynkowProxyOptions): NextMiddleware {\n const paths = createLocalePaths(config)\n const { defaultLocale, prefixDefaultLocale } = config\n const defaultSegment = `/${defaultLocale.toLowerCase()}`\n const matchRedirect = createRedirectMatcher(() => client(defaultLocale))\n\n /** The configured locale the first segment of a pathname names, and the path after it. */\n function prefixOf(pathname: string): { locale: string; rest: string } | null {\n const segment = pathname.split('/')[1] ?? ''\n const locale = paths.toLocale(segment)\n return locale ? { locale, rest: pathname.slice(segment.length + 1) || '/' } : null\n }\n\n /**\n * The locale this request will be served in. Shared by both steps, so a\n * redirect target and the locale step can never disagree on one request.\n */\n function resolveLocale(request: NextRequest): string {\n return (\n paths.localeFromPath(request.nextUrl.pathname) ??\n paths.toLocale(request.cookies.get(LOCALE_COOKIE)?.value) ??\n paths.negotiateLocale(request.headers.get('accept-language'))\n )\n }\n\n /**\n * With the default locale unprefixed, a visitor types `/old` for a rule an\n * editor may have written as `/old` or, the way the API writes paths, as\n * `/en/old`. Both are looked up at once, through the same cache, and the path\n * as typed wins.\n */\n async function findRedirect(pathname: string): Promise<Redirect | null> {\n if (prefixDefaultLocale || prefixOf(pathname)) return matchRedirect(pathname)\n const [typed, prefixed] = await Promise.all([\n matchRedirect(pathname),\n matchRedirect(pathname === '/' ? defaultSegment : defaultSegment + pathname),\n ])\n return typed ?? prefixed\n }\n\n /**\n * The response for a matched rule, or null to carry on routing.\n *\n * The target arrives resolved: the API has already substituted any glob or\n * regex capture, so there is nothing to interpolate here. `preserveQueryString`\n * is the caller's to apply, and a parameter the target names itself wins over\n * the incoming one.\n */\n function redirectTo(request: NextRequest, redirect: Redirect, locale: string): NextResponse | null {\n const from = request.nextUrl\n let url: URL\n try {\n url = new URL(paths.localizeHref(redirect.target, locale), from.href)\n } catch {\n // A stored target that will not parse is an admin-side problem this site\n // must not turn into a broken page.\n console.error(`[lynkow] unparseable redirect target ${redirect.target} for ${redirect.source}`)\n return null\n }\n\n // Next shortens a Location on this origin to a relative one, and a browser\n // reads a relative `//evil.example` as another host. A wildcard rule whose\n // target is this site's own absolute URL, or a relative target the visitor\n // fills with dot segments, can resolve to exactly that path, so it is\n // collapsed to one slash. A target on another origin is the editor's choice.\n if (url.origin === from.origin) url.pathname = url.pathname.replace(/^\\/{2,}/, '/')\n\n if (redirect.preserveQueryString) {\n for (const [key, value] of from.searchParams) {\n if (!url.searchParams.has(key)) url.searchParams.append(key, value)\n }\n }\n\n // A rule pointing at its own source is a loop the browser would discover.\n if (url.origin === from.origin && url.pathname === from.pathname && url.search === from.search) {\n console.error(`[lynkow] ignoring self-referential redirect on ${redirect.source}`)\n return null\n }\n\n return NextResponse.redirect(url, redirect.statusCode)\n }\n\n function redirectToPath(request: NextRequest, pathname: string, status: 307 | 308): NextResponse {\n const url = request.nextUrl.clone()\n url.pathname = pathname\n return NextResponse.redirect(url, status)\n }\n\n function route(request: NextRequest, locale: string): NextResponse {\n const { pathname } = request.nextUrl\n const prefix = prefixOf(pathname)\n\n if (prefix) {\n const canonical = paths.localePath(prefix.locale, prefix.rest)\n return canonical === pathname ? NextResponse.next() : redirectToPath(request, canonical, 308)\n }\n\n if (!prefixDefaultLocale) {\n const url = request.nextUrl.clone()\n url.pathname = pathname === '/' ? defaultSegment : defaultSegment + pathname\n return NextResponse.rewrite(url)\n }\n\n const response = redirectToPath(request, paths.localePath(locale, pathname), 307)\n response.cookies.set(LOCALE_COOKIE, locale, {\n path: '/',\n maxAge: 60 * 60 * 24 * 365,\n sameSite: 'lax',\n })\n return response\n }\n\n async function site(request: NextRequest): Promise<NextResponse> {\n const locale = resolveLocale(request)\n const redirect = await findRedirect(request.nextUrl.pathname)\n return (redirect && redirectTo(request, redirect, locale)) || route(request, locale)\n }\n\n return withLynkowPreview(site, { cmsOrigin })\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst DEFAULT_MD_PREFIX = '/md'\n\n/**\n * Options for the {@link markdownProxy} helper.\n */\nexport interface MarkdownProxyOptions {\n /**\n * URL prefix where the Markdown route handler is mounted. The helper\n * rewrites `Accept: text/markdown` requests from `<path>` to\n * `<mdPathPrefix><path>`. Defaults to `'/md'`.\n *\n * If your route handler lives at `app/markdown/[...path]/route.ts`,\n * set this to `'/markdown'`.\n */\n mdPathPrefix?: string\n\n /**\n * Whether to set `Vary: Accept` on the rewritten response. Defaults\n * to `true`. Set to `false` only if your edge or CDN already sets the\n * header and you want to avoid duplication.\n */\n setVary?: boolean\n\n /**\n * Whether to set `X-Forwarded-Accept: text/markdown` on the rewritten\n * request so the route handler can confirm the request originated\n * from a Markdown negotiation. Defaults to `true`.\n */\n setForwardedAccept?: boolean\n}\n\n/**\n * Parses an `Accept` header per RFC 9110 §12.5.1 and returns the\n * highest q-value for a target media type. Supports exact match\n * (`text/markdown`), type wildcard (`text/*`), full wildcard (`*\\/*`),\n * and q-values.\n *\n * Returns `0` if the type is not present or has `q=0`. Returns `1` if\n * no q value is given (per RFC, missing q means q=1). Out-of-range or\n * non-numeric q values fall back to the default `1`.\n *\n * @param acceptHeader - The raw `Accept` header value, or `null` if absent\n * @param targetType - The target media type, e.g. `'text/markdown'`\n * @returns The highest q-value for the target type (0 to 1)\n *\n * @example\n * ```typescript\n * acceptQuality('text/html;q=0.9, text/markdown;q=0.8', 'text/markdown') // 0.8\n * acceptQuality('text/markdown', 'text/markdown') // 1\n * acceptQuality(null, 'text/markdown') // 0\n * ```\n */\nexport function acceptQuality(acceptHeader: string | null, targetType: string): number {\n if (!acceptHeader) return 0\n const [targetMain, targetSub] = targetType.toLowerCase().split('/')\n let best = 0\n\n for (const raw of acceptHeader.split(',')) {\n const parts = raw\n .trim()\n .split(';')\n .map((p) => p.trim())\n .filter(Boolean)\n const mediaType = parts[0]?.toLowerCase()\n if (!mediaType || !mediaType.includes('/')) continue\n\n const [main, sub] = mediaType.split('/')\n const matches =\n (main === targetMain && sub === targetSub) ||\n (main === targetMain && sub === '*') ||\n (main === '*' && sub === '*')\n\n if (!matches) continue\n\n let q = 1\n for (const param of parts.slice(1)) {\n if (param.startsWith('q=')) {\n const parsed = Number.parseFloat(param.slice(2))\n if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 1) q = parsed\n }\n }\n if (q > best) best = q\n }\n\n return best\n}\n\n/**\n * Decides whether the request prefers Markdown over HTML based on its\n * `Accept` header.\n *\n * Returns `true` when `Accept` contains `text/markdown` with q strictly\n * higher than `text/html`. Ties break in favor of HTML (the default\n * browsers expect).\n *\n * @param acceptHeader - The raw `Accept` header value, or `null` if absent\n * @returns `true` when Markdown is the preferred representation\n *\n * @example\n * ```typescript\n * prefersMarkdown('text/markdown;q=0.9, text/html;q=0.8') // true\n * prefersMarkdown('text/markdown, text/html') // false (tie -> HTML)\n * prefersMarkdown('text/html') // false\n * ```\n */\nexport function prefersMarkdown(acceptHeader: string | null): boolean {\n const md = acceptQuality(acceptHeader, 'text/markdown')\n if (md === 0) return false\n const html = acceptQuality(acceptHeader, 'text/html')\n return md > html\n}\n\n/**\n * Next.js proxy/middleware helper that translates\n * `Accept: text/markdown` requests into rewrites to a dedicated\n * Markdown route handler.\n *\n * Returns the rewritten `NextResponse` when the request preferred\n * Markdown, or `null` when the request did not (so the caller can pass\n * through to the next middleware step).\n *\n * Sets `Vary: Accept` on the rewritten response so intermediate caches\n * keep separate variants for HTML and Markdown.\n *\n * The helper is Edge runtime compatible (no `node:` imports).\n *\n * @param request - The incoming `NextRequest`\n * @param options - Optional {@link MarkdownProxyOptions} overrides\n * @returns A `NextResponse.rewrite(...)` when Markdown was preferred, or `null` otherwise\n *\n * @example\n * ```typescript\n * // Next 16, proxy.ts\n * import { markdownProxy } from '@lynkow/next/proxy'\n * import { NextResponse, type NextRequest } from 'next/server'\n *\n * export function proxy(request: NextRequest) {\n * const md = markdownProxy(request)\n * if (md) return md\n * return NextResponse.next()\n * }\n *\n * export const config = {\n * matcher: ['/((?!_next|favicon.ico|md|api).*)'],\n * }\n * ```\n */\nexport function markdownProxy(\n request: NextRequest,\n options?: MarkdownProxyOptions\n): NextResponse | null {\n const accept = request.headers.get('accept')\n if (!prefersMarkdown(accept)) return null\n\n const prefix = options?.mdPathPrefix ?? DEFAULT_MD_PREFIX\n const setVary = options?.setVary ?? true\n const setForwardedAccept = options?.setForwardedAccept ?? true\n\n const url = request.nextUrl.clone()\n if (url.pathname.startsWith(`${prefix}/`) || url.pathname === prefix) {\n return null\n }\n url.pathname = `${prefix}${url.pathname}`\n\n const headers = new Headers(request.headers)\n if (setForwardedAccept) headers.set('x-forwarded-accept', 'text/markdown')\n\n const response = NextResponse.rewrite(url, { request: { headers } })\n if (setVary) response.headers.set('vary', 'accept')\n\n return response\n}\n"]}
@@ -0,0 +1,196 @@
1
+ import { Page, Client, BaseRequestOptions } from 'lynkow';
2
+ import { L as LynkowNextConfig } from '../config-CwDV7jFS.js';
3
+ import * as react from 'react';
4
+ import { Metadata } from 'next';
5
+
6
+ /** Props of the `JsonLdScript` component {@link createLynkowServer} returns. */
7
+ interface JsonLdScriptProps {
8
+ /** The schema.org nodes, such as a page's `structuredData.graph`. Nothing renders when empty. */
9
+ nodes: object[] | null | undefined;
10
+ locale: string;
11
+ }
12
+ declare function createJsonLdScript(siteUrl: string, getSiteDomain: (locale: string) => Promise<string | null | undefined>): ({ nodes, locale }: JsonLdScriptProps) => Promise<react.JSX.Element | null>;
13
+
14
+ /** The two metadata builders {@link createLynkowServer} returns. */
15
+ interface MetadataBuilders {
16
+ /**
17
+ * Next metadata from a Lynkow page's SEO block.
18
+ *
19
+ * The hreflang set is built from `page.alternates`, so a page translated into
20
+ * two of five locales advertises two alternates rather than five dead ones.
21
+ * The fallbacks walk the same order the API does: an explicit SEO field, then
22
+ * the Open Graph twin, then the page name.
23
+ *
24
+ * An editor's `metaTitle` is the whole `<title>`, brand included, so it is
25
+ * set `absolute` and a layout's `%s | <site name>` template is not appended
26
+ * to it. Only the page name, the fallback, goes through the template.
27
+ *
28
+ * @param fallbackPath - The locale-agnostic path of the route, used for the
29
+ * canonical URL when the page advertises no alternate for `locale`
30
+ */
31
+ pageMetadata(page: Page, locale: string, fallbackPath: string): Metadata;
32
+ /**
33
+ * Metadata for a route the application owns rather than one Lynkow drives,
34
+ * such as `/reviews`. Its hreflang set is every configured locale: the route
35
+ * exists in each of them by construction.
36
+ *
37
+ * @param path - The locale-agnostic path, with a leading slash
38
+ */
39
+ routeMetadata(locale: string, path: string, title: string, description: string): Metadata;
40
+ }
41
+
42
+ /** `Content-Type` of an XML text route, such as `sitemap.xml`. */
43
+ declare const XML = "application/xml; charset=utf-8";
44
+ /** `Content-Type` of a plain text route, such as `robots.txt` or `llms.txt`. */
45
+ declare const TEXT = "text/plain; charset=utf-8";
46
+ /**
47
+ * Reads one text file from Lynkow for a route. `options` carries the cache tags
48
+ * and duration; pass it to the SDK call. `locale` is the route's, which a
49
+ * localized file such as `llms.txt` has to pass explicitly, since the SDK reads
50
+ * the locale of that call from its options and not from the client.
51
+ */
52
+ type TextReader = (client: Client, options: BaseRequestOptions, locale: string) => Promise<string>;
53
+ /** The route handler {@link LynkowServer.textRoute} returns. */
54
+ type TextRouteHandler = (request: Request, context?: {
55
+ params?: Promise<{
56
+ locale?: string;
57
+ }>;
58
+ }) => Promise<Response>;
59
+ /** Options of {@link createLynkowServer}. */
60
+ interface LynkowServerOptions {
61
+ config: LynkowNextConfig;
62
+ /** The client of a locale, typically the function `createClients(config)` returns. */
63
+ client: (locale: string) => Client;
64
+ /**
65
+ * The site's configured domain, which Lynkow builds the absolute URLs of its
66
+ * generated files from, or null when it has none. Read it from a cached call,
67
+ * such as the site configuration the layout already fetches.
68
+ */
69
+ getSiteDomain: (locale: string) => Promise<string | null | undefined>;
70
+ }
71
+ interface LynkowServer extends MetadataBuilders {
72
+ /**
73
+ * A route handler serving a text file Lynkow generates, such as
74
+ * `sitemap.xml`, `robots.txt` or `llms.txt`.
75
+ *
76
+ * Regenerating those files in the application would mean re-deriving rules
77
+ * the API already computes from the site's own settings, and the two would
78
+ * drift on the first setting change. So the handler does two things: cache
79
+ * the upstream read under `TAGS.seo`, and put its URLs on this deployment's
80
+ * origin. Under a `[locale]` segment the route's locale is used; anywhere
81
+ * else, or for a locale that is not configured, the default one.
82
+ *
83
+ * @example
84
+ * // src/app/sitemap.xml/route.ts
85
+ * export const revalidate = 3600
86
+ * export const GET = server.textRoute((client, options) => client.seo.sitemap(options), XML)
87
+ */
88
+ textRoute(read: TextReader, contentType: string): TextRouteHandler;
89
+ /** An async server component rendering schema.org nodes as one JSON-LD script. */
90
+ JsonLdScript: ReturnType<typeof createJsonLdScript>;
91
+ /** {@link rewriteOrigin} bound to this site's origin. */
92
+ rewriteOrigin(body: string, siteDomain: string | null | undefined): string;
93
+ }
94
+ /**
95
+ * The server-side helpers of a Lynkow site on Next: text routes, JSON-LD and
96
+ * page metadata, bound to one configuration.
97
+ *
98
+ * @example
99
+ * // src/lib/server.ts
100
+ * import { createClients } from '@lynkow/next'
101
+ * import { createLynkowServer } from '@lynkow/next/server'
102
+ * import { config } from './config'
103
+ *
104
+ * export const lynkow = createClients(config)
105
+ * export const server = createLynkowServer({
106
+ * config,
107
+ * client: (locale) => lynkow(locale),
108
+ * getSiteDomain: async (locale) => (await getSiteShell(locale)).domain,
109
+ * })
110
+ */
111
+ declare function createLynkowServer({ config, client, getSiteDomain, }: LynkowServerOptions): LynkowServer;
112
+
113
+ /**
114
+ * Rewrite the site's own domain to the origin this deployment is served from.
115
+ *
116
+ * Lynkow generates `sitemap.xml`, `robots.txt`, `llms.txt` and the JSON-LD
117
+ * graph with absolute URLs built from the site's configured domain, which is
118
+ * correct in production and wrong everywhere else: on localhost, on a preview
119
+ * deployment or on a staging host, a verbatim copy would point at another site.
120
+ *
121
+ * When the two match, the body passes through untouched.
122
+ *
123
+ * @param body - What the API returned
124
+ * @param siteDomain - The site's configured domain, such as `www.example.com`
125
+ * @param siteUrl - The origin this deployment is served from, as `resolveConfig` returns it
126
+ */
127
+ declare function rewriteOrigin(body: string, siteDomain: string | null | undefined, siteUrl: string): string;
128
+
129
+ /** The envelope the Lynkow API POSTs to a webhook. */
130
+ interface WebhookEnvelope {
131
+ event: string;
132
+ timestamp: string;
133
+ data: Record<string, unknown>;
134
+ }
135
+ /**
136
+ * The tags one delivered event invalidates, deduplicated.
137
+ *
138
+ * An unknown event returns an empty list rather than throwing. Lynkow adds
139
+ * events without asking its consumers, and a site that fails on an event it has
140
+ * never heard of turns a new CMS feature into an outage.
141
+ */
142
+ declare function tagsForEvent(envelope: WebhookEnvelope): string[];
143
+ /** Options of {@link createRevalidateHandler}. */
144
+ interface RevalidateHandlerOptions {
145
+ /**
146
+ * The webhook's signing secret, read by the application from a server-only
147
+ * environment variable. Absent or empty, the endpoint stays closed.
148
+ */
149
+ secret: string | undefined;
150
+ }
151
+ /**
152
+ * The `POST` handler of a revalidation webhook.
153
+ *
154
+ * Point a Lynkow webhook at the route that exports it and give it the secret
155
+ * passed here. Every event the webhook subscribes to drops the cache tags that
156
+ * event can invalidate, so an edit is live in seconds instead of waiting out
157
+ * the `REVALIDATE` ceiling. The route must run on Node, since the signature
158
+ * check needs `node:crypto`:
159
+ *
160
+ * @example
161
+ * // src/app/api/revalidate/route.ts
162
+ * import { createRevalidateHandler } from '@lynkow/next/server'
163
+ *
164
+ * export const runtime = 'nodejs'
165
+ * export const POST = createRevalidateHandler({ secret: process.env.LYNKOW_WEBHOOK_SECRET })
166
+ */
167
+ declare function createRevalidateHandler({ secret }: RevalidateHandlerOptions): (request: Request) => Promise<Response>;
168
+
169
+ /**
170
+ * Verify a Lynkow webhook: its HMAC first, then the freshness of the timestamp
171
+ * that HMAC covers.
172
+ *
173
+ * The API builds `{ event, timestamp, data }` once per delivery attempt, signs
174
+ * `JSON.stringify` of it with the webhook's secret, sends the digest as
175
+ * `X-Webhook-Signature: sha256=<hex>`, and POSTs that exact same string as the
176
+ * body (`webhook_service.ts`, `sendWebhook` and `generateSignature`). So the raw
177
+ * request text is verifiable byte for byte, and the `timestamp` inside it is
178
+ * authenticated.
179
+ *
180
+ * That is why the handler reads `await request.text()` and never `request.json()`:
181
+ * parsing and re-serialising would reorder no keys today, but it is not a
182
+ * property JSON guarantees, and a signature checked against a re-serialisation
183
+ * is a signature that fails for reasons nobody can reproduce.
184
+ *
185
+ * Returns `true` only when every check passes, and `false` for anything else,
186
+ * including an empty secret, a missing or malformed header, a body that is not
187
+ * JSON, and a stale or future timestamp. It never throws on request input, so a
188
+ * crafted header is a rejection rather than a 500. The result deliberately does
189
+ * not say which check failed.
190
+ *
191
+ * It is safe to call on its own: it does not rely on the caller having checked
192
+ * that a secret is configured.
193
+ */
194
+ declare function verifySignature(rawBody: string, header: string | null, secret: string): boolean;
195
+
196
+ export { type JsonLdScriptProps, type LynkowServer, type LynkowServerOptions, type MetadataBuilders, type RevalidateHandlerOptions, TEXT, type TextReader, type TextRouteHandler, type WebhookEnvelope, XML, createLynkowServer, createRevalidateHandler, rewriteOrigin, tagsForEvent, verifySignature };
@@ -0,0 +1,263 @@
1
+ import { cached, TAGS, REVALIDATE } from '../chunk-XNN4SECG.js';
2
+ import { createLocalePaths } from '../chunk-4QTGOTFV.js';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { revalidateTag } from 'next/cache';
5
+ import { createHmac, timingSafeEqual } from 'node:crypto';
6
+
7
+ // src/server/origin.ts
8
+ function rewriteOrigin(body, siteDomain, siteUrl) {
9
+ if (!siteDomain) return body;
10
+ const sources = [`https://${siteDomain}`, `http://${siteDomain}`];
11
+ return sources.reduce(
12
+ (acc, source) => source === siteUrl ? acc : acc.split(source).join(siteUrl),
13
+ body
14
+ );
15
+ }
16
+ function jsonLdPayload(nodes, siteDomain, siteUrl) {
17
+ const stripped = nodes.map((node) => {
18
+ const copy = { ...node };
19
+ delete copy["@context"];
20
+ return copy;
21
+ });
22
+ const payload = JSON.stringify({ "@context": "https://schema.org", "@graph": stripped });
23
+ return rewriteOrigin(payload, siteDomain, siteUrl).replace(/</g, "\\u003c");
24
+ }
25
+ function createJsonLdScript(siteUrl, getSiteDomain) {
26
+ return async function JsonLdScript({ nodes, locale }) {
27
+ if (!nodes || nodes.length === 0) return null;
28
+ const html = jsonLdPayload(nodes, await getSiteDomain(locale), siteUrl);
29
+ return /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: html } });
30
+ };
31
+ }
32
+
33
+ // src/server/metadata.ts
34
+ function createMetadataBuilders(paths) {
35
+ function apiUrl(path, locale) {
36
+ return paths.absoluteUrl(paths.fromApiPath(path, locale));
37
+ }
38
+ function routeUrl(locale, path) {
39
+ return paths.absoluteUrl(paths.localePath(locale, path));
40
+ }
41
+ function pageMetadata(page, locale, fallbackPath) {
42
+ const seo = page.seo;
43
+ const title = seo?.metaTitle || page.name || void 0;
44
+ const description = seo?.metaDescription || void 0;
45
+ const wanted = paths.toLocale(locale);
46
+ const current = wanted ? page.alternates?.find((alt) => paths.toLocale(alt.locale) === wanted) : void 0;
47
+ const canonical = seo?.canonicalUrl || (current ? apiUrl(current.path, locale) : routeUrl(locale, fallbackPath));
48
+ const languages = {};
49
+ for (const alternate of page.alternates ?? []) {
50
+ const code = paths.toLocale(alternate.locale);
51
+ if (code) languages[code] = apiUrl(alternate.path, code);
52
+ }
53
+ return {
54
+ title: seo?.metaTitle ? { absolute: seo.metaTitle } : title,
55
+ description,
56
+ keywords: seo?.keywords?.length ? seo.keywords : void 0,
57
+ alternates: {
58
+ canonical,
59
+ languages: Object.keys(languages).length > 0 ? languages : void 0
60
+ },
61
+ robots: seo?.noIndex ? { index: false, follow: false } : void 0,
62
+ openGraph: {
63
+ type: "website",
64
+ locale,
65
+ url: seo?.ogUrl || canonical,
66
+ siteName: seo?.ogSiteName || void 0,
67
+ title: seo?.ogTitle || title,
68
+ description: seo?.ogDescription || description,
69
+ images: seo?.ogImage?.url ? [{ url: seo.ogImage.url, alt: seo.ogImage.alt || void 0 }] : void 0
70
+ },
71
+ twitter: {
72
+ card: seo?.twitterCard || "summary_large_image",
73
+ title: seo?.twitterTitle || seo?.ogTitle || title,
74
+ description: seo?.twitterDescription || seo?.ogDescription || description,
75
+ images: seo?.twitterImage?.url ? [seo.twitterImage.url] : void 0
76
+ }
77
+ };
78
+ }
79
+ function routeMetadata(locale, path, title, description) {
80
+ const url = routeUrl(locale, path);
81
+ const languages = Object.fromEntries(paths.locales.map((code) => [code, routeUrl(code, path)]));
82
+ return {
83
+ title,
84
+ description,
85
+ alternates: {
86
+ canonical: url,
87
+ languages: paths.locales.length > 1 ? languages : void 0
88
+ },
89
+ openGraph: { type: "website", locale, url, title, description }
90
+ };
91
+ }
92
+ return { pageMetadata, routeMetadata };
93
+ }
94
+
95
+ // src/server/create-server.ts
96
+ var XML = "application/xml; charset=utf-8";
97
+ var TEXT = "text/plain; charset=utf-8";
98
+ function createLynkowServer({
99
+ config,
100
+ client,
101
+ getSiteDomain
102
+ }) {
103
+ const paths = createLocalePaths(config);
104
+ function textRoute(read, contentType) {
105
+ return async function GET(_request, context) {
106
+ const locale = paths.toLocale((await context?.params)?.locale) ?? config.defaultLocale;
107
+ const [domain, body] = await Promise.all([
108
+ getSiteDomain(locale),
109
+ read(client(locale), cached([TAGS.seo], REVALIDATE.seo), locale)
110
+ ]);
111
+ return new Response(rewriteOrigin(body, domain, config.siteUrl), {
112
+ headers: {
113
+ "content-type": contentType,
114
+ "cache-control": `public, max-age=0, s-maxage=${REVALIDATE.seo}, stale-while-revalidate=86400`
115
+ }
116
+ });
117
+ };
118
+ }
119
+ return {
120
+ textRoute,
121
+ JsonLdScript: createJsonLdScript(config.siteUrl, getSiteDomain),
122
+ rewriteOrigin: (body, siteDomain) => rewriteOrigin(body, siteDomain, config.siteUrl),
123
+ ...createMetadataBuilders(paths)
124
+ };
125
+ }
126
+ var PREFIX = "sha256=";
127
+ var HEX_DIGEST = /^[0-9a-f]{64}$/i;
128
+ function verifySignature(rawBody, header, secret) {
129
+ if (typeof secret !== "string" || secret.length === 0) return false;
130
+ if (typeof rawBody !== "string") return false;
131
+ if (!hasSignatureShape(header)) return false;
132
+ const received = header.slice(PREFIX.length);
133
+ const expected = createHmac("sha256", secret).update(rawBody).digest();
134
+ if (!timingSafeEqual(Buffer.from(received, "hex"), expected)) return false;
135
+ return isFresh(rawBody, Date.now());
136
+ }
137
+ function hasSignatureShape(header) {
138
+ return typeof header === "string" && header.startsWith(PREFIX) && HEX_DIGEST.test(header.slice(PREFIX.length));
139
+ }
140
+ var FRESHNESS_WINDOW_MS = 5 * 60 * 1e3;
141
+ function isFresh(rawBody, now) {
142
+ let envelope;
143
+ try {
144
+ envelope = JSON.parse(rawBody);
145
+ } catch {
146
+ return false;
147
+ }
148
+ if (typeof envelope !== "object" || envelope === null) return false;
149
+ const timestamp = envelope.timestamp;
150
+ if (typeof timestamp !== "string") return false;
151
+ const signedAt = Date.parse(timestamp);
152
+ if (!Number.isFinite(signedAt)) return false;
153
+ return Math.abs(now - signedAt) <= FRESHNESS_WINDOW_MS;
154
+ }
155
+
156
+ // src/server/revalidation.ts
157
+ var MAX_BODY_BYTES = 5 * 1024 * 1024;
158
+ async function readBody(request) {
159
+ if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) return null;
160
+ if (!request.body) return "";
161
+ const reader = request.body.getReader();
162
+ const decoder = new TextDecoder();
163
+ let text = "";
164
+ let size = 0;
165
+ for (; ; ) {
166
+ const { done, value } = await reader.read();
167
+ if (done) return text + decoder.decode();
168
+ size += value.byteLength;
169
+ if (size > MAX_BODY_BYTES) {
170
+ await reader.cancel();
171
+ return null;
172
+ }
173
+ text += decoder.decode(value, { stream: true });
174
+ }
175
+ }
176
+ function tagsForResource(resource, slug) {
177
+ switch (resource) {
178
+ case "content":
179
+ return [
180
+ TAGS.contents,
181
+ TAGS.paths,
182
+ TAGS.seo,
183
+ TAGS.search,
184
+ ...slug ? [TAGS.content(slug)] : []
185
+ ];
186
+ case "category":
187
+ return [
188
+ TAGS.categories,
189
+ TAGS.contents,
190
+ TAGS.paths,
191
+ TAGS.seo,
192
+ TAGS.search,
193
+ ...slug ? [TAGS.content(slug)] : []
194
+ ];
195
+ case "tag":
196
+ return [TAGS.tags, TAGS.contents, TAGS.search];
197
+ case "site_block":
198
+ return [TAGS.pages, TAGS.paths, TAGS.seo, ...slug ? [TAGS.page(slug)] : []];
199
+ case "variable":
200
+ return [TAGS.pages, TAGS.site];
201
+ case "form":
202
+ return [TAGS.forms, ...slug ? [TAGS.form(slug)] : []];
203
+ case "review":
204
+ return [TAGS.reviews];
205
+ case "media":
206
+ return [TAGS.pages, TAGS.contents, TAGS.search];
207
+ case "redirect":
208
+ return [TAGS.paths];
209
+ case "sitemap_entry":
210
+ case "sitemap_rule":
211
+ case "seo":
212
+ return [TAGS.seo];
213
+ case "site":
214
+ return [TAGS.site, TAGS.seo];
215
+ case "search_profile":
216
+ case "search_config":
217
+ return [TAGS.search];
218
+ default:
219
+ return [];
220
+ }
221
+ }
222
+ function tagsForEvent(envelope) {
223
+ if (envelope.event === "form.submitted") return [];
224
+ const resource = envelope.event.split(".")[0] ?? "";
225
+ const slug = envelope.data?.["slug"];
226
+ return [...new Set(tagsForResource(resource, typeof slug === "string" ? slug : null))];
227
+ }
228
+ function createRevalidateHandler({ secret }) {
229
+ return async function POST(request) {
230
+ if (!secret) {
231
+ return Response.json({ error: "Revalidation webhook is not configured" }, { status: 503 });
232
+ }
233
+ const signature = request.headers.get("x-webhook-signature");
234
+ if (!hasSignatureShape(signature)) {
235
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
236
+ }
237
+ const rawBody = await readBody(request);
238
+ if (rawBody === null) {
239
+ return Response.json({ error: "Body too large" }, { status: 413 });
240
+ }
241
+ if (!verifySignature(rawBody, signature, secret)) {
242
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
243
+ }
244
+ let envelope;
245
+ try {
246
+ envelope = JSON.parse(rawBody);
247
+ } catch {
248
+ return Response.json({ error: "Body is not JSON" }, { status: 400 });
249
+ }
250
+ if (typeof envelope?.event !== "string") {
251
+ return Response.json({ error: "Body carries no event name" }, { status: 400 });
252
+ }
253
+ const tags = tagsForEvent(envelope);
254
+ for (const tag of tags) {
255
+ revalidateTag(tag, { expire: 0 });
256
+ }
257
+ return Response.json({ event: envelope.event, revalidated: tags });
258
+ };
259
+ }
260
+
261
+ export { TEXT, XML, createLynkowServer, createRevalidateHandler, rewriteOrigin, tagsForEvent, verifySignature };
262
+ //# sourceMappingURL=index.js.map
263
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/origin.ts","../../src/server/json-ld.tsx","../../src/server/metadata.ts","../../src/server/create-server.ts","../../src/server/signature.ts","../../src/server/revalidation.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Rewrite the site's own domain to the origin this deployment is served from.\n *\n * Lynkow generates `sitemap.xml`, `robots.txt`, `llms.txt` and the JSON-LD\n * graph with absolute URLs built from the site's configured domain, which is\n * correct in production and wrong everywhere else: on localhost, on a preview\n * deployment or on a staging host, a verbatim copy would point at another site.\n *\n * When the two match, the body passes through untouched.\n *\n * @param body - What the API returned\n * @param siteDomain - The site's configured domain, such as `www.example.com`\n * @param siteUrl - The origin this deployment is served from, as `resolveConfig` returns it\n */\nexport function rewriteOrigin(\n body: string,\n siteDomain: string | null | undefined,\n siteUrl: string\n): string {\n if (!siteDomain) return body\n\n const sources = [`https://${siteDomain}`, `http://${siteDomain}`]\n return sources.reduce(\n (acc, source) => (source === siteUrl ? acc : acc.split(source).join(siteUrl)),\n body\n )\n}\n","import { rewriteOrigin } from './origin.js'\n\n/** Props of the `JsonLdScript` component {@link createLynkowServer} returns. */\nexport interface JsonLdScriptProps {\n /** The schema.org nodes, such as a page's `structuredData.graph`. Nothing renders when empty. */\n nodes: object[] | null | undefined\n locale: string\n}\n\n/**\n * The payload of one `<script type=\"application/ld+json\">`: the nodes as a\n * single `@graph` under one `@context`, which is what lets nodes reference each\n * other by `@id`, with URLs on this deployment's origin.\n *\n * Every `<` is written `<`, which JSON reads back as the same character.\n * The HTML parser then never sees a tag inside the script: escaping only\n * `</script` would still let a `<!--` from CMS content change how the parser\n * reads the rest of the element.\n */\nexport function jsonLdPayload(\n nodes: object[],\n siteDomain: string | null | undefined,\n siteUrl: string\n): string {\n const stripped = nodes.map((node) => {\n const copy = { ...(node as Record<string, unknown>) }\n delete copy['@context']\n return copy\n })\n const payload = JSON.stringify({ '@context': 'https://schema.org', '@graph': stripped })\n return rewriteOrigin(payload, siteDomain, siteUrl).replace(/</g, '\\\\u003c')\n}\n\nexport function createJsonLdScript(\n siteUrl: string,\n getSiteDomain: (locale: string) => Promise<string | null | undefined>\n) {\n /**\n * A list of schema.org nodes as ONE `<script type=\"application/ld+json\">`.\n * An async server component: the site's domain is read to put the graph's\n * URLs on this deployment's origin, so they agree with the canonical tag.\n */\n return async function JsonLdScript({ nodes, locale }: JsonLdScriptProps) {\n if (!nodes || nodes.length === 0) return null\n const html = jsonLdPayload(nodes, await getSiteDomain(locale), siteUrl)\n return <script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: html }} />\n }\n}\n","import type { Metadata } from 'next'\nimport type { Page } from 'lynkow'\nimport type { LocalePaths } from '../locale-paths.js'\n\n/** The two metadata builders {@link createLynkowServer} returns. */\nexport interface MetadataBuilders {\n /**\n * Next metadata from a Lynkow page's SEO block.\n *\n * The hreflang set is built from `page.alternates`, so a page translated into\n * two of five locales advertises two alternates rather than five dead ones.\n * The fallbacks walk the same order the API does: an explicit SEO field, then\n * the Open Graph twin, then the page name.\n *\n * An editor's `metaTitle` is the whole `<title>`, brand included, so it is\n * set `absolute` and a layout's `%s | <site name>` template is not appended\n * to it. Only the page name, the fallback, goes through the template.\n *\n * @param fallbackPath - The locale-agnostic path of the route, used for the\n * canonical URL when the page advertises no alternate for `locale`\n */\n pageMetadata(page: Page, locale: string, fallbackPath: string): Metadata\n\n /**\n * Metadata for a route the application owns rather than one Lynkow drives,\n * such as `/reviews`. Its hreflang set is every configured locale: the route\n * exists in each of them by construction.\n *\n * @param path - The locale-agnostic path, with a leading slash\n */\n routeMetadata(locale: string, path: string, title: string, description: string): Metadata\n}\n\nexport function createMetadataBuilders(paths: LocalePaths): MetadataBuilders {\n /** A path the API returned, made public and absolute. */\n function apiUrl(path: string, locale: string): string {\n return paths.absoluteUrl(paths.fromApiPath(path, locale))\n }\n\n /** A route this application owns, which the CMS knows nothing about. */\n function routeUrl(locale: string, path: string): string {\n return paths.absoluteUrl(paths.localePath(locale, path))\n }\n\n function pageMetadata(page: Page, locale: string, fallbackPath: string): Metadata {\n const seo = page.seo\n const title = seo?.metaTitle || page.name || undefined\n const description = seo?.metaDescription || undefined\n\n const wanted = paths.toLocale(locale)\n const current = wanted\n ? page.alternates?.find((alt) => paths.toLocale(alt.locale) === wanted)\n : undefined\n const canonical =\n seo?.canonicalUrl ||\n (current ? apiUrl(current.path, locale) : routeUrl(locale, fallbackPath))\n\n const languages: Record<string, string> = {}\n for (const alternate of page.alternates ?? []) {\n const code = paths.toLocale(alternate.locale)\n if (code) languages[code] = apiUrl(alternate.path, code)\n }\n\n return {\n title: seo?.metaTitle ? { absolute: seo.metaTitle } : title,\n description,\n keywords: seo?.keywords?.length ? seo.keywords : undefined,\n alternates: {\n canonical,\n languages: Object.keys(languages).length > 0 ? languages : undefined,\n },\n robots: seo?.noIndex ? { index: false, follow: false } : undefined,\n openGraph: {\n type: 'website',\n locale,\n url: seo?.ogUrl || canonical,\n siteName: seo?.ogSiteName || undefined,\n title: seo?.ogTitle || title,\n description: seo?.ogDescription || description,\n images: seo?.ogImage?.url\n ? [{ url: seo.ogImage.url, alt: seo.ogImage.alt || undefined }]\n : undefined,\n },\n twitter: {\n card: seo?.twitterCard || 'summary_large_image',\n title: seo?.twitterTitle || seo?.ogTitle || title,\n description: seo?.twitterDescription || seo?.ogDescription || description,\n images: seo?.twitterImage?.url ? [seo.twitterImage.url] : undefined,\n },\n }\n }\n\n function routeMetadata(\n locale: string,\n path: string,\n title: string,\n description: string\n ): Metadata {\n const url = routeUrl(locale, path)\n const languages = Object.fromEntries(paths.locales.map((code) => [code, routeUrl(code, path)]))\n\n return {\n title,\n description,\n alternates: {\n canonical: url,\n languages: paths.locales.length > 1 ? languages : undefined,\n },\n openGraph: { type: 'website', locale, url, title, description },\n }\n }\n\n return { pageMetadata, routeMetadata }\n}\n","import type { BaseRequestOptions, Client } from 'lynkow'\nimport { cached, REVALIDATE, TAGS } from '../cache.js'\nimport type { LynkowNextConfig } from '../config.js'\nimport { createLocalePaths } from '../locale-paths.js'\nimport { createJsonLdScript } from './json-ld.js'\nimport { createMetadataBuilders, type MetadataBuilders } from './metadata.js'\nimport { rewriteOrigin } from './origin.js'\n\n/** `Content-Type` of an XML text route, such as `sitemap.xml`. */\nexport const XML = 'application/xml; charset=utf-8'\n/** `Content-Type` of a plain text route, such as `robots.txt` or `llms.txt`. */\nexport const TEXT = 'text/plain; charset=utf-8'\n\n/**\n * Reads one text file from Lynkow for a route. `options` carries the cache tags\n * and duration; pass it to the SDK call. `locale` is the route's, which a\n * localized file such as `llms.txt` has to pass explicitly, since the SDK reads\n * the locale of that call from its options and not from the client.\n */\nexport type TextReader = (\n client: Client,\n options: BaseRequestOptions,\n locale: string\n) => Promise<string>\n\n/** The route handler {@link LynkowServer.textRoute} returns. */\nexport type TextRouteHandler = (\n request: Request,\n context?: { params?: Promise<{ locale?: string }> }\n) => Promise<Response>\n\n/** Options of {@link createLynkowServer}. */\nexport interface LynkowServerOptions {\n config: LynkowNextConfig\n /** The client of a locale, typically the function `createClients(config)` returns. */\n client: (locale: string) => Client\n /**\n * The site's configured domain, which Lynkow builds the absolute URLs of its\n * generated files from, or null when it has none. Read it from a cached call,\n * such as the site configuration the layout already fetches.\n */\n getSiteDomain: (locale: string) => Promise<string | null | undefined>\n}\n\nexport interface LynkowServer extends MetadataBuilders {\n /**\n * A route handler serving a text file Lynkow generates, such as\n * `sitemap.xml`, `robots.txt` or `llms.txt`.\n *\n * Regenerating those files in the application would mean re-deriving rules\n * the API already computes from the site's own settings, and the two would\n * drift on the first setting change. So the handler does two things: cache\n * the upstream read under `TAGS.seo`, and put its URLs on this deployment's\n * origin. Under a `[locale]` segment the route's locale is used; anywhere\n * else, or for a locale that is not configured, the default one.\n *\n * @example\n * // src/app/sitemap.xml/route.ts\n * export const revalidate = 3600\n * export const GET = server.textRoute((client, options) => client.seo.sitemap(options), XML)\n */\n textRoute(read: TextReader, contentType: string): TextRouteHandler\n\n /** An async server component rendering schema.org nodes as one JSON-LD script. */\n JsonLdScript: ReturnType<typeof createJsonLdScript>\n\n /** {@link rewriteOrigin} bound to this site's origin. */\n rewriteOrigin(body: string, siteDomain: string | null | undefined): string\n}\n\n/**\n * The server-side helpers of a Lynkow site on Next: text routes, JSON-LD and\n * page metadata, bound to one configuration.\n *\n * @example\n * // src/lib/server.ts\n * import { createClients } from '@lynkow/next'\n * import { createLynkowServer } from '@lynkow/next/server'\n * import { config } from './config'\n *\n * export const lynkow = createClients(config)\n * export const server = createLynkowServer({\n * config,\n * client: (locale) => lynkow(locale),\n * getSiteDomain: async (locale) => (await getSiteShell(locale)).domain,\n * })\n */\nexport function createLynkowServer({\n config,\n client,\n getSiteDomain,\n}: LynkowServerOptions): LynkowServer {\n const paths = createLocalePaths(config)\n\n function textRoute(read: TextReader, contentType: string): TextRouteHandler {\n return async function GET(_request, context) {\n const locale = paths.toLocale((await context?.params)?.locale) ?? config.defaultLocale\n\n const [domain, body] = await Promise.all([\n getSiteDomain(locale),\n read(client(locale), cached([TAGS.seo], REVALIDATE.seo), locale),\n ])\n\n return new Response(rewriteOrigin(body, domain, config.siteUrl), {\n headers: {\n 'content-type': contentType,\n 'cache-control': `public, max-age=0, s-maxage=${REVALIDATE.seo}, stale-while-revalidate=86400`,\n },\n })\n }\n }\n\n return {\n textRoute,\n JsonLdScript: createJsonLdScript(config.siteUrl, getSiteDomain),\n rewriteOrigin: (body, siteDomain) => rewriteOrigin(body, siteDomain, config.siteUrl),\n ...createMetadataBuilders(paths),\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto'\n\nconst PREFIX = 'sha256='\nconst HEX_DIGEST = /^[0-9a-f]{64}$/i\n\n/**\n * Verify a Lynkow webhook: its HMAC first, then the freshness of the timestamp\n * that HMAC covers.\n *\n * The API builds `{ event, timestamp, data }` once per delivery attempt, signs\n * `JSON.stringify` of it with the webhook's secret, sends the digest as\n * `X-Webhook-Signature: sha256=<hex>`, and POSTs that exact same string as the\n * body (`webhook_service.ts`, `sendWebhook` and `generateSignature`). So the raw\n * request text is verifiable byte for byte, and the `timestamp` inside it is\n * authenticated.\n *\n * That is why the handler reads `await request.text()` and never `request.json()`:\n * parsing and re-serialising would reorder no keys today, but it is not a\n * property JSON guarantees, and a signature checked against a re-serialisation\n * is a signature that fails for reasons nobody can reproduce.\n *\n * Returns `true` only when every check passes, and `false` for anything else,\n * including an empty secret, a missing or malformed header, a body that is not\n * JSON, and a stale or future timestamp. It never throws on request input, so a\n * crafted header is a rejection rather than a 500. The result deliberately does\n * not say which check failed.\n *\n * It is safe to call on its own: it does not rely on the caller having checked\n * that a secret is configured.\n */\nexport function verifySignature(rawBody: string, header: string | null, secret: string): boolean {\n // An empty key still produces a valid HMAC, one anybody can compute. A\n // verifier that accepted it would authenticate every request.\n if (typeof secret !== 'string' || secret.length === 0) return false\n if (typeof rawBody !== 'string') return false\n if (!hasSignatureShape(header)) return false\n\n const received = header.slice(PREFIX.length)\n const expected = createHmac('sha256', secret).update(rawBody).digest()\n if (!timingSafeEqual(Buffer.from(received, 'hex'), expected)) return false\n\n // Only a body the signature vouches for is parsed.\n return isFresh(rawBody, Date.now())\n}\n\n/**\n * Whether a header has the shape of a Lynkow signature, `sha256=` and 64 hex\n * digits, which a handler can check before reading the body.\n *\n * The FORMAT is validated before anything is compared. A string length counts\n * UTF-16 code units and a UTF-8 buffer counts bytes, so comparing text lets 64\n * non-ASCII characters through a length check and into `timingSafeEqual`,\n * which throws on a byte-length mismatch. Decoding validated hex yields exactly\n * 32 bytes on both sides, so the comparison cannot throw. The shape of a\n * SHA-256 hex digest is public, so this test leaks nothing.\n */\nexport function hasSignatureShape(header: string | null | undefined): header is string {\n return (\n typeof header === 'string' &&\n header.startsWith(PREFIX) &&\n HEX_DIGEST.test(header.slice(PREFIX.length))\n )\n}\n\n/**\n * How far the signed timestamp may sit from this server's clock, in either\n * direction. Without it, one captured request replays forever, and replaying a\n * revalidation is a free cache eviction lever.\n *\n * The API stamps every delivery ATTEMPT afresh, retries included, so this window\n * does not have to absorb the retry schedule, only one attempt's delay plus\n * clock skew between the API and this site. That delay is not capped by the\n * API's 30-second abort: the stamp is taken BEFORE the destination's DNS lookup,\n * which has no timeout of its own, and the abort covers only the 30 seconds\n * after the address resolves. A lookup slow enough to push an attempt past the\n * window fails closed: this site answers 401. That 401 is not always retried.\n * The API retries an event only when EVERY webhook subscribed to it failed, so\n * a retry, with a fresh stamp, follows only when no other webhook on that event\n * accepted it. With several webhooks on one event, a stale 401 here while\n * another one answered 2xx loses this revalidation, and the page stays stale\n * until its `REVALIDATE` ceiling runs out.\n */\nconst FRESHNESS_WINDOW_MS = 5 * 60 * 1000\n\n/** Fail closed: a missing, non-string or unparseable timestamp is not fresh. */\nfunction isFresh(rawBody: string, now: number): boolean {\n let envelope: unknown\n try {\n envelope = JSON.parse(rawBody)\n } catch {\n return false\n }\n if (typeof envelope !== 'object' || envelope === null) return false\n\n const timestamp = (envelope as { timestamp?: unknown }).timestamp\n if (typeof timestamp !== 'string') return false\n\n const signedAt = Date.parse(timestamp)\n if (!Number.isFinite(signedAt)) return false\n\n return Math.abs(now - signedAt) <= FRESHNESS_WINDOW_MS\n}\n","import { revalidateTag } from 'next/cache'\nimport { TAGS } from '../cache.js'\nimport { hasSignatureShape, verifySignature } from './signature.js'\n\n/**\n * The largest body the handler reads. A delivery carries a summary of one\n * resource (the API leaves article bodies out of it) and the API itself refuses\n * a JSON body over 1 MiB, so every real delivery fits with room to spare, while\n * an unauthenticated caller cannot make this route buffer more.\n */\nconst MAX_BODY_BYTES = 5 * 1024 * 1024\n\n/** The body as text, or null past {@link MAX_BODY_BYTES}, declared or streamed. */\nasync function readBody(request: Request): Promise<string | null> {\n if (Number(request.headers.get('content-length')) > MAX_BODY_BYTES) return null\n if (!request.body) return ''\n\n const reader = request.body.getReader()\n const decoder = new TextDecoder()\n let text = ''\n let size = 0\n for (;;) {\n const { done, value } = await reader.read()\n if (done) return text + decoder.decode()\n size += value.byteLength\n if (size > MAX_BODY_BYTES) {\n await reader.cancel()\n return null\n }\n text += decoder.decode(value, { stream: true })\n }\n}\n\n/** The envelope the Lynkow API POSTs to a webhook. */\nexport interface WebhookEnvelope {\n event: string\n timestamp: string\n data: Record<string, unknown>\n}\n\n/**\n * Which cache tags one resource drops.\n *\n * Keyed on the RESOURCE half of the event name rather than on the full name,\n * because the read side does not distinguish them: a page that reads the\n * article list is equally stale after a `content.created` and after a\n * `content.archived`.\n *\n * Eviction is deliberately generous. A tag dropped for nothing costs one API\n * round trip on the next request; a tag NOT dropped serves stale content until\n * the `REVALIDATE` ceiling expires, which is up to an hour.\n *\n * `slug` is present on the `content.*`, `form.*`, `category.*`, `tag.*` and\n * `site_block.*` payloads, and absent on `media.*`, `seo.updated` and\n * `site.updated`, which ask for no slug-scoped tag. A `site_block.*` payload\n * carries `slug` and `locale`, never `path`: a page read by path is evicted by\n * the collective `TAGS.pages`, and `TAGS.page(slug)` exists for pages read by\n * slug.\n */\nfunction tagsForResource(resource: string, slug: string | null): string[] {\n switch (resource) {\n case 'content':\n return [\n TAGS.contents,\n TAGS.paths,\n TAGS.seo,\n TAGS.search,\n ...(slug ? [TAGS.content(slug)] : []),\n ]\n\n case 'category':\n // A category archive is resolved through the same `paths.resolve` call as\n // an article, under `TAGS.content(<last segment>)`, so a renamed category\n // has to drop that key too.\n return [\n TAGS.categories,\n TAGS.contents,\n TAGS.paths,\n TAGS.seo,\n TAGS.search,\n ...(slug ? [TAGS.content(slug)] : []),\n ]\n\n case 'tag':\n return [TAGS.tags, TAGS.contents, TAGS.search]\n\n case 'site_block':\n return [TAGS.pages, TAGS.paths, TAGS.seo, ...(slug ? [TAGS.page(slug)] : [])]\n\n case 'variable':\n // Variables are interpolated inside block data, so a change is visible on\n // any page, not on one identifiable page.\n return [TAGS.pages, TAGS.site]\n\n case 'form':\n return [TAGS.forms, ...(slug ? [TAGS.form(slug)] : [])]\n\n case 'review':\n return [TAGS.reviews]\n\n case 'media':\n // `media.replaced` swaps the file behind a URL that is embedded in block\n // data and in article bodies, and neither payload says where.\n return [TAGS.pages, TAGS.contents, TAGS.search]\n\n case 'redirect':\n return [TAGS.paths]\n\n case 'sitemap_entry':\n case 'sitemap_rule':\n case 'seo':\n return [TAGS.seo]\n\n case 'site':\n return [TAGS.site, TAGS.seo]\n\n case 'search_profile':\n case 'search_config':\n return [TAGS.search]\n\n default:\n return []\n }\n}\n\n/**\n * The tags one delivered event invalidates, deduplicated.\n *\n * An unknown event returns an empty list rather than throwing. Lynkow adds\n * events without asking its consumers, and a site that fails on an event it has\n * never heard of turns a new CMS feature into an outage.\n */\nexport function tagsForEvent(envelope: WebhookEnvelope): string[] {\n // `form.submitted` fires on every visitor submission and changes nothing a\n // site reads: the form schema is untouched and submissions are never listed.\n // Left in the table it would evict the schema of every form on a spam wave.\n if (envelope.event === 'form.submitted') return []\n\n const resource = envelope.event.split('.')[0] ?? ''\n const slug = envelope.data?.['slug']\n\n return [...new Set(tagsForResource(resource, typeof slug === 'string' ? slug : null))]\n}\n\n/** Options of {@link createRevalidateHandler}. */\nexport interface RevalidateHandlerOptions {\n /**\n * The webhook's signing secret, read by the application from a server-only\n * environment variable. Absent or empty, the endpoint stays closed.\n */\n secret: string | undefined\n}\n\n/**\n * The `POST` handler of a revalidation webhook.\n *\n * Point a Lynkow webhook at the route that exports it and give it the secret\n * passed here. Every event the webhook subscribes to drops the cache tags that\n * event can invalidate, so an edit is live in seconds instead of waiting out\n * the `REVALIDATE` ceiling. The route must run on Node, since the signature\n * check needs `node:crypto`:\n *\n * @example\n * // src/app/api/revalidate/route.ts\n * import { createRevalidateHandler } from '@lynkow/next/server'\n *\n * export const runtime = 'nodejs'\n * export const POST = createRevalidateHandler({ secret: process.env.LYNKOW_WEBHOOK_SECRET })\n */\nexport function createRevalidateHandler({ secret }: RevalidateHandlerOptions) {\n return async function POST(request: Request): Promise<Response> {\n if (!secret) {\n // Fail closed. An unauthenticated revalidation endpoint is a free cache\n // eviction lever: anyone who finds the URL can force every page of the\n // site back to the origin on every request.\n return Response.json({ error: 'Revalidation webhook is not configured' }, { status: 503 })\n }\n\n // One answer for a missing or bad signature and for a stale or replayed body\n // alike: the caller learns that the request was refused, never which check\n // refused it. A header without the shape of a signature is refused before\n // the body is read at all.\n const signature = request.headers.get('x-webhook-signature')\n if (!hasSignatureShape(signature)) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // The signature covers the bytes, so read the bytes. See `verifySignature`.\n const rawBody = await readBody(request)\n if (rawBody === null) {\n return Response.json({ error: 'Body too large' }, { status: 413 })\n }\n\n if (!verifySignature(rawBody, signature, secret)) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // The verifier has already parsed this body to read its timestamp, so this\n // cannot fail today. It stays guarded anyway: an unhandled throw here would\n // be a 500, which is exactly the failure a crafted request must never reach.\n let envelope: WebhookEnvelope\n try {\n envelope = JSON.parse(rawBody) as WebhookEnvelope\n } catch {\n return Response.json({ error: 'Body is not JSON' }, { status: 400 })\n }\n\n if (typeof envelope?.event !== 'string') {\n return Response.json({ error: 'Body carries no event name' }, { status: 400 })\n }\n\n const tags = tagsForEvent(envelope)\n for (const tag of tags) {\n // Next 16 made the second argument mandatory: it is the staleness a reader\n // that already holds the entry is still allowed. `{ expire: 0 }` grants\n // none, which is the only setting that matches what this route means. A\n // named profile such as `'max'` would say the opposite, and `updateTag`,\n // which expires immediately by definition, throws outside a Server Action.\n revalidateTag(tag, { expire: 0 })\n }\n\n // 200 with an empty `revalidated` is the right answer for an event this site\n // does not read, not an error: the webhook is working, the event just has no\n // consequence here. A 4xx would fill the delivery log with failures and\n // teach whoever reads it to ignore the log.\n return Response.json({ event: envelope.event, revalidated: tags })\n }\n}\n"]}