hazo_blog 0.2.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +48 -0
- package/README.md +15 -1
- package/dist/lib/text.d.ts +30 -0
- package/dist/lib/text.d.ts.map +1 -1
- package/dist/lib/text.js +58 -0
- package/dist/next/blog-content.d.ts +1 -1
- package/dist/next/blog-content.d.ts.map +1 -1
- package/dist/next/blog-content.js +21 -2
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +2 -0
- package/dist/next/mdx-error-boundary.d.ts +16 -0
- package/dist/next/mdx-error-boundary.d.ts.map +1 -0
- package/dist/next/mdx-error-boundary.js +15 -0
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/index.js +6 -5
- package/package.json +14 -14
package/CHANGE_LOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
All notable changes are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 0.3.3 — 2026-07-06
|
|
7
|
+
|
|
8
|
+
**`sanitizeMdx` hardened against raw WordPress HTML**
|
|
9
|
+
|
|
10
|
+
Extends the sanitizer beyond HTML comments to cover three more parser-breaking patterns commonly
|
|
11
|
+
found in imported WordPress post bodies. All run on the code-masked string, so fenced/inline code
|
|
12
|
+
spans stay byte-for-byte untouched, and the transform remains idempotent.
|
|
13
|
+
|
|
14
|
+
- **`<script>` / `<style>` blocks** — dropped entirely. Their bodies (JSON-LD, inline CSS) contain
|
|
15
|
+
raw `{ }` that MDX parses as a JS expression, throwing "Could not parse expression with acorn".
|
|
16
|
+
They carry no visible prose, so removing them never changes rendered output.
|
|
17
|
+
- **HTML void elements** (`<img>`, `<br>`, `<hr>`, `<input>`, …) — normalized to self-closing
|
|
18
|
+
(`<img src="x" />`). Un-self-closed voids throw "Expected a closing tag" in MDX/JSX.
|
|
19
|
+
- **String-valued `style="..."` / `style='...'` attributes** — dropped. Valid HTML but invalid JSX
|
|
20
|
+
(React requires the object form `style={{...}}`), else "The style prop expects a mapping … not a
|
|
21
|
+
string" at render. The JSX object form is deliberately left untouched.
|
|
22
|
+
|
|
23
|
+
No API change — same `sanitizeMdx(mdx: string): string` signature. **Consumer note:** `^0.3.0`
|
|
24
|
+
resolves to `0.3.3` (caret allows patch/minor within the same pre-1.0 minor).
|
|
25
|
+
|
|
26
|
+
## 0.3.0 — 2026-06-12
|
|
27
|
+
|
|
28
|
+
**Build resilience: sanitizer + async try/catch guard against malformed MDX**
|
|
29
|
+
|
|
30
|
+
- `sanitizeMdx(mdx: string): string` — new public utility in `src/lib/text`. Converts HTML
|
|
31
|
+
comments (`<!-- ... -->`) to MDX comments (`{/* ... */}`) and escapes stray `<!` that are not
|
|
32
|
+
comment openers, leaving fenced and inline code blocks untouched. Idempotent. This is the
|
|
33
|
+
primary protection layer against the known crash class (HTML comments in post body that caused
|
|
34
|
+
`next build` to 500 site-wide on gotimer).
|
|
35
|
+
- `BlogContent` is now an async server component. It calls `MDXRemote` directly with `await`
|
|
36
|
+
and wraps it in `try/catch`: a single malformed post logs an error and renders a "could not
|
|
37
|
+
be displayed" fallback instead of failing the whole SSG build.
|
|
38
|
+
- `MdxErrorBoundary` — new exported client error boundary (`src/next/mdx-error-boundary.tsx`).
|
|
39
|
+
Available for consumers who want an additional client-side defence layer.
|
|
40
|
+
- Write-time sanitize: `upsertBySlug` in `src/service` now runs `sanitizeMdx` before persisting,
|
|
41
|
+
so newly-saved posts are normalized at ingestion.
|
|
42
|
+
- **Consumer note:** `^0.2.0` does not auto-resolve to `0.3.0` (pre-1.0 caret pins the minor).
|
|
43
|
+
Bump your dependency range to `^0.3.0` to receive this fix.
|
|
44
|
+
|
|
45
|
+
## 0.2.0 — 2026-06-04
|
|
46
|
+
|
|
47
|
+
**SQLite driver unification (breaking internal change; API unchanged)**
|
|
48
|
+
|
|
49
|
+
Unified on native `better-sqlite3` (dropping `sql.js` fallback). Fixes the dual-engine 401
|
|
50
|
+
auth bug where a fresh, uncheckpointed WAL key was invisible to the sql.js reader. Consumers
|
|
51
|
+
who were using the `sql.js` adapter via `sqlite_driver` env-var are unaffected if they set
|
|
52
|
+
`sqlite_driver=better-sqlite3` (now the only valid option). Config option removed.
|
|
53
|
+
|
|
6
54
|
## 0.1.0 — 2026-06-01
|
|
7
55
|
|
|
8
56
|
Initial release. SEO-first blogging package extracted from gotimer.
|
package/README.md
CHANGED
|
@@ -204,13 +204,27 @@ const nextConfig = {
|
|
|
204
204
|
the package's classes are compiled. The admin editor's styles load automatically
|
|
205
205
|
via `hazo_ui`'s `MarkdownEditor`.
|
|
206
206
|
|
|
207
|
+
### Build resilience
|
|
208
|
+
|
|
209
|
+
`BlogContent` (≥ 0.3.0) is an async server component that catches MDX compilation errors with
|
|
210
|
+
`try/catch`, so a single malformed post renders a graceful fallback instead of failing the whole
|
|
211
|
+
`next build`. It also pre-sanitizes content with `sanitizeMdx` before passing it to the compiler.
|
|
212
|
+
The sanitizer fixes the common ways raw WordPress HTML trips the MDX/acorn parser: converts HTML
|
|
213
|
+
comments → MDX comments (and escapes stray `<!`), strips `<script>`/`<style>` blocks whose bodies
|
|
214
|
+
break parsing, self-closes HTML void elements (`<img>`, `<br>`, …), and drops string-valued
|
|
215
|
+
`style="..."` attributes (invalid JSX; the object form `style={{...}}` is left untouched). Fenced
|
|
216
|
+
and inline code spans are left byte-for-byte untouched, and the transform is idempotent. The
|
|
217
|
+
sanitizer is also available as a standalone export from `hazo_blog/lib` for custom rendering
|
|
218
|
+
pipelines.
|
|
219
|
+
|
|
207
220
|
## Exports
|
|
208
221
|
|
|
209
222
|
| Entry | Contents |
|
|
210
223
|
|---|---|
|
|
211
224
|
| `hazo_blog` | server: `createBlogService`, `createBlogRepository`, SEO builders, **route-handler factories** (React-free), types |
|
|
212
225
|
| `hazo_blog/client` | client components: `PostCard`, `PostHero`, `AuthorBio`, `FaqSection`, `TableOfContents`, `RelatedPosts`, `BlogSearch`, `PostForm`, MDX components, `trackBlogEvent` |
|
|
213
|
-
| `hazo_blog/next` | sealed page factories: public (`createBlogIndexPage`, `createBlogPostPage`, `createBlogTagPage`) + admin (`createBlogAdminListPage`, `createBlogAdminNewPage`, `createBlogAdminEditPage`) + `BlogContent` |
|
|
226
|
+
| `hazo_blog/next` | sealed page factories: public (`createBlogIndexPage`, `createBlogPostPage`, `createBlogTagPage`) + admin (`createBlogAdminListPage`, `createBlogAdminNewPage`, `createBlogAdminEditPage`) + `BlogContent` + `MdxErrorBoundary` |
|
|
227
|
+
| `hazo_blog/lib` | pure text utilities: `sanitizeMdx`, `mdxToPlainText`, `buildExcerpt`, `slugify`, `calculateReadingTime`, `extractToc` |
|
|
214
228
|
| `hazo_blog/seo` | `buildBlogPostingJsonLd`, `buildBreadcrumbJsonLd`, `buildFaqJsonLd`, `getBlogSitemapEntries`, `getBlogRobotsRules`, `getBlogRssXml` |
|
|
215
229
|
| `hazo_blog/config` | `BlogConfig` types + `resolveConfig` |
|
|
216
230
|
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -22,4 +22,34 @@ export interface TocHeading {
|
|
|
22
22
|
id: string;
|
|
23
23
|
}
|
|
24
24
|
export declare function extractToc(mdx: string): TocHeading[];
|
|
25
|
+
/**
|
|
26
|
+
* Make raw MDX safe to compile. Raw WordPress HTML stored as post content
|
|
27
|
+
* trips the MDX/acorn parser in a handful of unambiguously-fixable ways:
|
|
28
|
+
*
|
|
29
|
+
* 1. HTML comments — MDX has none, so a bare `<!--` makes the parser expect
|
|
30
|
+
* a JSX tag name after `<` and throw "Unexpected character `!` before
|
|
31
|
+
* name". Converted to MDX comments ({/*…*\/}, equally non-rendering).
|
|
32
|
+
* 2. `<script>`/`<style>` blocks — their bodies (JSON-LD, inline CSS)
|
|
33
|
+
* contain raw `{ }` that MDX parses as a JS expression, throwing "Could
|
|
34
|
+
* not parse expression with acorn". They carry no visible prose (SEO
|
|
35
|
+
* metadata / global CSS handle those concerns elsewhere), so dropping the
|
|
36
|
+
* whole block is safe and never changes rendered output.
|
|
37
|
+
* 3. HTML void elements (`<img>`, `<br>`, …) written un-self-closed — MDX/JSX
|
|
38
|
+
* requires them self-closed; `<img src="x">` alone throws "Expected a
|
|
39
|
+
* closing tag". Normalized to `<img src="x" />` regardless of whether a
|
|
40
|
+
* stray slash was already present.
|
|
41
|
+
* 4. String-valued `style="..."` / `style='...'` attributes — valid HTML but
|
|
42
|
+
* invalid JSX/MDX (React requires `style={{...}}`, an object, never a
|
|
43
|
+
* string); left alone, they compile fine but throw "The style prop
|
|
44
|
+
* expects a mapping ... not a string" at render time. Always broken, so
|
|
45
|
+
* the attribute is dropped outright. Only the quoted-string form is
|
|
46
|
+
* matched (`style="`/`style='`) — the legitimate JSX object form
|
|
47
|
+
* (`style={{...}}`) never matches and is left untouched.
|
|
48
|
+
*
|
|
49
|
+
* All four run on the code-MASKED string, so fenced/inline code spans are
|
|
50
|
+
* left byte-for-byte untouched (they are literal in MDX, never error, and
|
|
51
|
+
* may legitimately show a comment, a `<script>` sample, or a bare `<img>` in
|
|
52
|
+
* a code sample). Idempotent: running this twice yields the same output.
|
|
53
|
+
*/
|
|
54
|
+
export declare function sanitizeMdx(mdx: string): string;
|
|
25
55
|
//# sourceMappingURL=text.d.ts.map
|
package/dist/lib/text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/lib/text.ts"],"names":[],"mappings":"AAGA,6CAA6C;AAC7C,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAalD;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,SAAM,GAAG,MAAM,CAG9E;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAMhE;AAED,qFAAqF;AACrF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAYpD"}
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/lib/text.ts"],"names":[],"mappings":"AAGA,6CAA6C;AAC7C,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAalD;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,SAAM,GAAG,MAAM,CAG9E;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAMhE;AAED,qFAAqF;AACrF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAYpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA+B/C"}
|
package/dist/lib/text.js
CHANGED
|
@@ -67,3 +67,61 @@ export function extractToc(mdx) {
|
|
|
67
67
|
}
|
|
68
68
|
return headings;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Make raw MDX safe to compile. Raw WordPress HTML stored as post content
|
|
72
|
+
* trips the MDX/acorn parser in a handful of unambiguously-fixable ways:
|
|
73
|
+
*
|
|
74
|
+
* 1. HTML comments — MDX has none, so a bare `<!--` makes the parser expect
|
|
75
|
+
* a JSX tag name after `<` and throw "Unexpected character `!` before
|
|
76
|
+
* name". Converted to MDX comments ({/*…*\/}, equally non-rendering).
|
|
77
|
+
* 2. `<script>`/`<style>` blocks — their bodies (JSON-LD, inline CSS)
|
|
78
|
+
* contain raw `{ }` that MDX parses as a JS expression, throwing "Could
|
|
79
|
+
* not parse expression with acorn". They carry no visible prose (SEO
|
|
80
|
+
* metadata / global CSS handle those concerns elsewhere), so dropping the
|
|
81
|
+
* whole block is safe and never changes rendered output.
|
|
82
|
+
* 3. HTML void elements (`<img>`, `<br>`, …) written un-self-closed — MDX/JSX
|
|
83
|
+
* requires them self-closed; `<img src="x">` alone throws "Expected a
|
|
84
|
+
* closing tag". Normalized to `<img src="x" />` regardless of whether a
|
|
85
|
+
* stray slash was already present.
|
|
86
|
+
* 4. String-valued `style="..."` / `style='...'` attributes — valid HTML but
|
|
87
|
+
* invalid JSX/MDX (React requires `style={{...}}`, an object, never a
|
|
88
|
+
* string); left alone, they compile fine but throw "The style prop
|
|
89
|
+
* expects a mapping ... not a string" at render time. Always broken, so
|
|
90
|
+
* the attribute is dropped outright. Only the quoted-string form is
|
|
91
|
+
* matched (`style="`/`style='`) — the legitimate JSX object form
|
|
92
|
+
* (`style={{...}}`) never matches and is left untouched.
|
|
93
|
+
*
|
|
94
|
+
* All four run on the code-MASKED string, so fenced/inline code spans are
|
|
95
|
+
* left byte-for-byte untouched (they are literal in MDX, never error, and
|
|
96
|
+
* may legitimately show a comment, a `<script>` sample, or a bare `<img>` in
|
|
97
|
+
* a code sample). Idempotent: running this twice yields the same output.
|
|
98
|
+
*/
|
|
99
|
+
export function sanitizeMdx(mdx) {
|
|
100
|
+
const stash = [];
|
|
101
|
+
// U+E000 (Private Use Area) sentinel — cannot occur in markdown, so the
|
|
102
|
+
// restore step never collides with real text like "90 seconds".
|
|
103
|
+
const keep = (m) => {
|
|
104
|
+
stash.push(m);
|
|
105
|
+
return `${stash.length - 1}`;
|
|
106
|
+
};
|
|
107
|
+
// Mask fenced code blocks (multiline) first, then inline code spans.
|
|
108
|
+
let masked = mdx
|
|
109
|
+
.replace(/```[\s\S]*?```/g, keep)
|
|
110
|
+
.replace(/~~~[\s\S]*?~~~/g, keep)
|
|
111
|
+
.replace(/`[^`\n]*`/g, keep);
|
|
112
|
+
masked = masked
|
|
113
|
+
// Strip <script>/<style> blocks entirely — see rationale above.
|
|
114
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
115
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
116
|
+
// Comment body: neutralize any `*/` so it can't close the MDX comment early.
|
|
117
|
+
.replace(/<!--([\s\S]*?)-->/g, (_, body) => `{/*${body.replace(/\*\//g, "* /")}*/}`)
|
|
118
|
+
.replace(/<!(?!--)/g, "<!")
|
|
119
|
+
// Strip string-valued `style="..."` attributes — see rationale above.
|
|
120
|
+
// Deliberately does NOT match `style={` (the valid JSX object form).
|
|
121
|
+
.replace(/\sstyle=("[^"]*"|'[^']*')/gi, "")
|
|
122
|
+
// Self-close HTML void elements — see rationale above. Emits `/>`
|
|
123
|
+
// whether or not a slash was already present, so a second pass is a
|
|
124
|
+
// no-op (idempotent).
|
|
125
|
+
.replace(/<(img|br|hr|input|source|meta|link|col|area|base|embed|param|track|wbr)\b([^>]*?)\s*\/?>/gi, "<$1$2 />");
|
|
126
|
+
return masked.replace(/(\d+)/g, (_, i) => stash[Number(i)]);
|
|
127
|
+
}
|
|
@@ -3,5 +3,5 @@ export interface BlogContentProps {
|
|
|
3
3
|
source: string;
|
|
4
4
|
config: ResolvedBlogConfig;
|
|
5
5
|
}
|
|
6
|
-
export declare function BlogContent({ source, config }: BlogContentProps): import("react/jsx-runtime").JSX.Element
|
|
6
|
+
export declare function BlogContent({ source, config }: BlogContentProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
7
7
|
//# sourceMappingURL=blog-content.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-content.d.ts","sourceRoot":"","sources":["../../src/next/blog-content.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blog-content.d.ts","sourceRoot":"","sources":["../../src/next/blog-content.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAS5D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAsB,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,gBAAgB,oDAiBrE"}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// Server component that renders raw MDX to HTML at build/ISR time via
|
|
3
3
|
// next-mdx-remote/rsc, merging the default embed registry with host overrides.
|
|
4
|
+
// Calls MDXRemote as an async function so compilation errors are caught server-
|
|
5
|
+
// side with try/catch before they can fail the SSG build for the whole page.
|
|
4
6
|
import { MDXRemote } from "next-mdx-remote/rsc";
|
|
5
7
|
import remarkGfm from "remark-gfm";
|
|
6
8
|
import { defaultMdxComponents } from "../components/mdx/index.js";
|
|
7
|
-
|
|
9
|
+
import { sanitizeMdx } from "../lib/text.js";
|
|
10
|
+
// MDXRemote is an async RSC but TypeScript surfaces it as ComponentType via
|
|
11
|
+
// React 18 JSX types. Cast to the actual async-function signature so we can
|
|
12
|
+
// call it with await + try/catch to prevent one broken post from failing the
|
|
13
|
+
// entire SSG build.
|
|
14
|
+
const renderMDX = MDXRemote;
|
|
15
|
+
export async function BlogContent({ source, config }) {
|
|
8
16
|
const components = { ...defaultMdxComponents, ...(config.mdxComponents ?? {}) };
|
|
9
|
-
|
|
17
|
+
try {
|
|
18
|
+
const content = await renderMDX({
|
|
19
|
+
source: sanitizeMdx(source),
|
|
20
|
+
components,
|
|
21
|
+
options: { mdxOptions: { remarkPlugins: [remarkGfm] } },
|
|
22
|
+
});
|
|
23
|
+
return _jsx("div", { className: "prose prose-zinc max-w-none dark:prose-invert", children: content });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error("[hazo_blog] MDX compile failed:", error);
|
|
27
|
+
return (_jsx("div", { className: "prose prose-zinc max-w-none dark:prose-invert", children: _jsx("p", { className: "text-muted-foreground", children: "This content could not be displayed." }) }));
|
|
28
|
+
}
|
|
10
29
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createBlogIndexPage, createBlogPostPage, createBlogTagPage, createBlogAdminListPage, createBlogAdminNewPage, createBlogAdminEditPage, } from "./pages.js";
|
|
2
2
|
export { BlogContent, type BlogContentProps } from "./blog-content.js";
|
|
3
|
+
export { MdxErrorBoundary } from "./mdx-error-boundary.js";
|
|
3
4
|
export { resolveConfig } from "../service/index.js";
|
|
4
5
|
export type { BlogConfig, ResolvedBlogConfig } from "../types/index.js";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/next/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAO3D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/next/index.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// createBlogManageRoutes, createBlogAdminRoutes, createBlogSearchRoute,
|
|
7
7
|
// createBlogFeedRoute, createBlogSitemapRoute
|
|
8
8
|
// Server MDX renderer: BlogContent
|
|
9
|
+
// Error boundary: MdxErrorBoundary
|
|
9
10
|
export { createBlogIndexPage, createBlogPostPage, createBlogTagPage, createBlogAdminListPage, createBlogAdminNewPage, createBlogAdminEditPage, } from "./pages.js";
|
|
10
11
|
export { BlogContent } from "./blog-content.js";
|
|
12
|
+
export { MdxErrorBoundary } from "./mdx-error-boundary.js";
|
|
11
13
|
// NOTE: route-handler factories (createBlogAdminRoutes, createBlogManageRoutes,
|
|
12
14
|
// createBlogSearchRoute, createBlogFeedRoute, createBlogSitemapRoute) are exported
|
|
13
15
|
// from the main `hazo_blog` entry — they are React-free, so API routes import them
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
fallback: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
interface State {
|
|
7
|
+
hasError: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class MdxErrorBoundary extends Component<Props, State> {
|
|
10
|
+
state: State;
|
|
11
|
+
static getDerivedStateFromError(): State;
|
|
12
|
+
componentDidCatch(error: unknown): void;
|
|
13
|
+
render(): ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=mdx-error-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mdx-error-boundary.d.ts","sourceRoot":"","sources":["../../src/next/mdx-error-boundary.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,UAAU,KAAK;IAAG,QAAQ,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAC;CAAE;AAC7D,UAAU,KAAK;IAAG,QAAQ,EAAE,OAAO,CAAC;CAAE;AAEtC,qBAAa,gBAAiB,SAAQ,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;IAC3D,KAAK,EAAE,KAAK,CAAuB;IACnC,MAAM,CAAC,wBAAwB,IAAI,KAAK;IACxC,iBAAiB,CAAC,KAAK,EAAE,OAAO;IAGhC,MAAM;CAGP"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Component } from "react";
|
|
3
|
+
export class MdxErrorBoundary extends Component {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(...arguments);
|
|
6
|
+
this.state = { hasError: false };
|
|
7
|
+
}
|
|
8
|
+
static getDerivedStateFromError() { return { hasError: true }; }
|
|
9
|
+
componentDidCatch(error) {
|
|
10
|
+
console.error("[hazo_blog] MDX render failed:", error);
|
|
11
|
+
}
|
|
12
|
+
render() {
|
|
13
|
+
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/service/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,wBAAwB,CAAC;AAGhC,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,kBAAkB,CAQpE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACnD,SAAS,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrE,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,EACvC,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACzC,WAAW,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,aAAa,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChE,eAAe,CACb,IAAI,EAAE,qBAAqB,EAC3B,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;CACvF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,UAAU,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/service/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,wBAAwB,CAAC;AAGhC,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,kBAAkB,CAQpE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACnD,SAAS,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACrE,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,EACvC,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACzC,WAAW,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,aAAa,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChE,eAAe,CACb,IAAI,EAAE,qBAAqB,EAC3B,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;CACvF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,UAAU,GAAG,WAAW,CA+GpE"}
|
package/dist/service/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// repository per request, and owns business logic (slug/reading-time/publish
|
|
3
3
|
// rules, author resolution, FAQ serialization). Server-only.
|
|
4
4
|
import { createBlogRepository, } from "../repository/index.js";
|
|
5
|
-
import { buildExcerpt, calculateReadingTime, slugify } from "../lib/text.js";
|
|
5
|
+
import { buildExcerpt, calculateReadingTime, sanitizeMdx, slugify } from "../lib/text.js";
|
|
6
6
|
/** Apply defaults to a host-supplied BlogConfig. */
|
|
7
7
|
export function resolveConfig(config) {
|
|
8
8
|
return {
|
|
@@ -46,6 +46,7 @@ export function createBlogService(rawConfig) {
|
|
|
46
46
|
},
|
|
47
47
|
async upsertBySlug(input, req) {
|
|
48
48
|
const repo = await repository(req);
|
|
49
|
+
const content = sanitizeMdx(input.content);
|
|
49
50
|
const slug = input.slug?.trim() ? slugify(input.slug) : slugify(input.title);
|
|
50
51
|
const status = input.status ?? "draft";
|
|
51
52
|
// Resolve category from name when an id isn't supplied.
|
|
@@ -68,13 +69,13 @@ export function createBlogService(rawConfig) {
|
|
|
68
69
|
const row = {
|
|
69
70
|
slug,
|
|
70
71
|
title: input.title,
|
|
71
|
-
content
|
|
72
|
-
excerpt: buildExcerpt(
|
|
73
|
-
reading_time: calculateReadingTime(
|
|
72
|
+
content,
|
|
73
|
+
excerpt: buildExcerpt(content),
|
|
74
|
+
reading_time: calculateReadingTime(content),
|
|
74
75
|
category_id: categoryId,
|
|
75
76
|
author_id: input.author_id ?? null,
|
|
76
77
|
meta_title: input.meta_title ?? "",
|
|
77
|
-
meta_description: input.meta_description ?? buildExcerpt(
|
|
78
|
+
meta_description: input.meta_description ?? buildExcerpt(content),
|
|
78
79
|
canonical_url: input.canonical_url ?? null,
|
|
79
80
|
og_image: input.og_image ?? null,
|
|
80
81
|
featured_image: input.featured_image ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_blog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "SEO-optimized blogging package: posts, categories, tags, MDX content, and GA4/GSC/Bing-ready SEO.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"build:test-app": "npm run build && cd test-app && npm run build"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"hazo_core": "^1.
|
|
48
|
-
"hazo_connect": "^3.
|
|
49
|
-
"hazo_api": "^2.
|
|
50
|
-
"hazo_files": "^3.
|
|
51
|
-
"hazo_ui": "^
|
|
52
|
-
"hazo_images": "^1.
|
|
53
|
-
"hazo_jobs": "^0.
|
|
54
|
-
"hazo_auth": "^
|
|
47
|
+
"hazo_core": "^1.2.1",
|
|
48
|
+
"hazo_connect": "^3.9.0",
|
|
49
|
+
"hazo_api": "^2.5.1",
|
|
50
|
+
"hazo_files": "^3.1.1",
|
|
51
|
+
"hazo_ui": "^4.8.0",
|
|
52
|
+
"hazo_images": "^1.7.0",
|
|
53
|
+
"hazo_jobs": "^0.14.0",
|
|
54
|
+
"hazo_auth": "^10.5.0",
|
|
55
55
|
"react": "^18.0.0 || ^19.0.0",
|
|
56
56
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
57
57
|
"next": "^14.0.0 || ^16.0.0"
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"react": "^19.0.0",
|
|
84
84
|
"react-dom": "^19.0.0",
|
|
85
85
|
"next": "^16.0.10",
|
|
86
|
-
"hazo_core": "^1.
|
|
87
|
-
"hazo_connect": "^3.
|
|
88
|
-
"hazo_api": "^2.
|
|
89
|
-
"hazo_files": "^3.
|
|
90
|
-
"hazo_ui": "^
|
|
86
|
+
"hazo_core": "^1.2.1",
|
|
87
|
+
"hazo_connect": "^3.9.0",
|
|
88
|
+
"hazo_api": "^2.5.1",
|
|
89
|
+
"hazo_files": "^3.1.1",
|
|
90
|
+
"hazo_ui": "^4.8.0",
|
|
91
91
|
"tailwindcss": "^4.2.4",
|
|
92
92
|
"@tailwindcss/postcss": "^4.2.4",
|
|
93
93
|
"postcss": "^8.4.49"
|