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 +28 -0
- package/README.md +10 -1
- package/dist/lib/text.d.ts +10 -0
- package/dist/lib/text.d.ts.map +1 -1
- package/dist/lib/text.js +28 -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 +13 -13
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
|
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -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
|
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;;;;;;;;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, "<!");
|
|
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":"
|
|
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.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
|
|
48
|
-
"hazo_connect": "^3.
|
|
49
|
-
"hazo_api": "^2.
|
|
50
|
-
"hazo_files": "^3.
|
|
51
|
-
"hazo_ui": "^
|
|
52
|
-
"hazo_images": "^1.2.
|
|
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": "^
|
|
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.
|
|
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.7.0",
|
|
91
91
|
"tailwindcss": "^4.2.4",
|
|
92
92
|
"@tailwindcss/postcss": "^4.2.4",
|
|
93
93
|
"postcss": "^8.4.49"
|