@beatzball/create-litro 0.1.4 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/recipes/starlight/recipe.config.d.ts +4 -0
  3. package/dist/recipes/starlight/recipe.config.d.ts.map +1 -0
  4. package/dist/recipes/starlight/recipe.config.js +9 -0
  5. package/dist/recipes/starlight/recipe.config.js.map +1 -0
  6. package/dist/recipes/starlight/recipe.config.ts +11 -0
  7. package/dist/recipes/starlight/template/_data/metadata.js +10 -0
  8. package/dist/recipes/starlight/template/app.ts +18 -0
  9. package/dist/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  10. package/dist/recipes/starlight/template/content/blog/release-notes.md +44 -0
  11. package/dist/recipes/starlight/template/content/blog/welcome.md +44 -0
  12. package/dist/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  13. package/dist/recipes/starlight/template/content/docs/configuration.md +77 -0
  14. package/dist/recipes/starlight/template/content/docs/getting-started.md +53 -0
  15. package/dist/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  16. package/dist/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  17. package/dist/recipes/starlight/template/content/docs/installation.md +54 -0
  18. package/dist/recipes/starlight/template/litro.recipe.json +7 -0
  19. package/dist/recipes/starlight/template/nitro.config.ts +57 -0
  20. package/dist/recipes/starlight/template/package.json +26 -0
  21. package/dist/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  22. package/dist/recipes/starlight/template/pages/blog/index.ts +114 -0
  23. package/dist/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  24. package/dist/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  25. package/dist/recipes/starlight/template/pages/index.ts +135 -0
  26. package/dist/recipes/starlight/template/public/styles/starlight.css +215 -0
  27. package/dist/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  28. package/dist/recipes/starlight/template/server/routes/[...].ts +57 -0
  29. package/dist/recipes/starlight/template/server/starlight.config.js +29 -0
  30. package/dist/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  31. package/dist/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  32. package/dist/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  33. package/dist/recipes/starlight/template/src/components/sl-card.ts +91 -0
  34. package/dist/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  35. package/dist/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  36. package/dist/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  37. package/dist/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  38. package/dist/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  39. package/dist/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  40. package/dist/recipes/starlight/template/src/date-utils.ts +20 -0
  41. package/dist/recipes/starlight/template/src/extract-headings.ts +68 -0
  42. package/dist/recipes/starlight/template/src/route-meta.ts +16 -0
  43. package/dist/recipes/starlight/template/tsconfig.json +14 -0
  44. package/dist/recipes/starlight/template/vite.config.ts +19 -0
  45. package/dist/src/scaffold.test.js +134 -0
  46. package/dist/src/scaffold.test.js.map +1 -1
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +1 -1
  49. package/recipes/starlight/recipe.config.ts +11 -0
  50. package/recipes/starlight/template/_data/metadata.js +10 -0
  51. package/recipes/starlight/template/app.ts +18 -0
  52. package/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  53. package/recipes/starlight/template/content/blog/release-notes.md +44 -0
  54. package/recipes/starlight/template/content/blog/welcome.md +44 -0
  55. package/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  56. package/recipes/starlight/template/content/docs/configuration.md +77 -0
  57. package/recipes/starlight/template/content/docs/getting-started.md +53 -0
  58. package/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  59. package/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  60. package/recipes/starlight/template/content/docs/installation.md +54 -0
  61. package/recipes/starlight/template/litro.recipe.json +7 -0
  62. package/recipes/starlight/template/nitro.config.ts +57 -0
  63. package/recipes/starlight/template/package.json +26 -0
  64. package/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  65. package/recipes/starlight/template/pages/blog/index.ts +114 -0
  66. package/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  67. package/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  68. package/recipes/starlight/template/pages/index.ts +135 -0
  69. package/recipes/starlight/template/public/styles/starlight.css +215 -0
  70. package/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  71. package/recipes/starlight/template/server/routes/[...].ts +57 -0
  72. package/recipes/starlight/template/server/starlight.config.js +29 -0
  73. package/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  74. package/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  75. package/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  76. package/recipes/starlight/template/src/components/sl-card.ts +91 -0
  77. package/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  78. package/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  79. package/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  80. package/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  81. package/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  82. package/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  83. package/recipes/starlight/template/src/date-utils.ts +20 -0
  84. package/recipes/starlight/template/src/extract-headings.ts +68 -0
  85. package/recipes/starlight/template/src/route-meta.ts +16 -0
  86. package/recipes/starlight/template/tsconfig.json +14 -0
  87. package/recipes/starlight/template/vite.config.ts +19 -0
  88. package/src/scaffold.test.ts +148 -0
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Learn how to set up and run your Litro Starlight docs site.
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ ## Welcome
9
+
10
+ You've just scaffolded a docs and blog site powered by **Litro** — a fullstack web framework that combines Lit web components with Nitro's server engine.
11
+
12
+ This site is a **static site** (SSG mode). Run `pnpm build` to pre-render every page to HTML, then `pnpm preview` to serve the output locally.
13
+
14
+ ## Prerequisites
15
+
16
+ You'll need **Node.js 20+** and a package manager (`pnpm`, `npm`, or `yarn`).
17
+
18
+ ## Install Dependencies
19
+
20
+ ```bash
21
+ pnpm install
22
+ ```
23
+
24
+ ## Start the Dev Server
25
+
26
+ ```bash
27
+ pnpm dev
28
+ ```
29
+
30
+ The dev server starts on `http://localhost:3030`. Changes to Lit components and Markdown content are reflected immediately.
31
+
32
+ ## Project Structure
33
+
34
+ ```
35
+ my-docs/
36
+ pages/ Lit page components (filename = route)
37
+ src/
38
+ components/ Starlight layout and UI components
39
+ content/
40
+ docs/ Documentation Markdown files
41
+ blog/ Blog post Markdown files
42
+ server/
43
+ starlight.config.js Site title, nav, and sidebar config
44
+ public/
45
+ styles/ CSS design tokens (--sl-* variables)
46
+ ```
47
+
48
+ ## Next Steps
49
+
50
+ - Edit `server/starlight.config.js` to update the site title, nav links, and sidebar groups.
51
+ - Add new docs pages to `content/docs/` — each `.md` file becomes a route under `/docs/`.
52
+ - Write blog posts in `content/blog/`.
53
+ - Run `pnpm build` to generate the static site.
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: Deploying
3
+ description: Deploy your Litro Starlight docs site to any CDN or static hosting platform.
4
+ sidebar:
5
+ order: 5
6
+ ---
7
+
8
+ ## Build
9
+
10
+ Generate the static HTML output:
11
+
12
+ ```bash
13
+ pnpm build
14
+ ```
15
+
16
+ This runs `litro build` under the hood, which:
17
+
18
+ 1. Scans `pages/` to discover all routes
19
+ 2. Calls `generateRoutes()` on each dynamic page to enumerate all paths
20
+ 3. Pre-renders every path to a `.html` file in `.output/public/`
21
+
22
+ ## Output
23
+
24
+ After a successful build, the static site is in `.output/public/`. You can serve this directory from any web server or CDN.
25
+
26
+ ## Platforms
27
+
28
+ ### Vercel
29
+
30
+ ```bash
31
+ # vercel.json
32
+ {
33
+ "outputDirectory": ".output/public"
34
+ }
35
+ ```
36
+
37
+ Or connect the repository in the Vercel dashboard — Vercel detects the static output automatically.
38
+
39
+ ### Netlify
40
+
41
+ ```toml
42
+ # netlify.toml
43
+ [build]
44
+ command = "pnpm build"
45
+ publish = ".output/public"
46
+ ```
47
+
48
+ ### Cloudflare Pages
49
+
50
+ Set the build output directory to `.output/public` in the Cloudflare Pages dashboard.
51
+
52
+ ### GitHub Pages
53
+
54
+ Add a GitHub Actions workflow:
55
+
56
+ ```yaml
57
+ - name: Build
58
+ run: pnpm build
59
+
60
+ - name: Deploy
61
+ uses: peaceiris/actions-gh-pages@v3
62
+ with:
63
+ github_token: GITHUB_TOKEN # set via repository secrets
64
+ publish_dir: .output/public
65
+ ```
66
+
67
+ ## Custom Domain
68
+
69
+ Configure the custom domain in your hosting provider's dashboard. No Litro-specific changes are needed — the static output is plain HTML, CSS, and JS.
70
+
71
+ ## Preview Locally
72
+
73
+ Before deploying, preview the production build locally:
74
+
75
+ ```bash
76
+ pnpm preview
77
+ ```
78
+
79
+ This serves the `.output/` directory using Nitro's static preset server on `http://localhost:3030`.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Your First Page
3
+ description: Create a new docs page and add it to the sidebar navigation.
4
+ sidebar:
5
+ order: 4
6
+ ---
7
+
8
+ ## Create a Markdown File
9
+
10
+ Add a new `.md` file to `content/docs/`. The filename (without extension) becomes the URL slug.
11
+
12
+ For example, create `content/docs/my-topic.md`:
13
+
14
+ ```markdown
15
+ ---
16
+ title: My Topic
17
+ description: A brief description for SEO and the sidebar.
18
+ ---
19
+
20
+ ## Introduction
21
+
22
+ Write your documentation here using standard Markdown.
23
+ ```
24
+
25
+ ## Add It to the Sidebar
26
+
27
+ Open `server/starlight.config.js` and add an entry to the appropriate sidebar group:
28
+
29
+ ```js
30
+ sidebar: [
31
+ {
32
+ label: 'My Section',
33
+ items: [
34
+ { label: 'My Topic', slug: 'my-topic' },
35
+ ],
36
+ },
37
+ ],
38
+ ```
39
+
40
+ The `slug` must match the filename (without `.md`). The page will be available at `/docs/my-topic`.
41
+
42
+ ## Frontmatter Fields
43
+
44
+ | Field | Type | Required | Description |
45
+ |---|---|---|---|
46
+ | `title` | `string` | Yes | Page title (shown in sidebar and `<title>`) |
47
+ | `description` | `string` | No | Short summary for SEO |
48
+ | `sidebar.order` | `number` | No | Controls sort order within the sidebar group |
49
+ | `sidebar.label` | `string` | No | Override the label shown in the sidebar |
50
+
51
+ ## Markdown Features
52
+
53
+ This site supports **GitHub Flavored Markdown (GFM)**, including:
54
+
55
+ - Tables (like the one above)
56
+ - Fenced code blocks with syntax highlighting
57
+ - Task lists: `- [ ] Todo`
58
+ - Strikethrough: `~~text~~`
59
+
60
+ Headings (`##`, `###`, `####`) are automatically extracted to build the table of contents shown on the right side of each docs page.
61
+
62
+ ## After Adding a Page
63
+
64
+ Run `pnpm build` to regenerate the static HTML for all routes, then `pnpm preview` to verify the new page appears in the sidebar and TOC.
@@ -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,7 @@
1
+ {
2
+ "recipe": "starlight",
3
+ "version": "{{recipeVersion}}",
4
+ "mode": "ssg",
5
+ "contentDir": "content",
6
+ "options": {}
7
+ }
@@ -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&hellip;</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;