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 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
 
@@ -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
@@ -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, "&lt;!")
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":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAW/D"}
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
- export function BlogContent({ source, config }) {
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
- return (_jsx("div", { className: "prose prose-zinc max-w-none dark:prose-invert", children: _jsx(MDXRemote, { source: source, components: components, options: { mdxOptions: { remarkPlugins: [remarkGfm] } } }) }));
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
  }
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AASA,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;AAOvE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
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"}
@@ -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,CA6GpE"}
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"}
@@ -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: input.content,
72
- excerpt: buildExcerpt(input.content),
73
- reading_time: calculateReadingTime(input.content),
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(input.content),
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.2.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.0.1",
48
- "hazo_connect": "^3.1.0",
49
- "hazo_api": "^2.1.1",
50
- "hazo_files": "^3.0.0",
51
- "hazo_ui": "^3.2.0",
52
- "hazo_images": "^1.2.0",
53
- "hazo_jobs": "^0.12.0",
54
- "hazo_auth": "^9.0.1",
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.0.1",
87
- "hazo_connect": "^3.1.0",
88
- "hazo_api": "^2.1.1",
89
- "hazo_files": "^3.0.0",
90
- "hazo_ui": "^3.2.1",
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"