@prudentbird/voxx 1.0.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/index.mjs +1346 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +65 -0
  6. package/templates/blog/hello-world.md.tpl +42 -0
  7. package/templates/blog/layout.tsx.tpl +44 -0
  8. package/templates/blog/page.tsx.tpl +27 -0
  9. package/templates/blog/post-list.tsx.tpl +45 -0
  10. package/templates/blog/post-page.tsx.tpl +41 -0
  11. package/templates/blog/slug-page.tsx.tpl +40 -0
  12. package/templates/changelog/layout.tsx.tpl +44 -0
  13. package/templates/changelog/page.tsx.tpl +27 -0
  14. package/templates/changelog/release-list.tsx.tpl +33 -0
  15. package/templates/changelog/release.md.tpl +15 -0
  16. package/templates/docs/doc-page.tsx.tpl +65 -0
  17. package/templates/docs/getting-started-index.md.tpl +9 -0
  18. package/templates/docs/index.md.tpl +16 -0
  19. package/templates/docs/installation.md.tpl +17 -0
  20. package/templates/docs/layout-root.tsx.tpl +52 -0
  21. package/templates/docs/layout.tsx.tpl +37 -0
  22. package/templates/docs/mobile-nav.tsx.tpl +90 -0
  23. package/templates/docs/page.tsx.tpl +50 -0
  24. package/templates/docs/sidebar-nav.tsx.tpl +39 -0
  25. package/templates/shared/content-version.ts.tpl +1 -0
  26. package/templates/shared/data.ts.tpl +34 -0
  27. package/templates/shared/instrumentation.ts.tpl +5 -0
  28. package/templates/shared/llms-full-route.ts.tpl +9 -0
  29. package/templates/shared/llms-route.ts.tpl +9 -0
  30. package/templates/shared/metadata.ts.tpl +39 -0
  31. package/templates/shared/on-this-page.tsx.tpl +213 -0
  32. package/templates/shared/robots.ts.tpl +13 -0
  33. package/templates/shared/rss-route.ts.tpl +9 -0
  34. package/templates/shared/sitemap.ts.tpl +23 -0
  35. package/templates/shared/theme-toggle.tsx.tpl +64 -0
  36. package/templates/shared/voxx.json.tpl +26 -0
@@ -0,0 +1,33 @@
1
+ import { formatDate } from "@prudentbird/voxx-core";
2
+ import type { Post, VoxxConfig } from "@prudentbird/voxx-core";
3
+
4
+ export function ReleaseList({
5
+ posts,
6
+ config,
7
+ }: {
8
+ posts: Post[];
9
+ config: VoxxConfig;
10
+ }) {
11
+ if (posts.length === 0) {
12
+ return <p className="voxx-empty">No releases yet.</p>;
13
+ }
14
+
15
+ return (
16
+ <div className="voxx-releases">
17
+ {posts.map((post) => (
18
+ <section key={post.slug} id={post.slug} className="voxx-release">
19
+ <header className="voxx-release__header">
20
+ <h2 className="voxx-release__version">
21
+ <a href={`#${post.slug}`}>{post.version ? `v${post.version}` : post.title}</a>
22
+ </h2>
23
+ <time dateTime={post.date}>{formatDate(post.date, config.site.locale)}</time>
24
+ </header>
25
+ <div
26
+ className="voxx-prose"
27
+ dangerouslySetInnerHTML={{ __html: post.html }}
28
+ />
29
+ </section>
30
+ ))}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: v0.1.0
3
+ version: "0.1.0"
4
+ date: {{DATE}}
5
+ ---
6
+
7
+ ### Added
8
+
9
+ - Initial release. Each release is a markdown file named after its version
10
+ (`0.1.0.md`) — or set `version:` in frontmatter and name the file anything.
11
+
12
+ ### How this works
13
+
14
+ Releases render newest-first on a single timeline page, and each one gets a
15
+ stable anchor link. Run `voxx new "0.2.0"` to add the next release.
@@ -0,0 +1,65 @@
1
+ import Link from "next/link";
2
+ import type { Post, VoxxConfig } from "@prudentbird/voxx-core";
3
+ import { OnThisPage } from "./on-this-page";
4
+
5
+ export function DocPage({
6
+ post,
7
+ config,
8
+ prev,
9
+ next,
10
+ }: {
11
+ post: Post;
12
+ config: VoxxConfig;
13
+ prev?: Post | null;
14
+ next?: Post | null;
15
+ }) {
16
+ const showToc = config.features.toc && post.toc.length > 0;
17
+
18
+ return (
19
+ <div className="voxx-layout">
20
+ <article className="voxx-article">
21
+ <header className="voxx-article__header">
22
+ <h1>{post.title}</h1>
23
+ {post.description ? (
24
+ <p className="voxx-article__meta">{post.description}</p>
25
+ ) : null}
26
+ </header>
27
+ <div
28
+ className="voxx-prose"
29
+ dangerouslySetInnerHTML={{ __html: post.html }}
30
+ />
31
+ {prev || next ? (
32
+ <nav className="voxx-pager">
33
+ {prev ? (
34
+ <Link href={prev.url} className="voxx-pager__link">
35
+ <span className="voxx-pager__label">Previous</span>
36
+ <span className="voxx-pager__title">{prev.title}</span>
37
+ </Link>
38
+ ) : (
39
+ <span />
40
+ )}
41
+ {next ? (
42
+ <Link
43
+ href={next.url}
44
+ className="voxx-pager__link voxx-pager__link--next"
45
+ >
46
+ <span className="voxx-pager__label">Next</span>
47
+ <span className="voxx-pager__title">{next.title}</span>
48
+ </Link>
49
+ ) : (
50
+ <span />
51
+ )}
52
+ </nav>
53
+ ) : null}
54
+ </article>
55
+
56
+ {showToc ? (
57
+ <aside className="voxx-aside">
58
+ <div className="voxx-aside__inner">
59
+ <OnThisPage toc={post.toc} />
60
+ </div>
61
+ </aside>
62
+ ) : null}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Everything you need to go from zero to running.
4
+ ---
5
+
6
+ This is a **section landing page** — it lives at `01-getting-started/index.md`,
7
+ so it gives the "Getting Started" sidebar section its title and URL.
8
+
9
+ Pages inside this folder appear nested under it in the sidebar.
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Welcome
3
+ description: What this documentation covers and how it's organized.
4
+ ---
5
+
6
+ These docs are plain markdown files. The folder structure *is* the navigation:
7
+
8
+ - Folders become sidebar sections, files become pages.
9
+ - A numeric prefix (`01-install.md`) controls ordering without leaking into the URL.
10
+ - An `index.md` inside a folder becomes that section's landing page.
11
+ - Frontmatter `order: 2` overrides the filename prefix when you need it.
12
+
13
+ ## Editing this page
14
+
15
+ This file is `index.md` at the root of your content directory — it's the page
16
+ readers land on first. Replace this text with an overview of your project.
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Installation
3
+ description: Install the project and verify it runs.
4
+ ---
5
+
6
+ ## Requirements
7
+
8
+ - Node.js 20 or newer
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install your-package
14
+ ```
15
+
16
+ The `01-` prefix on this file (`01-installation.md`) pins it to the top of its
17
+ section; the URL stays clean: `/docs/getting-started/installation`.
@@ -0,0 +1,52 @@
1
+ import "./_voxx/voxx.css";
2
+ {{GLOBALS_IMPORT}}
3
+ import type { Metadata } from "next";
4
+ import type { ReactNode } from "react";
5
+ import Link from "next/link";
6
+ import { buildNavTree } from "@prudentbird/voxx-core";
7
+ import { getConfig, getPosts } from "./_voxx/data";
8
+ import { SidebarNav } from "./_voxx/sidebar-nav";
9
+ import { MobileNav } from "./_voxx/mobile-nav";
10
+ import { ThemeToggle } from "./_voxx/theme-toggle";
11
+
12
+ export async function generateMetadata(): Promise<Metadata> {
13
+ const config = await getConfig();
14
+ return {
15
+ title: {
16
+ default: config.site.title,
17
+ template: `%s · ${config.site.title}`,
18
+ },
19
+ };
20
+ }
21
+
22
+ export default async function DocsLayout({
23
+ children,
24
+ }: {
25
+ children: ReactNode;
26
+ }) {
27
+ const [posts, config] = await Promise.all([getPosts(), getConfig()]);
28
+ const tree = buildNavTree(posts);
29
+ return (
30
+ <html lang="en" suppressHydrationWarning>
31
+ <body>
32
+ <div className="voxx voxx-docs">
33
+ <aside className="voxx-docs__nav">
34
+ <div className="voxx-docs__nav-inner">
35
+ <div className="voxx-docs__nav-header">
36
+ <MobileNav items={tree} title={config.site.title} />
37
+ <Link href="{{BASE_PATH}}" className="voxx-docs__title">
38
+ {config.site.title}
39
+ </Link>
40
+ </div>
41
+ <SidebarNav items={tree} />
42
+ <div className="voxx-docs__nav-footer">
43
+ <ThemeToggle />
44
+ </div>
45
+ </div>
46
+ </aside>
47
+ {children}
48
+ </div>
49
+ </body>
50
+ </html>
51
+ );
52
+ }
@@ -0,0 +1,37 @@
1
+ import "./_voxx/voxx.css";
2
+ {{GLOBALS_IMPORT}}
3
+ import type { ReactNode } from "react";
4
+ import Link from "next/link";
5
+ import { buildNavTree } from "@prudentbird/voxx-core";
6
+ import { getConfig, getPosts } from "./_voxx/data";
7
+ import { SidebarNav } from "./_voxx/sidebar-nav";
8
+ import { MobileNav } from "./_voxx/mobile-nav";
9
+ import { ThemeToggle } from "./_voxx/theme-toggle";
10
+
11
+ export default async function DocsLayout({
12
+ children,
13
+ }: {
14
+ children: ReactNode;
15
+ }) {
16
+ const [posts, config] = await Promise.all([getPosts(), getConfig()]);
17
+ const tree = buildNavTree(posts);
18
+ return (
19
+ <div className="voxx voxx-docs">
20
+ <aside className="voxx-docs__nav">
21
+ <div className="voxx-docs__nav-inner">
22
+ <div className="voxx-docs__nav-header">
23
+ <MobileNav items={tree} title={config.site.title} />
24
+ <Link href="{{BASE_PATH}}" className="voxx-docs__title">
25
+ {config.site.title}
26
+ </Link>
27
+ </div>
28
+ <SidebarNav items={tree} />
29
+ <div className="voxx-docs__nav-footer">
30
+ <ThemeToggle />
31
+ </div>
32
+ </div>
33
+ </aside>
34
+ {children}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { usePathname } from "next/navigation";
5
+ import type { NavNode } from "@prudentbird/voxx-core";
6
+ import { SidebarNav } from "./sidebar-nav";
7
+
8
+ export function MobileNav({
9
+ items,
10
+ title,
11
+ }: {
12
+ items: NavNode[];
13
+ title: string;
14
+ }) {
15
+ const [open, setOpen] = useState(false);
16
+ const pathname = usePathname();
17
+
18
+ useEffect(() => {
19
+ setOpen(false);
20
+ }, [pathname]);
21
+
22
+ useEffect(() => {
23
+ if (!open) return;
24
+ document.body.style.overflow = "hidden";
25
+ const onKey = (e: KeyboardEvent) => {
26
+ if (e.key === "Escape") setOpen(false);
27
+ };
28
+ window.addEventListener("keydown", onKey);
29
+ return () => {
30
+ document.body.style.overflow = "";
31
+ window.removeEventListener("keydown", onKey);
32
+ };
33
+ }, [open]);
34
+
35
+ return (
36
+ <>
37
+ <button
38
+ type="button"
39
+ className="voxx-icon-button voxx-menu-button"
40
+ aria-label="Open navigation"
41
+ aria-expanded={open}
42
+ onClick={() => setOpen(true)}
43
+ >
44
+ <svg
45
+ viewBox="0 0 24 24"
46
+ fill="none"
47
+ stroke="currentColor"
48
+ strokeWidth="2"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ aria-hidden="true"
52
+ >
53
+ <rect width="18" height="18" x="3" y="3" rx="2" />
54
+ <path d="M9 3v18" />
55
+ </svg>
56
+ </button>
57
+ {open ? (
58
+ <div className="voxx-drawer" role="dialog" aria-modal="true">
59
+ <div
60
+ className="voxx-drawer__overlay"
61
+ onClick={() => setOpen(false)}
62
+ />
63
+ <div className="voxx-drawer__panel">
64
+ <div className="voxx-drawer__header">
65
+ <span className="voxx-docs__title">{title}</span>
66
+ <button
67
+ type="button"
68
+ className="voxx-icon-button"
69
+ aria-label="Close navigation"
70
+ onClick={() => setOpen(false)}
71
+ >
72
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
73
+ <path
74
+ d="M6 6l12 12M18 6L6 18"
75
+ stroke="currentColor"
76
+ strokeWidth="2"
77
+ strokeLinecap="round"
78
+ />
79
+ </svg>
80
+ </button>
81
+ </div>
82
+ <div className="voxx-drawer__body">
83
+ <SidebarNav items={items} />
84
+ </div>
85
+ </div>
86
+ </div>
87
+ ) : null}
88
+ </>
89
+ );
90
+ }
@@ -0,0 +1,50 @@
1
+ import type { Metadata } from "next";
2
+ import { notFound } from "next/navigation";
3
+ import { buildSeo, serializeJsonLd } from "@prudentbird/voxx-core";
4
+ import { getConfig, getPost, getPosts } from "../_voxx/data";
5
+ import { toMetadata } from "../_voxx/metadata";
6
+ import { DocPage } from "../_voxx/doc-page";
7
+
8
+ type Params = { params: Promise<{ slug?: string[] }> };
9
+
10
+ export async function generateStaticParams() {
11
+ const posts = await getPosts();
12
+ return posts.map((post) => ({ slug: post.path }));
13
+ }
14
+
15
+ export async function generateMetadata({ params }: Params): Promise<Metadata> {
16
+ const { slug = [] } = await params;
17
+ const [post, config] = await Promise.all([
18
+ getPost(slug.join("/")),
19
+ getConfig(),
20
+ ]);
21
+ if (!post) return {};
22
+ return toMetadata(buildSeo(post, config));
23
+ }
24
+
25
+ export default async function DocRoute({ params }: Params) {
26
+ const { slug = [] } = await params;
27
+ const [post, posts, config] = await Promise.all([
28
+ getPost(slug.join("/")),
29
+ getPosts(),
30
+ getConfig(),
31
+ ]);
32
+ if (!post) notFound();
33
+
34
+ const index = posts.findIndex((p) => p.url === post.url);
35
+ const prev = index > 0 ? posts[index - 1] : null;
36
+ const next = index >= 0 && index < posts.length - 1 ? posts[index + 1] : null;
37
+ const seo = buildSeo(post, config);
38
+
39
+ return (
40
+ <>
41
+ {config.seo.jsonLd && seo.jsonLd ? (
42
+ <script
43
+ type="application/ld+json"
44
+ dangerouslySetInnerHTML={{ __html: serializeJsonLd(seo.jsonLd) }}
45
+ />
46
+ ) : null}
47
+ <DocPage post={post} config={config} prev={prev} next={next} />
48
+ </>
49
+ );
50
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import type { NavNode } from "@prudentbird/voxx-core";
6
+
7
+ export function SidebarNav({ items }: { items: NavNode[] }) {
8
+ const pathname = usePathname();
9
+ return (
10
+ <nav className="voxx-nav">
11
+ <NavList items={items} pathname={pathname} />
12
+ </nav>
13
+ );
14
+ }
15
+
16
+ function NavList({ items, pathname }: { items: NavNode[]; pathname: string }) {
17
+ return (
18
+ <ul className="voxx-nav__list">
19
+ {items.map((item) => (
20
+ <li key={`${item.url ?? ""}:${item.title}`}>
21
+ {item.url ? (
22
+ <Link
23
+ href={item.url}
24
+ className="voxx-nav__link"
25
+ data-active={pathname === item.url || undefined}
26
+ >
27
+ {item.title}
28
+ </Link>
29
+ ) : (
30
+ <span className="voxx-nav__section">{item.title}</span>
31
+ )}
32
+ {item.children.length > 0 ? (
33
+ <NavList items={item.children} pathname={pathname} />
34
+ ) : null}
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ );
39
+ }
@@ -0,0 +1 @@
1
+ export const CONTENT_VERSION = 0;
@@ -0,0 +1,34 @@
1
+ import "server-only";
2
+ import {
3
+ findPost,
4
+ getPosts as coreGetPosts,
5
+ loadConfig as coreLoadConfig,
6
+ type Post,
7
+ type VoxxConfig,
8
+ } from "@prudentbird/voxx-core";
9
+ import { CONTENT_VERSION } from "./content-version";
10
+
11
+ async function getPostsCached(version: number): Promise<Post[]> {
12
+ "use cache";
13
+ void version;
14
+ return coreGetPosts({{COLLECTION_ARG}});
15
+ }
16
+
17
+ export async function getPosts(): Promise<Post[]> {
18
+ return getPostsCached(CONTENT_VERSION);
19
+ }
20
+
21
+ async function getConfigCached(version: number): Promise<VoxxConfig> {
22
+ "use cache";
23
+ void version;
24
+ return coreLoadConfig();
25
+ }
26
+
27
+ export async function getConfig(): Promise<VoxxConfig> {
28
+ return getConfigCached(CONTENT_VERSION);
29
+ }
30
+
31
+ export async function getPost(slug: string): Promise<Post | null> {
32
+ const posts = await getPosts();
33
+ return findPost(posts, slug) ?? null;
34
+ }
@@ -0,0 +1,5 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
3
+ const { registerContentWatcher } = await import("@prudentbird/voxx-core");
4
+ await registerContentWatcher();
5
+ }
@@ -0,0 +1,9 @@
1
+ import { renderLlmsFull } from "@prudentbird/voxx-core";
2
+ import { getConfig, getPosts } from "{{DATA_IMPORT}}";
3
+
4
+ export async function GET() {
5
+ const [posts, config] = await Promise.all([getPosts(), getConfig()]);
6
+ return new Response(renderLlmsFull(posts, config), {
7
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
8
+ });
9
+ }
@@ -0,0 +1,9 @@
1
+ import { renderLlmsTxt } from "@prudentbird/voxx-core";
2
+ import { getConfig, getPosts } from "{{DATA_IMPORT}}";
3
+
4
+ export async function GET() {
5
+ const [posts, config] = await Promise.all([getPosts(), getConfig()]);
6
+ return new Response(renderLlmsTxt(posts, config), {
7
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
8
+ });
9
+ }
@@ -0,0 +1,39 @@
1
+ import type { Metadata } from "next";
2
+ import type { SeoData } from "@prudentbird/voxx-core";
3
+
4
+ export function toMetadata(seo: SeoData): Metadata {
5
+ const metadata: Metadata = {
6
+ title: seo.title,
7
+ description: seo.description,
8
+ alternates: { canonical: seo.canonical },
9
+ };
10
+
11
+ if (seo.openGraph) {
12
+ metadata.openGraph = {
13
+ type: "article",
14
+ title: seo.openGraph.title,
15
+ description: seo.openGraph.description,
16
+ url: seo.openGraph.url,
17
+ siteName: seo.openGraph.siteName,
18
+ locale: seo.openGraph.locale,
19
+ images: seo.openGraph.images,
20
+ publishedTime: seo.openGraph.publishedTime,
21
+ modifiedTime: seo.openGraph.modifiedTime,
22
+ authors: seo.openGraph.authors,
23
+ tags: seo.openGraph.tags,
24
+ };
25
+ }
26
+
27
+ if (seo.twitter) {
28
+ metadata.twitter = {
29
+ card: "summary_large_image",
30
+ title: seo.twitter.title,
31
+ description: seo.twitter.description,
32
+ images: seo.twitter.images,
33
+ site: seo.twitter.site,
34
+ creator: seo.twitter.creator,
35
+ };
36
+ }
37
+
38
+ return metadata;
39
+ }