hazo_blog 0.1.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 +45 -0
- package/README.md +179 -0
- package/SETUP_CHECKLIST.md +54 -0
- package/db_setup_postgres.sql +71 -0
- package/db_setup_sqlite.sql +77 -0
- package/dist/components/admin/post-form.d.ts +18 -0
- package/dist/components/admin/post-form.d.ts.map +1 -0
- package/dist/components/admin/post-form.js +78 -0
- package/dist/components/author-bio.d.ts +7 -0
- package/dist/components/author-bio.d.ts.map +1 -0
- package/dist/components/author-bio.js +6 -0
- package/dist/components/blog-post-view-tracker.d.ts +10 -0
- package/dist/components/blog-post-view-tracker.d.ts.map +1 -0
- package/dist/components/blog-post-view-tracker.js +27 -0
- package/dist/components/blog-search.d.ts +9 -0
- package/dist/components/blog-search.d.ts.map +1 -0
- package/dist/components/blog-search.js +43 -0
- package/dist/components/faq-section.d.ts +9 -0
- package/dist/components/faq-section.d.ts.map +1 -0
- package/dist/components/faq-section.js +8 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +13 -0
- package/dist/components/mdx/blog-image.d.ts +10 -0
- package/dist/components/mdx/blog-image.d.ts.map +1 -0
- package/dist/components/mdx/blog-image.js +7 -0
- package/dist/components/mdx/callout.d.ts +10 -0
- package/dist/components/mdx/callout.d.ts.map +1 -0
- package/dist/components/mdx/callout.js +17 -0
- package/dist/components/mdx/code-block.d.ts +8 -0
- package/dist/components/mdx/code-block.d.ts.map +1 -0
- package/dist/components/mdx/code-block.js +8 -0
- package/dist/components/mdx/index.d.ts +13 -0
- package/dist/components/mdx/index.d.ts.map +1 -0
- package/dist/components/mdx/index.js +13 -0
- package/dist/components/mdx/youtube.d.ts +8 -0
- package/dist/components/mdx/youtube.d.ts.map +1 -0
- package/dist/components/mdx/youtube.js +7 -0
- package/dist/components/post-card.d.ts +8 -0
- package/dist/components/post-card.d.ts.map +1 -0
- package/dist/components/post-card.js +10 -0
- package/dist/components/post-hero.d.ts +7 -0
- package/dist/components/post-hero.d.ts.map +1 -0
- package/dist/components/post-hero.js +17 -0
- package/dist/components/related-posts.d.ts +9 -0
- package/dist/components/related-posts.d.ts.map +1 -0
- package/dist/components/related-posts.js +8 -0
- package/dist/components/table-of-contents.d.ts +8 -0
- package/dist/components/table-of-contents.d.ts.map +1 -0
- package/dist/components/table-of-contents.js +7 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +1 -0
- package/dist/index.client.d.ts +5 -0
- package/dist/index.client.d.ts.map +1 -0
- package/dist/index.client.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/analytics.d.ts +4 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/analytics.js +14 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/text.d.ts +25 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/lib/text.js +69 -0
- package/dist/next/blog-content.d.ts +7 -0
- package/dist/next/blog-content.d.ts.map +1 -0
- package/dist/next/blog-content.js +10 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +15 -0
- package/dist/next/pages.d.ts +52 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +133 -0
- package/dist/next/routes.d.ts +42 -0
- package/dist/next/routes.d.ts.map +1 -0
- package/dist/next/routes.js +172 -0
- package/dist/repository/index.d.ts +34 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +285 -0
- package/dist/seo/index.d.ts +44 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +175 -0
- package/dist/service/index.d.ts +21 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +110 -0
- package/dist/types/index.d.ts +141 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/package.json +100 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { cn } from "hazo_ui/utils";
|
|
6
|
+
import { trackBlogEvent } from "../lib/analytics.js";
|
|
7
|
+
export function BlogSearch({ endpoint, basePath = "/blog", placeholder = "Search posts…", className, }) {
|
|
8
|
+
const url = endpoint ?? `${basePath}/api/search`;
|
|
9
|
+
const [query, setQuery] = useState("");
|
|
10
|
+
const [results, setResults] = useState([]);
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const timer = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (timer.current)
|
|
15
|
+
clearTimeout(timer.current);
|
|
16
|
+
const q = query.trim();
|
|
17
|
+
if (q.length < 2) {
|
|
18
|
+
setResults([]);
|
|
19
|
+
setOpen(false);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
timer.current = setTimeout(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${url}?q=${encodeURIComponent(q)}`);
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
return;
|
|
27
|
+
const data = (await res.json());
|
|
28
|
+
const posts = ("posts" in data && data.posts) || ("data" in data && data.data) || [];
|
|
29
|
+
setResults(posts);
|
|
30
|
+
setOpen(true);
|
|
31
|
+
trackBlogEvent("blog_search", { query: q, results: posts.length });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* ignore network errors */
|
|
35
|
+
}
|
|
36
|
+
}, 250);
|
|
37
|
+
return () => {
|
|
38
|
+
if (timer.current)
|
|
39
|
+
clearTimeout(timer.current);
|
|
40
|
+
};
|
|
41
|
+
}, [query, url]);
|
|
42
|
+
return (_jsxs("div", { className: cn("relative", className), children: [_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), 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-10 mt-1 max-h-80 w-full overflow-auto rounded-lg border border-border bg-popover shadow-lg", children: results.map((r) => (_jsx("li", { children: _jsxs(Link, { href: `${basePath}/${r.slug}`, className: "block px-4 py-2 text-sm hover:bg-accent", onClick: () => setOpen(false), children: [_jsx("span", { className: "font-medium", children: r.title }), r.excerpt && (_jsx("span", { className: "block truncate text-xs text-muted-foreground", children: r.excerpt }))] }) }, r.slug))) }))] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FaqItem } from "../types/index.js";
|
|
2
|
+
export interface FaqSectionProps {
|
|
3
|
+
faq: FaqItem[];
|
|
4
|
+
title?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Renders FAQ as a no-JS <details> accordion (the FAQPage JSON-LD is emitted separately). */
|
|
8
|
+
export declare function FaqSection({ faq, title, className }: FaqSectionProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=faq-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"faq-section.d.ts","sourceRoot":"","sources":["../../src/components/faq-section.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAoC,EAAE,SAAS,EAAE,EAAE,eAAe,kDAiBnG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "hazo_ui/utils";
|
|
3
|
+
/** Renders FAQ as a no-JS <details> accordion (the FAQPage JSON-LD is emitted separately). */
|
|
4
|
+
export function FaqSection({ faq, title = "Frequently asked questions", className }) {
|
|
5
|
+
if (!faq || faq.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs("section", { className: cn("my-8", className), children: [_jsx("h2", { className: "mb-4 text-2xl font-bold text-foreground", children: title }), _jsx("dl", { className: "divide-y divide-border rounded-xl border border-border", children: faq.map((item, i) => (_jsxs("details", { className: "group p-4", children: [_jsx("summary", { className: "cursor-pointer list-none font-medium text-foreground", children: _jsx("dt", { className: "inline", children: item.question }) }), _jsx("dd", { className: "mt-2 text-sm text-muted-foreground", children: item.answer })] }, i))) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { PostCard, type PostCardProps } from "./post-card.js";
|
|
2
|
+
export { PostHero, type PostHeroProps } from "./post-hero.js";
|
|
3
|
+
export { AuthorBio, type AuthorBioProps } from "./author-bio.js";
|
|
4
|
+
export { FaqSection, type FaqSectionProps } from "./faq-section.js";
|
|
5
|
+
export { TableOfContents, type TableOfContentsProps } from "./table-of-contents.js";
|
|
6
|
+
export { RelatedPosts, type RelatedPostsProps } from "./related-posts.js";
|
|
7
|
+
export { BlogSearch, type BlogSearchProps } from "./blog-search.js";
|
|
8
|
+
export { BlogPostViewTracker, type BlogPostViewTrackerProps, } from "./blog-post-view-tracker.js";
|
|
9
|
+
export { PostForm, type PostFormProps } from "./admin/post-form.js";
|
|
10
|
+
export { YouTube, Callout, BlogImage, CodeBlock, InlineCode, defaultMdxComponents, } from "./mdx/index.js";
|
|
11
|
+
export type { YouTubeProps, CalloutProps, CalloutType, BlogImageProps, CodeBlockProps, } from "./mdx/index.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGpE,OAAO,EACL,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,EACV,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,GACf,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// hazo_blog client component barrel. All client-safe (no Node/server imports).
|
|
2
|
+
export { PostCard } from "./post-card.js";
|
|
3
|
+
export { PostHero } from "./post-hero.js";
|
|
4
|
+
export { AuthorBio } from "./author-bio.js";
|
|
5
|
+
export { FaqSection } from "./faq-section.js";
|
|
6
|
+
export { TableOfContents } from "./table-of-contents.js";
|
|
7
|
+
export { RelatedPosts } from "./related-posts.js";
|
|
8
|
+
export { BlogSearch } from "./blog-search.js";
|
|
9
|
+
export { BlogPostViewTracker, } from "./blog-post-view-tracker.js";
|
|
10
|
+
// Admin authoring UI.
|
|
11
|
+
export { PostForm } from "./admin/post-form.js";
|
|
12
|
+
// MDX embed registry + components.
|
|
13
|
+
export { YouTube, Callout, BlogImage, CodeBlock, InlineCode, defaultMdxComponents, } from "./mdx/index.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface BlogImageProps {
|
|
2
|
+
src: string;
|
|
3
|
+
alt?: string;
|
|
4
|
+
caption?: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function BlogImage({ src, alt, caption, width, height, className, }: BlogImageProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=blog-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-image.d.ts","sourceRoot":"","sources":["../../../src/components/mdx/blog-image.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EACxB,GAAG,EACH,GAAQ,EACR,OAAO,EACP,KAAY,EACZ,MAAY,EACZ,SAAS,GACV,EAAE,cAAc,2CAkBhB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// <BlogImage src caption alt /> — next/image wrapper with optional caption.
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { cn } from "hazo_ui/utils";
|
|
5
|
+
export function BlogImage({ src, alt = "", caption, width = 1200, height = 675, className, }) {
|
|
6
|
+
return (_jsxs("figure", { className: cn("my-6", className), children: [_jsx(Image, { src: src, alt: alt || caption || "", width: width, height: height, className: "h-auto w-full rounded-lg", sizes: "(max-width: 768px) 100vw, 768px" }), caption && (_jsx("figcaption", { className: "mt-2 text-center text-sm text-muted-foreground", children: caption }))] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type CalloutType = "info" | "warning" | "tip" | "note";
|
|
3
|
+
export interface CalloutProps {
|
|
4
|
+
type?: CalloutType;
|
|
5
|
+
title?: string;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Callout({ type, title, children, className }: CalloutProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=callout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callout.d.ts","sourceRoot":"","sources":["../../../src/components/mdx/callout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgBD,wBAAgB,OAAO,CAAC,EAAE,IAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,YAAY,2CAclF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "hazo_ui/utils";
|
|
3
|
+
const STYLES = {
|
|
4
|
+
info: "border-blue-500/40 bg-blue-500/10 text-blue-900 dark:text-blue-100",
|
|
5
|
+
warning: "border-amber-500/40 bg-amber-500/10 text-amber-900 dark:text-amber-100",
|
|
6
|
+
tip: "border-emerald-500/40 bg-emerald-500/10 text-emerald-900 dark:text-emerald-100",
|
|
7
|
+
note: "border-border bg-muted/50 text-foreground",
|
|
8
|
+
};
|
|
9
|
+
const ICONS = {
|
|
10
|
+
info: "ℹ️",
|
|
11
|
+
warning: "⚠️",
|
|
12
|
+
tip: "💡",
|
|
13
|
+
note: "📝",
|
|
14
|
+
};
|
|
15
|
+
export function Callout({ type = "info", title, children, className }) {
|
|
16
|
+
return (_jsx("div", { className: cn("my-6 rounded-lg border-l-4 p-4", STYLES[type], className), children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { "aria-hidden": true, className: "text-lg leading-none", children: ICONS[type] }), _jsxs("div", { className: "min-w-0 flex-1", children: [title && _jsx("p", { className: "mb-1 font-semibold", children: title }), _jsx("div", { className: "prose-sm [&>p]:my-1", children: children })] })] }) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface CodeBlockProps {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function CodeBlock({ children, className }: CodeBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function InlineCode({ children, className }: CodeBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=code-block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-block.d.ts","sourceRoot":"","sources":["../../../src/components/mdx/code-block.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,2CAWhE;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,2CAMjE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "hazo_ui/utils";
|
|
3
|
+
export function CodeBlock({ children, className }) {
|
|
4
|
+
return (_jsx("pre", { className: cn("my-6 overflow-x-auto rounded-lg bg-zinc-900 p-4 text-sm text-zinc-100", className), children: children }));
|
|
5
|
+
}
|
|
6
|
+
export function InlineCode({ children, className }) {
|
|
7
|
+
return (_jsx("code", { className: cn("rounded bg-muted px-1.5 py-0.5 text-[0.9em]", className), children: children }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import { YouTube } from "./youtube.js";
|
|
3
|
+
import { Callout } from "./callout.js";
|
|
4
|
+
import { BlogImage } from "./blog-image.js";
|
|
5
|
+
import { CodeBlock, InlineCode } from "./code-block.js";
|
|
6
|
+
export { YouTube, Callout, BlogImage, CodeBlock, InlineCode };
|
|
7
|
+
export type { YouTubeProps } from "./youtube.js";
|
|
8
|
+
export type { CalloutProps, CalloutType } from "./callout.js";
|
|
9
|
+
export type { BlogImageProps } from "./blog-image.js";
|
|
10
|
+
export type { CodeBlockProps } from "./code-block.js";
|
|
11
|
+
/** The default embed components available inside MDX content. */
|
|
12
|
+
export declare const defaultMdxComponents: Record<string, ComponentType<unknown>>;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/mdx/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAC9D,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAMvE,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { YouTube } from "./youtube.js";
|
|
2
|
+
import { Callout } from "./callout.js";
|
|
3
|
+
import { BlogImage } from "./blog-image.js";
|
|
4
|
+
import { CodeBlock, InlineCode } from "./code-block.js";
|
|
5
|
+
export { YouTube, Callout, BlogImage, CodeBlock, InlineCode };
|
|
6
|
+
/** The default embed components available inside MDX content. */
|
|
7
|
+
export const defaultMdxComponents = {
|
|
8
|
+
YouTube: YouTube,
|
|
9
|
+
Callout: Callout,
|
|
10
|
+
BlogImage: BlogImage,
|
|
11
|
+
pre: CodeBlock,
|
|
12
|
+
code: InlineCode,
|
|
13
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface YouTubeProps {
|
|
2
|
+
id: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
start?: number;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function YouTube({ id, title, start, className }: YouTubeProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=youtube.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../../../src/components/mdx/youtube.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,EAAE,KAAuB,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,YAAY,2CActF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// <YouTube id="..." title="..." start={30} /> — privacy-friendly lazy embed.
|
|
3
|
+
import { cn } from "hazo_ui/utils";
|
|
4
|
+
export function YouTube({ id, title = "YouTube video", start, className }) {
|
|
5
|
+
const src = `https://www.youtube-nocookie.com/embed/${id}${start ? `?start=${start}` : ""}`;
|
|
6
|
+
return (_jsx("div", { className: cn("my-6 aspect-video w-full overflow-hidden rounded-lg", className), children: _jsx("iframe", { src: src, title: title, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture", allowFullScreen: true, className: "h-full w-full border-0" }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BlogPostWithRelations } from "../types/index.js";
|
|
2
|
+
export interface PostCardProps {
|
|
3
|
+
post: BlogPostWithRelations;
|
|
4
|
+
basePath?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function PostCard({ post, basePath, className }: PostCardProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=post-card.d.ts.map
|
|
@@ -0,0 +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,2CA2C9E"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { cn } from "hazo_ui/utils";
|
|
5
|
+
export function PostCard({ post, basePath = "/blog", className }) {
|
|
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
|
+
backgroundColor: `${post.category.colour}20`,
|
|
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"] })] })] }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BlogPostWithRelations } from "../types/index.js";
|
|
2
|
+
export interface PostHeroProps {
|
|
3
|
+
post: BlogPostWithRelations;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function PostHero({ post, className }: PostHeroProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=post-hero.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-hero.d.ts","sourceRoot":"","sources":["../../src/components/post-hero.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,aAAa,2CAmC1D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
import { cn } from "hazo_ui/utils";
|
|
4
|
+
function formatDate(value) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return "";
|
|
7
|
+
const d = new Date(value);
|
|
8
|
+
if (Number.isNaN(d.getTime()))
|
|
9
|
+
return "";
|
|
10
|
+
return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
11
|
+
}
|
|
12
|
+
export function PostHero({ post, className }) {
|
|
13
|
+
return (_jsxs("header", { className: cn("mb-8", className), children: [_jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-3 text-sm text-muted-foreground", children: [post.category && (_jsx("span", { className: "rounded-full px-2.5 py-0.5 text-xs font-medium", style: {
|
|
14
|
+
backgroundColor: `${post.category.colour}20`,
|
|
15
|
+
color: post.category.colour,
|
|
16
|
+
}, children: post.category.name })), post.publish_date && _jsx("time", { dateTime: post.publish_date, children: formatDate(post.publish_date) }), _jsxs("span", { children: ["\u00B7 ", post.reading_time, " min read"] })] }), _jsx("h1", { className: "text-3xl font-bold tracking-tight text-foreground sm:text-4xl", children: post.title }), post.featured_image && (_jsx("div", { className: "relative mt-6 aspect-video w-full overflow-hidden rounded-xl", children: _jsx(Image, { src: post.featured_image, alt: post.title, fill: true, priority: true, className: "object-cover", sizes: "(max-width: 768px) 100vw, 768px" }) }))] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlogPostWithRelations } from "../types/index.js";
|
|
2
|
+
export interface RelatedPostsProps {
|
|
3
|
+
posts: BlogPostWithRelations[];
|
|
4
|
+
basePath?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function RelatedPosts({ posts, basePath, title, className, }: RelatedPostsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=related-posts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related-posts.d.ts","sourceRoot":"","sources":["../../src/components/related-posts.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAkB,EAClB,KAAuB,EACvB,SAAS,GACV,EAAE,iBAAiB,kDAYnB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "hazo_ui/utils";
|
|
3
|
+
import { PostCard } from "./post-card.js";
|
|
4
|
+
export function RelatedPosts({ posts, basePath = "/blog", title = "Related posts", className, }) {
|
|
5
|
+
if (!posts || posts.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs("section", { className: cn("my-10", className), children: [_jsx("h2", { className: "mb-4 text-2xl font-bold text-foreground", children: title }), _jsx("div", { className: "grid gap-5 sm:grid-cols-2 lg:grid-cols-3", children: posts.map((p) => (_jsx(PostCard, { post: p, basePath: basePath }, p.id))) })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TocHeading } from "../lib/text.js";
|
|
2
|
+
export interface TableOfContentsProps {
|
|
3
|
+
headings: TocHeading[];
|
|
4
|
+
title?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function TableOfContents({ headings, title, className }: TableOfContentsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
//# sourceMappingURL=table-of-contents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-of-contents.d.ts","sourceRoot":"","sources":["../../src/components/table-of-contents.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,KAAsB,EAAE,SAAS,EAAE,EAAE,oBAAoB,kDAgBpG"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "hazo_ui/utils";
|
|
3
|
+
export function TableOfContents({ headings, title = "On this page", className }) {
|
|
4
|
+
if (!headings || headings.length === 0)
|
|
5
|
+
return null;
|
|
6
|
+
return (_jsxs("nav", { className: cn("rounded-xl border border-border bg-muted/30 p-4 text-sm", className), "aria-label": title, children: [_jsx("p", { className: "mb-2 font-semibold text-foreground", children: title }), _jsx("ul", { className: "space-y-1", children: headings.map((h) => (_jsx("li", { className: h.depth === 3 ? "pl-4" : "", children: _jsx("a", { href: `#${h.id}`, className: "text-muted-foreground hover:text-primary", children: h.text }) }, h.id))) })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,UAAU,EACV,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveConfig } from "../service/index.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { BlogPost, BlogPostWithRelations, BlogCategory, BlogTag, FaqItem, AuthorInfo, PostStatus, BlogAnalyticsEvent, BlogConfig, } from "./types/index.js";
|
|
2
|
+
export { slugify, mdxToPlainText, calculateReadingTime, buildExcerpt, extractToc, type TocHeading, } from "./lib/text.js";
|
|
3
|
+
export { trackBlogEvent } from "./lib/analytics.js";
|
|
4
|
+
export * from "./components/index.js";
|
|
5
|
+
//# sourceMappingURL=index.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAIA,YAAY,EACV,QAAQ,EACR,qBAAqB,EACrB,YAAY,EACZ,OAAO,EACP,OAAO,EACP,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,UAAU,GACX,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,OAAO,EACP,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,UAAU,EACV,KAAK,UAAU,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGpD,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// hazo_blog/src/index.client.ts — client-safe entry point.
|
|
2
|
+
// Never import Node.js builtins (fs/path/crypto) from here or anything it imports.
|
|
3
|
+
// Pure utilities.
|
|
4
|
+
export { slugify, mdxToPlainText, calculateReadingTime, buildExcerpt, extractToc, } from "./lib/text.js";
|
|
5
|
+
// Client-safe analytics emitter.
|
|
6
|
+
export { trackBlogEvent } from "./lib/analytics.js";
|
|
7
|
+
// Client/presentational components + MDX registry.
|
|
8
|
+
export * from "./components/index.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./types/index.js";
|
|
2
|
+
export * from "./lib/index.js";
|
|
3
|
+
export { createBlogRepository, type BlogRepository, type RepositoryOptions, type ListQuery, } from "./repository/index.js";
|
|
4
|
+
export { createBlogService, resolveConfig, type BlogService, } from "./service/index.js";
|
|
5
|
+
export * from "./seo/index.js";
|
|
6
|
+
export { createBlogManageRoutes, createBlogAdminRoutes, createBlogSearchRoute, createBlogFeedRoute, createBlogSitemapRoute, } from "./next/routes.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAC;AAGjC,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,SAAS,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,KAAK,WAAW,GACjB,MAAM,oBAAoB,CAAC;AAG5B,cAAc,gBAAgB,CAAC;AAI/B,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// hazo_blog/src/index.ts — server entry point.
|
|
2
|
+
// Import hazo_core / hazo_connect via peer deps. Never import this from a
|
|
3
|
+
// client component (it pulls in server-only repository code).
|
|
4
|
+
// Domain types + the BlogConfig contract.
|
|
5
|
+
export * from "./types/index.js";
|
|
6
|
+
// Pure utilities (slug, reading-time, excerpt, toc).
|
|
7
|
+
export * from "./lib/index.js";
|
|
8
|
+
// Storage + business logic.
|
|
9
|
+
export { createBlogRepository, } from "./repository/index.js";
|
|
10
|
+
export { createBlogService, resolveConfig, } from "./service/index.js";
|
|
11
|
+
// SEO emission helpers (also available via the `hazo_blog/seo` subpath).
|
|
12
|
+
export * from "./seo/index.js";
|
|
13
|
+
// Route-handler factories — React-free, so API routes can import them without
|
|
14
|
+
// pulling in the React page components from `hazo_blog/next`.
|
|
15
|
+
export { createBlogManageRoutes, createBlogAdminRoutes, createBlogSearchRoute, createBlogFeedRoute, createBlogSitemapRoute, } from "./next/routes.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BlogAnalyticsEvent } from "../types/index.js";
|
|
2
|
+
/** Emit a blog analytics event to whatever GA the host already wired. */
|
|
3
|
+
export declare function trackBlogEvent(name: BlogAnalyticsEvent, params?: Record<string, unknown>): void;
|
|
4
|
+
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/lib/analytics.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAS5D,yEAAyE;AACzE,wBAAgB,cAAc,CAC5B,IAAI,EAAE,kBAAkB,EACxB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,IAAI,CAQN"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Client-safe analytics emitter. Sends blog events to the host's existing GA4
|
|
2
|
+
// via window.gtag, falling back to window.dataLayer. No GA script is bundled.
|
|
3
|
+
/** Emit a blog analytics event to whatever GA the host already wired. */
|
|
4
|
+
export function trackBlogEvent(name, params = {}) {
|
|
5
|
+
if (typeof window === "undefined")
|
|
6
|
+
return;
|
|
7
|
+
const w = window;
|
|
8
|
+
if (typeof w.gtag === "function") {
|
|
9
|
+
w.gtag("event", name, params);
|
|
10
|
+
}
|
|
11
|
+
else if (Array.isArray(w.dataLayer)) {
|
|
12
|
+
w.dataLayer.push({ event: name, ...params });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AACA,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Convert a string into a URL-safe slug. */
|
|
2
|
+
export declare function slugify(input: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Strip MDX/Markdown down to readable plain text. Removes JSX tags, code
|
|
5
|
+
* fences, images, link syntax, headings markers, emphasis, and HTML comments.
|
|
6
|
+
* Good enough for reading-time, excerpts, and RSS descriptions.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mdxToPlainText(mdx: string): string;
|
|
9
|
+
/** Count words in a plain-text string. */
|
|
10
|
+
export declare function countWords(text: string): number;
|
|
11
|
+
/**
|
|
12
|
+
* Reading time in minutes from raw MDX. Mirrors the original gotimer formula:
|
|
13
|
+
* max(3, ceil(words / 220)).
|
|
14
|
+
*/
|
|
15
|
+
export declare function calculateReadingTime(mdx: string, wordsPerMinute?: number): number;
|
|
16
|
+
/** Build a plain-text excerpt of at most `maxChars`, breaking on a word boundary. */
|
|
17
|
+
export declare function buildExcerpt(mdx: string, maxChars?: number): string;
|
|
18
|
+
/** Extract `{depth, text, id}` headings (h2/h3) from MDX for a table of contents. */
|
|
19
|
+
export interface TocHeading {
|
|
20
|
+
depth: 2 | 3;
|
|
21
|
+
text: string;
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function extractToc(mdx: string): TocHeading[];
|
|
25
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/lib/text.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// hazo_blog — pure text utilities (slug, reading-time, MDX→plaintext, excerpt).
|
|
2
|
+
// No I/O, no Node builtins — safe to unit-test and import anywhere.
|
|
3
|
+
/** Convert a string into a URL-safe slug. */
|
|
4
|
+
export function slugify(input) {
|
|
5
|
+
return input
|
|
6
|
+
.normalize("NFKD")
|
|
7
|
+
.replace(/[̀-ͯ]/g, "") // strip diacritics
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-") // non-alphanumerics → hyphen
|
|
11
|
+
.replace(/^-+|-+$/g, "") // trim leading/trailing hyphens
|
|
12
|
+
.replace(/-{2,}/g, "-"); // collapse repeats
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Strip MDX/Markdown down to readable plain text. Removes JSX tags, code
|
|
16
|
+
* fences, images, link syntax, headings markers, emphasis, and HTML comments.
|
|
17
|
+
* Good enough for reading-time, excerpts, and RSS descriptions.
|
|
18
|
+
*/
|
|
19
|
+
export function mdxToPlainText(mdx) {
|
|
20
|
+
return mdx
|
|
21
|
+
.replace(/<!--[\s\S]*?-->/g, " ") // html comments
|
|
22
|
+
.replace(/```[\s\S]*?```/g, " ") // fenced code blocks
|
|
23
|
+
.replace(/`[^`]*`/g, " ") // inline code
|
|
24
|
+
.replace(/<[^>]+>/g, " ") // JSX / HTML tags
|
|
25
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ") // images
|
|
26
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links → text
|
|
27
|
+
.replace(/^#{1,6}\s+/gm, "") // heading markers
|
|
28
|
+
.replace(/[*_~>]/g, " ") // emphasis / blockquote markers
|
|
29
|
+
.replace(/\|/g, " ") // table pipes
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
/** Count words in a plain-text string. */
|
|
34
|
+
export function countWords(text) {
|
|
35
|
+
const trimmed = text.trim();
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
return 0;
|
|
38
|
+
return trimmed.split(/\s+/).length;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reading time in minutes from raw MDX. Mirrors the original gotimer formula:
|
|
42
|
+
* max(3, ceil(words / 220)).
|
|
43
|
+
*/
|
|
44
|
+
export function calculateReadingTime(mdx, wordsPerMinute = 220) {
|
|
45
|
+
const words = countWords(mdxToPlainText(mdx));
|
|
46
|
+
return Math.max(3, Math.ceil(words / wordsPerMinute));
|
|
47
|
+
}
|
|
48
|
+
/** Build a plain-text excerpt of at most `maxChars`, breaking on a word boundary. */
|
|
49
|
+
export function buildExcerpt(mdx, maxChars = 160) {
|
|
50
|
+
const text = mdxToPlainText(mdx);
|
|
51
|
+
if (text.length <= maxChars)
|
|
52
|
+
return text;
|
|
53
|
+
const slice = text.slice(0, maxChars);
|
|
54
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
55
|
+
return (lastSpace > 0 ? slice.slice(0, lastSpace) : slice).trimEnd() + "…";
|
|
56
|
+
}
|
|
57
|
+
export function extractToc(mdx) {
|
|
58
|
+
const headings = [];
|
|
59
|
+
// Ignore content inside fenced code blocks.
|
|
60
|
+
const withoutCode = mdx.replace(/```[\s\S]*?```/g, "");
|
|
61
|
+
const re = /^(#{2,3})\s+(.+?)\s*#*\s*$/gm;
|
|
62
|
+
let m;
|
|
63
|
+
while ((m = re.exec(withoutCode)) !== null) {
|
|
64
|
+
const depth = m[1].length;
|
|
65
|
+
const text = m[2].replace(/[*_`~]/g, "").trim();
|
|
66
|
+
headings.push({ depth, text, id: slugify(text) });
|
|
67
|
+
}
|
|
68
|
+
return headings;
|
|
69
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedBlogConfig } from "../types/index.js";
|
|
2
|
+
export interface BlogContentProps {
|
|
3
|
+
source: string;
|
|
4
|
+
config: ResolvedBlogConfig;
|
|
5
|
+
}
|
|
6
|
+
export declare function BlogContent({ source, config }: BlogContentProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=blog-content.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Server component that renders raw MDX to HTML at build/ISR time via
|
|
3
|
+
// next-mdx-remote/rsc, merging the default embed registry with host overrides.
|
|
4
|
+
import { MDXRemote } from "next-mdx-remote/rsc";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { defaultMdxComponents } from "../components/mdx/index.js";
|
|
7
|
+
export function BlogContent({ source, config }) {
|
|
8
|
+
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] } } }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createBlogIndexPage, createBlogPostPage, createBlogTagPage, } from "./pages.js";
|
|
2
|
+
export { BlogContent, type BlogContentProps } from "./blog-content.js";
|
|
3
|
+
export { resolveConfig } from "../service/index.js";
|
|
4
|
+
export type { BlogConfig, ResolvedBlogConfig } from "../types/index.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AASA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// hazo_blog/src/next — Next.js sealed adapters the host re-exports.
|
|
2
|
+
//
|
|
3
|
+
// Page factories (host passes BlogConfig in a tiny file):
|
|
4
|
+
// createBlogIndexPage, createBlogPostPage, createBlogTagPage
|
|
5
|
+
// Route-handler factories:
|
|
6
|
+
// createBlogManageRoutes, createBlogAdminRoutes, createBlogSearchRoute,
|
|
7
|
+
// createBlogFeedRoute, createBlogSitemapRoute
|
|
8
|
+
// Server MDX renderer: BlogContent
|
|
9
|
+
export { createBlogIndexPage, createBlogPostPage, createBlogTagPage, } from "./pages.js";
|
|
10
|
+
export { BlogContent } from "./blog-content.js";
|
|
11
|
+
// NOTE: route-handler factories (createBlogAdminRoutes, createBlogManageRoutes,
|
|
12
|
+
// createBlogSearchRoute, createBlogFeedRoute, createBlogSitemapRoute) are exported
|
|
13
|
+
// from the main `hazo_blog` entry — they are React-free, so API routes import them
|
|
14
|
+
// without pulling in the React page components above.
|
|
15
|
+
export { resolveConfig } from "../service/index.js";
|