hazo_blog 0.1.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 +67 -2
- package/SETUP_CHECKLIST.md +11 -1
- package/dist/components/blog-search.d.ts.map +1 -1
- package/dist/components/blog-search.js +21 -1
- package/dist/components/post-card.d.ts.map +1 -1
- package/dist/components/post-card.js +1 -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/admin-client.d.ts +9 -0
- package/dist/next/admin-client.d.ts.map +1 -0
- package/dist/next/admin-client.js +9 -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 +2 -1
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +3 -1
- 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/next/pages.d.ts +18 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +62 -4
- package/dist/seo/index.js +1 -1
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/index.js +8 -5
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +25 -17
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
|
@@ -47,6 +47,10 @@ export const blogConfig: BlogConfig = {
|
|
|
47
47
|
// getAuthor: (id) => lookupAuthor(id), // per-post authors
|
|
48
48
|
// onAnalyticsEvent: (name, params) => gtag("event", name, params),
|
|
49
49
|
revalidateSeconds: 3600,
|
|
50
|
+
// Admin URL overrides (omit to use defaults below):
|
|
51
|
+
// adminBasePath: "/admin/blog", // where admin UI pages are mounted
|
|
52
|
+
// adminApiBasePath: "/api/admin/blog", // where admin API routes are mounted
|
|
53
|
+
// searchApiPath: "/api/blog/search", // where search API route is mounted
|
|
50
54
|
};
|
|
51
55
|
```
|
|
52
56
|
|
|
@@ -133,13 +137,52 @@ export default async function sitemap() {
|
|
|
133
137
|
}
|
|
134
138
|
```
|
|
135
139
|
|
|
136
|
-
## 6. Admin
|
|
140
|
+
## 6. Admin pages
|
|
141
|
+
|
|
142
|
+
### Option A — Sealed admin pages (recommended)
|
|
143
|
+
|
|
144
|
+
Drop in the three sealed admin page factories. They render a full admin UI
|
|
145
|
+
(post list with status/publish-date badges + edit links, create form, edit form)
|
|
146
|
+
and respect your `authorize(req)` gate via the API routes.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// app/admin/blog/page.tsx — post list
|
|
150
|
+
import { createBlogAdminListPage } from "hazo_blog/next";
|
|
151
|
+
import { blogConfig } from "@/lib/blog-config";
|
|
152
|
+
const page = createBlogAdminListPage(blogConfig);
|
|
153
|
+
export default page.default;
|
|
154
|
+
export const dynamic = "force-dynamic"; // must be a static literal
|
|
155
|
+
|
|
156
|
+
// app/admin/blog/new/page.tsx — create form
|
|
157
|
+
import { createBlogAdminNewPage } from "hazo_blog/next";
|
|
158
|
+
const page = createBlogAdminNewPage(blogConfig);
|
|
159
|
+
export default page.default;
|
|
160
|
+
export const dynamic = "force-dynamic";
|
|
161
|
+
|
|
162
|
+
// app/admin/blog/[slug]/edit/page.tsx — edit form
|
|
163
|
+
import { createBlogAdminEditPage } from "hazo_blog/next";
|
|
164
|
+
const page = createBlogAdminEditPage(blogConfig);
|
|
165
|
+
export default page.default;
|
|
166
|
+
export const dynamic = "force-dynamic";
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If your admin API or UI routes live at non-default paths, override in `BlogConfig`:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
adminBasePath: "/admin/blog", // default — where factory pages link to
|
|
173
|
+
adminApiBasePath: "/api/admin/blog", // default — where PostForm POSTs to
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Option B — Custom admin form
|
|
177
|
+
|
|
178
|
+
Use `PostForm` directly for a fully custom admin UI:
|
|
137
179
|
|
|
138
180
|
```tsx
|
|
139
181
|
"use client";
|
|
140
182
|
import { PostForm } from "hazo_blog/client";
|
|
141
183
|
// pass categories + an endpoint; wire onImageUpload to hazo_files for paste-upload
|
|
142
184
|
<PostForm categories={categories} endpoint="/api/admin/blog" onSaved={...} />
|
|
185
|
+
// For edit: <PostForm post={existingPost} categories={categories} ... />
|
|
143
186
|
```
|
|
144
187
|
|
|
145
188
|
## Required Next.js config
|
|
@@ -161,16 +204,38 @@ const nextConfig = {
|
|
|
161
204
|
the package's classes are compiled. The admin editor's styles load automatically
|
|
162
205
|
via `hazo_ui`'s `MarkdownEditor`.
|
|
163
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
|
+
|
|
164
215
|
## Exports
|
|
165
216
|
|
|
166
217
|
| Entry | Contents |
|
|
167
218
|
|---|---|
|
|
168
219
|
| `hazo_blog` | server: `createBlogService`, `createBlogRepository`, SEO builders, **route-handler factories** (React-free), types |
|
|
169
220
|
| `hazo_blog/client` | client components: `PostCard`, `PostHero`, `AuthorBio`, `FaqSection`, `TableOfContents`, `RelatedPosts`, `BlogSearch`, `PostForm`, MDX components, `trackBlogEvent` |
|
|
170
|
-
| `hazo_blog/next` | sealed page factories (`createBlogIndexPage
|
|
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` |
|
|
171
223
|
| `hazo_blog/seo` | `buildBlogPostingJsonLd`, `buildBreadcrumbJsonLd`, `buildFaqJsonLd`, `getBlogSitemapEntries`, `getBlogRobotsRules`, `getBlogRssXml` |
|
|
172
224
|
| `hazo_blog/config` | `BlogConfig` types + `resolveConfig` |
|
|
173
225
|
|
|
226
|
+
### Search
|
|
227
|
+
|
|
228
|
+
`BlogSearch` (rendered inside `createBlogIndexPage`) supports:
|
|
229
|
+
- **Dropdown**: appears after 2+ characters with a 250 ms debounce
|
|
230
|
+
- **Enter key**: navigates to `${basePath}?q={term}` — the index page renders a filtered grid with a result count and Clear link
|
|
231
|
+
- **Escape**: dismisses the dropdown
|
|
232
|
+
|
|
233
|
+
If your search API lives at a non-default path, set `searchApiPath` in `BlogConfig`:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
searchApiPath: "/api/blog/search", // default
|
|
237
|
+
```
|
|
238
|
+
|
|
174
239
|
See `SETUP_CHECKLIST.md` for a step-by-step integration list. A runnable demo
|
|
175
240
|
lives in `test-app/`.
|
|
176
241
|
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -25,6 +25,7 @@ for full code samples.
|
|
|
25
25
|
- [ ] (multi-tenant only) set `resolveScope(req)`
|
|
26
26
|
- [ ] (per-post authors) set `getAuthor(id)`
|
|
27
27
|
- [ ] (analytics) set `onAnalyticsEvent` or rely on the `window.gtag` fallback
|
|
28
|
+
- [ ] (non-default paths) set `adminBasePath`, `adminApiBasePath`, `searchApiPath` if your routes differ from defaults (`/admin/blog`, `/api/admin/blog`, `/api/blog/search`)
|
|
28
29
|
|
|
29
30
|
## 4. Routes
|
|
30
31
|
|
|
@@ -37,6 +38,13 @@ for full code samples.
|
|
|
37
38
|
- [ ] (optional) `app/api/blog/manage/route.ts` → `createBlogManageRoutes`
|
|
38
39
|
- [ ] Merge `getBlogSitemapEntries()` into root `app/sitemap.ts` and `getBlogRobotsRules()` into `app/robots.ts`
|
|
39
40
|
|
|
41
|
+
### Admin UI pages (sealed, recommended)
|
|
42
|
+
|
|
43
|
+
- [ ] `app/admin/blog/page.tsx` → `createBlogAdminListPage` + `export const dynamic = "force-dynamic"`
|
|
44
|
+
- [ ] `app/admin/blog/new/page.tsx` → `createBlogAdminNewPage` + `export const dynamic = "force-dynamic"`
|
|
45
|
+
- [ ] `app/admin/blog/[slug]/edit/page.tsx` → `createBlogAdminEditPage` + `export const dynamic = "force-dynamic"`
|
|
46
|
+
- [ ] **Note**: `dynamic` must be a static string literal — cannot re-export from the factory object
|
|
47
|
+
|
|
40
48
|
## 5. Next.js config (required)
|
|
41
49
|
|
|
42
50
|
- [ ] `transpilePackages: ["hazo_blog", "hazo_ui", "next-mdx-remote"]` ← transpiling `next-mdx-remote` prevents the "React Element from an older version" MDX prerender error
|
|
@@ -50,5 +58,7 @@ for full code samples.
|
|
|
50
58
|
- [ ] View-source a post: BlogPosting + BreadcrumbList JSON-LD present (FAQPage when the post has FAQ)
|
|
51
59
|
- [ ] Drafts/future-scheduled posts return 404 and are absent from sitemap/RSS
|
|
52
60
|
- [ ] `/api/blog/feed` returns valid RSS; `/api/blog/search?q=...` returns matches
|
|
53
|
-
- [ ] Admin
|
|
61
|
+
- [ ] Admin list page shows status badges (green=published, grey=draft) and publish dates
|
|
62
|
+
- [ ] Admin edit page pre-populates all fields; Save draft / Publish both work
|
|
63
|
+
- [ ] Search: typing 2+ chars shows dropdown; Enter navigates to `?q=` filtered index
|
|
54
64
|
- [ ] Paste a post's HTML into Google's Rich Results Test — no errors
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-search.d.ts","sourceRoot":"","sources":["../../src/components/blog-search.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blog-search.d.ts","sourceRoot":"","sources":["../../src/components/blog-search.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,QAAkB,EAClB,WAA6B,EAC7B,SAAS,GACV,EAAE,eAAe,2CAqFjB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { cn } from "hazo_ui/utils";
|
|
6
7
|
import { trackBlogEvent } from "../lib/analytics.js";
|
|
@@ -10,6 +11,7 @@ export function BlogSearch({ endpoint, basePath = "/blog", placeholder = "Search
|
|
|
10
11
|
const [results, setResults] = useState([]);
|
|
11
12
|
const [open, setOpen] = useState(false);
|
|
12
13
|
const timer = useRef(null);
|
|
14
|
+
const router = useRouter();
|
|
13
15
|
useEffect(() => {
|
|
14
16
|
if (timer.current)
|
|
15
17
|
clearTimeout(timer.current);
|
|
@@ -39,5 +41,23 @@ export function BlogSearch({ endpoint, basePath = "/blog", placeholder = "Search
|
|
|
39
41
|
clearTimeout(timer.current);
|
|
40
42
|
};
|
|
41
43
|
}, [query, url]);
|
|
42
|
-
|
|
44
|
+
function handleKeyDown(e) {
|
|
45
|
+
if (e.key === "Enter") {
|
|
46
|
+
const q = query.trim();
|
|
47
|
+
if (q.length >= 2) {
|
|
48
|
+
if (timer.current)
|
|
49
|
+
clearTimeout(timer.current);
|
|
50
|
+
setOpen(false);
|
|
51
|
+
setResults([]);
|
|
52
|
+
router.push(`${basePath}?q=${encodeURIComponent(q)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (e.key === "Escape") {
|
|
56
|
+
if (timer.current)
|
|
57
|
+
clearTimeout(timer.current);
|
|
58
|
+
setOpen(false);
|
|
59
|
+
setResults([]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return (_jsxs("div", { className: cn("relative", className), children: [_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, className: "w-full rounded-lg border border-border bg-background px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-primary" }), open && results.length > 0 && (_jsx("ul", { className: "absolute z-50 mt-1 max-h-80 w-full overflow-auto rounded-lg border border-zinc-200 shadow-xl", style: { backgroundColor: "white" }, children: results.map((r) => (_jsx("li", { className: "border-b border-zinc-100 last:border-0", children: _jsxs(Link, { href: `${basePath}/${r.slug}`, className: "block px-4 py-3 text-sm hover:bg-zinc-50", onClick: () => setOpen(false), children: [_jsx("span", { className: "font-medium text-zinc-900", children: r.title }), r.excerpt && (_jsx("span", { className: "block truncate text-xs text-zinc-500", children: r.excerpt }))] }) }, r.slug))) }))] }));
|
|
43
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-card.d.ts","sourceRoot":"","sources":["../../src/components/post-card.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAkB,EAAE,SAAS,EAAE,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"post-card.d.ts","sourceRoot":"","sources":["../../src/components/post-card.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAkB,EAAE,SAAS,EAAE,EAAE,aAAa,2CAkD9E"}
|
|
@@ -6,5 +6,5 @@ export function PostCard({ post, basePath = "/blog", className }) {
|
|
|
6
6
|
return (_jsx("article", { className: cn("group overflow-hidden rounded-xl border border-border bg-card transition-shadow hover:shadow-md", className), children: _jsxs(Link, { href: `${basePath}/${post.slug}`, className: "block", children: [post.featured_image && (_jsx("div", { className: "relative aspect-video w-full overflow-hidden", children: _jsx(Image, { src: post.featured_image, alt: post.title, fill: true, className: "object-cover transition-transform duration-300 group-hover:scale-105", sizes: "(max-width: 768px) 100vw, 33vw" }) })), _jsxs("div", { className: "p-5", children: [post.category && (_jsx("span", { className: "mb-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium", style: {
|
|
7
7
|
backgroundColor: `${post.category.colour}20`,
|
|
8
8
|
color: post.category.colour,
|
|
9
|
-
}, children: post.category.name })), _jsx("h3", { className: "mb-1 line-clamp-2 text-lg font-semibold text-foreground group-hover:text-primary", children: post.title }), post.excerpt && (_jsx("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: post.excerpt })), _jsxs("p", { className: "mt-3 text-xs text-muted-foreground", children: [post.reading_time, " min read"] })] })] }) }));
|
|
9
|
+
}, children: post.category.name })), _jsx("h3", { className: "mb-1 line-clamp-2 text-lg font-semibold text-foreground group-hover:text-primary", children: post.title }), post.excerpt && (_jsx("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: post.excerpt })), _jsxs("p", { className: "mt-3 text-xs text-muted-foreground", children: [post.reading_time, " min read", post.publish_date && (_jsxs("span", { className: "ml-2 text-muted-foreground/60", children: ["\u00B7 ", new Date(post.publish_date).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })] }))] })] })] }) }));
|
|
10
10
|
}
|
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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlogCategory, BlogPostWithRelations } from "../types/index.js";
|
|
2
|
+
/** Client wrapper used by sealed admin page factories to handle post-save routing. */
|
|
3
|
+
export declare function AdminFormClient({ post, categories, endpoint, redirectTo, }: {
|
|
4
|
+
post?: BlogPostWithRelations | null;
|
|
5
|
+
categories: BlogCategory[];
|
|
6
|
+
endpoint: string;
|
|
7
|
+
redirectTo: string;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=admin-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-client.d.ts","sourceRoot":"","sources":["../../src/next/admin-client.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE7E,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,UAAU,GACX,EAAE;IACD,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,2CAUA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { PostForm } from "../components/admin/post-form.js";
|
|
5
|
+
/** Client wrapper used by sealed admin page factories to handle post-save routing. */
|
|
6
|
+
export function AdminFormClient({ post, categories, endpoint, redirectTo, }) {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
return (_jsx(PostForm, { post: post ?? undefined, categories: categories, endpoint: endpoint, onSaved: () => router.push(redirectTo) }));
|
|
9
|
+
}
|
|
@@ -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
|
-
export { createBlogIndexPage, createBlogPostPage, createBlogTagPage, } from "./pages.js";
|
|
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
|
-
|
|
9
|
+
// Error boundary: MdxErrorBoundary
|
|
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
|
+
}
|
package/dist/next/pages.d.ts
CHANGED
|
@@ -48,5 +48,23 @@ export declare function createBlogTagPage(rawConfig: BlogConfig): {
|
|
|
48
48
|
}) => Promise<Metadata>;
|
|
49
49
|
revalidate: number;
|
|
50
50
|
};
|
|
51
|
+
/** Sealed admin list page: mount at `${adminBasePath}` (default `/admin/blog`). */
|
|
52
|
+
export declare function createBlogAdminListPage(rawConfig: BlogConfig): {
|
|
53
|
+
default: () => Promise<import("react/jsx-runtime").JSX.Element>;
|
|
54
|
+
};
|
|
55
|
+
/** Sealed admin new-post page: mount at `${adminBasePath}/new`. */
|
|
56
|
+
export declare function createBlogAdminNewPage(rawConfig: BlogConfig): {
|
|
57
|
+
default: () => Promise<import("react/jsx-runtime").JSX.Element>;
|
|
58
|
+
};
|
|
59
|
+
/** Sealed admin edit page: mount at `${adminBasePath}/[slug]/edit`. */
|
|
60
|
+
export declare function createBlogAdminEditPage(rawConfig: BlogConfig): {
|
|
61
|
+
default: ({ params, }: {
|
|
62
|
+
params: Promise<{
|
|
63
|
+
slug: string;
|
|
64
|
+
}> | {
|
|
65
|
+
slug: string;
|
|
66
|
+
};
|
|
67
|
+
}) => Promise<import("react/jsx-runtime").JSX.Element>;
|
|
68
|
+
};
|
|
51
69
|
export {};
|
|
52
70
|
//# sourceMappingURL=pages.d.ts.map
|
package/dist/next/pages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/next/pages.tsx"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,UAAU,EAAyB,MAAM,mBAAmB,CAAC;AAG3E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAexC,qCAAqC;AACrC,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,UAAU;2BAuCnD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KACtD;oCA9BE;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KACtD,KAAG,OAAO,CAAC,QAAQ,CAAC;;;;;;EA8DtB;AAED,+BAA+B;AAC/B,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU;iCAcpD;QACD,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACzE;4BAZkC,OAAO,CAAC,QAAQ,CAAC;;EAuFrD;AAED,uCAAuC;AACvC,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,UAAU;2BAMlD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KACpD;oCAuBE;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KACpD,KAAG,OAAO,CAAC,QAAQ,CAAC;;EAStB;AAOD,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,UAAU;;EAuE5D;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,UAAU;;EAoB3D;AAED,uEAAuE;AACvE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,UAAU;2BAMxD;QACD,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;KACtD;EAsBF"}
|
package/dist/next/pages.js
CHANGED
|
@@ -10,8 +10,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
// export const { default, generateMetadata, generateStaticParams,
|
|
11
11
|
// revalidate, dynamicParams } = createBlogPostPage(blogConfig);
|
|
12
12
|
import { notFound } from "next/navigation";
|
|
13
|
-
import
|
|
14
|
-
import { resolveConfig } from "../service/index.js";
|
|
13
|
+
import Link from "next/link";
|
|
14
|
+
import { createBlogService, resolveConfig } from "../service/index.js";
|
|
15
|
+
import { AdminFormClient } from "./admin-client.js";
|
|
15
16
|
import { buildBlogPostingJsonLd, buildBreadcrumbJsonLd, buildFaqJsonLd, canonicalFor, ogImageFor, } from "../seo/index.js";
|
|
16
17
|
import { extractToc } from "../lib/text.js";
|
|
17
18
|
import { PostHero } from "../components/post-hero.js";
|
|
@@ -101,9 +102,19 @@ export function createBlogIndexPage(rawConfig) {
|
|
|
101
102
|
? await searchParams
|
|
102
103
|
: searchParams
|
|
103
104
|
: {};
|
|
105
|
+
const q = (sp.q ?? "").trim();
|
|
104
106
|
const page = Number(sp.page ?? "1") || 1;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
let displayPosts;
|
|
108
|
+
let totalPages = 1;
|
|
109
|
+
if (q) {
|
|
110
|
+
displayPosts = await service.search(q, 50);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const paged = await service.listPosts({ page, perPage: 12 });
|
|
114
|
+
displayPosts = paged.posts;
|
|
115
|
+
totalPages = paged.totalPages;
|
|
116
|
+
}
|
|
117
|
+
return (_jsxs("div", { className: "mx-auto max-w-6xl px-4 py-10", children: [_jsxs("div", { className: "mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold text-foreground", children: "Blog" }), q && (_jsxs("p", { className: "mt-1 text-sm text-muted-foreground", children: [displayPosts.length, " result", displayPosts.length !== 1 ? "s" : "", " for \u201C", q, "\u201D \u00A0\u00B7\u00A0", _jsx("a", { href: config.basePath, className: "text-primary hover:underline", children: "Clear" })] }))] }), _jsx(BlogSearch, { basePath: config.basePath, endpoint: config.searchApiPath ?? `${config.basePath}/api/search`, className: "sm:w-72" })] }), displayPosts.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: q ? `No posts matching "${q}".` : "No posts yet." })) : (_jsx("div", { className: "grid gap-6 sm:grid-cols-2 lg:grid-cols-3", children: displayPosts.map((p) => (_jsx(PostCard, { post: p, basePath: config.basePath }, p.id))) })), !q && totalPages > 1 && (_jsx("nav", { className: "mt-10 flex justify-center gap-2 text-sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (_jsx("a", { href: `${config.basePath}?page=${n}`, className: n === page
|
|
107
118
|
? "rounded bg-primary px-3 py-1 text-primary-foreground"
|
|
108
119
|
: "rounded border border-border px-3 py-1 hover:bg-accent", children: n }, n))) }))] }));
|
|
109
120
|
}
|
|
@@ -131,3 +142,50 @@ export function createBlogTagPage(rawConfig) {
|
|
|
131
142
|
}
|
|
132
143
|
return { default: Page, generateMetadata, revalidate: config.revalidateSeconds };
|
|
133
144
|
}
|
|
145
|
+
function fmtDate(iso) {
|
|
146
|
+
if (!iso)
|
|
147
|
+
return "—";
|
|
148
|
+
return new Date(iso).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
149
|
+
}
|
|
150
|
+
/** Sealed admin list page: mount at `${adminBasePath}` (default `/admin/blog`). */
|
|
151
|
+
export function createBlogAdminListPage(rawConfig) {
|
|
152
|
+
const config = resolveConfig(rawConfig);
|
|
153
|
+
const service = createBlogService(config);
|
|
154
|
+
async function Page() {
|
|
155
|
+
const { posts } = await service.listPosts({ perPage: 200, includeUnpublished: true });
|
|
156
|
+
return (_jsxs("div", { className: "mx-auto max-w-4xl px-4 py-10", children: [_jsxs("div", { className: "mb-8 flex items-center justify-between", children: [_jsx("h1", { className: "text-2xl font-bold text-foreground", children: "Blog posts" }), _jsx(Link, { href: `${config.adminBasePath}/new`, className: "rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90", children: "+ New post" })] }), posts.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: "No posts yet." })) : (_jsxs("table", { className: "w-full border-collapse text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [_jsx("th", { className: "pb-2 pr-6 font-medium", children: "Title" }), _jsx("th", { className: "pb-2 pr-6 font-medium", children: "Status" }), _jsx("th", { className: "pb-2 pr-6 font-medium", children: "Published" }), _jsx("th", { className: "pb-2 pr-6 font-medium", children: "Slug" }), _jsx("th", { className: "pb-2 font-medium" })] }) }), _jsx("tbody", { children: posts.map((p) => (_jsxs("tr", { className: "border-b border-border/40", children: [_jsx("td", { className: "py-3 pr-6 font-medium", children: _jsx(Link, { href: `${config.basePath}/${p.slug}`, className: "hover:text-primary", children: p.title }) }), _jsx("td", { className: "py-3 pr-6", children: _jsx("span", { className: p.status === "published"
|
|
157
|
+
? "rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700"
|
|
158
|
+
: p.status === "scheduled"
|
|
159
|
+
? "rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700"
|
|
160
|
+
: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground", children: p.status }) }), _jsx("td", { className: "py-3 pr-6 text-muted-foreground", children: fmtDate(p.publish_date) }), _jsx("td", { className: "py-3 pr-6 text-muted-foreground", children: p.slug }), _jsx("td", { className: "py-3", children: _jsx(Link, { href: `${config.adminBasePath}/${p.slug}/edit`, className: "text-xs text-primary hover:underline", children: "Edit" }) })] }, p.id))) })] }))] }));
|
|
161
|
+
}
|
|
162
|
+
return { default: Page };
|
|
163
|
+
}
|
|
164
|
+
/** Sealed admin new-post page: mount at `${adminBasePath}/new`. */
|
|
165
|
+
export function createBlogAdminNewPage(rawConfig) {
|
|
166
|
+
const config = resolveConfig(rawConfig);
|
|
167
|
+
const service = createBlogService(config);
|
|
168
|
+
async function Page() {
|
|
169
|
+
const repo = await service.repository();
|
|
170
|
+
const categories = await repo.listCategories();
|
|
171
|
+
return (_jsxs("div", { className: "mx-auto max-w-4xl px-4 py-10", children: [_jsx("h1", { className: "mb-6 text-2xl font-bold text-foreground", children: "New post" }), _jsx(AdminFormClient, { categories: categories, endpoint: config.adminApiBasePath, redirectTo: config.adminBasePath })] }));
|
|
172
|
+
}
|
|
173
|
+
return { default: Page };
|
|
174
|
+
}
|
|
175
|
+
/** Sealed admin edit page: mount at `${adminBasePath}/[slug]/edit`. */
|
|
176
|
+
export function createBlogAdminEditPage(rawConfig) {
|
|
177
|
+
const config = resolveConfig(rawConfig);
|
|
178
|
+
const service = createBlogService(config);
|
|
179
|
+
async function Page({ params, }) {
|
|
180
|
+
const { slug } = params instanceof Promise ? await params : params;
|
|
181
|
+
const [post, repo] = await Promise.all([
|
|
182
|
+
service.getPost(slug, { includeUnpublished: true }),
|
|
183
|
+
service.repository(),
|
|
184
|
+
]);
|
|
185
|
+
if (!post)
|
|
186
|
+
notFound();
|
|
187
|
+
const categories = await repo.listCategories();
|
|
188
|
+
return (_jsxs("div", { className: "mx-auto max-w-4xl px-4 py-10", children: [_jsxs("h1", { className: "mb-6 text-2xl font-bold text-foreground", children: ["Edit: ", post.title] }), _jsx(AdminFormClient, { post: post, categories: categories, endpoint: config.adminApiBasePath, redirectTo: config.adminBasePath })] }));
|
|
189
|
+
}
|
|
190
|
+
return { default: Page };
|
|
191
|
+
}
|
package/dist/seo/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export function buildBlogPostingJsonLd(config, post, author) {
|
|
|
41
41
|
return {
|
|
42
42
|
"@context": "https://schema.org",
|
|
43
43
|
"@type": "BlogPosting",
|
|
44
|
-
headline: post.
|
|
44
|
+
headline: post.title,
|
|
45
45
|
description: post.meta_description || buildExcerpt(post.content),
|
|
46
46
|
image: ogImageFor(config, post),
|
|
47
47
|
datePublished: post.publish_date ?? post.created_at,
|
|
@@ -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,
|
|
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,13 +2,15 @@
|
|
|
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 {
|
|
9
9
|
...config,
|
|
10
10
|
basePath: config.basePath?.replace(/\/$/, "") || "/blog",
|
|
11
11
|
revalidateSeconds: config.revalidateSeconds ?? 3600,
|
|
12
|
+
adminBasePath: config.adminBasePath?.replace(/\/$/, "") || "/admin/blog",
|
|
13
|
+
adminApiBasePath: config.adminApiBasePath?.replace(/\/$/, "") || "/api/admin/blog",
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
export function createBlogService(rawConfig) {
|
|
@@ -44,6 +46,7 @@ export function createBlogService(rawConfig) {
|
|
|
44
46
|
},
|
|
45
47
|
async upsertBySlug(input, req) {
|
|
46
48
|
const repo = await repository(req);
|
|
49
|
+
const content = sanitizeMdx(input.content);
|
|
47
50
|
const slug = input.slug?.trim() ? slugify(input.slug) : slugify(input.title);
|
|
48
51
|
const status = input.status ?? "draft";
|
|
49
52
|
// Resolve category from name when an id isn't supplied.
|
|
@@ -66,13 +69,13 @@ export function createBlogService(rawConfig) {
|
|
|
66
69
|
const row = {
|
|
67
70
|
slug,
|
|
68
71
|
title: input.title,
|
|
69
|
-
content
|
|
70
|
-
excerpt: buildExcerpt(
|
|
71
|
-
reading_time: calculateReadingTime(
|
|
72
|
+
content,
|
|
73
|
+
excerpt: buildExcerpt(content),
|
|
74
|
+
reading_time: calculateReadingTime(content),
|
|
72
75
|
category_id: categoryId,
|
|
73
76
|
author_id: input.author_id ?? null,
|
|
74
77
|
meta_title: input.meta_title ?? "",
|
|
75
|
-
meta_description: input.meta_description ?? buildExcerpt(
|
|
78
|
+
meta_description: input.meta_description ?? buildExcerpt(content),
|
|
76
79
|
canonical_url: input.canonical_url ?? null,
|
|
77
80
|
og_image: input.og_image ?? null,
|
|
78
81
|
featured_image: input.featured_image ?? null,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -132,10 +132,18 @@ export interface BlogConfig {
|
|
|
132
132
|
onAnalyticsEvent?: (name: BlogAnalyticsEvent, params: Record<string, unknown>) => void;
|
|
133
133
|
/** ISR revalidate window in seconds. Default 3600. */
|
|
134
134
|
revalidateSeconds?: number;
|
|
135
|
+
/** Override the search API endpoint (default: `${basePath}/api/search`). */
|
|
136
|
+
searchApiPath?: string;
|
|
137
|
+
/** Where the admin UI pages are mounted (default `/admin/blog`). */
|
|
138
|
+
adminBasePath?: string;
|
|
139
|
+
/** Where the admin API routes are mounted (default `/api/admin/blog`). */
|
|
140
|
+
adminApiBasePath?: string;
|
|
135
141
|
}
|
|
136
142
|
/** Resolved config with defaults applied. */
|
|
137
143
|
export interface ResolvedBlogConfig extends BlogConfig {
|
|
138
144
|
basePath: string;
|
|
139
145
|
revalidateSeconds: number;
|
|
146
|
+
adminBasePath: string;
|
|
147
|
+
adminApiBasePath: string;
|
|
140
148
|
}
|
|
141
149
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,WAAW,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,iDAAiD;AACjD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,oEAAoE;AACpE,MAAM,WAAW,qBAAsB,SAAQ,QAAQ;IACrD,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,GAAG,EAAE,OAAO,EAAE,CAAC;CAChB;AAED,iEAAiE;AACjE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,iCAAiC;AACjC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iDAAiD;AACjD,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,oBAAoB,GACpB,aAAa,GACb,YAAY,CAAC;AAEjB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,MAAM,EAAE,UAAU,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7D,0EAA0E;IAC1E,cAAc,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACjD,qEAAqE;IACrE,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,iEAAiE;IACjE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACvD,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvF,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
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",
|
|
@@ -39,25 +39,33 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -p tsconfig.build.json",
|
|
41
41
|
"type-check": "tsc --noEmit",
|
|
42
|
-
"test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs"
|
|
42
|
+
"test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs",
|
|
43
|
+
"dev:test-app": "npm run build && cd test-app && npm run dev",
|
|
44
|
+
"build:test-app": "npm run build && cd test-app && npm run build"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
45
|
-
"hazo_core": "^1.0
|
|
46
|
-
"hazo_connect": "^3.
|
|
47
|
-
"hazo_api": "^2.
|
|
48
|
-
"hazo_files": "^3.
|
|
49
|
-
"hazo_ui": "^
|
|
50
|
-
"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",
|
|
51
53
|
"hazo_jobs": "^0.12.0",
|
|
52
|
-
"hazo_auth": "^
|
|
54
|
+
"hazo_auth": "^10.0.0",
|
|
53
55
|
"react": "^18.0.0 || ^19.0.0",
|
|
54
56
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
55
57
|
"next": "^14.0.0 || ^16.0.0"
|
|
56
58
|
},
|
|
57
59
|
"peerDependenciesMeta": {
|
|
58
|
-
"hazo_images": {
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
"hazo_images": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"hazo_jobs": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"hazo_auth": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
61
69
|
},
|
|
62
70
|
"dependencies": {
|
|
63
71
|
"next-mdx-remote": "^6.0.0",
|
|
@@ -75,11 +83,11 @@
|
|
|
75
83
|
"react": "^19.0.0",
|
|
76
84
|
"react-dom": "^19.0.0",
|
|
77
85
|
"next": "^16.0.10",
|
|
78
|
-
"hazo_core": "^1.
|
|
79
|
-
"hazo_connect": "^3.
|
|
80
|
-
"hazo_api": "^2.
|
|
81
|
-
"hazo_files": "^3.
|
|
82
|
-
"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",
|
|
83
91
|
"tailwindcss": "^4.2.4",
|
|
84
92
|
"@tailwindcss/postcss": "^4.2.4",
|
|
85
93
|
"postcss": "^8.4.49"
|