@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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.mjs +1346 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
- package/templates/blog/hello-world.md.tpl +42 -0
- package/templates/blog/layout.tsx.tpl +44 -0
- package/templates/blog/page.tsx.tpl +27 -0
- package/templates/blog/post-list.tsx.tpl +45 -0
- package/templates/blog/post-page.tsx.tpl +41 -0
- package/templates/blog/slug-page.tsx.tpl +40 -0
- package/templates/changelog/layout.tsx.tpl +44 -0
- package/templates/changelog/page.tsx.tpl +27 -0
- package/templates/changelog/release-list.tsx.tpl +33 -0
- package/templates/changelog/release.md.tpl +15 -0
- package/templates/docs/doc-page.tsx.tpl +65 -0
- package/templates/docs/getting-started-index.md.tpl +9 -0
- package/templates/docs/index.md.tpl +16 -0
- package/templates/docs/installation.md.tpl +17 -0
- package/templates/docs/layout-root.tsx.tpl +52 -0
- package/templates/docs/layout.tsx.tpl +37 -0
- package/templates/docs/mobile-nav.tsx.tpl +90 -0
- package/templates/docs/page.tsx.tpl +50 -0
- package/templates/docs/sidebar-nav.tsx.tpl +39 -0
- package/templates/shared/content-version.ts.tpl +1 -0
- package/templates/shared/data.ts.tpl +34 -0
- package/templates/shared/instrumentation.ts.tpl +5 -0
- package/templates/shared/llms-full-route.ts.tpl +9 -0
- package/templates/shared/llms-route.ts.tpl +9 -0
- package/templates/shared/metadata.ts.tpl +39 -0
- package/templates/shared/on-this-page.tsx.tpl +213 -0
- package/templates/shared/robots.ts.tpl +13 -0
- package/templates/shared/rss-route.ts.tpl +9 -0
- package/templates/shared/sitemap.ts.tpl +23 -0
- package/templates/shared/theme-toggle.tsx.tpl +64 -0
- 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,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
|
+
}
|