@beatzball/create-litro 0.1.3 → 0.2.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/CHANGELOG.md +133 -0
- package/dist/recipes/fullstack/template/app.ts +11 -8
- package/dist/recipes/fullstack/template/pages/blog/[slug].ts +4 -5
- package/dist/recipes/fullstack/template/pages/index.ts +4 -5
- package/dist/recipes/starlight/recipe.config.d.ts +4 -0
- package/dist/recipes/starlight/recipe.config.d.ts.map +1 -0
- package/dist/recipes/starlight/recipe.config.js +9 -0
- package/dist/recipes/starlight/recipe.config.js.map +1 -0
- package/dist/recipes/starlight/recipe.config.ts +11 -0
- package/dist/recipes/starlight/template/_data/metadata.js +10 -0
- package/dist/recipes/starlight/template/app.ts +18 -0
- package/dist/recipes/starlight/template/content/blog/.11tydata.json +1 -0
- package/dist/recipes/starlight/template/content/blog/release-notes.md +44 -0
- package/dist/recipes/starlight/template/content/blog/welcome.md +44 -0
- package/dist/recipes/starlight/template/content/docs/.11tydata.json +1 -0
- package/dist/recipes/starlight/template/content/docs/configuration.md +77 -0
- package/dist/recipes/starlight/template/content/docs/getting-started.md +53 -0
- package/dist/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
- package/dist/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
- package/dist/recipes/starlight/template/content/docs/installation.md +54 -0
- package/dist/recipes/starlight/template/litro.recipe.json +7 -0
- package/dist/recipes/starlight/template/nitro.config.ts +57 -0
- package/dist/recipes/starlight/template/package.json +26 -0
- package/dist/recipes/starlight/template/pages/blog/[slug].ts +125 -0
- package/dist/recipes/starlight/template/pages/blog/index.ts +114 -0
- package/dist/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
- package/dist/recipes/starlight/template/pages/docs/[slug].ts +147 -0
- package/dist/recipes/starlight/template/pages/index.ts +135 -0
- package/dist/recipes/starlight/template/public/styles/starlight.css +215 -0
- package/dist/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
- package/dist/recipes/starlight/template/server/routes/[...].ts +57 -0
- package/dist/recipes/starlight/template/server/starlight.config.js +29 -0
- package/dist/recipes/starlight/template/src/components/sl-aside.ts +91 -0
- package/dist/recipes/starlight/template/src/components/sl-badge.ts +76 -0
- package/dist/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
- package/dist/recipes/starlight/template/src/components/sl-card.ts +91 -0
- package/dist/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
- package/dist/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
- package/dist/recipes/starlight/template/src/components/starlight-header.ts +152 -0
- package/dist/recipes/starlight/template/src/components/starlight-page.ts +168 -0
- package/dist/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
- package/dist/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
- package/dist/recipes/starlight/template/src/date-utils.ts +20 -0
- package/dist/recipes/starlight/template/src/extract-headings.ts +68 -0
- package/dist/recipes/starlight/template/src/route-meta.ts +16 -0
- package/dist/recipes/starlight/template/tsconfig.json +14 -0
- package/dist/recipes/starlight/template/vite.config.ts +19 -0
- package/dist/src/scaffold.test.js +134 -0
- package/dist/src/scaffold.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/recipes/fullstack/template/app.ts +11 -8
- package/recipes/fullstack/template/pages/blog/[slug].ts +4 -5
- package/recipes/fullstack/template/pages/index.ts +4 -5
- package/recipes/starlight/recipe.config.ts +11 -0
- package/recipes/starlight/template/_data/metadata.js +10 -0
- package/recipes/starlight/template/app.ts +18 -0
- package/recipes/starlight/template/content/blog/.11tydata.json +1 -0
- package/recipes/starlight/template/content/blog/release-notes.md +44 -0
- package/recipes/starlight/template/content/blog/welcome.md +44 -0
- package/recipes/starlight/template/content/docs/.11tydata.json +1 -0
- package/recipes/starlight/template/content/docs/configuration.md +77 -0
- package/recipes/starlight/template/content/docs/getting-started.md +53 -0
- package/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
- package/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
- package/recipes/starlight/template/content/docs/installation.md +54 -0
- package/recipes/starlight/template/litro.recipe.json +7 -0
- package/recipes/starlight/template/nitro.config.ts +57 -0
- package/recipes/starlight/template/package.json +26 -0
- package/recipes/starlight/template/pages/blog/[slug].ts +125 -0
- package/recipes/starlight/template/pages/blog/index.ts +114 -0
- package/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
- package/recipes/starlight/template/pages/docs/[slug].ts +147 -0
- package/recipes/starlight/template/pages/index.ts +135 -0
- package/recipes/starlight/template/public/styles/starlight.css +215 -0
- package/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
- package/recipes/starlight/template/server/routes/[...].ts +57 -0
- package/recipes/starlight/template/server/starlight.config.js +29 -0
- package/recipes/starlight/template/src/components/sl-aside.ts +91 -0
- package/recipes/starlight/template/src/components/sl-badge.ts +76 -0
- package/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
- package/recipes/starlight/template/src/components/sl-card.ts +91 -0
- package/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
- package/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
- package/recipes/starlight/template/src/components/starlight-header.ts +152 -0
- package/recipes/starlight/template/src/components/starlight-page.ts +168 -0
- package/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
- package/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
- package/recipes/starlight/template/src/date-utils.ts +20 -0
- package/recipes/starlight/template/src/extract-headings.ts +68 -0
- package/recipes/starlight/template/src/route-meta.ts +16 -0
- package/recipes/starlight/template/tsconfig.json +14 -0
- package/recipes/starlight/template/vite.config.ts +19 -0
- package/src/scaffold.test.ts +148 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Installation
|
|
3
|
+
description: Install dependencies and configure your Litro Starlight project.
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Package Manager
|
|
9
|
+
|
|
10
|
+
This project uses **pnpm** by default. You can also use `npm` or `yarn` — just replace `pnpm` in the commands below.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
From the project root, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This installs all dependencies listed in `package.json`, including:
|
|
21
|
+
|
|
22
|
+
- **`@beatzball/litro`** — the Litro framework (pages plugin, SSR/SSG, content layer)
|
|
23
|
+
- **`lit`** — Lit web components library
|
|
24
|
+
- **`@lit-labs/ssr`** — server-side rendering via Declarative Shadow DOM
|
|
25
|
+
- **`@lit-labs/ssr-client`** — hydration support script
|
|
26
|
+
|
|
27
|
+
## TypeScript
|
|
28
|
+
|
|
29
|
+
The project ships with `tsconfig.json` preconfigured for Lit decorators:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"compilerOptions": {
|
|
34
|
+
"experimentalDecorators": true,
|
|
35
|
+
"useDefineForClassFields": false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
These two settings are required for Lit's `@customElement`, `@state`, and `@property` decorators to work correctly with TypeScript's decorator transform.
|
|
41
|
+
|
|
42
|
+
## Environment
|
|
43
|
+
|
|
44
|
+
No environment variables are required for SSG mode. If you add API routes later, create a `.env` file at the project root.
|
|
45
|
+
|
|
46
|
+
## Verify
|
|
47
|
+
|
|
48
|
+
After installing, start the dev server:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm dev
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Navigate to `http://localhost:3030`. You should see the splash page with the sidebar navigation.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineNitroConfig } from 'nitropack/config';
|
|
2
|
+
import type { Nitro } from 'nitropack';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { ssgPreset } from '@beatzball/litro/config';
|
|
5
|
+
import pagesPlugin from '@beatzball/litro/plugins';
|
|
6
|
+
import ssgPlugin from '@beatzball/litro/plugins/ssg';
|
|
7
|
+
import contentPlugin from '@beatzball/litro/content/plugin';
|
|
8
|
+
|
|
9
|
+
export default defineNitroConfig({
|
|
10
|
+
...ssgPreset(),
|
|
11
|
+
|
|
12
|
+
srcDir: 'server',
|
|
13
|
+
|
|
14
|
+
publicAssets: [
|
|
15
|
+
{ dir: '../dist/client', baseURL: '/_litro/', maxAge: 31536000 },
|
|
16
|
+
{ dir: '../public', baseURL: '/', maxAge: 0 },
|
|
17
|
+
{ dir: '../content', baseURL: '/content/', maxAge: 86400 },
|
|
18
|
+
],
|
|
19
|
+
|
|
20
|
+
externals: { inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'] },
|
|
21
|
+
|
|
22
|
+
esbuild: {
|
|
23
|
+
options: {
|
|
24
|
+
tsconfigRaw: {
|
|
25
|
+
compilerOptions: {
|
|
26
|
+
experimentalDecorators: true,
|
|
27
|
+
useDefineForClassFields: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
ignore: ['**/middleware/vite-dev.ts'],
|
|
34
|
+
handlers: [
|
|
35
|
+
{
|
|
36
|
+
middleware: true,
|
|
37
|
+
handler: resolve('./server/middleware/vite-dev.ts'),
|
|
38
|
+
env: 'dev',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
hooks: {
|
|
43
|
+
'build:before': async (nitro: Nitro) => {
|
|
44
|
+
await contentPlugin(nitro);
|
|
45
|
+
await pagesPlugin(nitro);
|
|
46
|
+
await ssgPlugin(nitro);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
compatibilityDate: '2025-01-01',
|
|
51
|
+
|
|
52
|
+
routeRules: {
|
|
53
|
+
'/_litro/**': {
|
|
54
|
+
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"imports": {
|
|
6
|
+
"#litro/page-manifest": "./server/stubs/page-manifest.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "litro dev",
|
|
10
|
+
"build": "litro build",
|
|
11
|
+
"preview": "litro preview",
|
|
12
|
+
"generate": "litro generate"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@beatzball/litro": "latest",
|
|
16
|
+
"lit": "^3.2.1",
|
|
17
|
+
"@lit-labs/ssr": "^3.3.0",
|
|
18
|
+
"@lit-labs/ssr-client": "^1.1.7",
|
|
19
|
+
"h3": "^1.13.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"nitropack": "^2.13.1",
|
|
23
|
+
"vite": "^5.4.11",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
3
|
+
import { customElement } from 'lit/decorators.js';
|
|
4
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
5
|
+
import { definePageData } from '@beatzball/litro';
|
|
6
|
+
import { createError } from 'h3';
|
|
7
|
+
import type { Post } from 'litro:content';
|
|
8
|
+
import { getPosts } from 'litro:content';
|
|
9
|
+
import { siteConfig } from '../../server/starlight.config.js';
|
|
10
|
+
import { extractHeadings, addHeadingIds } from '../../src/extract-headings.js';
|
|
11
|
+
import { starlightHead } from '../../src/route-meta.js';
|
|
12
|
+
import { formatDate, isoDate } from '../../src/date-utils.js';
|
|
13
|
+
|
|
14
|
+
// Register components used in render()
|
|
15
|
+
import '../../src/components/starlight-header.js';
|
|
16
|
+
|
|
17
|
+
export interface BlogPostData {
|
|
18
|
+
post: Post;
|
|
19
|
+
body: string;
|
|
20
|
+
toc: Array<{ depth: number; text: string; slug: string }>;
|
|
21
|
+
siteTitle: string;
|
|
22
|
+
nav: typeof siteConfig.nav;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const pageData = definePageData(async (event) => {
|
|
26
|
+
const slug = event.context.params?.slug ?? '';
|
|
27
|
+
|
|
28
|
+
// Content URLs are /content/blog/<slug> (contentDir = 'content')
|
|
29
|
+
const posts = await getPosts();
|
|
30
|
+
const post = posts.find(p => p.url === `/content/blog/${slug}`);
|
|
31
|
+
|
|
32
|
+
if (!post) {
|
|
33
|
+
throw createError({ statusCode: 404, message: `Post not found: ${slug}` });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const toc = extractHeadings(post.rawBody);
|
|
37
|
+
const body = addHeadingIds(post.body);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
post,
|
|
41
|
+
body,
|
|
42
|
+
toc,
|
|
43
|
+
siteTitle: siteConfig.title,
|
|
44
|
+
nav: siteConfig.nav,
|
|
45
|
+
} satisfies BlogPostData;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
49
|
+
const posts = await getPosts();
|
|
50
|
+
return posts
|
|
51
|
+
.filter(p => p.url.startsWith('/content/blog/'))
|
|
52
|
+
.map(p => '/blog' + p.url.slice('/content/blog'.length));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const routeMeta = {
|
|
56
|
+
head: starlightHead,
|
|
57
|
+
title: 'Blog — {{projectName}}',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
@customElement('page-blog-slug')
|
|
61
|
+
export class BlogPostPage extends LitroPage {
|
|
62
|
+
override render() {
|
|
63
|
+
const data = this.serverData as BlogPostData | null;
|
|
64
|
+
if (!data?.post) return html`<p>Loading…</p>`;
|
|
65
|
+
|
|
66
|
+
const { post, body, siteTitle, nav } = data;
|
|
67
|
+
const blogSlug = post.url.slice('/content/blog/'.length);
|
|
68
|
+
|
|
69
|
+
return html`
|
|
70
|
+
<div style="min-height:100vh;display:flex;flex-direction:column;">
|
|
71
|
+
<starlight-header
|
|
72
|
+
siteTitle="${siteTitle}"
|
|
73
|
+
.nav="${nav}"
|
|
74
|
+
currentPath="/blog/${blogSlug}"
|
|
75
|
+
></starlight-header>
|
|
76
|
+
<main style="
|
|
77
|
+
flex:1;
|
|
78
|
+
max-width:52rem;
|
|
79
|
+
margin:0 auto;
|
|
80
|
+
padding:var(--sl-content-pad-y,2rem) var(--sl-content-pad-x,1.5rem);
|
|
81
|
+
width:100%;
|
|
82
|
+
">
|
|
83
|
+
<article>
|
|
84
|
+
<header style="margin-bottom:2rem;">
|
|
85
|
+
<h1 style="font-size:var(--sl-text-4xl);font-weight:700;margin:0 0 0.75rem;line-height:1.15;">
|
|
86
|
+
${post.title}
|
|
87
|
+
</h1>
|
|
88
|
+
<time
|
|
89
|
+
datetime="${isoDate(post.date)}"
|
|
90
|
+
style="font-size:var(--sl-text-sm);color:var(--sl-color-gray-4);"
|
|
91
|
+
>${formatDate(post.date)}</time>
|
|
92
|
+
${post.tags.filter(t => t !== 'posts').length > 0 ? html`
|
|
93
|
+
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;margin-top:0.75rem;">
|
|
94
|
+
${post.tags.filter(t => t !== 'posts').map(tag => html`
|
|
95
|
+
<a href="/blog/tags/${tag}" style="
|
|
96
|
+
display:inline-block;
|
|
97
|
+
padding:0.15em 0.55em;
|
|
98
|
+
font-size:var(--sl-text-xs);
|
|
99
|
+
border-radius:9999px;
|
|
100
|
+
background:var(--sl-color-accent-low);
|
|
101
|
+
color:var(--sl-color-accent-high,#5b21b6);
|
|
102
|
+
text-decoration:none;
|
|
103
|
+
font-weight:600;
|
|
104
|
+
">#${tag}</a>
|
|
105
|
+
`)}
|
|
106
|
+
</div>
|
|
107
|
+
` : ''}
|
|
108
|
+
</header>
|
|
109
|
+
<!-- unsafeHTML renders the Markdown-generated HTML directly.
|
|
110
|
+
The content directory is trusted-author-only; do not place
|
|
111
|
+
user-submitted or untrusted content here without sanitizing. -->
|
|
112
|
+
${unsafeHTML(body)}
|
|
113
|
+
</article>
|
|
114
|
+
<footer style="margin-top:3rem;padding-top:1.5rem;border-top:1px solid var(--sl-color-border);">
|
|
115
|
+
<a href="/blog" style="font-size:var(--sl-text-sm);color:var(--sl-color-accent);text-decoration:none;">
|
|
116
|
+
← Back to Blog
|
|
117
|
+
</a>
|
|
118
|
+
</footer>
|
|
119
|
+
</main>
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default BlogPostPage;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { Post } from 'litro:content';
|
|
6
|
+
import { getPosts } from 'litro:content';
|
|
7
|
+
import { siteConfig } from '../../server/starlight.config.js';
|
|
8
|
+
import { starlightHead } from '../../src/route-meta.js';
|
|
9
|
+
import { formatDate, isoDate } from '../../src/date-utils.js';
|
|
10
|
+
|
|
11
|
+
// Register components used in render()
|
|
12
|
+
import '../../src/components/starlight-header.js';
|
|
13
|
+
|
|
14
|
+
export interface BlogIndexData {
|
|
15
|
+
posts: Post[];
|
|
16
|
+
siteTitle: string;
|
|
17
|
+
nav: typeof siteConfig.nav;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const pageData = definePageData(async (_event) => {
|
|
21
|
+
const all = await getPosts();
|
|
22
|
+
// Filter to only blog posts (URL prefix from contentDir='content')
|
|
23
|
+
const posts = all.filter(p => p.url.startsWith('/content/blog/'));
|
|
24
|
+
return {
|
|
25
|
+
posts,
|
|
26
|
+
siteTitle: siteConfig.title,
|
|
27
|
+
nav: siteConfig.nav,
|
|
28
|
+
} satisfies BlogIndexData;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const routeMeta = {
|
|
32
|
+
head: starlightHead,
|
|
33
|
+
title: 'Blog — {{projectName}}',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
@customElement('page-blog')
|
|
37
|
+
export class BlogIndexPage extends LitroPage {
|
|
38
|
+
override render() {
|
|
39
|
+
const data = this.serverData as BlogIndexData | null;
|
|
40
|
+
const { posts = [], siteTitle = '{{projectName}}', nav = [] } = data ?? {};
|
|
41
|
+
|
|
42
|
+
return html`
|
|
43
|
+
<div style="min-height:100vh;display:flex;flex-direction:column;">
|
|
44
|
+
<starlight-header
|
|
45
|
+
siteTitle="${siteTitle}"
|
|
46
|
+
.nav="${nav}"
|
|
47
|
+
currentPath="/blog"
|
|
48
|
+
></starlight-header>
|
|
49
|
+
<main style="
|
|
50
|
+
flex:1;
|
|
51
|
+
max-width:56rem;
|
|
52
|
+
margin:0 auto;
|
|
53
|
+
padding:var(--sl-content-pad-y,2rem) var(--sl-content-pad-x,1.5rem);
|
|
54
|
+
width:100%;
|
|
55
|
+
">
|
|
56
|
+
<h1 style="
|
|
57
|
+
font-size:var(--sl-text-4xl);
|
|
58
|
+
font-weight:700;
|
|
59
|
+
margin:0 0 2rem;
|
|
60
|
+
">Blog</h1>
|
|
61
|
+
|
|
62
|
+
${posts.length === 0 ? html`
|
|
63
|
+
<p style="color:var(--sl-color-gray-4);">No posts yet.</p>
|
|
64
|
+
` : html`
|
|
65
|
+
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:2rem;">
|
|
66
|
+
${posts.map(post => {
|
|
67
|
+
const blogSlug = post.url.slice('/content/blog/'.length);
|
|
68
|
+
return html`
|
|
69
|
+
<li style="border-bottom:1px solid var(--sl-color-border);padding-bottom:2rem;">
|
|
70
|
+
<a href="/blog/${blogSlug}" style="
|
|
71
|
+
display:block;
|
|
72
|
+
font-size:var(--sl-text-2xl);
|
|
73
|
+
font-weight:600;
|
|
74
|
+
color:var(--sl-color-text);
|
|
75
|
+
text-decoration:none;
|
|
76
|
+
margin-bottom:0.4rem;
|
|
77
|
+
">${post.title}</a>
|
|
78
|
+
<time
|
|
79
|
+
datetime="${isoDate(post.date)}"
|
|
80
|
+
style="font-size:var(--sl-text-sm);color:var(--sl-color-gray-4);"
|
|
81
|
+
>${formatDate(post.date)}</time>
|
|
82
|
+
${post.description ? html`
|
|
83
|
+
<p style="margin:0.5rem 0 0.75rem;color:var(--sl-color-gray-5);line-height:1.6;">
|
|
84
|
+
${post.description}
|
|
85
|
+
</p>
|
|
86
|
+
` : ''}
|
|
87
|
+
${post.tags.filter(t => t !== 'posts').length > 0 ? html`
|
|
88
|
+
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;margin-top:0.5rem;">
|
|
89
|
+
${post.tags.filter(t => t !== 'posts').map(tag => html`
|
|
90
|
+
<a href="/blog/tags/${tag}" style="
|
|
91
|
+
display:inline-block;
|
|
92
|
+
padding:0.15em 0.55em;
|
|
93
|
+
font-size:var(--sl-text-xs);
|
|
94
|
+
border-radius:9999px;
|
|
95
|
+
background:var(--sl-color-accent-low);
|
|
96
|
+
color:var(--sl-color-accent-high,#5b21b6);
|
|
97
|
+
text-decoration:none;
|
|
98
|
+
font-weight:600;
|
|
99
|
+
">#${tag}</a>
|
|
100
|
+
`)}
|
|
101
|
+
</div>
|
|
102
|
+
` : ''}
|
|
103
|
+
</li>
|
|
104
|
+
`;
|
|
105
|
+
})}
|
|
106
|
+
</ul>
|
|
107
|
+
`}
|
|
108
|
+
</main>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default BlogIndexPage;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { Post } from 'litro:content';
|
|
6
|
+
import { getPosts } from 'litro:content';
|
|
7
|
+
import { siteConfig } from '../../../server/starlight.config.js';
|
|
8
|
+
import { starlightHead } from '../../../src/route-meta.js';
|
|
9
|
+
import { formatDate, isoDate } from '../../../src/date-utils.js';
|
|
10
|
+
|
|
11
|
+
// Register components used in render()
|
|
12
|
+
import '../../../src/components/starlight-header.js';
|
|
13
|
+
|
|
14
|
+
export interface TagPageData {
|
|
15
|
+
tag: string;
|
|
16
|
+
posts: Post[];
|
|
17
|
+
siteTitle: string;
|
|
18
|
+
nav: typeof siteConfig.nav;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const pageData = definePageData(async (event) => {
|
|
22
|
+
const tag = event.context.params?.tag ?? '';
|
|
23
|
+
const all = await getPosts({ tag });
|
|
24
|
+
// Filter to only blog posts (docs might have unexpected tags)
|
|
25
|
+
const posts = all.filter(p => p.url.startsWith('/content/blog/'));
|
|
26
|
+
return {
|
|
27
|
+
tag,
|
|
28
|
+
posts,
|
|
29
|
+
siteTitle: siteConfig.title,
|
|
30
|
+
nav: siteConfig.nav,
|
|
31
|
+
} satisfies TagPageData;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
35
|
+
const all = await getPosts();
|
|
36
|
+
const blogPosts = all.filter(p => p.url.startsWith('/content/blog/'));
|
|
37
|
+
const tags = [...new Set(blogPosts.flatMap(p => p.tags))].sort();
|
|
38
|
+
return tags.map(tag => `/blog/tags/${tag}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const routeMeta = {
|
|
42
|
+
head: starlightHead,
|
|
43
|
+
title: 'Tags — {{projectName}}',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
@customElement('page-blog-tags-tag')
|
|
47
|
+
export class TagPage extends LitroPage {
|
|
48
|
+
override render() {
|
|
49
|
+
const data = this.serverData as TagPageData | null;
|
|
50
|
+
const { tag = '', posts = [], siteTitle = '{{projectName}}', nav = [] } = data ?? {};
|
|
51
|
+
|
|
52
|
+
return html`
|
|
53
|
+
<div style="min-height:100vh;display:flex;flex-direction:column;">
|
|
54
|
+
<starlight-header
|
|
55
|
+
siteTitle="${siteTitle}"
|
|
56
|
+
.nav="${nav}"
|
|
57
|
+
currentPath="/blog/tags/${tag}"
|
|
58
|
+
></starlight-header>
|
|
59
|
+
<main style="
|
|
60
|
+
flex:1;
|
|
61
|
+
max-width:56rem;
|
|
62
|
+
margin:0 auto;
|
|
63
|
+
padding:var(--sl-content-pad-y,2rem) var(--sl-content-pad-x,1.5rem);
|
|
64
|
+
width:100%;
|
|
65
|
+
">
|
|
66
|
+
<h1 style="font-size:var(--sl-text-4xl);font-weight:700;margin:0 0 2rem;">
|
|
67
|
+
Posts tagged: <span style="color:var(--sl-color-accent);">#${tag}</span>
|
|
68
|
+
</h1>
|
|
69
|
+
|
|
70
|
+
${posts.length === 0 ? html`
|
|
71
|
+
<p style="color:var(--sl-color-gray-4);">No posts found for this tag.</p>
|
|
72
|
+
` : html`
|
|
73
|
+
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1.5rem;">
|
|
74
|
+
${posts.map(post => {
|
|
75
|
+
const blogSlug = post.url.slice('/content/blog/'.length);
|
|
76
|
+
return html`
|
|
77
|
+
<li style="border-bottom:1px solid var(--sl-color-border);padding-bottom:1.5rem;">
|
|
78
|
+
<a href="/blog/${blogSlug}" style="
|
|
79
|
+
display:block;
|
|
80
|
+
font-size:var(--sl-text-xl);
|
|
81
|
+
font-weight:600;
|
|
82
|
+
color:var(--sl-color-text);
|
|
83
|
+
text-decoration:none;
|
|
84
|
+
margin-bottom:0.3rem;
|
|
85
|
+
">${post.title}</a>
|
|
86
|
+
<time
|
|
87
|
+
datetime="${isoDate(post.date)}"
|
|
88
|
+
style="font-size:var(--sl-text-sm);color:var(--sl-color-gray-4);"
|
|
89
|
+
>${formatDate(post.date)}</time>
|
|
90
|
+
${post.description ? html`
|
|
91
|
+
<p style="margin:0.4rem 0 0;color:var(--sl-color-gray-5);">${post.description}</p>
|
|
92
|
+
` : ''}
|
|
93
|
+
</li>
|
|
94
|
+
`;
|
|
95
|
+
})}
|
|
96
|
+
</ul>
|
|
97
|
+
`}
|
|
98
|
+
|
|
99
|
+
<p style="margin-top:2rem;">
|
|
100
|
+
<a href="/blog" style="font-size:var(--sl-text-sm);color:var(--sl-color-accent);text-decoration:none;">
|
|
101
|
+
← All Posts
|
|
102
|
+
</a>
|
|
103
|
+
</p>
|
|
104
|
+
</main>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default TagPage;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
3
|
+
import { customElement } from 'lit/decorators.js';
|
|
4
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
5
|
+
import { definePageData } from '@beatzball/litro';
|
|
6
|
+
import { createError } from 'h3';
|
|
7
|
+
import type { Post } from 'litro:content';
|
|
8
|
+
import { getPosts } from 'litro:content';
|
|
9
|
+
import { siteConfig } from '../../server/starlight.config.js';
|
|
10
|
+
import { extractHeadings, addHeadingIds } from '../../src/extract-headings.js';
|
|
11
|
+
import { starlightHead } from '../../src/route-meta.js';
|
|
12
|
+
|
|
13
|
+
// Register components used in render()
|
|
14
|
+
import '../../src/components/starlight-page.js';
|
|
15
|
+
|
|
16
|
+
export interface DocPageData {
|
|
17
|
+
doc: Post;
|
|
18
|
+
body: string;
|
|
19
|
+
toc: Array<{ depth: number; text: string; slug: string }>;
|
|
20
|
+
sidebar: typeof siteConfig.sidebar;
|
|
21
|
+
siteTitle: string;
|
|
22
|
+
currentSlug: string;
|
|
23
|
+
prevDoc: { label: string; href: string } | null;
|
|
24
|
+
nextDoc: { label: string; href: string } | null;
|
|
25
|
+
nav: typeof siteConfig.nav;
|
|
26
|
+
editUrl: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function computePrevNext(
|
|
30
|
+
sidebar: typeof siteConfig.sidebar,
|
|
31
|
+
currentSlug: string,
|
|
32
|
+
): { prevDoc: DocPageData['prevDoc']; nextDoc: DocPageData['nextDoc'] } {
|
|
33
|
+
const flat = sidebar.flatMap(g => g.items);
|
|
34
|
+
const idx = flat.findIndex(item => item.slug === currentSlug);
|
|
35
|
+
return {
|
|
36
|
+
prevDoc: idx > 0
|
|
37
|
+
? { label: flat[idx - 1].label, href: `/docs/${flat[idx - 1].slug}` }
|
|
38
|
+
: null,
|
|
39
|
+
nextDoc: idx < flat.length - 1
|
|
40
|
+
? { label: flat[idx + 1].label, href: `/docs/${flat[idx + 1].slug}` }
|
|
41
|
+
: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const pageData = definePageData(async (event) => {
|
|
46
|
+
const slug = event.context.params?.slug ?? '';
|
|
47
|
+
|
|
48
|
+
// Content URLs are /content/docs/<slug> (contentDir = 'content', so
|
|
49
|
+
// dirname is project root, and paths include the 'content/' prefix in the URL).
|
|
50
|
+
const posts = await getPosts();
|
|
51
|
+
const doc = posts.find(p => p.url === `/content/docs/${slug}`);
|
|
52
|
+
|
|
53
|
+
if (!doc) {
|
|
54
|
+
throw createError({ statusCode: 404, message: `Doc not found: ${slug}` });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const toc = extractHeadings(doc.rawBody);
|
|
58
|
+
const body = addHeadingIds(doc.body);
|
|
59
|
+
const { prevDoc, nextDoc } = computePrevNext(siteConfig.sidebar, slug);
|
|
60
|
+
const editUrl = siteConfig.editUrlBase
|
|
61
|
+
? `${siteConfig.editUrlBase}/content/docs/${slug}.md`
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
doc,
|
|
66
|
+
body,
|
|
67
|
+
toc,
|
|
68
|
+
sidebar: siteConfig.sidebar,
|
|
69
|
+
siteTitle: siteConfig.title,
|
|
70
|
+
currentSlug: slug,
|
|
71
|
+
prevDoc,
|
|
72
|
+
nextDoc,
|
|
73
|
+
nav: siteConfig.nav,
|
|
74
|
+
editUrl,
|
|
75
|
+
} satisfies DocPageData;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
79
|
+
const posts = await getPosts();
|
|
80
|
+
return posts
|
|
81
|
+
.filter(p => p.url.startsWith('/content/docs/'))
|
|
82
|
+
.map(p => '/docs' + p.url.slice('/content/docs'.length));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const routeMeta = {
|
|
86
|
+
head: starlightHead,
|
|
87
|
+
title: 'Docs — {{projectName}}',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
@customElement('page-docs-slug')
|
|
91
|
+
export class DocPage extends LitroPage {
|
|
92
|
+
override render() {
|
|
93
|
+
const data = this.serverData as DocPageData | null;
|
|
94
|
+
if (!data?.doc) return html`<p>Loading…</p>`;
|
|
95
|
+
|
|
96
|
+
return html`
|
|
97
|
+
<starlight-page
|
|
98
|
+
siteTitle="${data.siteTitle}"
|
|
99
|
+
pageTitle="${data.doc.title}"
|
|
100
|
+
.nav="${data.nav}"
|
|
101
|
+
.sidebar="${data.sidebar}"
|
|
102
|
+
.toc="${data.toc}"
|
|
103
|
+
currentSlug="${data.currentSlug}"
|
|
104
|
+
currentPath="/docs/${data.currentSlug}"
|
|
105
|
+
>
|
|
106
|
+
<div slot="content">
|
|
107
|
+
<!-- unsafeHTML renders the Markdown-generated HTML directly.
|
|
108
|
+
The content/docs directory is trusted-author-only; do not place
|
|
109
|
+
user-submitted or untrusted content here without sanitizing. -->
|
|
110
|
+
${unsafeHTML(data.body)}
|
|
111
|
+
|
|
112
|
+
${data.prevDoc || data.nextDoc ? html`
|
|
113
|
+
<nav style="
|
|
114
|
+
display:flex;
|
|
115
|
+
justify-content:space-between;
|
|
116
|
+
padding-top:2rem;
|
|
117
|
+
margin-top:2rem;
|
|
118
|
+
border-top:1px solid var(--sl-color-border);
|
|
119
|
+
font-size:var(--sl-text-sm);
|
|
120
|
+
" aria-label="Previous and next pages">
|
|
121
|
+
${data.prevDoc ? html`
|
|
122
|
+
<a href="${data.prevDoc.href}" style="color:var(--sl-color-accent);text-decoration:none;">
|
|
123
|
+
← ${data.prevDoc.label}
|
|
124
|
+
</a>
|
|
125
|
+
` : html`<span></span>`}
|
|
126
|
+
${data.nextDoc ? html`
|
|
127
|
+
<a href="${data.nextDoc.href}" style="color:var(--sl-color-accent);text-decoration:none;">
|
|
128
|
+
${data.nextDoc.label} →
|
|
129
|
+
</a>
|
|
130
|
+
` : ''}
|
|
131
|
+
</nav>
|
|
132
|
+
` : ''}
|
|
133
|
+
|
|
134
|
+
${data.editUrl ? html`
|
|
135
|
+
<p style="margin-top:1.5rem;font-size:var(--sl-text-xs);color:var(--sl-color-gray-4);">
|
|
136
|
+
<a href="${data.editUrl}" style="color:var(--sl-color-accent);" target="_blank" rel="noopener">
|
|
137
|
+
Edit this page
|
|
138
|
+
</a>
|
|
139
|
+
</p>
|
|
140
|
+
` : ''}
|
|
141
|
+
</div>
|
|
142
|
+
</starlight-page>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default DocPage;
|