@softure-ai/blog 0.1.8 → 0.1.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/CHANGELOG.md +23 -0
- package/README.md +34 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +2 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +2 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +2 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts +2 -0
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +2 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/body.d.ts +16 -0
- package/dist/next/body.d.ts.map +1 -0
- package/dist/next/body.js +22 -0
- package/dist/next/body.js.map +1 -0
- package/dist/next/context.d.ts.map +1 -1
- package/dist/next/context.js +1 -0
- package/dist/next/context.js.map +1 -1
- package/dist/next/discovery.js +2 -2
- package/dist/next/discovery.js.map +1 -1
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +1 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/json-ld.d.ts.map +1 -1
- package/dist/next/json-ld.js +11 -3
- package/dist/next/json-ld.js.map +1 -1
- package/dist/next/metadata.d.ts +1 -1
- package/dist/next/metadata.d.ts.map +1 -1
- package/dist/next/metadata.js +8 -6
- package/dist/next/metadata.js.map +1 -1
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +6 -11
- package/dist/next/pages.js.map +1 -1
- package/dist/options.d.ts +30 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +35 -0
- package/dist/options.js.map +1 -1
- package/dist/pages/dates.d.ts +2 -2
- package/dist/pages/dates.d.ts.map +1 -1
- package/dist/pages/dates.js +5 -3
- package/dist/pages/dates.js.map +1 -1
- package/dist/pages/index.d.ts +2 -2
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js +2 -2
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/json-ld.d.ts +11 -0
- package/dist/pages/json-ld.d.ts.map +1 -1
- package/dist/pages/json-ld.js +14 -7
- package/dist/pages/json-ld.js.map +1 -1
- package/dist/pages/listing.d.ts +9 -3
- package/dist/pages/listing.d.ts.map +1 -1
- package/dist/pages/listing.js +7 -5
- package/dist/pages/listing.js.map +1 -1
- package/dist/quality/settings.d.ts +2 -2
- package/dist/quality/settings.d.ts.map +1 -1
- package/dist/quality/settings.js +5 -2
- package/dist/quality/settings.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/options.d.ts +9 -1
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +6 -0
- package/dist/server/options.js.map +1 -1
- package/dist/ui/blog-listing.js +2 -2
- package/dist/ui/blog-listing.js.map +1 -1
- package/dist/ui/page-context.d.ts +2 -0
- package/dist/ui/page-context.d.ts.map +1 -1
- package/module.json +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/messages/en.ts +2 -0
- package/src/messages/pl.ts +2 -0
- package/src/next/body.ts +33 -0
- package/src/next/context.ts +1 -0
- package/src/next/discovery.ts +2 -2
- package/src/next/index.ts +1 -0
- package/src/next/json-ld.ts +11 -3
- package/src/next/metadata.ts +9 -7
- package/src/next/pages.tsx +7 -14
- package/src/options.ts +42 -0
- package/src/pages/dates.ts +3 -4
- package/src/pages/index.ts +3 -1
- package/src/pages/json-ld.ts +29 -8
- package/src/pages/listing.ts +18 -5
- package/src/quality/settings.ts +3 -3
- package/src/server/index.ts +1 -1
- package/src/server/options.ts +14 -1
- package/src/ui/blog-listing.tsx +2 -2
- package/src/ui/page-context.ts +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAiD,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAiD,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAA8B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAatF,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAGjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,YAAY,CAGnE;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAS/D;AAED,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,2GAA2G;AAC3G,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAGvE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAKhE;AAED,uGAAuG;AACvG,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CAGpE;AAED,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG,IAAI,CAKhF"}
|
package/dist/server/options.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The blog options, routes and copy of the running app, read from the configuration.
|
|
2
2
|
import { getModule, getSiteUrls } from "@softure-ai/core";
|
|
3
|
+
import { DEFAULT_OPEN_GRAPH_LOCALES } from "../options.js";
|
|
3
4
|
import { getReservedSlugs, normalizeRoute } from "../pages/paths.js";
|
|
4
5
|
import { resolveQualitySettings } from "../quality/settings.js";
|
|
5
6
|
const MODULE_ID = "blog";
|
|
@@ -32,6 +33,11 @@ export function getBlogRoutes(config) {
|
|
|
32
33
|
};
|
|
33
34
|
return { index: read("index"), glossary: read("glossary"), method: read("method"), rss: read("rss") };
|
|
34
35
|
}
|
|
36
|
+
/** The app locale's language tags: the app's `locales` entry, else the bare code and `en_US` / `pl_PL`. */
|
|
37
|
+
export function getBlogLocaleTags(config) {
|
|
38
|
+
const tags = getBlogOptions(config).locales[config.locale];
|
|
39
|
+
return { bcp47: tags?.bcp47 ?? config.locale, openGraph: tags?.openGraph ?? DEFAULT_OPEN_GRAPH_LOCALES[config.locale] };
|
|
40
|
+
}
|
|
35
41
|
/** The path of the cache refresh route (`refreshBlogCache`), outside the pages' routes. */
|
|
36
42
|
export function getBlogRefreshPath(config) {
|
|
37
43
|
const path = getBlogModule(config).routes.refresh;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,SAAS,EAAE,WAAW,EAA6C,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/blog-listing.js
CHANGED
|
@@ -21,8 +21,8 @@ function ArticleCard({ article, context, timezone, isLead }) {
|
|
|
21
21
|
export function BlogListingView({ context, groups, termCount, timezone, cta }) {
|
|
22
22
|
const copy = context.messages.pages;
|
|
23
23
|
return (_jsxs(BlogLayout, { context: context, title: copy.blogTitle, lead: copy.blogDescription, children: [groups.length === 0 ? (_jsx("p", { className: "blog-empty", children: copy.empty })) : (_jsx("div", { className: "blog-groups", children: groups.map((group) => {
|
|
24
|
-
const id = group.cluster === null ? undefined : getClusterAnchor(group.cluster);
|
|
25
|
-
const headingId = `${id ?? "
|
|
24
|
+
const id = group.cluster === null ? undefined : getClusterAnchor(group.cluster, context.clusterAnchorPrefix);
|
|
25
|
+
const headingId = `${id ?? getClusterAnchor("other", context.clusterAnchorPrefix)}-heading`;
|
|
26
26
|
return (_jsxs("section", { id: id, "aria-labelledby": headingId, className: "blog-group", children: [_jsx("h2", { id: headingId, className: "blog-group-title", children: group.label }), group.lead === null ? null : _jsx(ArticleCard, { article: group.lead, context: context, timezone: timezone, isLead: true }), group.rest.length === 0 ? null : (_jsx("ul", { className: "blog-cards", children: group.rest.map((article) => (_jsx("li", { children: _jsx(ArticleCard, { article: article, context: context, timezone: timezone, isLead: false }) }, article.id))) }))] }, group.cluster ?? ""));
|
|
27
27
|
}) })), cta === undefined ? null : _jsx("div", { className: "blog-slot", children: cta }), termCount > 0 ? (_jsxs("p", { className: "blog-glossary-teaser", children: [_jsx("a", { href: context.routes.glossary, children: copy.glossaryLink }), ": ", copy.glossaryTeaser] })) : null, _jsx(BlogFooterNote, { context: context })] }));
|
|
28
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-listing.js","sourceRoot":"","sources":["../../src/ui/blog-listing.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"blog-listing.js","sourceRoot":"","sources":["../../src/ui/blog-listing.tsx"],"names":[],"mappings":";AAAA,qGAAqG;AACrG,sGAAsG;AACtG,4EAA4E;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAqB,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAa9D,SAAS,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAmG;IAC/I,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACpC,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,CAAC;IAClD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,OAAO,CACL,aAAG,SAAS,EAAC,gBAAgB,aAC3B,eAAM,QAAQ,EAAE,QAAQ,YAAG,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,GAAQ,EAC1G,8BAAkB,MAAM,YAAE,KAAK,GAAQ,EACvC,yBAAO,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC,GAAQ,IAClG,CACL,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAA6H;IACpL,OAAO,CACL,mBAAS,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,WAAW,eAAa,OAAO,CAAC,IAAI,aAC3F,MAAM,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,YAAY,YAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAK,CAAC,CAAC,CAAC,IAAI,EACjF,aAAI,SAAS,EAAC,iBAAiB,YAC7B,YAAG,SAAS,EAAC,gBAAgB,EAAC,IAAI,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,YAC7E,OAAO,CAAC,KAAK,GACZ,GACD,EACL,YAAG,SAAS,EAAC,uBAAuB,YAAE,OAAO,CAAC,WAAW,GAAK,EAC9D,KAAC,QAAQ,IAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAI,IAC5D,CACX,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAwB;IACjG,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACpC,OAAO,CACL,MAAC,UAAU,IAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,eAAe,aAC5E,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACrB,YAAG,SAAS,EAAC,YAAY,YAAE,IAAI,CAAC,KAAK,GAAK,CAC3C,CAAC,CAAC,CAAC,CACF,cAAK,SAAS,EAAC,aAAa,YACzB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;oBACpB,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;oBAC7G,MAAM,SAAS,GAAG,GAAG,EAAE,IAAI,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,mBAAmB,CAAC,UAAU,CAAC;oBAC5F,OAAO,CACL,mBAAmC,EAAE,EAAE,EAAE,qBAAmB,SAAS,EAAE,SAAS,EAAC,YAAY,aAC3F,aAAI,EAAE,EAAE,SAAS,EAAE,SAAS,EAAC,kBAAkB,YAC5C,KAAK,CAAC,KAAK,GACT,EACJ,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,WAAW,IAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAG,EAC9G,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAChC,aAAI,SAAS,EAAC,YAAY,YACvB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAC3B,uBACE,KAAC,WAAW,IAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAI,IAD/E,OAAO,CAAC,EAAE,CAEd,CACN,CAAC,GACC,CACN,KAbW,KAAK,CAAC,OAAO,IAAI,EAAE,CAcvB,CACX,CAAC;gBACJ,CAAC,CAAC,GACE,CACP,EACA,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,WAAW,YAAE,GAAG,GAAO,EACjE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CACf,aAAG,SAAS,EAAC,sBAAsB,aACjC,YAAG,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,YAAG,IAAI,CAAC,YAAY,GAAK,EACxD,IAAI,EACJ,IAAI,CAAC,cAAc,IAClB,CACL,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,cAAc,IAAC,OAAO,EAAE,OAAO,GAAI,IACzB,CACd,CAAC;AACJ,CAAC"}
|
|
@@ -11,5 +11,7 @@ export interface BlogPageContext {
|
|
|
11
11
|
readonly brand: string | null;
|
|
12
12
|
/** The note under every text in the locale; `null` without one. */
|
|
13
13
|
readonly disclaimer: string | null;
|
|
14
|
+
/** The listing's cluster anchor prefix (`<prefix>-<cluster>`); `cluster` when left out. */
|
|
15
|
+
readonly clusterAnchorPrefix?: string;
|
|
14
16
|
}
|
|
15
17
|
//# sourceMappingURL=page-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-context.d.ts","sourceRoot":"","sources":["../../src/ui/page-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"page-context.d.ts","sourceRoot":"","sources":["../../src/ui/page-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,2FAA2F;IAC3F,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC"}
|
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/blog",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "SOFTURE AI blog module: articles and glossary terms kept as Markdown files with a strict YAML frontmatter (extendable by the app's own fields), stored in the module's Postgres schema by the softure-blog publish command (dry run by default, all or nothing, content hash, slug history, one pillar per cluster), a server-side Markdown renderer (no raw HTML, safe links, glossary links, block plugins), a text quality gate (softure-blog check, run before every publish) with language rulesets and rule plugins, the blog's pages ready to mount in Next.js (listing, article, glossary, method page, OG image, JSON-LD) with a proxy piece for 301 and 410, an article writing skill (softure-blog skill install) generated from the gate's rules, and discovery: an RSS feed, read next, sitemap entries and an IndexNow submit on publish through the optional @softure-ai/seo, with a rate-limited cache refresh route the publish calls first (through the optional @softure-ai/security).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"build": "tsc -p tsconfig.build.json"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@softure-ai/core": "^0.1.
|
|
73
|
+
"@softure-ai/core": "^0.1.7",
|
|
74
74
|
"@softure-ai/db": "^0.1.0",
|
|
75
75
|
"markdown-it": "^15.0.2",
|
|
76
76
|
"markdown-it-footnote": "^4.0.0",
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const BLOG_RATE_LIMIT_BUCKETS = {
|
|
|
29
29
|
export const blog = defineModule({
|
|
30
30
|
manifest: {
|
|
31
31
|
id: MODULE_ID,
|
|
32
|
-
version: "0.1.
|
|
32
|
+
version: "0.1.10",
|
|
33
33
|
// seo is optional: with it the sitemap lists the texts and a publish pings IndexNow. security is
|
|
34
34
|
// optional too: only the cache refresh route needs it, for its rate limit.
|
|
35
35
|
dependsOn: { seo: "^0.1.0?", security: "^0.1.0?" },
|
package/src/messages/en.ts
CHANGED
|
@@ -47,6 +47,8 @@ export const en = {
|
|
|
47
47
|
},
|
|
48
48
|
glossary: {
|
|
49
49
|
title: "Glossary",
|
|
50
|
+
/** A glossary term's page title with the brand: `{title}` the term, `{brand}` the site. */
|
|
51
|
+
termTitleWithBrand: "{title} | {brand}",
|
|
50
52
|
description: "Short definitions of the terms the texts use, each with the day it was checked and its sources.",
|
|
51
53
|
empty: "The first definitions are on their way.",
|
|
52
54
|
explainedIn: "Explained in these texts",
|
package/src/messages/pl.ts
CHANGED
|
@@ -42,6 +42,8 @@ export const pl = {
|
|
|
42
42
|
},
|
|
43
43
|
glossary: {
|
|
44
44
|
title: "Słownik pojęć",
|
|
45
|
+
/** A glossary term's page title with the brand: `{title}` the term, `{brand}` the site. */
|
|
46
|
+
termTitleWithBrand: "{title} | {brand}",
|
|
45
47
|
description: "Krótkie definicje pojęć, których używają teksty, każda z datą sprawdzenia i źródłami.",
|
|
46
48
|
empty: "Pierwsze definicje są w drodze.",
|
|
47
49
|
explainedIn: "Rozwijamy w tekstach",
|
package/src/next/body.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The body input of the blog's pages, bound to the config: the glossary from the published terms, the
|
|
2
|
+
// routes, the blog options, the site's own origins and the copy. The ready-made pages render with it,
|
|
3
|
+
// and an app with its own page components uses the same function, so its "explained in these texts"
|
|
4
|
+
// list and its body links follow the same rule as the package's pages:
|
|
5
|
+
//
|
|
6
|
+
// const articles = findArticlesLinkingTermFor(config, { articles: published, termSlug: term.slug, terms });
|
|
7
|
+
// const body = renderPageBody(term, getBodyOptions(config, terms));
|
|
8
|
+
import { getSiteUrls, type SoftureConfig } from "@softure-ai/core";
|
|
9
|
+
import type { BlogArticle } from "../contract.js";
|
|
10
|
+
import { findArticlesLinkingTerm, type RenderPageBodyOptions } from "../pages/body.js";
|
|
11
|
+
import { toGlossary } from "../render/glossary.js";
|
|
12
|
+
import { getBlogOptions } from "../server/options.js";
|
|
13
|
+
import type { BlogPageContext } from "../ui/page-context.js";
|
|
14
|
+
import { getPageContext } from "./context.js";
|
|
15
|
+
|
|
16
|
+
/** What `renderPageBody` and `findArticlesLinkingTerm` (`/server`) take, as the ready-made pages build it. */
|
|
17
|
+
export function getBodyOptions(config: SoftureConfig, terms: readonly BlogArticle[], context: BlogPageContext = getPageContext(config)): RenderPageBodyOptions {
|
|
18
|
+
const origins = [config.appOrigin, getSiteUrls(config).origin];
|
|
19
|
+
return { glossary: toGlossary(terms), routes: context.routes, options: getBlogOptions(config), origins, messages: context.messages };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ArticlesLinkingTermInput {
|
|
23
|
+
/** The published articles to look through, e.g. `getPublishedArticles(config)`. */
|
|
24
|
+
readonly articles: readonly BlogArticle[];
|
|
25
|
+
readonly termSlug: string;
|
|
26
|
+
/** The published terms, e.g. `getPublishedTerms(config)`: the glossary the bodies link. */
|
|
27
|
+
readonly terms: readonly BlogArticle[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The published articles whose body links the term: the list under a glossary term's page. */
|
|
31
|
+
export function findArticlesLinkingTermFor(config: SoftureConfig, input: ArticlesLinkingTermInput): BlogArticle[] {
|
|
32
|
+
return findArticlesLinkingTerm(input.articles, input.termSlug, getBodyOptions(config, input.terms));
|
|
33
|
+
}
|
package/src/next/context.ts
CHANGED
|
@@ -26,5 +26,6 @@ export function getPageContext(config: SoftureConfig): BlogPageContext {
|
|
|
26
26
|
methodPath: options.methodPage ? routes.method : null,
|
|
27
27
|
brand: options.brand?.name ?? null,
|
|
28
28
|
disclaimer: options.disclaimer === undefined ? null : (options.disclaimer[config.locale] ?? options.disclaimer.en),
|
|
29
|
+
clusterAnchorPrefix: options.anchors.cluster,
|
|
29
30
|
};
|
|
30
31
|
}
|
package/src/next/discovery.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { errorLogLabel, formatMessage, getSiteUrls } from "@softure-ai/core";
|
|
|
7
7
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
8
8
|
import { buildBlogRss } from "../discovery/rss.js";
|
|
9
9
|
import { getClusterLabel } from "../pages/listing.js";
|
|
10
|
-
import { getBlogOptions } from "../server/options.js";
|
|
10
|
+
import { getBlogLocaleTags, getBlogOptions } from "../server/options.js";
|
|
11
11
|
import { getPageContext } from "./context.js";
|
|
12
12
|
import { getPublishedArticles, getPublishedTerms } from "./data.js";
|
|
13
13
|
|
|
@@ -34,7 +34,7 @@ export async function serveBlogRss(): Promise<Response> {
|
|
|
34
34
|
channel: {
|
|
35
35
|
title: context.brand === null ? copy.pages.blogTitle : formatMessage(copy.pages.titleWithBrand, { title: copy.pages.blogTitle, brand: context.brand }),
|
|
36
36
|
description: copy.pages.blogDescription,
|
|
37
|
-
language: config.
|
|
37
|
+
language: getBlogLocaleTags(config).bcp47,
|
|
38
38
|
},
|
|
39
39
|
getCategory: (text) => (text.kind === "term" ? copy.glossary.title : text.cluster === null ? null : getClusterLabel(text.cluster, clusters, config.locale)),
|
|
40
40
|
});
|
package/src/next/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The Next.js adapter of @softure-ai/blog: the pages, their metadata, the article's OG image, the RSS
|
|
2
2
|
// feed, the cache refresh route and the cached reads (docs/02-module-standard.md §8).
|
|
3
|
+
export { findArticlesLinkingTermFor, getBodyOptions, type ArticlesLinkingTermInput } from "./body.js";
|
|
3
4
|
export { getBlogContext, getPageContext } from "./context.js";
|
|
4
5
|
export { serveBlogRss } from "./discovery.js";
|
|
5
6
|
export { buildArticleJsonLd, buildGlossaryJsonLd, buildTermJsonLd, getCrumbLabels } from "./json-ld.js";
|
package/src/next/json-ld.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { getSiteUrls, type SoftureConfig } from "@softure-ai/core";
|
|
|
7
7
|
import type { BlogArticle } from "../contract.js";
|
|
8
8
|
import { getArticleJsonLd, getGlossaryJsonLd, getTermJsonLd, serializeJsonLd, type JsonLdContext } from "../pages/json-ld.js";
|
|
9
9
|
import { getArticleCrumbs, getClusterLabel, getTermCrumbs, sortTerms, type CrumbLabels } from "../pages/listing.js";
|
|
10
|
-
import { getBlogOptions } from "../server/options.js";
|
|
10
|
+
import { getBlogLocaleTags, getBlogOptions } from "../server/options.js";
|
|
11
11
|
import type { BlogPageContext } from "../ui/page-context.js";
|
|
12
12
|
import { getPageContext } from "./context.js";
|
|
13
13
|
|
|
@@ -22,13 +22,21 @@ export function getCrumbLabels(config: SoftureConfig, context: BlogPageContext =
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function getJsonLdContext(config: SoftureConfig, context: BlogPageContext): JsonLdContext {
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
urls: getSiteUrls(config),
|
|
27
|
+
routes: context.routes,
|
|
28
|
+
locale: config.locale,
|
|
29
|
+
language: getBlogLocaleTags(config).bcp47,
|
|
30
|
+
ids: getBlogOptions(config).jsonLd.ids,
|
|
31
|
+
timezone: config.timezone,
|
|
32
|
+
brand: context.brand,
|
|
33
|
+
};
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/** An article's `BlogPosting`, breadcrumbs and FAQ, serialized. */
|
|
29
37
|
export function buildArticleJsonLd(config: SoftureConfig, article: BlogArticle): string {
|
|
30
38
|
const context = getPageContext(config);
|
|
31
|
-
const crumbs = getArticleCrumbs(article, context.routes, getCrumbLabels(config, context));
|
|
39
|
+
const crumbs = getArticleCrumbs(article, context.routes, getCrumbLabels(config, context), { clusterAnchorPrefix: context.clusterAnchorPrefix });
|
|
32
40
|
return serializeJsonLd(getArticleJsonLd(article, crumbs, getJsonLdContext(config, context)));
|
|
33
41
|
}
|
|
34
42
|
|
package/src/next/metadata.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Metadata } from "next/types.js";
|
|
|
16
16
|
import type { BlogArticle } from "../contract.js";
|
|
17
17
|
import { getArticleDates } from "../pages/dates.js";
|
|
18
18
|
import { getArticlePath, getTermPath } from "../pages/paths.js";
|
|
19
|
+
import { getBlogLocaleTags } from "../server/options.js";
|
|
19
20
|
import type { BlogPageContext } from "../ui/page-context.js";
|
|
20
21
|
import { getPageContext } from "./context.js";
|
|
21
22
|
|
|
@@ -24,8 +25,9 @@ export interface ListingMetadataInput {
|
|
|
24
25
|
readonly isEmpty: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/** `pattern`: the page kind's title message (`pages.titleWithBrand`, or `glossary.termTitleWithBrand` for a term). */
|
|
29
|
+
function withBrand(title: string, context: BlogPageContext, pattern: string = context.messages.pages.titleWithBrand): string {
|
|
30
|
+
return context.brand === null ? title : formatMessage(pattern, { title, brand: context.brand });
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** A page's canonical URL: seo's host and trailing-slash rule when the app lists seo, else on `appOrigin`. */
|
|
@@ -48,11 +50,11 @@ function getStaticMetadata(config: SoftureConfig, context: BlogPageContext, page
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function getTextMetadata(config: SoftureConfig, context: BlogPageContext, text: BlogArticle, path: string, options: { hasFeed?: boolean } = {}): Metadata {
|
|
53
|
+
function getTextMetadata(config: SoftureConfig, context: BlogPageContext, text: BlogArticle, path: string, options: { hasFeed?: boolean; titlePattern?: string } = {}): Metadata {
|
|
52
54
|
const dates = getArticleDates(text, config.timezone);
|
|
53
55
|
const url = getCanonicalUrl(config, path);
|
|
54
56
|
return {
|
|
55
|
-
title: withBrand(text.title, context),
|
|
57
|
+
title: withBrand(text.title, context, options.titlePattern),
|
|
56
58
|
description: text.description,
|
|
57
59
|
robots: { index: true, follow: true },
|
|
58
60
|
alternates: { canonical: url, ...(options.hasFeed === true ? { types: getFeedAlternates(config, context) } : {}) },
|
|
@@ -61,7 +63,7 @@ function getTextMetadata(config: SoftureConfig, context: BlogPageContext, text:
|
|
|
61
63
|
title: text.title,
|
|
62
64
|
description: text.description,
|
|
63
65
|
url,
|
|
64
|
-
locale: config.
|
|
66
|
+
locale: getBlogLocaleTags(config).openGraph,
|
|
65
67
|
publishedTime: dates.published,
|
|
66
68
|
modifiedTime: dates.updated ?? dates.published,
|
|
67
69
|
...(context.brand === null ? {} : { siteName: context.brand }),
|
|
@@ -89,10 +91,10 @@ export function buildGlossaryIndexMetadata(config: SoftureConfig, { isEmpty }: L
|
|
|
89
91
|
return getStaticMetadata(config, context, { title: copy.title, description: copy.description, path: context.routes.glossary, isEmpty });
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
/** A glossary term's metadata. The caller checks the text is a published term. */
|
|
94
|
+
/** A glossary term's metadata, titled by `glossary.termTitleWithBrand`. The caller checks the text is a published term. */
|
|
93
95
|
export function buildTermMetadata(config: SoftureConfig, term: BlogArticle): Metadata {
|
|
94
96
|
const context = getPageContext(config);
|
|
95
|
-
return getTextMetadata(config, context, term, getTermPath(context.routes, term.slug));
|
|
97
|
+
return getTextMetadata(config, context, term, getTermPath(context.routes, term.slug), { titlePattern: context.messages.glossary.termTitleWithBrand });
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/** The method page's metadata. */
|
package/src/next/pages.tsx
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
//
|
|
18
18
|
// An app with its own page components keeps the rest: `generate*Metadata` and `BlogArticleOgImage`
|
|
19
19
|
// mount next to its own page, and `build*Metadata` (metadata.ts) and `build*JsonLd` (json-ld.ts) take
|
|
20
|
-
// a text it already read.
|
|
21
|
-
|
|
20
|
+
// a text it already read; `getBodyOptions` and `findArticlesLinkingTermFor` (body.ts) give the body
|
|
21
|
+
// input and a term's "explained in these texts" list.
|
|
22
22
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
23
23
|
// `next/types.js`, not `next`: the root entry adds Next's globals (a read-only NODE_ENV) to every
|
|
24
24
|
// program that includes this file.
|
|
@@ -27,16 +27,14 @@ import { notFound } from "next/navigation";
|
|
|
27
27
|
import type { ReactNode } from "react";
|
|
28
28
|
import type { BlogArticle } from "../contract.js";
|
|
29
29
|
import { getRelatedArticles } from "../discovery/related.js";
|
|
30
|
-
import { findArticlesLinkingTerm, renderPageBody
|
|
30
|
+
import { findArticlesLinkingTerm, renderPageBody } from "../pages/body.js";
|
|
31
31
|
import { getArticleDates } from "../pages/dates.js";
|
|
32
32
|
import { getArticleCrumbs, groupByCluster, getTermCrumbs, sortTerms } from "../pages/listing.js";
|
|
33
|
-
import { toGlossary } from "../render/glossary.js";
|
|
34
|
-
import { getBlogOptions } from "../server/options.js";
|
|
35
33
|
import { BlogArticleView } from "../ui/blog-article.js";
|
|
36
34
|
import { GlossaryIndexView, GlossaryTermView } from "../ui/blog-glossary.js";
|
|
37
35
|
import { BlogListingView } from "../ui/blog-listing.js";
|
|
38
36
|
import { BlogMethodView } from "../ui/blog-method.js";
|
|
39
|
-
import
|
|
37
|
+
import { getBodyOptions } from "./body.js";
|
|
40
38
|
import { getPageContext } from "./context.js";
|
|
41
39
|
import { getPublishedArticles, getPublishedTerms, getTextBySlug } from "./data.js";
|
|
42
40
|
import { buildArticleJsonLd, buildGlossaryJsonLd, buildTermJsonLd, getCrumbLabels } from "./json-ld.js";
|
|
@@ -71,11 +69,6 @@ function isPublished(text: BlogArticle | null, kind: BlogArticle["kind"]): text
|
|
|
71
69
|
return text !== null && text.status === "published" && text.kind === kind;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
function getBodyOptions(config: SoftureConfig, context: BlogPageContext, terms: readonly BlogArticle[]): RenderPageBodyOptions {
|
|
75
|
-
const origins = [config.appOrigin, getSiteUrls(config).origin];
|
|
76
|
-
return { glossary: toGlossary(terms), routes: context.routes, options: getBlogOptions(config), origins, messages: context.messages };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
72
|
export async function generateBlogIndexMetadata(): Promise<Metadata> {
|
|
80
73
|
const config = getSoftureConfig();
|
|
81
74
|
const articles = await getPublishedArticles(config);
|
|
@@ -107,8 +100,8 @@ export async function BlogArticlePage({ params, cta, afterArticle }: BlogArticle
|
|
|
107
100
|
if (!isPublished(article, "article")) notFound();
|
|
108
101
|
const context = getPageContext(config);
|
|
109
102
|
const [terms, published] = await Promise.all([getPublishedTerms(config), getPublishedArticles(config)]);
|
|
110
|
-
const body = renderPageBody<ReactNode>(article, getBodyOptions(config,
|
|
111
|
-
const crumbs = getArticleCrumbs(article, context.routes, getCrumbLabels(config, context));
|
|
103
|
+
const body = renderPageBody<ReactNode>(article, getBodyOptions(config, terms, context));
|
|
104
|
+
const crumbs = getArticleCrumbs(article, context.routes, getCrumbLabels(config, context), { clusterAnchorPrefix: context.clusterAnchorPrefix });
|
|
112
105
|
const jsonLd = buildArticleJsonLd(config, article);
|
|
113
106
|
return (
|
|
114
107
|
<BlogArticleView
|
|
@@ -155,7 +148,7 @@ export async function GlossaryTermPage({ params, cta }: GlossaryTermPageProps) {
|
|
|
155
148
|
if (!isPublished(term, "term")) notFound();
|
|
156
149
|
const context = getPageContext(config);
|
|
157
150
|
const [terms, articles] = await Promise.all([getPublishedTerms(config), getPublishedArticles(config)]);
|
|
158
|
-
const bodyOptions = getBodyOptions(config,
|
|
151
|
+
const bodyOptions = getBodyOptions(config, terms, context);
|
|
159
152
|
const crumbs = getTermCrumbs(term, context.routes, getCrumbLabels(config, context));
|
|
160
153
|
const jsonLd = buildTermJsonLd(config, term);
|
|
161
154
|
return (
|
package/src/options.ts
CHANGED
|
@@ -200,6 +200,42 @@ const imagePolicySchema = z.strictObject({
|
|
|
200
200
|
dimensions: z.custom<ArticleImagePolicy["dimensions"]>((value) => typeof value === "function", "must be a function: (src) => ({ width, height }) or null"),
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
/** A URL fragment or HTML id part, without `#`: a JSON-LD node id or the cluster anchor prefix. */
|
|
204
|
+
const fragmentSchema = z
|
|
205
|
+
.string()
|
|
206
|
+
.max(40)
|
|
207
|
+
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "must be a letter followed by letters, digits, - or _, without #, e.g. article");
|
|
208
|
+
|
|
209
|
+
const jsonLdSchema = z.strictObject({
|
|
210
|
+
/** The `@id` fragments of the nodes: `<url>#<article>`, `<url>#<term>`, `<glossary url>#<glossary>`. */
|
|
211
|
+
ids: z
|
|
212
|
+
.strictObject({ article: fragmentSchema.default("article"), term: fragmentSchema.default("term"), glossary: fragmentSchema.default("glossary") })
|
|
213
|
+
.default({ article: "article", term: "term", glossary: "glossary" }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const anchorsSchema = z.strictObject({
|
|
217
|
+
/** A cluster's section on the listing is `<cluster>-<key>`, the breadcrumb of its articles points there. */
|
|
218
|
+
cluster: fragmentSchema.default("cluster"),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/** The Open Graph locale of a language when the app sets none: `og:locale` is `language_TERRITORY`. */
|
|
222
|
+
export const DEFAULT_OPEN_GRAPH_LOCALES = { en: "en_US", pl: "pl_PL" } as const;
|
|
223
|
+
|
|
224
|
+
const localeTagsSchema = z.strictObject({
|
|
225
|
+
/** The content's language as a BCP-47 tag: JSON-LD `inLanguage` and the feed's `<language>`; the bare code by default. */
|
|
226
|
+
bcp47: z
|
|
227
|
+
.string()
|
|
228
|
+
.regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, "must be a BCP-47 tag, e.g. pl-PL")
|
|
229
|
+
.optional(),
|
|
230
|
+
/** `og:locale` (`language_TERRITORY`); `en_US` or `pl_PL` by default. */
|
|
231
|
+
openGraph: z
|
|
232
|
+
.string()
|
|
233
|
+
.regex(/^[a-z]{2,3}_[A-Z]{2}$/, "must be an Open Graph locale (language_TERRITORY), e.g. pl_PL")
|
|
234
|
+
.optional(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export type BlogLocaleTagsInput = z.output<typeof localeTagsSchema>;
|
|
238
|
+
|
|
203
239
|
export const blogOptionsSchema = z
|
|
204
240
|
.strictObject({
|
|
205
241
|
/** The folder with the article files, relative to the app's root. */
|
|
@@ -232,6 +268,12 @@ export const blogOptionsSchema = z
|
|
|
232
268
|
* quality gate; without it every image renders as its alt text and the gate refuses it.
|
|
233
269
|
*/
|
|
234
270
|
images: imagePolicySchema.optional(),
|
|
271
|
+
/** The JSON-LD node ids; an app that already published other fragments keeps them here. */
|
|
272
|
+
jsonLd: jsonLdSchema.default({ ids: { article: "article", term: "term", glossary: "glossary" } }),
|
|
273
|
+
/** The anchors the pages write; an app whose listing already used another prefix keeps it here. */
|
|
274
|
+
anchors: anchorsSchema.default({ cluster: "cluster" }),
|
|
275
|
+
/** The language tags per app locale: BCP-47 for the content, Open Graph for `og:locale`. */
|
|
276
|
+
locales: z.strictObject({ en: localeTagsSchema.optional(), pl: localeTagsSchema.optional() }).default({}),
|
|
235
277
|
/** The page a withdrawn text answers with (410): extra links, or the app's own body. */
|
|
236
278
|
gonePage: gonePageSchema.default({ links: [] }),
|
|
237
279
|
/** How long the listing and glossary cache their reads; keep it equal to the pages' `revalidate`. */
|
package/src/pages/dates.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The days a page shows for a text, in the app's time zone. JSON-LD takes the same days, so the dates
|
|
2
2
|
// a search engine compares (`dateModified` against the visible update) cannot drift apart.
|
|
3
|
-
import type
|
|
3
|
+
import { toCalendarDay, type Locale } from "@softure-ai/core";
|
|
4
4
|
import type { BlogArticle } from "../contract.js";
|
|
5
5
|
|
|
6
6
|
export interface ArticleDates {
|
|
@@ -12,10 +12,9 @@ export interface ArticleDates {
|
|
|
12
12
|
readonly currentAsOf: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/** The calendar day of a moment in a time zone, `YYYY-MM-DD
|
|
15
|
+
/** The calendar day of a moment in a time zone, `YYYY-MM-DD`: core's `toCalendarDay` under the name this module exports. */
|
|
16
16
|
export function getDayInZone(moment: Date, timezone: string): string {
|
|
17
|
-
|
|
18
|
-
return new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit" }).format(moment);
|
|
17
|
+
return toCalendarDay(moment, timezone);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/**
|
package/src/pages/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// 301/410 decisions. The components (`../ui/`) and the Next adapter (`../next/`) render what it returns.
|
|
3
3
|
export { findArticlesLinkingTerm, renderPageBody, type RenderPageBodyOptions } from "./body.js";
|
|
4
4
|
export { formatDay, getArticleDates, getDayInZone, type ArticleDates } from "./dates.js";
|
|
5
|
-
export { getArticleImageUrl, getArticleJsonLd, getGlossaryJsonLd, getTermJsonLd, serializeJsonLd, type JsonLdContext } from "./json-ld.js";
|
|
5
|
+
export { DEFAULT_JSON_LD_IDS, getArticleImageUrl, getArticleJsonLd, getGlossaryJsonLd, getTermJsonLd, serializeJsonLd, type JsonLdContext, type JsonLdIds } from "./json-ld.js";
|
|
6
6
|
export {
|
|
7
|
+
DEFAULT_CLUSTER_ANCHOR_PREFIX,
|
|
7
8
|
getArticleCrumbs,
|
|
8
9
|
getClusterAnchor,
|
|
9
10
|
getClusterLabel,
|
|
@@ -11,6 +12,7 @@ export {
|
|
|
11
12
|
groupByCluster,
|
|
12
13
|
sortTerms,
|
|
13
14
|
splitClusterLead,
|
|
15
|
+
type ArticleCrumbOptions,
|
|
14
16
|
type ClusterGroup,
|
|
15
17
|
type Crumb,
|
|
16
18
|
type CrumbLabels,
|
package/src/pages/json-ld.ts
CHANGED
|
@@ -12,13 +12,34 @@ export interface JsonLdContext {
|
|
|
12
12
|
readonly urls: SiteUrls;
|
|
13
13
|
readonly routes: BlogRoutes;
|
|
14
14
|
readonly locale: Locale;
|
|
15
|
+
/** The BCP-47 tag written as `inLanguage` (`getBlogLocaleTags`); `locale` when left out. */
|
|
16
|
+
readonly language?: string;
|
|
17
|
+
/** The `@id` fragments; `article`, `term` and `glossary` when left out. */
|
|
18
|
+
readonly ids?: JsonLdIds;
|
|
15
19
|
readonly timezone: string;
|
|
16
20
|
/** The brand as author and publisher; `null` leaves them out. */
|
|
17
21
|
readonly brand: string | null;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
/** The fragments after `#` in the nodes' `@id`s. */
|
|
25
|
+
export interface JsonLdIds {
|
|
26
|
+
readonly article: string;
|
|
27
|
+
readonly term: string;
|
|
28
|
+
readonly glossary: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_JSON_LD_IDS: JsonLdIds = { article: "article", term: "term", glossary: "glossary" };
|
|
32
|
+
|
|
20
33
|
type JsonLd = Record<string, unknown>;
|
|
21
34
|
|
|
35
|
+
function getIds(ctx: Pick<JsonLdContext, "ids">): JsonLdIds {
|
|
36
|
+
return ctx.ids ?? DEFAULT_JSON_LD_IDS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLanguage(ctx: Pick<JsonLdContext, "language" | "locale">): string {
|
|
40
|
+
return ctx.language ?? ctx.locale;
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
/** JSON for a `<script type="application/ld+json">`: no text in it can close the tag. */
|
|
23
44
|
export function serializeJsonLd(data: unknown): string {
|
|
24
45
|
return JSON.stringify(data).replace(/</g, "\\u003c");
|
|
@@ -57,12 +78,12 @@ export function getArticleJsonLd(article: BlogArticle, crumbs: readonly Crumb[],
|
|
|
57
78
|
const graph: JsonLd[] = [
|
|
58
79
|
{
|
|
59
80
|
"@type": "BlogPosting",
|
|
60
|
-
"@id": `${url}
|
|
81
|
+
"@id": `${url}#${getIds(ctx).article}`,
|
|
61
82
|
mainEntityOfPage: url,
|
|
62
83
|
url,
|
|
63
84
|
headline: article.title,
|
|
64
85
|
description: article.description,
|
|
65
|
-
inLanguage: ctx
|
|
86
|
+
inLanguage: getLanguage(ctx),
|
|
66
87
|
datePublished: dates.published,
|
|
67
88
|
dateModified: dates.updated ?? dates.published,
|
|
68
89
|
...(brand === null ? {} : { author: brand, publisher: brand }),
|
|
@@ -80,8 +101,8 @@ export function getArticleJsonLd(article: BlogArticle, crumbs: readonly Crumb[],
|
|
|
80
101
|
return { "@context": "https://schema.org", "@graph": graph };
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
function getTermSetReference(ctx: Pick<JsonLdContext, "urls" | "routes">, glossaryTitle: string): JsonLd {
|
|
84
|
-
return { "@type": "DefinedTermSet", "@id": `${ctx.urls.getCanonicalUrl(ctx.routes.glossary)}
|
|
104
|
+
function getTermSetReference(ctx: Pick<JsonLdContext, "urls" | "routes" | "ids">, glossaryTitle: string): JsonLd {
|
|
105
|
+
return { "@type": "DefinedTermSet", "@id": `${ctx.urls.getCanonicalUrl(ctx.routes.glossary)}#${getIds(ctx).glossary}`, name: glossaryTitle };
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
/** A term: `DefinedTerm` in the glossary's `DefinedTermSet`, and its `BreadcrumbList`. */
|
|
@@ -92,11 +113,11 @@ export function getTermJsonLd(term: BlogArticle, crumbs: readonly Crumb[], ctx:
|
|
|
92
113
|
"@graph": [
|
|
93
114
|
{
|
|
94
115
|
"@type": "DefinedTerm",
|
|
95
|
-
"@id": `${url}
|
|
116
|
+
"@id": `${url}#${getIds(ctx).term}`,
|
|
96
117
|
url,
|
|
97
118
|
name: term.title,
|
|
98
119
|
description: term.description,
|
|
99
|
-
inLanguage: ctx
|
|
120
|
+
inLanguage: getLanguage(ctx),
|
|
100
121
|
...(term.termForms.length > 0 ? { alternateName: term.termForms } : {}),
|
|
101
122
|
inDefinedTermSet: getTermSetReference(ctx, glossaryTitle),
|
|
102
123
|
},
|
|
@@ -111,10 +132,10 @@ export function getGlossaryJsonLd(terms: readonly BlogArticle[], ctx: JsonLdCont
|
|
|
111
132
|
"@context": "https://schema.org",
|
|
112
133
|
...getTermSetReference(ctx, glossaryTitle),
|
|
113
134
|
url: ctx.urls.getCanonicalUrl(ctx.routes.glossary),
|
|
114
|
-
inLanguage: ctx
|
|
135
|
+
inLanguage: getLanguage(ctx),
|
|
115
136
|
hasDefinedTerm: terms.map((term) => {
|
|
116
137
|
const url = ctx.urls.getCanonicalUrl(getTermPath(ctx.routes, term.slug));
|
|
117
|
-
return { "@type": "DefinedTerm", "@id": `${url}
|
|
138
|
+
return { "@type": "DefinedTerm", "@id": `${url}#${getIds(ctx).term}`, name: term.title, description: term.description, url };
|
|
118
139
|
}),
|
|
119
140
|
};
|
|
120
141
|
}
|
package/src/pages/listing.ts
CHANGED
|
@@ -23,9 +23,12 @@ export function getClusterLabel(cluster: string, labels: Readonly<Record<string,
|
|
|
23
23
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** The anchor
|
|
27
|
-
export
|
|
28
|
-
|
|
26
|
+
/** The cluster anchor prefix when the app sets none (`blog({ anchors: { cluster } })`). */
|
|
27
|
+
export const DEFAULT_CLUSTER_ANCHOR_PREFIX = "cluster";
|
|
28
|
+
|
|
29
|
+
/** The anchor of a cluster's section on the listing (`<prefix>-<cluster>`): the target of the article's middle crumb. */
|
|
30
|
+
export function getClusterAnchor(cluster: string, prefix: string = DEFAULT_CLUSTER_ANCHOR_PREFIX): string {
|
|
31
|
+
return `${prefix}-${cluster}`;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/** The pillar leads its group; the rest keep their order. Without a pillar there is no lead. */
|
|
@@ -75,11 +78,21 @@ export interface CrumbLabels {
|
|
|
75
78
|
readonly cluster: (cluster: string) => string;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
export interface ArticleCrumbOptions {
|
|
82
|
+
/** The listing's cluster anchor prefix; `cluster` when left out. */
|
|
83
|
+
readonly clusterAnchorPrefix?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
/** Blog › cluster › title; without a cluster, Blog › title. */
|
|
79
|
-
export function getArticleCrumbs(
|
|
87
|
+
export function getArticleCrumbs(
|
|
88
|
+
article: Pick<BlogArticle, "slug" | "title" | "cluster">,
|
|
89
|
+
routes: BlogRoutes,
|
|
90
|
+
labels: CrumbLabels,
|
|
91
|
+
options: ArticleCrumbOptions = {},
|
|
92
|
+
): Crumb[] {
|
|
80
93
|
const crumbs: Crumb[] = [{ name: labels.blog, path: routes.index }];
|
|
81
94
|
if (article.cluster !== null) {
|
|
82
|
-
crumbs.push({ name: labels.cluster(article.cluster), path: `${routes.index}#${getClusterAnchor(article.cluster)}` });
|
|
95
|
+
crumbs.push({ name: labels.cluster(article.cluster), path: `${routes.index}#${getClusterAnchor(article.cluster, options.clusterAnchorPrefix)}` });
|
|
83
96
|
}
|
|
84
97
|
crumbs.push({ name: article.title, path: getArticlePath(routes, article.slug) });
|
|
85
98
|
return crumbs;
|
package/src/quality/settings.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The gate's resolved settings: the parsed options plus what the app config adds (its origins and time
|
|
2
2
|
// zone) and the blog's image policy, with the ruleset and voice patterns compiled once.
|
|
3
|
-
import type
|
|
3
|
+
import { toCalendarDay, type SoftureConfig } from "@softure-ai/core";
|
|
4
4
|
import type { ArticleImagePolicy } from "../render/images.js";
|
|
5
5
|
import type { QualitySeverity } from "./finding.js";
|
|
6
6
|
import type { QualityOptions } from "./options.js";
|
|
@@ -63,9 +63,9 @@ export function resolveQualitySettings(
|
|
|
63
63
|
return { options, ruleset, voicePatterns, ownOrigins: [...new Set(origins)], timeZone: config.timezone, images, paths };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** `YYYY-MM-DD` of a moment in a time zone. */
|
|
66
|
+
/** `YYYY-MM-DD` of a moment in a time zone: core's `toCalendarDay` under the name this module exports. */
|
|
67
67
|
export function getLocalDate(moment: Date, timeZone: string): string {
|
|
68
|
-
return
|
|
68
|
+
return toCalendarDay(moment, timeZone);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** The severity a finding of `rule` gets: the override, else its own; `null` for "off". */
|
package/src/server/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export {
|
|
|
23
23
|
type RunBlogPublishOptions,
|
|
24
24
|
} from "../db/publish-run.js";
|
|
25
25
|
export { checkArticlesTable } from "./health.js";
|
|
26
|
-
export { getBlogMessages, getBlogOptions, getBlogRefreshPath, getBlogReservedSlugs, getBlogRoutes, getQualitySettings } from "./options.js";
|
|
26
|
+
export { getBlogLocaleTags, getBlogMessages, getBlogOptions, getBlogRefreshPath, getBlogReservedSlugs, getBlogRoutes, getQualitySettings, type BlogLocaleTags } from "./options.js";
|
|
27
27
|
export * from "../discovery/index.js";
|
|
28
28
|
export * from "../pages/index.js";
|
|
29
29
|
export * from "../quality/index.js";
|