hazo_blog 0.2.0 → 0.3.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 CHANGED
@@ -3,6 +3,34 @@
3
3
  All notable changes are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 0.3.0 — 2026-06-12
7
+
8
+ **Build resilience: sanitizer + async try/catch guard against malformed MDX**
9
+
10
+ - `sanitizeMdx(mdx: string): string` — new public utility in `src/lib/text`. Converts HTML
11
+ comments (`<!-- ... -->`) to MDX comments (`{/* ... */}`) and escapes stray `<!` that are not
12
+ comment openers, leaving fenced and inline code blocks untouched. Idempotent. This is the
13
+ primary protection layer against the known crash class (HTML comments in post body that caused
14
+ `next build` to 500 site-wide on gotimer).
15
+ - `BlogContent` is now an async server component. It calls `MDXRemote` directly with `await`
16
+ and wraps it in `try/catch`: a single malformed post logs an error and renders a "could not
17
+ be displayed" fallback instead of failing the whole SSG build.
18
+ - `MdxErrorBoundary` — new exported client error boundary (`src/next/mdx-error-boundary.tsx`).
19
+ Available for consumers who want an additional client-side defence layer.
20
+ - Write-time sanitize: `upsertBySlug` in `src/service` now runs `sanitizeMdx` before persisting,
21
+ so newly-saved posts are normalized at ingestion.
22
+ - **Consumer note:** `^0.2.0` does not auto-resolve to `0.3.0` (pre-1.0 caret pins the minor).
23
+ Bump your dependency range to `^0.3.0` to receive this fix.
24
+
25
+ ## 0.2.0 — 2026-06-04
26
+
27
+ **SQLite driver unification (breaking internal change; API unchanged)**
28
+
29
+ Unified on native `better-sqlite3` (dropping `sql.js` fallback). Fixes the dual-engine 401
30
+ auth bug where a fresh, uncheckpointed WAL key was invisible to the sql.js reader. Consumers
31
+ who were using the `sql.js` adapter via `sqlite_driver` env-var are unaffected if they set
32
+ `sqlite_driver=better-sqlite3` (now the only valid option). Config option removed.
33
+
6
34
  ## 0.1.0 — 2026-06-01
7
35
 
8
36
  Initial release. SEO-first blogging package extracted from gotimer.
package/README.md CHANGED
@@ -204,13 +204,22 @@ 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` (converts HTML comments → MDX
212
+ comments, escapes stray `<!`) before passing it to the compiler. The sanitizer is also available
213
+ as a standalone export from `hazo_blog/lib` for custom rendering pipelines.
214
+
207
215
  ## Exports
208
216
 
209
217
  | Entry | Contents |
210
218
  |---|---|
211
219
  | `hazo_blog` | server: `createBlogService`, `createBlogRepository`, SEO builders, **route-handler factories** (React-free), types |
212
220
  | `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` |
221
+ | `hazo_blog/next` | sealed page factories: public (`createBlogIndexPage`, `createBlogPostPage`, `createBlogTagPage`) + admin (`createBlogAdminListPage`, `createBlogAdminNewPage`, `createBlogAdminEditPage`) + `BlogContent` + `MdxErrorBoundary` |
222
+ | `hazo_blog/lib` | pure text utilities: `sanitizeMdx`, `mdxToPlainText`, `buildExcerpt`, `slugify`, `calculateReadingTime`, `extractToc` |
214
223
  | `hazo_blog/seo` | `buildBlogPostingJsonLd`, `buildBreadcrumbJsonLd`, `buildFaqJsonLd`, `getBlogSitemapEntries`, `getBlogRobotsRules`, `getBlogRssXml` |
215
224
  | `hazo_blog/config` | `BlogConfig` types + `resolveConfig` |
216
225
 
@@ -22,4 +22,14 @@ 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. MDX has no HTML comments — a `<!--` in prose
27
+ * makes the parser expect a JSX tag name after `<` and throw "Unexpected
28
+ * character `!` before name", crashing the whole Next build at prerender.
29
+ * Convert HTML comments to MDX comments ({/*…*\/}, equally non-rendering) and
30
+ * escape any stray `<!`, while leaving fenced/inline code untouched (it is
31
+ * literal in MDX, never errors, and may legitimately show an HTML comment in a
32
+ * sample). Idempotent: after one pass no literal `<!` survives in prose.
33
+ */
34
+ export declare function sanitizeMdx(mdx: string): string;
25
35
  //# 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;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkB/C"}
package/dist/lib/text.js CHANGED
@@ -67,3 +67,31 @@ export function extractToc(mdx) {
67
67
  }
68
68
  return headings;
69
69
  }
70
+ /**
71
+ * Make raw MDX safe to compile. MDX has no HTML comments — a `<!--` in prose
72
+ * makes the parser expect a JSX tag name after `<` and throw "Unexpected
73
+ * character `!` before name", crashing the whole Next build at prerender.
74
+ * Convert HTML comments to MDX comments ({/*…*\/}, equally non-rendering) and
75
+ * escape any stray `<!`, while leaving fenced/inline code untouched (it is
76
+ * literal in MDX, never errors, and may legitimately show an HTML comment in a
77
+ * sample). Idempotent: after one pass no literal `<!` survives in prose.
78
+ */
79
+ export function sanitizeMdx(mdx) {
80
+ const stash = [];
81
+ // U+E000 (Private Use Area) sentinel — cannot occur in markdown, so the
82
+ // restore step never collides with real text like "90 seconds".
83
+ const keep = (m) => {
84
+ stash.push(m);
85
+ return `${stash.length - 1}`;
86
+ };
87
+ // Mask fenced code blocks (multiline) first, then inline code spans.
88
+ let masked = mdx
89
+ .replace(/```[\s\S]*?```/g, keep)
90
+ .replace(/~~~[\s\S]*?~~~/g, keep)
91
+ .replace(/`[^`\n]*`/g, keep);
92
+ masked = masked
93
+ // Comment body: neutralize any `*/` so it can't close the MDX comment early.
94
+ .replace(/<!--([\s\S]*?)-->/g, (_, body) => `{/*${body.replace(/\*\//g, "* /")}*/}`)
95
+ .replace(/<!(?!--)/g, "&lt;!");
96
+ return masked.replace(/(\d+)/g, (_, i) => stash[Number(i)]);
97
+ }
@@ -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.0",
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",
47
+ "hazo_core": "^1.2.0",
48
+ "hazo_connect": "^3.9.0",
49
+ "hazo_api": "^2.4.0",
50
+ "hazo_files": "^3.1.0",
51
+ "hazo_ui": "^4.6.2",
52
+ "hazo_images": "^1.2.1",
53
53
  "hazo_jobs": "^0.12.0",
54
- "hazo_auth": "^9.0.1",
54
+ "hazo_auth": "^10.0.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.7.0",
91
91
  "tailwindcss": "^4.2.4",
92
92
  "@tailwindcss/postcss": "^4.2.4",
93
93
  "postcss": "^8.4.49"