@jini-ai/cms 0.5.2 → 0.5.3
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.
- package/CHANGELOG.md +11 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- package/package.json +220 -11
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a rule's `fromPattern` is matched against an incoming path.
|
|
3
|
+
* - `exact` — full-path equality; index-backed O(1) lookup.
|
|
4
|
+
* - `prefix` — longest-prefix match; index/trie-backed, with optional tail
|
|
5
|
+
* pass-through to the target (`/old` → `/new` maps `/old/x` → `/new/x`).
|
|
6
|
+
* - `wildcard` — glob with `*` capture segments; interpolated into the target
|
|
7
|
+
* via `$1..$n`. Part of the bounded "dynamic" seam.
|
|
8
|
+
* - `regex` — anchored, bounded (linear-time / RE2-semantics) regular
|
|
9
|
+
* expression with `$1..$n` capture interpolation. Authoring is
|
|
10
|
+
* gated behind `redirects.use_regex` (ADR-021) and OFF by default:
|
|
11
|
+
* arbitrary regex is exactly the unbounded-expression trust hazard
|
|
12
|
+
* ADR-022's amendment forbids, so it stays a named, gated seam.
|
|
13
|
+
*/
|
|
14
|
+
export const REDIRECT_MATCH_TYPES = ["exact", "prefix", "wildcard", "regex"];
|
|
15
|
+
/**
|
|
16
|
+
* Provenance of a rule. `auto_slug_change` rows are minted by core inside the
|
|
17
|
+
* content write transaction (never-break-links, ADR-033 §5) and carry the entry
|
|
18
|
+
* lineage below; `manual` and `import` come from operators/AI/migration.
|
|
19
|
+
*/
|
|
20
|
+
export const REDIRECT_SOURCES = ["manual", "auto_slug_change", "import"];
|
|
21
|
+
/** Lifecycle of a rule. Disabled rules are retained (audit) but never matched. */
|
|
22
|
+
export const REDIRECT_STATUSES = ["active", "disabled"];
|
|
23
|
+
/** Validation / safety failures raised by the redirects chokepoint. */
|
|
24
|
+
export class RedirectNotFoundError extends Error {
|
|
25
|
+
}
|
|
26
|
+
export class RedirectValidationError extends Error {
|
|
27
|
+
}
|
|
28
|
+
export class RedirectConflictError extends Error {
|
|
29
|
+
}
|
|
30
|
+
/** Raised when a create/update would introduce a cycle that cannot be collapsed. */
|
|
31
|
+
export class RedirectLoopError extends Error {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Raised by the write chokepoint (`redirects.ts`) when an absolute/off-site
|
|
35
|
+
* `toTarget` fails `OriginRegistryPort.isAllowedRedirectTarget` (REQ-08,
|
|
36
|
+
* errors.spec.md `REDIRECT_TARGET_NOT_ALLOWED`). The read-path oracle
|
|
37
|
+
* rejection (`phase-handler.ts`, REQ-09/10) never raises this — it resolves
|
|
38
|
+
* to `{ matched: false }` instead (never surfaced as an HTTP error).
|
|
39
|
+
*/
|
|
40
|
+
export class RedirectTargetNotAllowedError extends Error {
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OriginRegistryPort, VerifiedOrigin } from "@jini-ai/http-kit/verified-origin";
|
|
2
|
+
/**
|
|
3
|
+
* Joins `path` onto `origin`'s base URL, producing an absolute URL.
|
|
4
|
+
*
|
|
5
|
+
* - `path` already absolute (an author's cross-domain canonical override, EC-10, or an
|
|
6
|
+
* already-resolved `ogImage`/`twitterImage` ref) is returned UNCHANGED — never double-prefixed.
|
|
7
|
+
* - `origin` absent (no verified origin registered for the workspace yet — the deliberate,
|
|
8
|
+
* disclosed degradation) returns `path` unchanged, matching today's relative-path behavior. Never
|
|
9
|
+
* fabricates a broken `undefined/path` URL.
|
|
10
|
+
* - Never doubles the slash on the root path: `postPublicPath`'s own contract guarantees `path`
|
|
11
|
+
* always starts with exactly one `/` (the root case is the literal string `"/"`, never `""`), and
|
|
12
|
+
* {@link originBaseUrl} never ends in one, so plain concatenation cannot produce `//`.
|
|
13
|
+
*
|
|
14
|
+
* @complexity O(1).
|
|
15
|
+
*/
|
|
16
|
+
export declare function toAbsoluteUrl(required: {
|
|
17
|
+
origin: VerifiedOrigin | undefined;
|
|
18
|
+
path: string;
|
|
19
|
+
}, _optional?: Record<string, never>): string;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the workspace's verified origin for {@link toAbsoluteUrl}, degrading to `undefined`
|
|
22
|
+
* when none is registered yet (`OriginNotVerifiedError` — ADR-040's documented fail-closed
|
|
23
|
+
* precondition on `canonicalOrigin`, not a bug). Any OTHER error is a real registry failure and is
|
|
24
|
+
* rethrown rather than silently swallowed into the same degraded path.
|
|
25
|
+
*
|
|
26
|
+
* 2026-09-18 production sitemap fix: a `dev-capability`-sourced origin (always `http://localhost`,
|
|
27
|
+
* per `origin/types.ts`'s own invariant) is ALSO degraded to `undefined` whenever this process is
|
|
28
|
+
* running in production (`resolveRuntimeMode() === "production"`, which `fly.toml` sets). Root
|
|
29
|
+
* cause this guards against: `server/runtime/composition/deps.ts`'s `seedDevCapabilityOrigin` call
|
|
30
|
+
* used to be the origin registry's ONLY writer, and ran unconditionally on every boot, including
|
|
31
|
+
* the Fly deployment's. Its first boot durably persisted `http://localhost:3000` into
|
|
32
|
+
* `content.db`'s `origin_settings` table, and its own idempotent find-or-create contract (by
|
|
33
|
+
* design, so a future real registration is never clobbered) means that row survives every later
|
|
34
|
+
* boot too — it can never self-correct on its own. Every public SEO
|
|
35
|
+
* document (`sitemap.xml`, `robots.txt`'s `Sitemap:` line, `canonical`, `og:url`, `og:image`) goes
|
|
36
|
+
* through this one seam, so this is the one place that must refuse to let that row leak into a
|
|
37
|
+
* document served to real crawlers. Degrading here reuses the SAME pre-existing "no verified
|
|
38
|
+
* origin" relative-path fallback `toAbsoluteUrl` already had — never a fabricated origin, per
|
|
39
|
+
* INV-07. Outside production (local dev, tests), a `dev-capability` origin still resolves as
|
|
40
|
+
* before — its entire purpose is to let a dev server exercise absolute-URL code paths.
|
|
41
|
+
*
|
|
42
|
+
* STILL LOAD-BEARING after the 2026-09-18 registration work (`features/origin/configured-origin.ts`,
|
|
43
|
+
* design note `ADS-memory/reports/2026-09-18-public-origin-registration-design.md`), which stopped
|
|
44
|
+
* the dev seed from running in production at all. That change prevents NEW poisoned rows; it cannot
|
|
45
|
+
* remove the one production's `content.db` already holds. Until the host's public URL is set on that
|
|
46
|
+
* deployment (and `registerConfiguredOrigin` corrects the row on the next boot), this degradation
|
|
47
|
+
* is the only thing keeping that row out of documents served to real crawlers. Do not delete it as
|
|
48
|
+
* "now redundant".
|
|
49
|
+
*
|
|
50
|
+
* @param required.runtimeMode - Required host runtime mode; the port never chooses a deployment policy.
|
|
51
|
+
* @complexity O(1) plus one `canonicalOrigin` lookup.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveWorkspaceOrigin(required: {
|
|
54
|
+
originRegistry: OriginRegistryPort;
|
|
55
|
+
workspaceId: string;
|
|
56
|
+
runtimeMode: string;
|
|
57
|
+
}, _optional?: Record<string, never>): Promise<VerifiedOrigin | undefined>;
|
|
58
|
+
//# sourceMappingURL=absolute-url.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { OriginNotVerifiedError } from "@jini-ai/http-kit/verified-origin";
|
|
2
|
+
/**
|
|
3
|
+
* @file `toAbsoluteUrl`/`resolveWorkspaceOrigin` — the one seam that joins a workspace's verified
|
|
4
|
+
* origin onto a site-relative path (SPEC-008 gap fix, 2026-09-03).
|
|
5
|
+
*
|
|
6
|
+
* Before this file existed, `canonical`, `og:url`, `og:image`, and `twitter:image` all emitted bare
|
|
7
|
+
* paths (`/documentation`, `/m/{assetId}/...`) on both production and local — reproduced with
|
|
8
|
+
* `curl` against both. A relative `rel="canonical"` is technically resolvable by a search engine
|
|
9
|
+
* (it resolves against the page's own URL), but `og:url`/`og:image` MUST be absolute per the Open
|
|
10
|
+
* Graph protocol: every social crawler and link-preview fetcher fails to resolve a relative one, so
|
|
11
|
+
* link previews for the live site were silently broken.
|
|
12
|
+
*
|
|
13
|
+
* Root cause: `platform/routing/routing.ts`'s `composeCanonicalUrl` has carried a `TODO(ADR-040)`
|
|
14
|
+
* since routing was built — "compose canonicalUrl from OriginRegistryPort.canonicalOrigin(ctx) once
|
|
15
|
+
* src/origin exists" — but nothing ever wired it up once `src/origin` (now `features/origin`)
|
|
16
|
+
* landed; `RouteResolveContext.originOverride` (the seam that TODO describes) is never populated by
|
|
17
|
+
* any real caller. This file is that wiring, scoped to SEO's own two call sites
|
|
18
|
+
* (`seo.ts`'s `resolveCanonical`/`resolveShareImages`, and `routes/site/pages.ts`'s
|
|
19
|
+
* `buildExtraHead` for the entry-less home/page canonical) rather than a change to the shared
|
|
20
|
+
* `routing` library itself — `urlFor`/`entryPublicPath`/`RouteResolverDeps` have call sites well
|
|
21
|
+
* outside SEO's ownership (menus, redirects, post tool-registrations), so widening their signature
|
|
22
|
+
* to do an async origin lookup on every call is a separate, larger change than this bug fix.
|
|
23
|
+
*
|
|
24
|
+
* ONE join point, not scattered string concatenation: every caller in this feature that needs an
|
|
25
|
+
* absolute URL goes through {@link toAbsoluteUrl}.
|
|
26
|
+
*/
|
|
27
|
+
/** Renders a `VerifiedOrigin` as a base URL with no trailing slash (`https://example.com`,
|
|
28
|
+
* `https://example.com/site`, `http://localhost:3000`).
|
|
29
|
+
*
|
|
30
|
+
* Local copy, not a shared import — `features/site-evidence/same-origin.ts`'s own
|
|
31
|
+
* `verifiedOriginToBaseUrl` (the same three-line join) documents why: `newsletter/unsubscribe.ts`,
|
|
32
|
+
* `newsletter/confirmation.ts`, `redirects/phase-handler.ts`, and `members/write-service.ts` each
|
|
33
|
+
* already keep a private copy rather than import one, because `features/origin` deliberately
|
|
34
|
+
* exports no shared formatter — promoting one is a cross-cutting change owned by that module, not
|
|
35
|
+
* by any one feature. This is the same established convention, not a new one. */
|
|
36
|
+
function originBaseUrl(origin) {
|
|
37
|
+
const authority = origin.port === undefined ? origin.host : `${origin.host}:${origin.port}`;
|
|
38
|
+
return `${origin.scheme}://${authority}${origin.basePath ?? ""}`;
|
|
39
|
+
}
|
|
40
|
+
const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
41
|
+
/** Whether `value` already names a full URL (a scheme is present) rather than a site-relative path
|
|
42
|
+
* — mirrors `platform/routing/routing.ts`'s and `features/seo/media.ts`'s own local copies of the
|
|
43
|
+
* identical check (both pre-existing, both unexported, so this is a third local copy by the same
|
|
44
|
+
* convention {@link originBaseUrl} follows above, not a new pattern). */
|
|
45
|
+
function isAbsoluteUrl(value) {
|
|
46
|
+
return ABSOLUTE_URL_PATTERN.test(value) || value.startsWith("//");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Joins `path` onto `origin`'s base URL, producing an absolute URL.
|
|
50
|
+
*
|
|
51
|
+
* - `path` already absolute (an author's cross-domain canonical override, EC-10, or an
|
|
52
|
+
* already-resolved `ogImage`/`twitterImage` ref) is returned UNCHANGED — never double-prefixed.
|
|
53
|
+
* - `origin` absent (no verified origin registered for the workspace yet — the deliberate,
|
|
54
|
+
* disclosed degradation) returns `path` unchanged, matching today's relative-path behavior. Never
|
|
55
|
+
* fabricates a broken `undefined/path` URL.
|
|
56
|
+
* - Never doubles the slash on the root path: `postPublicPath`'s own contract guarantees `path`
|
|
57
|
+
* always starts with exactly one `/` (the root case is the literal string `"/"`, never `""`), and
|
|
58
|
+
* {@link originBaseUrl} never ends in one, so plain concatenation cannot produce `//`.
|
|
59
|
+
*
|
|
60
|
+
* @complexity O(1).
|
|
61
|
+
*/
|
|
62
|
+
export function toAbsoluteUrl(required, _optional = {}) {
|
|
63
|
+
const { origin, path } = required;
|
|
64
|
+
if (isAbsoluteUrl(path))
|
|
65
|
+
return path;
|
|
66
|
+
if (!origin)
|
|
67
|
+
return path;
|
|
68
|
+
return `${originBaseUrl(origin)}${path}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the workspace's verified origin for {@link toAbsoluteUrl}, degrading to `undefined`
|
|
72
|
+
* when none is registered yet (`OriginNotVerifiedError` — ADR-040's documented fail-closed
|
|
73
|
+
* precondition on `canonicalOrigin`, not a bug). Any OTHER error is a real registry failure and is
|
|
74
|
+
* rethrown rather than silently swallowed into the same degraded path.
|
|
75
|
+
*
|
|
76
|
+
* 2026-09-18 production sitemap fix: a `dev-capability`-sourced origin (always `http://localhost`,
|
|
77
|
+
* per `origin/types.ts`'s own invariant) is ALSO degraded to `undefined` whenever this process is
|
|
78
|
+
* running in production (`resolveRuntimeMode() === "production"`, which `fly.toml` sets). Root
|
|
79
|
+
* cause this guards against: `server/runtime/composition/deps.ts`'s `seedDevCapabilityOrigin` call
|
|
80
|
+
* used to be the origin registry's ONLY writer, and ran unconditionally on every boot, including
|
|
81
|
+
* the Fly deployment's. Its first boot durably persisted `http://localhost:3000` into
|
|
82
|
+
* `content.db`'s `origin_settings` table, and its own idempotent find-or-create contract (by
|
|
83
|
+
* design, so a future real registration is never clobbered) means that row survives every later
|
|
84
|
+
* boot too — it can never self-correct on its own. Every public SEO
|
|
85
|
+
* document (`sitemap.xml`, `robots.txt`'s `Sitemap:` line, `canonical`, `og:url`, `og:image`) goes
|
|
86
|
+
* through this one seam, so this is the one place that must refuse to let that row leak into a
|
|
87
|
+
* document served to real crawlers. Degrading here reuses the SAME pre-existing "no verified
|
|
88
|
+
* origin" relative-path fallback `toAbsoluteUrl` already had — never a fabricated origin, per
|
|
89
|
+
* INV-07. Outside production (local dev, tests), a `dev-capability` origin still resolves as
|
|
90
|
+
* before — its entire purpose is to let a dev server exercise absolute-URL code paths.
|
|
91
|
+
*
|
|
92
|
+
* STILL LOAD-BEARING after the 2026-09-18 registration work (`features/origin/configured-origin.ts`,
|
|
93
|
+
* design note `ADS-memory/reports/2026-09-18-public-origin-registration-design.md`), which stopped
|
|
94
|
+
* the dev seed from running in production at all. That change prevents NEW poisoned rows; it cannot
|
|
95
|
+
* remove the one production's `content.db` already holds. Until the host's public URL is set on that
|
|
96
|
+
* deployment (and `registerConfiguredOrigin` corrects the row on the next boot), this degradation
|
|
97
|
+
* is the only thing keeping that row out of documents served to real crawlers. Do not delete it as
|
|
98
|
+
* "now redundant".
|
|
99
|
+
*
|
|
100
|
+
* @param required.runtimeMode - Required host runtime mode; the port never chooses a deployment policy.
|
|
101
|
+
* @complexity O(1) plus one `canonicalOrigin` lookup.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveWorkspaceOrigin(required, _optional = {}) {
|
|
104
|
+
const { originRegistry, workspaceId, runtimeMode } = required;
|
|
105
|
+
try {
|
|
106
|
+
const origin = await originRegistry.canonicalOrigin({ workspaceId });
|
|
107
|
+
if (origin.source === "dev-capability" && runtimeMode === "production")
|
|
108
|
+
return undefined;
|
|
109
|
+
return origin;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof OriginNotVerifiedError)
|
|
113
|
+
return undefined;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=absolute-url.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HeadElement } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Applies the finalized head descriptors using the browser's text/attribute sinks. The caller
|
|
4
|
+
* owns registration, deduplication and replacement of previous contributions. JSON-LD retains the
|
|
5
|
+
* server serializer's `<` escape so an embedded closing script tag cannot become markup when the
|
|
6
|
+
* document is serialized. Returns the inserted nodes so the caller can remove its contribution.
|
|
7
|
+
* @complexity O(total descriptor text length), one element per descriptor.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyHeadElements(required: {
|
|
10
|
+
elements: readonly HeadElement[];
|
|
11
|
+
}, optional: {
|
|
12
|
+
document: Document;
|
|
13
|
+
}): HTMLElement[];
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies the finalized head descriptors using the browser's text/attribute sinks. The caller
|
|
3
|
+
* owns registration, deduplication and replacement of previous contributions. JSON-LD retains the
|
|
4
|
+
* server serializer's `<` escape so an embedded closing script tag cannot become markup when the
|
|
5
|
+
* document is serialized. Returns the inserted nodes so the caller can remove its contribution.
|
|
6
|
+
* @complexity O(total descriptor text length), one element per descriptor.
|
|
7
|
+
*/
|
|
8
|
+
export function applyHeadElements(required, optional) {
|
|
9
|
+
const { document } = optional;
|
|
10
|
+
const nodes = required.elements.map((element) => {
|
|
11
|
+
switch (element.kind) {
|
|
12
|
+
case "title": {
|
|
13
|
+
const node = document.createElement("title");
|
|
14
|
+
node.textContent = element.text;
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
case "meta": {
|
|
18
|
+
const node = document.createElement("meta");
|
|
19
|
+
node.setAttribute("name", element.name);
|
|
20
|
+
node.setAttribute("content", element.content);
|
|
21
|
+
return node;
|
|
22
|
+
}
|
|
23
|
+
case "og": {
|
|
24
|
+
const node = document.createElement("meta");
|
|
25
|
+
node.setAttribute("property", element.property);
|
|
26
|
+
node.setAttribute("content", element.content);
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
case "link": {
|
|
30
|
+
const node = document.createElement("link");
|
|
31
|
+
node.setAttribute("rel", element.rel);
|
|
32
|
+
node.setAttribute("href", element.href);
|
|
33
|
+
if (element.hreflang)
|
|
34
|
+
node.setAttribute("hreflang", element.hreflang);
|
|
35
|
+
if (element.type)
|
|
36
|
+
node.setAttribute("type", element.type);
|
|
37
|
+
if (element.title)
|
|
38
|
+
node.setAttribute("title", element.title);
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
case "jsonld": {
|
|
42
|
+
const node = document.createElement("script");
|
|
43
|
+
node.setAttribute("type", "application/ld+json");
|
|
44
|
+
node.textContent = JSON.stringify(element.data).replace(/</g, "\\u003c");
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
document.head.append(...nodes);
|
|
50
|
+
return nodes;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Typed domain errors for `seo` (SPEC-008 errors.spec.md §2).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* One class per error this module originates, mirroring
|
|
6
|
+
* `features/settings/errors.ts`'s one-class-per-code convention. Route
|
|
7
|
+
* handlers (`src/server/routes/admin/seo/*.ts`) map these 1:1 to the HTTP
|
|
8
|
+
* codes in api.spec.md §6. `FORBIDDEN` reuses `core/commands`'s shared
|
|
9
|
+
* `ForbiddenError` (same authorize-then-throw shape `write-service.ts`'s
|
|
10
|
+
* settings counterpart and every command-gateway route already use) rather
|
|
11
|
+
* than a second, SEO-local class — `UNAUTHENTICATED` is a route/session-
|
|
12
|
+
* middleware concern (`getAuthedPrincipal`), never thrown by this module.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SeoFieldValidationError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare class SeoInvalidCanonicalUrlError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export declare class SeoEntryNotFoundError extends Error {
|
|
19
|
+
}
|
|
20
|
+
export declare class SeoSettingsValidationError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A concurrent write kept an SEO override from landing (2026-09-07, fable bugs audit SEO-01).
|
|
24
|
+
*
|
|
25
|
+
* `setEntrySeoOverrides` writes the whole `posts` row, so it now writes it under a version
|
|
26
|
+
* predicate and re-reads/re-merges when another writer wins the row. This error is what remains
|
|
27
|
+
* after that bounded retry is exhausted — a row being rewritten faster than this chokepoint can
|
|
28
|
+
* merge onto it. Deliberately its own class rather than reusing `features/post`'s
|
|
29
|
+
* `PostVersionConflictError`: no SEO caller states a version basis, so there is no basis to report
|
|
30
|
+
* back and nothing for the client to reconcile — the honest answer is "retry", not "your version
|
|
31
|
+
* was N and theirs is M".
|
|
32
|
+
*/
|
|
33
|
+
export declare class SeoConcurrentWriteError extends Error {
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Typed domain errors for `seo` (SPEC-008 errors.spec.md §2).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* One class per error this module originates, mirroring
|
|
6
|
+
* `features/settings/errors.ts`'s one-class-per-code convention. Route
|
|
7
|
+
* handlers (`src/server/routes/admin/seo/*.ts`) map these 1:1 to the HTTP
|
|
8
|
+
* codes in api.spec.md §6. `FORBIDDEN` reuses `core/commands`'s shared
|
|
9
|
+
* `ForbiddenError` (same authorize-then-throw shape `write-service.ts`'s
|
|
10
|
+
* settings counterpart and every command-gateway route already use) rather
|
|
11
|
+
* than a second, SEO-local class — `UNAUTHENTICATED` is a route/session-
|
|
12
|
+
* middleware concern (`getAuthedPrincipal`), never thrown by this module.
|
|
13
|
+
*/
|
|
14
|
+
export class SeoFieldValidationError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export class SeoInvalidCanonicalUrlError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export class SeoEntryNotFoundError extends Error {
|
|
19
|
+
}
|
|
20
|
+
export class SeoSettingsValidationError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A concurrent write kept an SEO override from landing (2026-09-07, fable bugs audit SEO-01).
|
|
24
|
+
*
|
|
25
|
+
* `setEntrySeoOverrides` writes the whole `posts` row, so it now writes it under a version
|
|
26
|
+
* predicate and re-reads/re-merges when another writer wins the row. This error is what remains
|
|
27
|
+
* after that bounded retry is exhausted — a row being rewritten faster than this chokepoint can
|
|
28
|
+
* merge onto it. Deliberately its own class rather than reusing `features/post`'s
|
|
29
|
+
* `PostVersionConflictError`: no SEO caller states a version basis, so there is no basis to report
|
|
30
|
+
* back and nothing for the client to reconcile — the honest answer is "retry", not "your version
|
|
31
|
+
* was N and theirs is M".
|
|
32
|
+
*/
|
|
33
|
+
export class SeoConcurrentWriteError extends Error {
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
2
|
+
import { type SeoSitemapDeps } from "./sitemap.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file The site's RSS 2.0 feed (`GET /feed.xml`), built like `sitemap.xml`: from
|
|
5
|
+
* {@link computeIndexableEntries}, the one publish/visibility/`noindex` filter every public SEO
|
|
6
|
+
* document shares, so a draft, trashed or members-only post can never leak here either. Only blog
|
|
7
|
+
* posts (`kind: "post"`) are items; pages are not news.
|
|
8
|
+
*
|
|
9
|
+
* Links are absolute: feed readers resolve nothing against the site. They come from
|
|
10
|
+
* `meta.canonical`, which is absolute once the workspace has a verified origin and degrades to a
|
|
11
|
+
* root-relative path without one (the same disclosed fallback the sitemap has). A static export
|
|
12
|
+
* writes this file without its base-path rewrite (`site-exporter.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Hand-written rather than the `feed` package: one format, about sixty lines, no dependency.
|
|
15
|
+
*/
|
|
16
|
+
/** How many of the newest posts the feed carries. */
|
|
17
|
+
export declare const FEED_ITEM_LIMIT = 20;
|
|
18
|
+
export declare const FEED_PATH = "/feed.xml";
|
|
19
|
+
export interface FeedItem {
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly link: string;
|
|
22
|
+
/** ISO timestamp. The post's go-live time (`publishAt`) when it has one, else `updatedAt` — posts
|
|
23
|
+
* carry no separate published-at column (`postDisplayDateIso`, owner decision 2026-10-05). */
|
|
24
|
+
readonly publishedAt: string;
|
|
25
|
+
readonly description: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
export interface Feed {
|
|
28
|
+
readonly siteTitle: string;
|
|
29
|
+
readonly siteLink: string;
|
|
30
|
+
readonly feedUrl: string;
|
|
31
|
+
readonly items: readonly FeedItem[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The newest {@link FEED_ITEM_LIMIT} published, publicly visible, indexable posts, newest first.
|
|
35
|
+
* @complexity O(n log n) in the workspace's post count (the sort), after
|
|
36
|
+
* {@link computeIndexableEntries}' own O(n) pass.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildFeed(required: {
|
|
39
|
+
deps: SeoSitemapDeps;
|
|
40
|
+
input: {
|
|
41
|
+
workspaceId: UUID;
|
|
42
|
+
siteTitle: string;
|
|
43
|
+
};
|
|
44
|
+
}, _optional?: Record<string, never>): Promise<Feed>;
|
|
45
|
+
/** Serializes a {@link Feed} as RSS 2.0 XML. @complexity O(total text length). */
|
|
46
|
+
export declare function renderRssXml(required: {
|
|
47
|
+
feed: Feed;
|
|
48
|
+
}, _optional?: Record<string, never>): string;
|
|
49
|
+
//# sourceMappingURL=feed.d.ts.map
|
package/dist/seo/feed.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { escapeXml } from "./xml-escape.js";
|
|
2
|
+
import { resolveWorkspaceOrigin, toAbsoluteUrl } from "./absolute-url.js";
|
|
3
|
+
import { deriveExcerpt } from "./seo.js";
|
|
4
|
+
import { computeIndexableEntries } from "./sitemap.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file The site's RSS 2.0 feed (`GET /feed.xml`), built like `sitemap.xml`: from
|
|
7
|
+
* {@link computeIndexableEntries}, the one publish/visibility/`noindex` filter every public SEO
|
|
8
|
+
* document shares, so a draft, trashed or members-only post can never leak here either. Only blog
|
|
9
|
+
* posts (`kind: "post"`) are items; pages are not news.
|
|
10
|
+
*
|
|
11
|
+
* Links are absolute: feed readers resolve nothing against the site. They come from
|
|
12
|
+
* `meta.canonical`, which is absolute once the workspace has a verified origin and degrades to a
|
|
13
|
+
* root-relative path without one (the same disclosed fallback the sitemap has). A static export
|
|
14
|
+
* writes this file without its base-path rewrite (`site-exporter.ts`).
|
|
15
|
+
*
|
|
16
|
+
* Hand-written rather than the `feed` package: one format, about sixty lines, no dependency.
|
|
17
|
+
*/
|
|
18
|
+
/** How many of the newest posts the feed carries. */
|
|
19
|
+
export const FEED_ITEM_LIMIT = 20;
|
|
20
|
+
export const FEED_PATH = "/feed.xml";
|
|
21
|
+
/**
|
|
22
|
+
* The newest {@link FEED_ITEM_LIMIT} published, publicly visible, indexable posts, newest first.
|
|
23
|
+
* @complexity O(n log n) in the workspace's post count (the sort), after
|
|
24
|
+
* {@link computeIndexableEntries}' own O(n) pass.
|
|
25
|
+
*/
|
|
26
|
+
export async function buildFeed(required, _optional = {}) {
|
|
27
|
+
const { deps, input } = required;
|
|
28
|
+
const entries = await computeIndexableEntries({ deps, input: { workspaceId: input.workspaceId } });
|
|
29
|
+
const items = entries
|
|
30
|
+
.filter(({ post }) => post.kind === "post")
|
|
31
|
+
.sort((a, b) => (a.post.updatedAt < b.post.updatedAt ? 1 : a.post.updatedAt > b.post.updatedAt ? -1 : 0))
|
|
32
|
+
.slice(0, FEED_ITEM_LIMIT)
|
|
33
|
+
.map(({ post, meta }) => ({ title: post.title, link: meta.canonical, publishedAt: deps.liveness.postDisplayDateIso({ post }), description: deriveExcerpt({ post, postRepo: deps.postRepo }) }));
|
|
34
|
+
const origin = await resolveWorkspaceOrigin({ originRegistry: deps.originRegistry, workspaceId: input.workspaceId, runtimeMode: deps.runtimeMode });
|
|
35
|
+
return { siteTitle: input.siteTitle, siteLink: toAbsoluteUrl({ origin, path: "/" }), feedUrl: toAbsoluteUrl({ origin, path: FEED_PATH }), items };
|
|
36
|
+
}
|
|
37
|
+
function renderItem(item) {
|
|
38
|
+
const description = item.description === undefined ? "" : ` <description>${escapeXml({ text: item.description })}</description>\n`;
|
|
39
|
+
return (` <item>\n` +
|
|
40
|
+
` <title>${escapeXml({ text: item.title })}</title>\n` +
|
|
41
|
+
` <link>${escapeXml({ text: item.link })}</link>\n` +
|
|
42
|
+
` <guid isPermaLink="true">${escapeXml({ text: item.link })}</guid>\n` +
|
|
43
|
+
` <pubDate>${new Date(item.publishedAt).toUTCString()}</pubDate>\n` +
|
|
44
|
+
description +
|
|
45
|
+
` </item>\n`);
|
|
46
|
+
}
|
|
47
|
+
/** Serializes a {@link Feed} as RSS 2.0 XML. @complexity O(total text length). */
|
|
48
|
+
export function renderRssXml(required, _optional = {}) {
|
|
49
|
+
const { feed } = required;
|
|
50
|
+
return (`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
51
|
+
`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n` +
|
|
52
|
+
` <channel>\n` +
|
|
53
|
+
` <title>${escapeXml({ text: feed.siteTitle })}</title>\n` +
|
|
54
|
+
` <link>${escapeXml({ text: feed.siteLink })}</link>\n` +
|
|
55
|
+
` <description>${escapeXml({ text: feed.siteTitle })}</description>\n` +
|
|
56
|
+
` <atom:link href="${escapeXml({ text: feed.feedUrl })}" rel="self" type="application/rss+xml"/>\n` +
|
|
57
|
+
feed.items.map(renderItem).join("") +
|
|
58
|
+
` </channel>\n` +
|
|
59
|
+
`</rss>\n`);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=feed.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Universal SEO domain: pure producers and services, with host-owned dependencies supplied as ports. */
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./errors.js";
|
|
4
|
+
export * from "./ports.js";
|
|
5
|
+
export * from "./absolute-url.js";
|
|
6
|
+
export * from "./seo.js";
|
|
7
|
+
export * from "./media.js";
|
|
8
|
+
export * from "./sitemap.js";
|
|
9
|
+
export * from "./feed.js";
|
|
10
|
+
export * from "./page-head.js";
|
|
11
|
+
export * from "./write-service.js";
|
|
12
|
+
export * from "./xml-escape.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Universal SEO domain: pure producers and services, with host-owned dependencies supplied as ports. */
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./errors.js";
|
|
4
|
+
export * from "./ports.js";
|
|
5
|
+
export * from "./absolute-url.js";
|
|
6
|
+
export * from "./seo.js";
|
|
7
|
+
export * from "./media.js";
|
|
8
|
+
export * from "./sitemap.js";
|
|
9
|
+
export * from "./feed.js";
|
|
10
|
+
export * from "./page-head.js";
|
|
11
|
+
export * from "./write-service.js";
|
|
12
|
+
export * from "./xml-escape.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { SeoMediaPort, SeoFeaturedImagePort } from "./ports.js";
|
|
3
|
+
export type ResolveSeoImageRefDeps = SeoMediaPort;
|
|
4
|
+
export interface ResolveSeoImageRefInput {
|
|
5
|
+
workspaceId: UUID;
|
|
6
|
+
/** Either `"{idOrSlug}:{transformName}"` (the asset's id, or — since readable-slugs S4/S5b — its
|
|
7
|
+
* slug) or an already-absolute URL. */
|
|
8
|
+
ref: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves an `ogImage`/`twitterImage` field to a URL, or `undefined` on any
|
|
12
|
+
* miss. Returns `ref` unchanged when it is already an absolute URL; otherwise
|
|
13
|
+
* returns the site-relative `/m/{key}/...` URL contract (ADR-027 §4), keyed by the asset's current
|
|
14
|
+
* slug when it has a valid one (readable-slugs S4), otherwise its id —
|
|
15
|
+
* NOT necessarily absolute (this function has no origin to join, by design;
|
|
16
|
+
* `seo.ts`'s `resolveShareImages` is the one caller and absolutizes the
|
|
17
|
+
* result via `toAbsoluteUrl`, since `og:image`/`twitter:image` must be
|
|
18
|
+
* absolute for crawlers, same as `og:url`).
|
|
19
|
+
*
|
|
20
|
+
* Does not check whether a rendition row already exists (AMENDED 2026-09-05
|
|
21
|
+
* — see this file's header doc for the safety argument): the emitted URL
|
|
22
|
+
* always targets the latest registered transform version, which the public
|
|
23
|
+
* `/m/` route always lazily generates on first anonymous fetch.
|
|
24
|
+
*
|
|
25
|
+
* @complexity O(1) — bounded repo lookups (one asset read, one transform-
|
|
26
|
+
* version listing).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveSeoImageRef(required: {
|
|
29
|
+
deps: ResolveSeoImageRefDeps;
|
|
30
|
+
input: ResolveSeoImageRefInput;
|
|
31
|
+
}, _optional?: Record<string, never>): Promise<string | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* The write-path refusal for an `ogImage`/`twitterImage`/`defaultOgImage` value (2026-10-05): the
|
|
34
|
+
* message to refuse it with, or `null` to accept it. Both SEO write chokepoints call it
|
|
35
|
+
* (`setEntrySeoOverrides` in `write-service.ts`, `setSeoSettings` in `settings.ts`), so the chat
|
|
36
|
+
* tools and the admin REST routes share it.
|
|
37
|
+
*
|
|
38
|
+
* The check itself is the post featured image's ({@link resolveFeaturedImageRef}), run on the
|
|
39
|
+
* ref's `{idOrSlug}` half — one content-type rule for every "this must be an image" media field.
|
|
40
|
+
* Only a KNOWN non-image is refused. A missing or trashed asset is accepted, as before: the head
|
|
41
|
+
* render already drops such a ref fail-soft (EC-07, {@link resolveSeoImageRef}), and refusing it
|
|
42
|
+
* here would be a new rule this fix was not asked for. An absolute URL, a non-ref string and
|
|
43
|
+
* `null`/`undefined` (clear/omitted) are accepted untouched.
|
|
44
|
+
*
|
|
45
|
+
* @complexity O(1) — at most two repo reads and one content-type read.
|
|
46
|
+
*/
|
|
47
|
+
export declare function seoImageRefRefusal(required: {
|
|
48
|
+
deps: {
|
|
49
|
+
featuredImage: SeoFeaturedImagePort;
|
|
50
|
+
};
|
|
51
|
+
input: {
|
|
52
|
+
workspaceId: string;
|
|
53
|
+
field: string;
|
|
54
|
+
ref: unknown;
|
|
55
|
+
};
|
|
56
|
+
}, _optional?: Record<string, never>): Promise<string | null>;
|
|
57
|
+
//# sourceMappingURL=media.d.ts.map
|