@aotter/mantle-web 0.1.0-alpha.10
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/LICENSE +202 -0
- package/README.md +25 -0
- package/dist/MantleWeb.d.ts +23 -0
- package/dist/MantleWeb.d.ts.map +1 -0
- package/dist/MantleWeb.js +19 -0
- package/dist/MantleWeb.js.map +1 -0
- package/dist/dto/ComposeEntrySeoMetaRequest.d.ts +19 -0
- package/dist/dto/ComposeEntrySeoMetaRequest.d.ts.map +1 -0
- package/dist/dto/ComposeEntrySeoMetaRequest.js +2 -0
- package/dist/dto/ComposeEntrySeoMetaRequest.js.map +1 -0
- package/dist/dto/ComposeLlmsTxtRequest.d.ts +14 -0
- package/dist/dto/ComposeLlmsTxtRequest.d.ts.map +1 -0
- package/dist/dto/ComposeLlmsTxtRequest.js +2 -0
- package/dist/dto/ComposeLlmsTxtRequest.js.map +1 -0
- package/dist/dto/ComposeSitemapRequest.d.ts +14 -0
- package/dist/dto/ComposeSitemapRequest.d.ts.map +1 -0
- package/dist/dto/ComposeSitemapRequest.js +2 -0
- package/dist/dto/ComposeSitemapRequest.js.map +1 -0
- package/dist/dto/PreviewEntryRequest.d.ts +18 -0
- package/dist/dto/PreviewEntryRequest.d.ts.map +1 -0
- package/dist/dto/PreviewEntryRequest.js +2 -0
- package/dist/dto/PreviewEntryRequest.js.map +1 -0
- package/dist/dto/RenderEntryLiveRequest.d.ts +19 -0
- package/dist/dto/RenderEntryLiveRequest.d.ts.map +1 -0
- package/dist/dto/RenderEntryLiveRequest.js +2 -0
- package/dist/dto/RenderEntryLiveRequest.js.map +1 -0
- package/dist/dto/RenderListLiveRequest.d.ts +15 -0
- package/dist/dto/RenderListLiveRequest.d.ts.map +1 -0
- package/dist/dto/RenderListLiveRequest.js +2 -0
- package/dist/dto/RenderListLiveRequest.js.map +1 -0
- package/dist/dto/index.d.ts +7 -0
- package/dist/dto/index.d.ts.map +1 -0
- package/dist/dto/index.js +7 -0
- package/dist/dto/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/model/SeoMeta.d.ts +47 -0
- package/dist/model/SeoMeta.d.ts.map +1 -0
- package/dist/model/SeoMeta.js +2 -0
- package/dist/model/SeoMeta.js.map +1 -0
- package/dist/model/TemplateRegistry.d.ts +50 -0
- package/dist/model/TemplateRegistry.d.ts.map +1 -0
- package/dist/model/TemplateRegistry.js +17 -0
- package/dist/model/TemplateRegistry.js.map +1 -0
- package/dist/model/WebSiteConfig.d.ts +4 -0
- package/dist/model/WebSiteConfig.d.ts.map +1 -0
- package/dist/model/WebSiteConfig.js +2 -0
- package/dist/model/WebSiteConfig.js.map +1 -0
- package/dist/service/AbsoluteUrl.d.ts +6 -0
- package/dist/service/AbsoluteUrl.d.ts.map +1 -0
- package/dist/service/AbsoluteUrl.js +33 -0
- package/dist/service/AbsoluteUrl.js.map +1 -0
- package/dist/service/HtmlEscaping.d.ts +2 -0
- package/dist/service/HtmlEscaping.d.ts.map +1 -0
- package/dist/service/HtmlEscaping.js +9 -0
- package/dist/service/HtmlEscaping.js.map +1 -0
- package/dist/service/HtmlRenderer.d.ts +39 -0
- package/dist/service/HtmlRenderer.d.ts.map +1 -0
- package/dist/service/HtmlRenderer.js +73 -0
- package/dist/service/HtmlRenderer.js.map +1 -0
- package/dist/service/MarkdownSerializer.d.ts +46 -0
- package/dist/service/MarkdownSerializer.d.ts.map +1 -0
- package/dist/service/MarkdownSerializer.js +154 -0
- package/dist/service/MarkdownSerializer.js.map +1 -0
- package/dist/service/PreviewBanner.d.ts +10 -0
- package/dist/service/PreviewBanner.d.ts.map +1 -0
- package/dist/service/PreviewBanner.js +30 -0
- package/dist/service/PreviewBanner.js.map +1 -0
- package/dist/service/PublicPathResolver.d.ts +46 -0
- package/dist/service/PublicPathResolver.d.ts.map +1 -0
- package/dist/service/PublicPathResolver.js +36 -0
- package/dist/service/PublicPathResolver.js.map +1 -0
- package/dist/service/SeoMetaComposer.d.ts +61 -0
- package/dist/service/SeoMetaComposer.d.ts.map +1 -0
- package/dist/service/SeoMetaComposer.js +213 -0
- package/dist/service/SeoMetaComposer.js.map +1 -0
- package/dist/service/SitemapSerializer.d.ts +20 -0
- package/dist/service/SitemapSerializer.d.ts.map +1 -0
- package/dist/service/SitemapSerializer.js +27 -0
- package/dist/service/SitemapSerializer.js.map +1 -0
- package/dist/service/io/MediaAssetReferences.d.ts +15 -0
- package/dist/service/io/MediaAssetReferences.d.ts.map +1 -0
- package/dist/service/io/MediaAssetReferences.js +48 -0
- package/dist/service/io/MediaAssetReferences.js.map +1 -0
- package/dist/usecase/ComposeEntrySeoMetaUseCase.d.ts +29 -0
- package/dist/usecase/ComposeEntrySeoMetaUseCase.d.ts.map +1 -0
- package/dist/usecase/ComposeEntrySeoMetaUseCase.js +66 -0
- package/dist/usecase/ComposeEntrySeoMetaUseCase.js.map +1 -0
- package/dist/usecase/ComposeLlmsTxtUseCase.d.ts +18 -0
- package/dist/usecase/ComposeLlmsTxtUseCase.d.ts.map +1 -0
- package/dist/usecase/ComposeLlmsTxtUseCase.js +47 -0
- package/dist/usecase/ComposeLlmsTxtUseCase.js.map +1 -0
- package/dist/usecase/ComposeSitemapUseCase.d.ts +15 -0
- package/dist/usecase/ComposeSitemapUseCase.d.ts.map +1 -0
- package/dist/usecase/ComposeSitemapUseCase.js +38 -0
- package/dist/usecase/ComposeSitemapUseCase.js.map +1 -0
- package/dist/usecase/PreviewEntryUseCase.d.ts +28 -0
- package/dist/usecase/PreviewEntryUseCase.d.ts.map +1 -0
- package/dist/usecase/PreviewEntryUseCase.js +72 -0
- package/dist/usecase/PreviewEntryUseCase.js.map +1 -0
- package/dist/usecase/RenderEntryLiveUseCase.d.ts +36 -0
- package/dist/usecase/RenderEntryLiveUseCase.d.ts.map +1 -0
- package/dist/usecase/RenderEntryLiveUseCase.js +62 -0
- package/dist/usecase/RenderEntryLiveUseCase.js.map +1 -0
- package/dist/usecase/RenderListLiveUseCase.d.ts +25 -0
- package/dist/usecase/RenderListLiveUseCase.d.ts.map +1 -0
- package/dist/usecase/RenderListLiveUseCase.js +46 -0
- package/dist/usecase/RenderListLiveUseCase.js.map +1 -0
- package/dist/usecase/index.d.ts +7 -0
- package/dist/usecase/index.d.ts.map +1 -0
- package/dist/usecase/index.js +7 -0
- package/dist/usecase/index.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { toUrlLocale } from "@aotter/mantle-spec";
|
|
2
|
+
export function entrySlug(entry) {
|
|
3
|
+
const fromData = entry.data["slug"];
|
|
4
|
+
if (typeof fromData === "string" && /^[a-z0-9][a-z0-9-]*$/.test(fromData)) {
|
|
5
|
+
return fromData;
|
|
6
|
+
}
|
|
7
|
+
return entry.id;
|
|
8
|
+
}
|
|
9
|
+
export function entryPublicPath(entry) {
|
|
10
|
+
const slug = entrySlug(entry);
|
|
11
|
+
if (entry.locale)
|
|
12
|
+
return `/${toUrlLocale(entry.locale)}/${entry.collection}/${slug}`;
|
|
13
|
+
return `/${entry.collection}/${slug}`;
|
|
14
|
+
}
|
|
15
|
+
export function createPublicPathResolver(config) {
|
|
16
|
+
const routes = config.collectionRoutes;
|
|
17
|
+
return {
|
|
18
|
+
forEntry(entry) {
|
|
19
|
+
const route = routes[entry.collection];
|
|
20
|
+
if (!route || route.segment === null)
|
|
21
|
+
return null;
|
|
22
|
+
const slug = entrySlug({ id: entry.id, data: entry.data });
|
|
23
|
+
const locale = entry.locale ? toUrlLocale(entry.locale) : "";
|
|
24
|
+
const localePrefix = locale ? `/${locale}` : "";
|
|
25
|
+
if (route.homeSlug && slug === route.homeSlug) {
|
|
26
|
+
return localePrefix || "/";
|
|
27
|
+
}
|
|
28
|
+
const segment = route.segment;
|
|
29
|
+
return segment ? `${localePrefix}/${segment}/${slug}` : `${localePrefix}/${slug}`;
|
|
30
|
+
},
|
|
31
|
+
routeFor(collection) {
|
|
32
|
+
return routes[collection];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=PublicPathResolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PublicPathResolver.js","sourceRoot":"","sources":["../../src/service/PublicPathResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,qBAAqB,CAAC;AAE9D,MAAM,UAAU,SAAS,CAAC,KAAoD;IAC5E,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1E,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC,EAAE,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAY;IAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;IACrF,OAAO,IAAI,KAAK,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;AACxC,CAAC;AA2CD,MAAM,UAAU,wBAAwB,CACtC,MAAgC;IAEhC,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACvC,OAAO;QACL,QAAQ,CAAC,KAAY;YACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACvC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAClD,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC3D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChD,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC9C,OAAO,YAAY,IAAI,GAAG,CAAC;YAC7B,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,IAAI,EAAE,CAAC;QACpF,CAAC;QACD,QAAQ,CAAC,UAAkB;YACzB,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Entry } from "@aotter/mantle-spec";
|
|
2
|
+
import type { SeoMeta } from "../model/SeoMeta.js";
|
|
3
|
+
import type { WebSiteConfig } from "../model/WebSiteConfig.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pure composer for SEO + AEO meta blocks emitted on every public
|
|
6
|
+
* entry page. Carries the conventions a consumer otherwise re-invents
|
|
7
|
+
* — canonical, hreflang siblings, the `.md` mirror alternate that
|
|
8
|
+
* lets LLM crawlers grab clean markdown, og: + twitter card, JSON-LD
|
|
9
|
+
* structured data.
|
|
10
|
+
*
|
|
11
|
+
* The composer is pure — no I/O, no DB. The caller resolves the
|
|
12
|
+
* sibling translations (for hreflang) and passes them in. See
|
|
13
|
+
* `ComposeEntrySeoMetaUseCase` for the wired version that does the
|
|
14
|
+
* sibling-read.
|
|
15
|
+
*
|
|
16
|
+
* The shape of the result lives in `domain/model/SeoMeta` so
|
|
17
|
+
* `TemplateRegistry`'s `EntryContext.seo` can reference it without
|
|
18
|
+
* pulling service code into model.
|
|
19
|
+
*/
|
|
20
|
+
export type { SeoMeta } from "../model/SeoMeta.js";
|
|
21
|
+
export interface SiblingTranslation {
|
|
22
|
+
readonly locale: string;
|
|
23
|
+
readonly publicPath: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ComposeEntrySeoMetaArgs {
|
|
26
|
+
readonly entry: Entry;
|
|
27
|
+
readonly site: WebSiteConfig;
|
|
28
|
+
/** The current entry's public path (already includes locale prefix).
|
|
29
|
+
* Composer prepends `site.origin` to absolutize. */
|
|
30
|
+
readonly publicPath: string;
|
|
31
|
+
/** Optional sibling translations for hreflang. The current entry's
|
|
32
|
+
* locale entry is added by the composer if not already present. */
|
|
33
|
+
readonly siblings?: ReadonlyArray<SiblingTranslation>;
|
|
34
|
+
/** og:type. Defaults to `article` when the entry has a `publishedAt`
|
|
35
|
+
* (or `updatedAt`) field, `website` otherwise. */
|
|
36
|
+
readonly type?: "article" | "website";
|
|
37
|
+
/** Public-route locale for non-localized entries mounted under a locale URL. */
|
|
38
|
+
readonly locale?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function composeEntrySeoMeta(args: ComposeEntrySeoMetaArgs): SeoMeta;
|
|
41
|
+
export interface ComposePageSeoMetaArgs {
|
|
42
|
+
readonly site: WebSiteConfig;
|
|
43
|
+
readonly locale: string;
|
|
44
|
+
readonly publicPath: string;
|
|
45
|
+
readonly title?: string;
|
|
46
|
+
readonly description?: string | null;
|
|
47
|
+
readonly markdown: boolean;
|
|
48
|
+
readonly pathForLocale: (locale: string) => string;
|
|
49
|
+
}
|
|
50
|
+
/** SEO for public home/list pages that are not represented by one Entry. */
|
|
51
|
+
export declare function composePageSeoMeta(args: ComposePageSeoMetaArgs): SeoMeta;
|
|
52
|
+
/**
|
|
53
|
+
* Render the SEO meta block as an HTML fragment. Templates embed this
|
|
54
|
+
* verbatim inside `<head>` via `raw(renderSeoTagsHtml(seo))`.
|
|
55
|
+
*
|
|
56
|
+
* Order matters slightly for crawlers — canonical first, alternates
|
|
57
|
+
* (markdown then hreflang) before og:/twitter so the discoverability
|
|
58
|
+
* bones land before the rich-preview block.
|
|
59
|
+
*/
|
|
60
|
+
export declare function renderSeoTagsHtml(meta: SeoMeta): string;
|
|
61
|
+
//# sourceMappingURL=SeoMetaComposer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoMetaComposer.d.ts","sourceRoot":"","sources":["../../src/service/SeoMetaComposer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAK/D;;;;;;;;;;;;;;;GAeG;AACH,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B;yDACqD;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;wEACoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACtD;uDACmD;IACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAkD1E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACpD;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAiDxE;AAmFD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAgDvD"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { toUrlLocale } from "@aotter/mantle-spec";
|
|
2
|
+
import { absoluteUrl, appendMarkdownExt } from "./AbsoluteUrl.js";
|
|
3
|
+
import { escapeHtml as escapeAttr } from "./HtmlEscaping.js";
|
|
4
|
+
import { getEntryDescription, hasMarkdownBody } from "./MarkdownSerializer.js";
|
|
5
|
+
export function composeEntrySeoMeta(args) {
|
|
6
|
+
const { entry, site, publicPath, siblings = [], type } = args;
|
|
7
|
+
const data = entry.data;
|
|
8
|
+
const canonical = absoluteUrl(site.origin, publicPath);
|
|
9
|
+
const alternateMarkdown = hasMarkdownBody(entry) ? appendMarkdownExt(canonical) : null;
|
|
10
|
+
const title = stringField(data, "title") ?? site.title;
|
|
11
|
+
const description = getEntryDescription(entry) ?? site.description ?? null;
|
|
12
|
+
const image = stringField(data, "coverUrl") ?? stringField(data, "ogImage") ?? null;
|
|
13
|
+
const ogType = type ?? (hasArticleSignal(entry) ? "article" : "website");
|
|
14
|
+
const entryLocale = args.locale ?? entry.locale ?? site.canonicalLocale ?? "en";
|
|
15
|
+
const hreflangs = buildHreflangs({
|
|
16
|
+
site,
|
|
17
|
+
currentLocale: entryLocale,
|
|
18
|
+
currentPublicPath: publicPath,
|
|
19
|
+
siblings,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
canonical,
|
|
23
|
+
alternateMarkdown,
|
|
24
|
+
hreflangs,
|
|
25
|
+
description: description ?? null,
|
|
26
|
+
og: {
|
|
27
|
+
type: ogType,
|
|
28
|
+
url: canonical,
|
|
29
|
+
title,
|
|
30
|
+
siteName: site.title || site.brand,
|
|
31
|
+
description: description ?? null,
|
|
32
|
+
image,
|
|
33
|
+
locale: entryLocale,
|
|
34
|
+
},
|
|
35
|
+
twitter: {
|
|
36
|
+
card: image ? "summary_large_image" : "summary",
|
|
37
|
+
title,
|
|
38
|
+
description: description ?? null,
|
|
39
|
+
image,
|
|
40
|
+
},
|
|
41
|
+
jsonLd: buildJsonLd({
|
|
42
|
+
ogType,
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
canonical,
|
|
46
|
+
image,
|
|
47
|
+
data,
|
|
48
|
+
site,
|
|
49
|
+
locale: entryLocale,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** SEO for public home/list pages that are not represented by one Entry. */
|
|
54
|
+
export function composePageSeoMeta(args) {
|
|
55
|
+
const { site, locale, publicPath, pathForLocale } = args;
|
|
56
|
+
const canonical = absoluteUrl(site.origin, publicPath);
|
|
57
|
+
const title = args.title ?? site.title;
|
|
58
|
+
const description = args.description ?? site.description ?? null;
|
|
59
|
+
const hreflangs = [
|
|
60
|
+
...site.locales.map((candidate) => ({
|
|
61
|
+
locale: candidate,
|
|
62
|
+
href: absoluteUrl(site.origin, pathForLocale(candidate)),
|
|
63
|
+
})),
|
|
64
|
+
{
|
|
65
|
+
locale: "x-default",
|
|
66
|
+
href: absoluteUrl(site.origin, pathForLocale(site.canonicalLocale ?? site.locales[0] ?? locale)),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
return {
|
|
70
|
+
canonical,
|
|
71
|
+
alternateMarkdown: args.markdown ? appendMarkdownExt(canonical) : null,
|
|
72
|
+
hreflangs,
|
|
73
|
+
description,
|
|
74
|
+
og: {
|
|
75
|
+
type: "website",
|
|
76
|
+
url: canonical,
|
|
77
|
+
title,
|
|
78
|
+
siteName: site.title || site.brand,
|
|
79
|
+
description,
|
|
80
|
+
image: null,
|
|
81
|
+
locale,
|
|
82
|
+
},
|
|
83
|
+
twitter: {
|
|
84
|
+
card: "summary",
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
image: null,
|
|
88
|
+
},
|
|
89
|
+
jsonLd: buildJsonLd({
|
|
90
|
+
ogType: "website",
|
|
91
|
+
title,
|
|
92
|
+
description,
|
|
93
|
+
canonical,
|
|
94
|
+
image: null,
|
|
95
|
+
data: {},
|
|
96
|
+
site,
|
|
97
|
+
locale,
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function buildHreflangs(args) {
|
|
102
|
+
const { site, currentLocale, currentPublicPath, siblings } = args;
|
|
103
|
+
const out = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const push = (locale, path) => {
|
|
106
|
+
const key = locale.toLowerCase();
|
|
107
|
+
if (seen.has(key))
|
|
108
|
+
return;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
out.push({ locale, href: absoluteUrl(site.origin, path) });
|
|
111
|
+
};
|
|
112
|
+
push(currentLocale, currentPublicPath);
|
|
113
|
+
for (const s of siblings)
|
|
114
|
+
push(s.locale, s.publicPath);
|
|
115
|
+
// x-default points at the canonical-locale variant when known,
|
|
116
|
+
// falling back to the current entry's URL otherwise. Search engines
|
|
117
|
+
// expect this row alongside the per-locale ones.
|
|
118
|
+
const canonicalSibling = siblings.find((s) => site.canonicalLocale && s.locale.toLowerCase() === site.canonicalLocale.toLowerCase());
|
|
119
|
+
out.push({
|
|
120
|
+
locale: "x-default",
|
|
121
|
+
href: absoluteUrl(site.origin, canonicalSibling?.publicPath ?? currentPublicPath),
|
|
122
|
+
});
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function buildJsonLd(args) {
|
|
126
|
+
const { ogType, title, description, canonical, image, data, site, locale } = args;
|
|
127
|
+
const publishedAtMs = numericField(data, "publishedAt");
|
|
128
|
+
const updatedAtMs = numericField(data, "updatedAt");
|
|
129
|
+
const base = {
|
|
130
|
+
"@context": "https://schema.org",
|
|
131
|
+
"@type": ogType === "article" ? "Article" : "WebPage",
|
|
132
|
+
headline: title,
|
|
133
|
+
name: title,
|
|
134
|
+
url: canonical,
|
|
135
|
+
description: description ?? undefined,
|
|
136
|
+
image: image ?? undefined,
|
|
137
|
+
inLanguage: locale,
|
|
138
|
+
publisher: site.title || site.brand
|
|
139
|
+
? {
|
|
140
|
+
"@type": "Organization",
|
|
141
|
+
name: site.title || site.brand,
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
datePublished: publishedAtMs ? new Date(publishedAtMs).toISOString() : undefined,
|
|
145
|
+
dateModified: updatedAtMs ? new Date(updatedAtMs).toISOString() : undefined,
|
|
146
|
+
};
|
|
147
|
+
// Drop undefined keys so JSON.stringify produces a clean object.
|
|
148
|
+
return Object.fromEntries(Object.entries(base).filter(([, v]) => v !== undefined));
|
|
149
|
+
}
|
|
150
|
+
function stringField(data, key) {
|
|
151
|
+
const v = data[key];
|
|
152
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
153
|
+
}
|
|
154
|
+
function numericField(data, key) {
|
|
155
|
+
const v = data[key];
|
|
156
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
157
|
+
}
|
|
158
|
+
function hasArticleSignal(entry) {
|
|
159
|
+
return typeof entry.data["publishedAt"] === "number" || hasMarkdownBody(entry);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Render the SEO meta block as an HTML fragment. Templates embed this
|
|
163
|
+
* verbatim inside `<head>` via `raw(renderSeoTagsHtml(seo))`.
|
|
164
|
+
*
|
|
165
|
+
* Order matters slightly for crawlers — canonical first, alternates
|
|
166
|
+
* (markdown then hreflang) before og:/twitter so the discoverability
|
|
167
|
+
* bones land before the rich-preview block.
|
|
168
|
+
*/
|
|
169
|
+
export function renderSeoTagsHtml(meta) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
parts.push(`<link rel="canonical" href="${escapeAttr(meta.canonical)}">`);
|
|
172
|
+
if (meta.alternateMarkdown) {
|
|
173
|
+
parts.push(`<link rel="alternate" type="text/markdown" href="${escapeAttr(meta.alternateMarkdown)}">`);
|
|
174
|
+
}
|
|
175
|
+
for (const h of meta.hreflangs) {
|
|
176
|
+
parts.push(`<link rel="alternate" hreflang="${escapeAttr(h.locale)}" href="${escapeAttr(h.href)}">`);
|
|
177
|
+
}
|
|
178
|
+
if (meta.description) {
|
|
179
|
+
parts.push(`<meta name="description" content="${escapeAttr(meta.description)}">`);
|
|
180
|
+
}
|
|
181
|
+
parts.push(`<meta name="generator" content="mantle">`);
|
|
182
|
+
parts.push(`<meta property="og:type" content="${escapeAttr(meta.og.type)}">`);
|
|
183
|
+
parts.push(`<meta property="og:url" content="${escapeAttr(meta.og.url)}">`);
|
|
184
|
+
parts.push(`<meta property="og:title" content="${escapeAttr(meta.og.title)}">`);
|
|
185
|
+
if (meta.og.siteName) {
|
|
186
|
+
parts.push(`<meta property="og:site_name" content="${escapeAttr(meta.og.siteName)}">`);
|
|
187
|
+
}
|
|
188
|
+
if (meta.og.description) {
|
|
189
|
+
parts.push(`<meta property="og:description" content="${escapeAttr(meta.og.description)}">`);
|
|
190
|
+
}
|
|
191
|
+
if (meta.og.image) {
|
|
192
|
+
parts.push(`<meta property="og:image" content="${escapeAttr(meta.og.image)}">`);
|
|
193
|
+
}
|
|
194
|
+
parts.push(`<meta property="og:locale" content="${escapeAttr(toUrlLocale(meta.og.locale))}">`);
|
|
195
|
+
parts.push(`<meta name="twitter:card" content="${escapeAttr(meta.twitter.card)}">`);
|
|
196
|
+
parts.push(`<meta name="twitter:title" content="${escapeAttr(meta.twitter.title)}">`);
|
|
197
|
+
if (meta.twitter.description) {
|
|
198
|
+
parts.push(`<meta name="twitter:description" content="${escapeAttr(meta.twitter.description)}">`);
|
|
199
|
+
}
|
|
200
|
+
if (meta.twitter.image) {
|
|
201
|
+
parts.push(`<meta name="twitter:image" content="${escapeAttr(meta.twitter.image)}">`);
|
|
202
|
+
}
|
|
203
|
+
if (meta.jsonLd) {
|
|
204
|
+
parts.push(`<script type="application/ld+json">${escapeJsonLd(meta.jsonLd)}</script>`);
|
|
205
|
+
}
|
|
206
|
+
return parts.join("\n");
|
|
207
|
+
}
|
|
208
|
+
function escapeJsonLd(obj) {
|
|
209
|
+
// `</script>` inside JSON-LD is the only nested-script pitfall;
|
|
210
|
+
// escaping the closing `</` keeps the embedded JSON strict-parseable.
|
|
211
|
+
return JSON.stringify(obj).replace(/<\//g, "<\\/");
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=SeoMetaComposer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeoMetaComposer.js","sourceRoot":"","sources":["../../src/service/SeoMetaComposer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Entry } from "@aotter/mantle-spec";
|
|
2
|
+
import type { WebSiteConfig } from "../model/WebSiteConfig.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hreflang alternates are deferred to v0.1.x (need cross-locale entry
|
|
5
|
+
* pairing first); only minimal `loc + lastmod` shipped.
|
|
6
|
+
*
|
|
7
|
+
* Entries arrive pre-mapped from storage row to public path — the
|
|
8
|
+
* caller (`ComposeSitemapUseCase`) decides how a (collection, slug,
|
|
9
|
+
* locale) row surfaces under the consumer's routing.
|
|
10
|
+
*/
|
|
11
|
+
export interface SitemapEntry {
|
|
12
|
+
readonly entry?: Entry;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SitemapInput {
|
|
16
|
+
readonly site: WebSiteConfig;
|
|
17
|
+
readonly entries: readonly SitemapEntry[];
|
|
18
|
+
}
|
|
19
|
+
export declare function serializeSitemap(input: SitemapInput): string;
|
|
20
|
+
//# sourceMappingURL=SitemapSerializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SitemapSerializer.d.ts","sourceRoot":"","sources":["../../src/service/SitemapSerializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAgB5D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { absoluteUrl } from "./AbsoluteUrl.js";
|
|
2
|
+
export function serializeSitemap(input) {
|
|
3
|
+
const { site, entries } = input;
|
|
4
|
+
const out = [];
|
|
5
|
+
out.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
6
|
+
out.push(`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`);
|
|
7
|
+
for (const { entry, path } of entries) {
|
|
8
|
+
const loc = absoluteUrl(site.origin ?? "", path);
|
|
9
|
+
out.push(" <url>");
|
|
10
|
+
out.push(` <loc>${escapeXml(loc)}</loc>`);
|
|
11
|
+
if (entry?.updatedAt) {
|
|
12
|
+
out.push(` <lastmod>${new Date(entry.updatedAt).toISOString()}</lastmod>`);
|
|
13
|
+
}
|
|
14
|
+
out.push(" </url>");
|
|
15
|
+
}
|
|
16
|
+
out.push("</urlset>");
|
|
17
|
+
return out.join("\n") + "\n";
|
|
18
|
+
}
|
|
19
|
+
function escapeXml(s) {
|
|
20
|
+
return s
|
|
21
|
+
.replace(/&/g, "&")
|
|
22
|
+
.replace(/</g, "<")
|
|
23
|
+
.replace(/>/g, ">")
|
|
24
|
+
.replace(/"/g, """)
|
|
25
|
+
.replace(/'/g, "'");
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=SitemapSerializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SitemapSerializer.js","sourceRoot":"","sources":["../../src/service/SitemapSerializer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAoB/C,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAChC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,GAAG,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;IACnD,GAAG,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IACzE,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACpB,GAAG,CAAC,IAAI,CAAC,YAAY,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,KAAK,EAAE,SAAS,EAAE,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAChF,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACtB,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAC/B,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,CAAC;SACL,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Entry } from "@aotter/mantle-spec";
|
|
2
|
+
import type { MediaAsset } from "@aotter/mantle-runtime";
|
|
3
|
+
export interface MediaAssetResolver {
|
|
4
|
+
resolveMany(ids: readonly string[]): Promise<ReadonlyMap<string, MediaAsset>>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract media asset references from entry data by convention.
|
|
8
|
+
* Starters use `coverAssetId`, `imageAssetId`, nested `{ assetId }`, etc.
|
|
9
|
+
* The schema-level source of truth remains `x-mantle-ref: media_assets`;
|
|
10
|
+
* this convention keeps the render path schema-agnostic until the
|
|
11
|
+
* manifest compiler exposes field metadata at runtime.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectMediaAssetIds(value: unknown): readonly string[];
|
|
14
|
+
export declare function resolveMediaAssetsForEntries(resolver: MediaAssetResolver | null, entries: readonly Entry[]): Promise<ReadonlyMap<string, MediaAsset> | undefined>;
|
|
15
|
+
//# sourceMappingURL=MediaAssetReferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaAssetReferences.d.ts","sourceRoot":"","sources":["../../../src/service/io/MediaAssetReferences.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;CAC/E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAItE;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,kBAAkB,GAAG,IAAI,EACnC,OAAO,EAAE,SAAS,KAAK,EAAE,GACxB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC,CAKtD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract media asset references from entry data by convention.
|
|
3
|
+
* Starters use `coverAssetId`, `imageAssetId`, nested `{ assetId }`, etc.
|
|
4
|
+
* The schema-level source of truth remains `x-mantle-ref: media_assets`;
|
|
5
|
+
* this convention keeps the render path schema-agnostic until the
|
|
6
|
+
* manifest compiler exposes field metadata at runtime.
|
|
7
|
+
*/
|
|
8
|
+
export function collectMediaAssetIds(value) {
|
|
9
|
+
const out = [];
|
|
10
|
+
collect(value, out);
|
|
11
|
+
return [...new Set(out)];
|
|
12
|
+
}
|
|
13
|
+
export async function resolveMediaAssetsForEntries(resolver, entries) {
|
|
14
|
+
if (!resolver)
|
|
15
|
+
return undefined;
|
|
16
|
+
const ids = collectMediaAssetIds(entries.map((entry) => entry.data));
|
|
17
|
+
if (ids.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return resolver.resolveMany(ids);
|
|
20
|
+
}
|
|
21
|
+
function collect(value, out) {
|
|
22
|
+
if (value == null)
|
|
23
|
+
return;
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const item of value)
|
|
26
|
+
collect(item, out);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "object")
|
|
30
|
+
return;
|
|
31
|
+
for (const [key, child] of Object.entries(value)) {
|
|
32
|
+
if (typeof child === "string" &&
|
|
33
|
+
(key === "assetId" || key.endsWith("AssetId")) &&
|
|
34
|
+
child.length > 0) {
|
|
35
|
+
out.push(child);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(child) && key.endsWith("AssetIds")) {
|
|
39
|
+
for (const item of child) {
|
|
40
|
+
if (typeof item === "string" && item.length > 0)
|
|
41
|
+
out.push(item);
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
collect(child, out);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=MediaAssetReferences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaAssetReferences.js","sourceRoot":"","sources":["../../../src/service/io/MediaAssetReferences.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpB,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,QAAmC,EACnC,OAAyB;IAEzB,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChC,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACrE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACvC,OAAO,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,OAAO,CAAC,KAAc,EAAE,GAAa;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO;IAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO;IACtC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QAC5E,IACE,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAC9C,KAAK,CAAC,MAAM,GAAG,CAAC,EAChB,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClE,CAAC;YACD,SAAS;QACX,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Entry } from "@aotter/mantle-spec";
|
|
2
|
+
import type { EntryReader } from "@aotter/mantle-runtime";
|
|
3
|
+
import type { WebSiteConfig } from "../model/WebSiteConfig.js";
|
|
4
|
+
import { type SeoMeta } from "../service/SeoMetaComposer.js";
|
|
5
|
+
import type { PublicPathResolver } from "../service/PublicPathResolver.js";
|
|
6
|
+
import type { ComposeEntrySeoMetaRequest } from "../dto/ComposeEntrySeoMetaRequest.js";
|
|
7
|
+
/**
|
|
8
|
+
* Compose the SEO/AEO meta block for an entry. Reads sibling
|
|
9
|
+
* translations from the DB (same collection, same slug, other
|
|
10
|
+
* locales) so hreflang links are accurate; calls the pure composer
|
|
11
|
+
* for the rest.
|
|
12
|
+
*
|
|
13
|
+
* "Same collection" is a deliberate simplification — works correctly
|
|
14
|
+
* for `post-translations`-style design where each locale has its own
|
|
15
|
+
* row in the same collection. Sites that split locales across
|
|
16
|
+
* collections (`posts-en`, `posts-zh`) need a custom resolver — out
|
|
17
|
+
* of scope for v0.1.
|
|
18
|
+
*
|
|
19
|
+
* Sibling lookup is one indexed `readEntryBySlug` per non-current
|
|
20
|
+
* locale — O(locales), not O(collection). Runs in parallel via
|
|
21
|
+
* `Promise.all`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ComposeEntrySeoMetaUseCase {
|
|
24
|
+
private readonly reader;
|
|
25
|
+
constructor(reader: EntryReader);
|
|
26
|
+
execute(request: ComposeEntrySeoMetaRequest): Promise<SeoMeta>;
|
|
27
|
+
}
|
|
28
|
+
export declare function composeSeoIfPathed(composer: Pick<ComposeEntrySeoMetaUseCase, "execute">, paths: PublicPathResolver | null, entry: Entry, site: WebSiteConfig, overrides?: Pick<ComposeEntrySeoMetaRequest, "publicPath" | "publicLocale" | "siblings">): Promise<SeoMeta | undefined>;
|
|
29
|
+
//# sourceMappingURL=ComposeEntrySeoMetaUseCase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComposeEntrySeoMetaUseCase.d.ts","sourceRoot":"","sources":["../../src/usecase/ComposeEntrySeoMetaUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAEL,KAAK,OAAO,EAEb,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAEvF;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,WAAW;IAE1C,OAAO,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC;CAarE;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,SAAS,CAAC,EACrD,KAAK,EAAE,kBAAkB,GAAG,IAAI,EAChC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,aAAa,EACnB,SAAS,GAAE,IAAI,CACb,0BAA0B,EAC1B,YAAY,GAAG,cAAc,GAAG,UAAU,CACtC,GACL,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAG9B"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { composeEntrySeoMeta, } from "../service/SeoMetaComposer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compose the SEO/AEO meta block for an entry. Reads sibling
|
|
4
|
+
* translations from the DB (same collection, same slug, other
|
|
5
|
+
* locales) so hreflang links are accurate; calls the pure composer
|
|
6
|
+
* for the rest.
|
|
7
|
+
*
|
|
8
|
+
* "Same collection" is a deliberate simplification — works correctly
|
|
9
|
+
* for `post-translations`-style design where each locale has its own
|
|
10
|
+
* row in the same collection. Sites that split locales across
|
|
11
|
+
* collections (`posts-en`, `posts-zh`) need a custom resolver — out
|
|
12
|
+
* of scope for v0.1.
|
|
13
|
+
*
|
|
14
|
+
* Sibling lookup is one indexed `readEntryBySlug` per non-current
|
|
15
|
+
* locale — O(locales), not O(collection). Runs in parallel via
|
|
16
|
+
* `Promise.all`.
|
|
17
|
+
*/
|
|
18
|
+
export class ComposeEntrySeoMetaUseCase {
|
|
19
|
+
reader;
|
|
20
|
+
constructor(reader) {
|
|
21
|
+
this.reader = reader;
|
|
22
|
+
}
|
|
23
|
+
async execute(request) {
|
|
24
|
+
const { entry, site, paths, type } = request;
|
|
25
|
+
const publicPath = request.publicPath ?? paths.forEntry(entry) ?? "";
|
|
26
|
+
const siblings = request.siblings ?? await readSiblings(this.reader, entry, site, paths);
|
|
27
|
+
return composeEntrySeoMeta({
|
|
28
|
+
entry,
|
|
29
|
+
site,
|
|
30
|
+
publicPath,
|
|
31
|
+
siblings,
|
|
32
|
+
type,
|
|
33
|
+
locale: request.publicLocale,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function composeSeoIfPathed(composer, paths, entry, site, overrides = {}) {
|
|
38
|
+
if (!paths || !(overrides.publicPath ?? paths.forEntry(entry)))
|
|
39
|
+
return undefined;
|
|
40
|
+
return composer.execute({ entry, site, paths, ...overrides });
|
|
41
|
+
}
|
|
42
|
+
async function readSiblings(reader, current, site, paths) {
|
|
43
|
+
const slug = current.data["slug"];
|
|
44
|
+
if (typeof slug !== "string" || slug.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
if (site.locales.length <= 1)
|
|
47
|
+
return [];
|
|
48
|
+
const currentKey = current.locale?.toLowerCase();
|
|
49
|
+
const targetLocales = site.locales.filter((l) => l.toLowerCase() !== currentKey);
|
|
50
|
+
const lookups = await Promise.all(targetLocales.map((locale) => reader.readBySlug({
|
|
51
|
+
collection: current.collection,
|
|
52
|
+
slug,
|
|
53
|
+
locale,
|
|
54
|
+
status: "published",
|
|
55
|
+
}).then((e) => ({ locale, entry: e }))));
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const { locale, entry } of lookups) {
|
|
58
|
+
if (!entry)
|
|
59
|
+
continue;
|
|
60
|
+
const path = paths.forEntry(entry);
|
|
61
|
+
if (path)
|
|
62
|
+
out.push({ locale, publicPath: path });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=ComposeEntrySeoMetaUseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComposeEntrySeoMetaUseCase.js","sourceRoot":"","sources":["../../src/usecase/ComposeEntrySeoMetaUseCase.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,mBAAmB,GAGpB,MAAM,+BAA+B,CAAC;AAIvC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,MAAmB;QAAnB,WAAM,GAAN,MAAM,CAAa;IAAG,CAAC;IAEpD,KAAK,CAAC,OAAO,CAAC,OAAmC;QAC/C,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzF,OAAO,mBAAmB,CAAC;YACzB,KAAK;YACL,IAAI;YACJ,UAAU;YACV,QAAQ;YACR,IAAI;YACJ,MAAM,EAAE,OAAO,CAAC,YAAY;SAC7B,CAAC,CAAC;IACL,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAqD,EACrD,KAAgC,EAChC,KAAY,EACZ,IAAmB,EACnB,YAGI,EAAE;IAEN,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACjF,OAAO,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,MAAmB,EACnB,OAAc,EACd,IAAmB,EACnB,KAAyB;IAEzB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7D,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC;IACjD,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,aAAa,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC3B,MAAM,CAAC,UAAU,CAAC;QAChB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,IAAI;QACJ,MAAM;QACN,MAAM,EAAE,WAAW;KACpB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CACvC,CACF,CAAC;IACF,MAAM,GAAG,GAAyB,EAAE,CAAC;IACrC,KAAK,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntryReader } from "@aotter/mantle-runtime";
|
|
2
|
+
import type { PublicPathResolver } from "../service/PublicPathResolver.js";
|
|
3
|
+
import type { ComposeLlmsTxtRequest } from "../dto/ComposeLlmsTxtRequest.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compose a `/llms.txt` body from currently-published entries.
|
|
6
|
+
*
|
|
7
|
+
* - locale: string → entries with `data.locale === locale`
|
|
8
|
+
* - locale: null → non-localized entries only. Consumers that want
|
|
9
|
+
* a cross-locale aggregate at the root URL should
|
|
10
|
+
* iterate site.locales themselves and concat.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ComposeLlmsTxtUseCase {
|
|
13
|
+
private readonly reader;
|
|
14
|
+
private readonly paths;
|
|
15
|
+
constructor(reader: EntryReader, paths: PublicPathResolver | null);
|
|
16
|
+
execute(request: ComposeLlmsTxtRequest): Promise<string | null>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=ComposeLlmsTxtUseCase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComposeLlmsTxtUseCase.d.ts","sourceRoot":"","sources":["../../src/usecase/ComposeLlmsTxtUseCase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAE7E;;;;;;;GAOG;AACH,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAG7C,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAiBtE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { serializeLlmsTxt } from "../service/MarkdownSerializer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compose a `/llms.txt` body from currently-published entries.
|
|
4
|
+
*
|
|
5
|
+
* - locale: string → entries with `data.locale === locale`
|
|
6
|
+
* - locale: null → non-localized entries only. Consumers that want
|
|
7
|
+
* a cross-locale aggregate at the root URL should
|
|
8
|
+
* iterate site.locales themselves and concat.
|
|
9
|
+
*/
|
|
10
|
+
export class ComposeLlmsTxtUseCase {
|
|
11
|
+
reader;
|
|
12
|
+
paths;
|
|
13
|
+
constructor(reader, paths) {
|
|
14
|
+
this.reader = reader;
|
|
15
|
+
this.paths = paths;
|
|
16
|
+
}
|
|
17
|
+
async execute(request) {
|
|
18
|
+
if (!this.paths)
|
|
19
|
+
return null;
|
|
20
|
+
const query = (locale) => this.reader.readPublished({
|
|
21
|
+
locale,
|
|
22
|
+
collection: request.collection,
|
|
23
|
+
});
|
|
24
|
+
const entries = request.locale !== null && request.includeUnlocalized
|
|
25
|
+
? (await Promise.all([query(request.locale), query(null)])).flat()
|
|
26
|
+
: await query(request.locale);
|
|
27
|
+
const grouped = groupByCollection(entries);
|
|
28
|
+
return serializeLlmsTxt({
|
|
29
|
+
site: request.site,
|
|
30
|
+
locale: request.locale ?? "",
|
|
31
|
+
entriesByCollection: grouped,
|
|
32
|
+
pathFor: request.pathFor ?? ((entry) => this.paths.forEntry(entry)),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function groupByCollection(entries) {
|
|
37
|
+
const out = new Map();
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
const arr = out.get(e.collection);
|
|
40
|
+
if (arr)
|
|
41
|
+
arr.push(e);
|
|
42
|
+
else
|
|
43
|
+
out.set(e.collection, [e]);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=ComposeLlmsTxtUseCase.js.map
|