hazo_blog 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +189 -0
- package/db_setup_postgres.sql +76 -38
- package/db_setup_sqlite.sql +85 -38
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/index.js +6 -0
- package/dist/lib/mdx_mask.d.ts +28 -0
- package/dist/lib/mdx_mask.d.ts.map +1 -0
- package/dist/lib/mdx_mask.js +165 -0
- package/dist/lib/mdx_segment.d.ts +15 -0
- package/dist/lib/mdx_segment.d.ts.map +1 -0
- package/dist/lib/mdx_segment.js +56 -0
- package/dist/locales/de.json +4 -0
- package/dist/locales/en.json +4 -0
- package/dist/locales/sv.json +4 -0
- package/dist/next/pages.d.ts +1 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +8 -5
- package/dist/next/routes.d.ts +33 -0
- package/dist/next/routes.d.ts.map +1 -1
- package/dist/next/routes.js +115 -3
- package/dist/repository/index.d.ts +2 -0
- package/dist/repository/index.d.ts.map +1 -1
- package/dist/repository/index.js +13 -0
- package/dist/seo/index.d.ts +10 -2
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +17 -5
- package/dist/service/index.d.ts +88 -0
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/index.js +268 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/blog-admin-panel.d.ts.map +1 -1
- package/dist/ui/blog-admin-panel.js +20 -1
- package/dist/ui/post-list-table.d.ts +9 -1
- package/dist/ui/post-list-table.d.ts.map +1 -1
- package/dist/ui/post-list-table.js +10 -2
- package/dist/ui/use-blog-admin.d.ts +7 -0
- package/dist/ui/use-blog-admin.d.ts.map +1 -1
- package/dist/ui/use-blog-admin.js +24 -5
- package/package.json +18 -14
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// hazo_blog — MDX masking for the LLM translation round trip
|
|
2
|
+
// (`service/index.ts#translatePost()`). Built on `hazo_i18n/html`'s
|
|
3
|
+
// `maskHtml`/`unmaskHtml` (the same tag-masking primitives hazo_i18n's own
|
|
4
|
+
// export/import/translate pipeline uses for rich-text HTML values) plus a
|
|
5
|
+
// thin MDX-specific layer on top: frontmatter set-aside and a pre-pass for
|
|
6
|
+
// bare (attribute-less) capitalized JSX component tags.
|
|
7
|
+
//
|
|
8
|
+
// ── Frontmatter ──────────────────────────────────────────────────────────────
|
|
9
|
+
// `BlogPost.content` (`src/types/index.ts`) is documented as "raw MDX" and is
|
|
10
|
+
// built exclusively by `service#upsertBySlug()`/`createTranslation()` from a
|
|
11
|
+
// plain MDX body — title/excerpt/meta fields live in their own DB columns,
|
|
12
|
+
// never folded into a `---\n...\n---` frontmatter block anywhere in this
|
|
13
|
+
// package. So frontmatter extraction here is defensive, not load-bearing: if
|
|
14
|
+
// `content` ever does start with a `---` block (e.g. content authored/pasted
|
|
15
|
+
// elsewhere before being saved through this package), it's still set aside
|
|
16
|
+
// untouched rather than risk the LLM mangling YAML syntax it was never asked
|
|
17
|
+
// about.
|
|
18
|
+
//
|
|
19
|
+
// ── Why capitalized JSX tags need their own pass ────────────────────────────
|
|
20
|
+
// `hazo_i18n`'s `maskHtml()` only masks tags that carry attributes (its own
|
|
21
|
+
// doc: "only tags that carry attributes are masked... masking every tag
|
|
22
|
+
// would turn long-form content into an unreadable wall of placeholders").
|
|
23
|
+
// That heuristic is right for plain HTML, where an attribute-less tag like
|
|
24
|
+
// `<p>` or `<strong>` is always a plain structural tag. It is WRONG for MDX,
|
|
25
|
+
// where a bare `<Callout>` (no attrs) is a React component exactly as
|
|
26
|
+
// fragile to translate around as `<Callout type="info">` — an LLM "helpfully"
|
|
27
|
+
// paraphrasing or re-casing the tag name breaks the render. So this module
|
|
28
|
+
// pre-masks every capitalized-tag-name JSX tag (self-closing or paired,
|
|
29
|
+
// attributed or not) to its own placeholder space (`<j1>`/`<j2>`/...) BEFORE
|
|
30
|
+
// handing the remainder to `maskHtml()`, which still runs its own pass over
|
|
31
|
+
// whatever capitalized tags (now placeholders, ignored) and attributed plain
|
|
32
|
+
// HTML tags (e.g. a stray `<a href="...">` inside MDX body text) remain.
|
|
33
|
+
//
|
|
34
|
+
// Any component name is accepted generically (`[A-Z][A-Za-z0-9]*`) — this
|
|
35
|
+
// package can't know the host's `config.mdxComponents` additions in advance
|
|
36
|
+
// (`defaultMdxComponents` ships `YouTube`/`Callout`/`BlogImage`, but a host
|
|
37
|
+
// like gotimer can register more), so we mask by naming convention (JSX
|
|
38
|
+
// components are always capitalized; HTML tags never are), not by an
|
|
39
|
+
// enumerated component list.
|
|
40
|
+
//
|
|
41
|
+
// ── Why `hazo_i18n/html` is imported LAZILY, not statically ─────────────────
|
|
42
|
+
// `hazo_i18n` is an OPTIONAL peer of this package (`package.json`'s
|
|
43
|
+
// `peerDependenciesMeta`) — most hosts don't install it. This file is reached
|
|
44
|
+
// from `service/index.ts`'s top-level imports (every consumer pulls in
|
|
45
|
+
// `createBlogService`, not just ones using `translatePost()`), so a static
|
|
46
|
+
// `import ... from "hazo_i18n/html"` here would turn an optional peer into a
|
|
47
|
+
// hard runtime dependency for EVERY consumer, breaking the package for
|
|
48
|
+
// anyone who doesn't have `hazo_i18n` installed and never calls
|
|
49
|
+
// `translatePost()` in the first place. `optional_import()` (the same
|
|
50
|
+
// `hazo_core` helper `service/index.ts#requireLlmApi()` uses for the
|
|
51
|
+
// `hazo_llm_api` peer) resolves it lazily, only when `maskMdx()`/
|
|
52
|
+
// `unmaskMdx()` are actually called, and throws the same `HazoUnavailableError`
|
|
53
|
+
// 503 convention when it isn't installed.
|
|
54
|
+
import { HazoUnavailableError, optional_import } from "hazo_core";
|
|
55
|
+
async function requireHazoI18nHtml(fnName) {
|
|
56
|
+
const mod = await optional_import("hazo_i18n/html");
|
|
57
|
+
if (!mod) {
|
|
58
|
+
throw new HazoUnavailableError({
|
|
59
|
+
code: "HAZO_BLOG_MDX_MASK_UNAVAILABLE",
|
|
60
|
+
pkg: "hazo_blog",
|
|
61
|
+
message: `${fnName}() requires the hazo_i18n optional peer (its "./html" subpath) — it is not installed.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return mod;
|
|
65
|
+
}
|
|
66
|
+
const FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---\n?/;
|
|
67
|
+
/** Matches a capitalized JSX tag: `<Name ...>`, `<Name/>`, `<Name>`, `</Name>`. */
|
|
68
|
+
const JSX_TAG_PATTERN = /<(\/)?([A-Z][A-Za-z0-9]*)\b([^>]*?)(\/)?>/g;
|
|
69
|
+
/**
|
|
70
|
+
* Masks every capitalized JSX component tag in `text` (attributed or bare),
|
|
71
|
+
* giving each open/close pair the same placeholder number (`<jN>`/`</jN>`,
|
|
72
|
+
* self-closing `<jN/>`). Mirrors `maskHtml()`'s own stack-based pairing
|
|
73
|
+
* algorithm (`hazo_i18n/src/lib/html_mask.ts`) but keyed on capitalization
|
|
74
|
+
* instead of attribute presence, and in its own `jN` placeholder namespace so
|
|
75
|
+
* it can never collide with `maskHtml()`'s own `xN` placeholders when both
|
|
76
|
+
* passes run over the same text.
|
|
77
|
+
*/
|
|
78
|
+
function maskJsxComponents(text) {
|
|
79
|
+
const map = {};
|
|
80
|
+
let counter = 0;
|
|
81
|
+
let result = "";
|
|
82
|
+
let lastIndex = 0;
|
|
83
|
+
const stack = [];
|
|
84
|
+
const pattern = new RegExp(JSX_TAG_PATTERN.source, "g");
|
|
85
|
+
for (const match of text.matchAll(pattern)) {
|
|
86
|
+
const [full, closingSlash, name, , selfClosingSlash] = match;
|
|
87
|
+
const index = match.index ?? 0;
|
|
88
|
+
result += text.slice(lastIndex, index);
|
|
89
|
+
lastIndex = index + full.length;
|
|
90
|
+
if (closingSlash === "/") {
|
|
91
|
+
const top = stack[stack.length - 1];
|
|
92
|
+
if (top && top.name === name) {
|
|
93
|
+
stack.pop();
|
|
94
|
+
map[top.maskName].close = full;
|
|
95
|
+
result += `</${top.maskName}>`;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Unbalanced/mismatched closing tag — leave untouched rather than
|
|
99
|
+
// guess at a pairing (same defensive fallback as `maskHtml()`).
|
|
100
|
+
result += full;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
counter += 1;
|
|
105
|
+
const placeholder = `j${counter}`;
|
|
106
|
+
if (selfClosingSlash === "/") {
|
|
107
|
+
map[placeholder] = { open: full, close: "" };
|
|
108
|
+
result += `<${placeholder}/>`;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
map[placeholder] = { open: full, close: "" };
|
|
112
|
+
stack.push({ name, maskName: placeholder });
|
|
113
|
+
result += `<${placeholder}>`;
|
|
114
|
+
}
|
|
115
|
+
result += text.slice(lastIndex);
|
|
116
|
+
return { masked: result, map };
|
|
117
|
+
}
|
|
118
|
+
const JSX_PLACEHOLDER_PATTERN = /<(\/?)j(\d+)(\/)?>/g;
|
|
119
|
+
function unmaskJsxComponents(masked, map) {
|
|
120
|
+
const pattern = new RegExp(JSX_PLACEHOLDER_PATTERN.source, "g");
|
|
121
|
+
return masked.replace(pattern, (full, closingSlash, num, selfClosingSlash) => {
|
|
122
|
+
const entry = map[`j${num}`];
|
|
123
|
+
if (!entry)
|
|
124
|
+
return full;
|
|
125
|
+
if (selfClosingSlash === "/")
|
|
126
|
+
return entry.open;
|
|
127
|
+
if (closingSlash === "/")
|
|
128
|
+
return entry.close;
|
|
129
|
+
return entry.open;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sets aside leading YAML frontmatter (if present — see this file's header,
|
|
134
|
+
* most posts have none), then masks every capitalized JSX component tag
|
|
135
|
+
* (`maskJsxComponents`, this file's own `jN` namespace) and finally every
|
|
136
|
+
* remaining attributed plain-HTML tag (`maskHtml()`'s `xN` namespace). The
|
|
137
|
+
* two placeholder maps are merged into one — the numeric prefixes (`j`/`x`)
|
|
138
|
+
* never collide, so a single `map` round-trips through `unmaskMdx()`.
|
|
139
|
+
*/
|
|
140
|
+
export async function maskMdx(content) {
|
|
141
|
+
const html = await requireHazoI18nHtml("maskMdx");
|
|
142
|
+
const frontmatterMatch = content.match(FRONTMATTER_PATTERN);
|
|
143
|
+
const frontmatter = frontmatterMatch ? frontmatterMatch[0] : null;
|
|
144
|
+
const body = frontmatter ? content.slice(frontmatter.length) : content;
|
|
145
|
+
const jsxPass = maskJsxComponents(body);
|
|
146
|
+
const htmlPass = html.maskHtml(jsxPass.masked);
|
|
147
|
+
return {
|
|
148
|
+
masked: htmlPass.masked,
|
|
149
|
+
frontmatter,
|
|
150
|
+
map: { ...jsxPass.map, ...htmlPass.map },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Exact inverse of `maskMdx()`: restores HTML tag placeholders, then JSX
|
|
155
|
+
* component placeholders, then re-prepends the frontmatter byte-for-byte.
|
|
156
|
+
* Order doesn't matter between the two unmask passes (disjoint placeholder
|
|
157
|
+
* namespaces), but running `unmaskHtml` first mirrors `maskMdx()`'s own
|
|
158
|
+
* "JSX pass, then HTML pass" order in reverse.
|
|
159
|
+
*/
|
|
160
|
+
export async function unmaskMdx(masked, frontmatter, map) {
|
|
161
|
+
const html = await requireHazoI18nHtml("unmaskMdx");
|
|
162
|
+
const afterHtml = html.unmaskHtml(masked, map);
|
|
163
|
+
const afterJsx = unmaskJsxComponents(afterHtml, map);
|
|
164
|
+
return frontmatter ? frontmatter + afterJsx : afterJsx;
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups masked MDX content's paragraphs into chunks of at most `maxChars`
|
|
3
|
+
* characters each, never splitting a paragraph across two chunks. A single
|
|
4
|
+
* paragraph longer than `maxChars` still gets its own (oversized) chunk —
|
|
5
|
+
* same "never split a unit" rule `chunkRowsByLimits()` applies to an
|
|
6
|
+
* oversized row.
|
|
7
|
+
*
|
|
8
|
+
* Chunks are rejoined with `"\n\n"` so each chunk is itself valid
|
|
9
|
+
* Markdown/MDX (paragraphs separated by exactly one blank line), independent
|
|
10
|
+
* of whatever exact whitespace separated them in the source (1 blank line vs.
|
|
11
|
+
* 2+ — intentionally normalized; MDX treats any run of blank lines as one
|
|
12
|
+
* paragraph break).
|
|
13
|
+
*/
|
|
14
|
+
export declare function segmentMdxForTranslation(content: string, maxChars?: number): string[];
|
|
15
|
+
//# sourceMappingURL=mdx_segment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mdx_segment.d.ts","sourceRoot":"","sources":["../../src/lib/mdx_segment.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAO,GAAG,MAAM,EAAE,CAqBnF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// hazo_blog — MDX segmentation for the LLM translation round trip
|
|
2
|
+
// (`service/index.ts#translatePost()`). Splits MASKED MDX content (see
|
|
3
|
+
// `mdx_mask.ts`) into LLM-call-sized chunks, never splitting a paragraph
|
|
4
|
+
// across two chunks.
|
|
5
|
+
//
|
|
6
|
+
// Mirrors the character-budget reasoning `hazo_i18n/src/lib/export.ts`'s
|
|
7
|
+
// `chunkRowsByLimits()` uses for its own `chunkMaxChars` limit: group
|
|
8
|
+
// consecutive units up to a character budget, and a single unit that alone
|
|
9
|
+
// exceeds the budget still gets its own chunk rather than being split
|
|
10
|
+
// mid-unit (there, a "unit" is one message row; here, one paragraph block).
|
|
11
|
+
// Not literally reused — different package, different unit shape — but the
|
|
12
|
+
// same "character budget, never split a unit" philosophy.
|
|
13
|
+
/**
|
|
14
|
+
* Splits `content` into blank-line-separated paragraph blocks (the
|
|
15
|
+
* conventional Markdown/MDX paragraph boundary — one or more blank lines),
|
|
16
|
+
* preserving each block's internal newlines (a multi-line list or a fenced
|
|
17
|
+
* code block stays one block, since it contains no blank line itself).
|
|
18
|
+
*/
|
|
19
|
+
function splitIntoParagraphs(content) {
|
|
20
|
+
// Split on 1+ blank lines (any whitespace-only line counts as blank).
|
|
21
|
+
return content.split(/\n[ \t]*\n+/).filter((block) => block.trim().length > 0);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Groups masked MDX content's paragraphs into chunks of at most `maxChars`
|
|
25
|
+
* characters each, never splitting a paragraph across two chunks. A single
|
|
26
|
+
* paragraph longer than `maxChars` still gets its own (oversized) chunk —
|
|
27
|
+
* same "never split a unit" rule `chunkRowsByLimits()` applies to an
|
|
28
|
+
* oversized row.
|
|
29
|
+
*
|
|
30
|
+
* Chunks are rejoined with `"\n\n"` so each chunk is itself valid
|
|
31
|
+
* Markdown/MDX (paragraphs separated by exactly one blank line), independent
|
|
32
|
+
* of whatever exact whitespace separated them in the source (1 blank line vs.
|
|
33
|
+
* 2+ — intentionally normalized; MDX treats any run of blank lines as one
|
|
34
|
+
* paragraph break).
|
|
35
|
+
*/
|
|
36
|
+
export function segmentMdxForTranslation(content, maxChars = 2000) {
|
|
37
|
+
const paragraphs = splitIntoParagraphs(content);
|
|
38
|
+
if (paragraphs.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let current = [];
|
|
42
|
+
let currentChars = 0;
|
|
43
|
+
for (const paragraph of paragraphs) {
|
|
44
|
+
const paragraphChars = paragraph.length;
|
|
45
|
+
if (current.length > 0 && currentChars + 2 + paragraphChars > maxChars) {
|
|
46
|
+
chunks.push(current.join("\n\n"));
|
|
47
|
+
current = [];
|
|
48
|
+
currentChars = 0;
|
|
49
|
+
}
|
|
50
|
+
current.push(paragraph);
|
|
51
|
+
currentChars += (current.length > 1 ? 2 : 0) + paragraphChars;
|
|
52
|
+
}
|
|
53
|
+
if (current.length > 0)
|
|
54
|
+
chunks.push(current.join("\n\n"));
|
|
55
|
+
return chunks;
|
|
56
|
+
}
|
package/dist/locales/de.json
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"blog_admin_panel.preview": "Vorschau",
|
|
69
69
|
"blog_admin_panel.delete_confirm": "„{{title}}“ löschen? Dies kann nicht rückgängig gemacht werden.",
|
|
70
70
|
"blog_admin_panel.delete_failed": "Löschen fehlgeschlagen",
|
|
71
|
+
"blog_admin_panel.locale_filter_label": "Sprache",
|
|
71
72
|
"post_list_table.search_placeholder": "Titel oder Slug suchen…",
|
|
72
73
|
"post_list_table.filter_all": "Alle",
|
|
73
74
|
"post_list_table.filter_draft": "Entwurf",
|
|
@@ -81,6 +82,9 @@
|
|
|
81
82
|
"post_list_table.untitled": "(ohne Titel)",
|
|
82
83
|
"post_list_table.edit": "Bearbeiten",
|
|
83
84
|
"post_list_table.delete": "Löschen",
|
|
85
|
+
"post_list_table.col_locale": "Sprache",
|
|
86
|
+
"post_list_table.translate_to": "Übersetzen nach…",
|
|
87
|
+
"post_list_table.translate_failed": "Übersetzung fehlgeschlagen",
|
|
84
88
|
"layout_selector.not_persisted_notice": "Einstellungstabelle nicht migriert – Layout ist schreibgeschützt.",
|
|
85
89
|
"layout_selector.save_failed": "Layout konnte nicht gespeichert werden",
|
|
86
90
|
"layout_selector.saving": "Speichern…",
|
package/dist/locales/en.json
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"blog_admin_panel.preview": "Preview",
|
|
69
69
|
"blog_admin_panel.delete_confirm": "Delete \"{{title}}\"? This cannot be undone.",
|
|
70
70
|
"blog_admin_panel.delete_failed": "Delete failed",
|
|
71
|
+
"blog_admin_panel.locale_filter_label": "Locale",
|
|
71
72
|
"post_list_table.search_placeholder": "Search title or slug…",
|
|
72
73
|
"post_list_table.filter_all": "All",
|
|
73
74
|
"post_list_table.filter_draft": "Draft",
|
|
@@ -81,6 +82,9 @@
|
|
|
81
82
|
"post_list_table.untitled": "(untitled)",
|
|
82
83
|
"post_list_table.edit": "Edit",
|
|
83
84
|
"post_list_table.delete": "Delete",
|
|
85
|
+
"post_list_table.col_locale": "Locale",
|
|
86
|
+
"post_list_table.translate_to": "Translate to…",
|
|
87
|
+
"post_list_table.translate_failed": "Translation failed",
|
|
84
88
|
"layout_selector.not_persisted_notice": "Settings table not migrated — layout is read-only.",
|
|
85
89
|
"layout_selector.save_failed": "Failed to save layout",
|
|
86
90
|
"layout_selector.saving": "Saving…",
|
package/dist/locales/sv.json
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"blog_admin_panel.preview": "Förhandsgranska",
|
|
69
69
|
"blog_admin_panel.delete_confirm": "Ta bort “{{title}}”? Det går inte att ångra.",
|
|
70
70
|
"blog_admin_panel.delete_failed": "Borttagningen misslyckades",
|
|
71
|
+
"blog_admin_panel.locale_filter_label": "Språk",
|
|
71
72
|
"post_list_table.search_placeholder": "Sök på titel eller slug…",
|
|
72
73
|
"post_list_table.filter_all": "Alla",
|
|
73
74
|
"post_list_table.filter_draft": "Utkast",
|
|
@@ -81,6 +82,9 @@
|
|
|
81
82
|
"post_list_table.untitled": "(namnlös)",
|
|
82
83
|
"post_list_table.edit": "Redigera",
|
|
83
84
|
"post_list_table.delete": "Ta bort",
|
|
85
|
+
"post_list_table.col_locale": "Språk",
|
|
86
|
+
"post_list_table.translate_to": "Översätt till…",
|
|
87
|
+
"post_list_table.translate_failed": "Översättningen misslyckades",
|
|
84
88
|
"layout_selector.not_persisted_notice": "Inställningstabellen är inte migrerad – layouten är skrivskyddad.",
|
|
85
89
|
"layout_selector.save_failed": "Det gick inte att spara layouten",
|
|
86
90
|
"layout_selector.saving": "Sparar…",
|
package/dist/next/pages.d.ts
CHANGED
package/dist/next/pages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,UAAU,EAAyB,MAAM,mBAAmB,CAAC;AAI3E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAexC,qCAAqC;AACrC,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,UAAU;
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,UAAU,EAAyB,MAAM,mBAAmB,CAAC;AAI3E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAexC,qCAAqC;AACrC,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,UAAU;2BAyDnD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACxF;oCAzCE;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACxF,KAAG,OAAO,CAAC,QAAQ,CAAC;;;;;;;EAyEtB;AAED,+BAA+B;AAC/B,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU;iCAepD;QACD,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACzE;4BAZkC,OAAO,CAAC,QAAQ,CAAC;;EAsDrD;AAED,uCAAuC;AACvC,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,UAAU;2BAOlD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KACpD;oCAuBE;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KACpD,KAAG,OAAO,CAAC,QAAQ,CAAC;;EAStB;AAED,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,UAAU;;EA0E5D;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,UAAU;;EAqB3D;AAED,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,UAAU;2BAOxD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KACtD;EAwBF"}
|
package/dist/next/pages.js
CHANGED
|
@@ -43,11 +43,14 @@ export function createBlogPostPage(rawConfig) {
|
|
|
43
43
|
const service = createBlogService(config);
|
|
44
44
|
const t = blogTranslator(config.uiLocale);
|
|
45
45
|
async function generateStaticParams() {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
46
|
+
// Every locale variant's slug — the route this is designed for expects
|
|
47
|
+
// BOTH `slug` and `locale` segments (see Page/generateMetadata below,
|
|
48
|
+
// which destructure both from `params`), so default-locale rows (where
|
|
49
|
+
// `locale` is null in the DB) are given the configured default locale
|
|
50
|
+
// string here rather than being dropped.
|
|
51
|
+
const defaultLocale = config.defaultLocale || "en";
|
|
49
52
|
const slugs = await service.getAllSlugs(false);
|
|
50
|
-
return slugs.
|
|
53
|
+
return slugs.map((s) => ({ slug: s.slug, locale: s.locale ?? defaultLocale }));
|
|
51
54
|
}
|
|
52
55
|
async function generateMetadata({ params, }) {
|
|
53
56
|
const { slug, locale } = params instanceof Promise ? await params : params;
|
|
@@ -62,7 +65,7 @@ export function createBlogPostPage(rawConfig) {
|
|
|
62
65
|
const languages = translations
|
|
63
66
|
.filter((tr) => tr.indexable !== false)
|
|
64
67
|
.reduce((acc, tr) => {
|
|
65
|
-
acc[tr.locale] = postUrl(config, tr.slug);
|
|
68
|
+
acc[tr.locale] = postUrl(config, tr.slug, tr.locale);
|
|
66
69
|
return acc;
|
|
67
70
|
}, {});
|
|
68
71
|
return {
|
package/dist/next/routes.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare function createBlogAdminRoutes(config: BlogConfig): {
|
|
|
52
52
|
POST(req: Request): Promise<Response>;
|
|
53
53
|
};
|
|
54
54
|
item: {
|
|
55
|
+
/**
|
|
56
|
+
* Create a draft translation of this post ("Translate to...") — body
|
|
57
|
+
* `{ locale: string }`. Mirrors the PUT/DELETE pattern below: mounted on
|
|
58
|
+
* the same `[id]` route, one more HTTP method on the same resource.
|
|
59
|
+
*/
|
|
60
|
+
POST(req: Request, ctx?: RouteCtx): Promise<Response>;
|
|
55
61
|
PUT(req: Request, ctx?: RouteCtx): Promise<Response>;
|
|
56
62
|
DELETE(req: Request, ctx?: RouteCtx): Promise<Response>;
|
|
57
63
|
};
|
|
@@ -66,5 +72,32 @@ export declare function createBlogSettingsRoutes(config: BlogConfig): {
|
|
|
66
72
|
GET: (req: Request) => Promise<Response>;
|
|
67
73
|
PUT: (req: Request) => Promise<Response>;
|
|
68
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Translate an EXISTING translation-cluster draft's content (cookie/session,
|
|
77
|
+
* guarded by config.authorize). Mounted on its own `[id]/translate` resource
|
|
78
|
+
* rather than another method on the `[id]` route, since that route's `POST`
|
|
79
|
+
* is already `createTranslation` (Phase 4B's "Translate to..." — creates the
|
|
80
|
+
* draft row); this one fills an existing draft in.
|
|
81
|
+
*
|
|
82
|
+
* POST — `service.translatePost(id, req)`. 503 `SERVICE_UNAVAILABLE` if the
|
|
83
|
+
* `hazo_llm_api` optional peer isn't installed; 400 `VALIDATION_FAILED` for
|
|
84
|
+
* any other failure (unknown id, missing `translation_of`/source).
|
|
85
|
+
*/
|
|
86
|
+
export declare function createBlogTranslateRoutes(config: BlogConfig): {
|
|
87
|
+
POST: (req: Request, ctx?: RouteCtx) => Promise<Response>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* AI quality-review a translated post against its source (cookie/session,
|
|
91
|
+
* guarded by config.authorize). Mounted on its own `[id]/review` resource
|
|
92
|
+
* for the same reason `createBlogTranslateRoutes` gets its own —
|
|
93
|
+
* `[id]`'s `POST`/`PUT`/`DELETE` are already spoken for.
|
|
94
|
+
*
|
|
95
|
+
* POST — `service.reviewTranslatedPost(id, req)`. 503
|
|
96
|
+
* `SERVICE_UNAVAILABLE` if the `hazo_llm_api` optional peer isn't
|
|
97
|
+
* installed; 400 `VALIDATION_FAILED` for any other failure.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createBlogReviewRoutes(config: BlogConfig): {
|
|
100
|
+
POST: (req: Request, ctx?: RouteCtx) => Promise<Response>;
|
|
101
|
+
};
|
|
69
102
|
export {};
|
|
70
103
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/next/routes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/next/routes.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,mBAAmB,CAAC;AAErE,KAAK,QAAQ,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAYjG;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAK9F;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,IAE5B,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAS3D;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,IAE1B,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAO3D;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,IAE7B,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAI3D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU;;;;EAuDxD;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,UAAU;;;;EA4D5D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU;;iBAKrC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;kBAuB1B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;;;QAe3C;;;;WAIG;kBACa,OAAO,QAAQ,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;iBAmB5C,OAAO,QAAQ,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;oBAaxC,OAAO,QAAQ,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;;EAWhE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU;eAIjC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;eAe3B,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;EAsBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU;gBAIjC,OAAO,QAAQ,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAAC;EAkBrE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU;gBAI9B,OAAO,QAAQ,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAAC;EAkBrE"}
|
package/dist/next/routes.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Admin (cookie/session) routes are guarded by config.authorize.
|
|
4
4
|
// Management (programmatic) routes use hazo_api API keys + envelopes.
|
|
5
5
|
// Search / RSS / sitemap are public.
|
|
6
|
+
import { HazoUnavailableError } from "hazo_core";
|
|
6
7
|
import { ok, fail, withApiKey, createApiKeyService } from "hazo_api";
|
|
7
8
|
import { createBlogService, BlogSettingsUnavailableError } from "../service/index.js";
|
|
8
9
|
import { getBlogRssXml, getBlogSitemapEntries } from "../seo/index.js";
|
|
@@ -195,11 +196,25 @@ export function createBlogAdminRoutes(config) {
|
|
|
195
196
|
const denied = await guard(req);
|
|
196
197
|
if (denied)
|
|
197
198
|
return denied;
|
|
198
|
-
const
|
|
199
|
-
|
|
199
|
+
const url = new URL(req.url);
|
|
200
|
+
// Omitted => `listPosts`' own default (default-locale posts only),
|
|
201
|
+
// matching `ListPostsOptions.locale`'s existing semantics.
|
|
202
|
+
const locale = url.searchParams.get("locale") || undefined;
|
|
203
|
+
const [{ posts, total }, categories, allSlugs] = await Promise.all([
|
|
204
|
+
service.listPosts({ perPage: 1000, includeUnpublished: true, locale }, req),
|
|
200
205
|
service.repository(req).then((repo) => repo.listCategories()),
|
|
206
|
+
// Reuses the existing un-filtered slug lookup (already built for
|
|
207
|
+
// generateStaticParams) to derive the set of locales actually in use,
|
|
208
|
+
// rather than adding a new DB query — this package has no
|
|
209
|
+
// `enabledLocales` config field today.
|
|
210
|
+
service.getAllSlugs(true, req),
|
|
201
211
|
]);
|
|
202
|
-
|
|
212
|
+
const defaultLocale = service.config.defaultLocale || "en";
|
|
213
|
+
const locales = [
|
|
214
|
+
defaultLocale,
|
|
215
|
+
...new Set(allSlugs.map((s) => s.locale).filter((l) => !!l)),
|
|
216
|
+
].filter((l, i, arr) => arr.indexOf(l) === i);
|
|
217
|
+
return ok({ posts, total, categories, locales });
|
|
203
218
|
},
|
|
204
219
|
async POST(req) {
|
|
205
220
|
const denied = await guard(req);
|
|
@@ -217,6 +232,34 @@ export function createBlogAdminRoutes(config) {
|
|
|
217
232
|
},
|
|
218
233
|
};
|
|
219
234
|
const item = {
|
|
235
|
+
/**
|
|
236
|
+
* Create a draft translation of this post ("Translate to...") — body
|
|
237
|
+
* `{ locale: string }`. Mirrors the PUT/DELETE pattern below: mounted on
|
|
238
|
+
* the same `[id]` route, one more HTTP method on the same resource.
|
|
239
|
+
*/
|
|
240
|
+
async POST(req, ctx) {
|
|
241
|
+
const denied = await guard(req);
|
|
242
|
+
if (denied)
|
|
243
|
+
return denied;
|
|
244
|
+
const { id } = await readParams(ctx);
|
|
245
|
+
let body;
|
|
246
|
+
try {
|
|
247
|
+
body = (await req.json());
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return fail("VALIDATION_FAILED", "Invalid JSON body");
|
|
251
|
+
}
|
|
252
|
+
if (!body?.locale)
|
|
253
|
+
return fail("VALIDATION_FAILED", "locale is required");
|
|
254
|
+
try {
|
|
255
|
+
const saved = await service.createTranslation(id, body.locale, req);
|
|
256
|
+
return ok({ post: saved }, { status: 201 });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
260
|
+
return fail("NOT_FOUND", message);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
220
263
|
async PUT(req, ctx) {
|
|
221
264
|
const denied = await guard(req);
|
|
222
265
|
if (denied)
|
|
@@ -294,3 +337,72 @@ export function createBlogSettingsRoutes(config) {
|
|
|
294
337
|
}
|
|
295
338
|
return { GET, PUT };
|
|
296
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Translate an EXISTING translation-cluster draft's content (cookie/session,
|
|
342
|
+
* guarded by config.authorize). Mounted on its own `[id]/translate` resource
|
|
343
|
+
* rather than another method on the `[id]` route, since that route's `POST`
|
|
344
|
+
* is already `createTranslation` (Phase 4B's "Translate to..." — creates the
|
|
345
|
+
* draft row); this one fills an existing draft in.
|
|
346
|
+
*
|
|
347
|
+
* POST — `service.translatePost(id, req)`. 503 `SERVICE_UNAVAILABLE` if the
|
|
348
|
+
* `hazo_llm_api` optional peer isn't installed; 400 `VALIDATION_FAILED` for
|
|
349
|
+
* any other failure (unknown id, missing `translation_of`/source).
|
|
350
|
+
*/
|
|
351
|
+
export function createBlogTranslateRoutes(config) {
|
|
352
|
+
const service = createBlogService(config);
|
|
353
|
+
const guard = createAuthGuard(config);
|
|
354
|
+
async function POST(req, ctx) {
|
|
355
|
+
const denied = await guard(req);
|
|
356
|
+
if (denied)
|
|
357
|
+
return denied;
|
|
358
|
+
const { id } = await readParams(ctx);
|
|
359
|
+
if (!id)
|
|
360
|
+
return fail("VALIDATION_FAILED", "id is required");
|
|
361
|
+
try {
|
|
362
|
+
const result = await service.translatePost(id, req);
|
|
363
|
+
return ok(result);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
if (err instanceof HazoUnavailableError) {
|
|
367
|
+
return fail("SERVICE_UNAVAILABLE", err.message, { status: 503 });
|
|
368
|
+
}
|
|
369
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
370
|
+
return fail("VALIDATION_FAILED", message, { status: 400 });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { POST };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* AI quality-review a translated post against its source (cookie/session,
|
|
377
|
+
* guarded by config.authorize). Mounted on its own `[id]/review` resource
|
|
378
|
+
* for the same reason `createBlogTranslateRoutes` gets its own —
|
|
379
|
+
* `[id]`'s `POST`/`PUT`/`DELETE` are already spoken for.
|
|
380
|
+
*
|
|
381
|
+
* POST — `service.reviewTranslatedPost(id, req)`. 503
|
|
382
|
+
* `SERVICE_UNAVAILABLE` if the `hazo_llm_api` optional peer isn't
|
|
383
|
+
* installed; 400 `VALIDATION_FAILED` for any other failure.
|
|
384
|
+
*/
|
|
385
|
+
export function createBlogReviewRoutes(config) {
|
|
386
|
+
const service = createBlogService(config);
|
|
387
|
+
const guard = createAuthGuard(config);
|
|
388
|
+
async function POST(req, ctx) {
|
|
389
|
+
const denied = await guard(req);
|
|
390
|
+
if (denied)
|
|
391
|
+
return denied;
|
|
392
|
+
const { id } = await readParams(ctx);
|
|
393
|
+
if (!id)
|
|
394
|
+
return fail("VALIDATION_FAILED", "id is required");
|
|
395
|
+
try {
|
|
396
|
+
const result = await service.reviewTranslatedPost(id, req);
|
|
397
|
+
return ok(result);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err instanceof HazoUnavailableError) {
|
|
401
|
+
return fail("SERVICE_UNAVAILABLE", err.message, { status: 503 });
|
|
402
|
+
}
|
|
403
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
404
|
+
return fail("VALIDATION_FAILED", message, { status: 400 });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { POST };
|
|
408
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface BlogRepository {
|
|
|
25
25
|
includeUnpublished?: boolean;
|
|
26
26
|
locale?: string | null;
|
|
27
27
|
}): Promise<BlogPostWithRelations | null>;
|
|
28
|
+
/** Fetch a single post by id, regardless of status/locale. Null if not found. */
|
|
29
|
+
getPostById(id: string): Promise<BlogPostWithRelations | null>;
|
|
28
30
|
/** Returns every locale variant's slug — NOT filtered to the default locale. */
|
|
29
31
|
getAllSlugs(includeUnpublished?: boolean): Promise<Array<{
|
|
30
32
|
slug: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/repository/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/repository/index.ts"],"names":[],"mappings":"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"}
|
package/dist/repository/index.js
CHANGED
|
@@ -171,6 +171,19 @@ export function createBlogRepository(adapter, options = {}) {
|
|
|
171
171
|
const [hydrated] = await hydrate([row]);
|
|
172
172
|
return hydrated ?? null;
|
|
173
173
|
},
|
|
174
|
+
async getPostById(id) {
|
|
175
|
+
// Not findById: findById has no scope predicate, which would let one
|
|
176
|
+
// tenant fetch (and, via the translate/review flow, mutate) another
|
|
177
|
+
// tenant's post by guessing its id.
|
|
178
|
+
const rows = await posts.list(
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
+
(qb) => scoped(qb).where("id", "eq", id).limit(1));
|
|
181
|
+
const row = rows[0] ?? null;
|
|
182
|
+
if (!row)
|
|
183
|
+
return null;
|
|
184
|
+
const [hydrated] = await hydrate([row]);
|
|
185
|
+
return hydrated ?? null;
|
|
186
|
+
},
|
|
174
187
|
async getAllSlugs(includeUnpublished = false) {
|
|
175
188
|
// Deliberately NOT locale-filtered — every locale variant's slug is
|
|
176
189
|
// returned (e.g. for generateStaticParams to build every translation).
|
package/dist/seo/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { AuthorInfo, BlogPostWithRelations, ResolvedBlogConfig } from "../types/index.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Absolute URL for a post. `locale` omitted, `null`, or equal to
|
|
4
|
+
* `config.defaultLocale` ("en" when unset) produces the same URL as before
|
|
5
|
+
* this parameter existed — no prefix. Any other locale gets a `/${locale}`
|
|
6
|
+
* segment prepended BEFORE the blog's own `basePath`, matching gotimer's own
|
|
7
|
+
* `/[locale]/<slug>` routing convention (locale segment first, default
|
|
8
|
+
* locale unprefixed) so a locale-aware host doesn't have to special-case
|
|
9
|
+
* blog URLs: `https://site.com/de/blog/my-slug`, not `/blog/de/my-slug`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function postUrl(config: ResolvedBlogConfig, slug: string, locale?: string | null): string;
|
|
4
12
|
/** Absolute URL for the blog index. */
|
|
5
13
|
export declare function indexUrl(config: ResolvedBlogConfig): string;
|
|
6
14
|
/** Absolute URL for a tag page. */
|
package/dist/seo/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seo/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EAEnB,MAAM,mBAAmB,CAAC;AAS3B
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seo/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EAEnB,MAAM,mBAAmB,CAAC;AAS3B;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CAMR;AAED,uCAAuC;AACvC,wBAAgB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAE3D;AAED,mCAAmC;AACnC,wBAAgB,MAAM,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,qBAAqB,GAAG,MAAM,CAE5F;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,qBAAqB,GAAG,MAAM,CAE1F;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAKhE;AAED,uDAAuD;AACvD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,qBAAqB,EAC3B,MAAM,EAAE,UAAU,GACjB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,qBAAqB,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWhC;AAED,mEAAmE;AACnE,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,qBAAqB,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAezB;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EACX,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAcD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,qBAAqB,EAAE,GAC7B,YAAY,EAAE,CAqBhB;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAM1E;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,qBAAqB,EAAE,GAC7B,MAAM,CA6CR"}
|