@beatzball/create-litro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +185 -0
- package/README.md +89 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts +4 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.js +9 -0
- package/dist/recipes/11ty-blog/recipe.config.js.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.ts +11 -0
- package/dist/recipes/11ty-blog/template/app.ts +18 -0
- package/dist/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/dist/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/dist/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/dist/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/dist/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/dist/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/dist/recipes/11ty-blog/template/package.json +26 -0
- package/dist/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/dist/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/dist/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/dist/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/dist/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/dist/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/dist/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/dist/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/dist/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/dist/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/dist/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/dist/recipes/fullstack/recipe.config.d.ts +4 -0
- package/dist/recipes/fullstack/recipe.config.d.ts.map +1 -0
- package/dist/recipes/fullstack/recipe.config.js +8 -0
- package/dist/recipes/fullstack/recipe.config.js.map +1 -0
- package/dist/recipes/fullstack/recipe.config.ts +10 -0
- package/dist/recipes/fullstack/template/app.ts +20 -0
- package/dist/recipes/fullstack/template/nitro.config.ts +67 -0
- package/dist/recipes/fullstack/template/package.json +26 -0
- package/dist/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/dist/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/dist/recipes/fullstack/template/pages/index.ts +43 -0
- package/dist/recipes/fullstack/template/public/.gitkeep +0 -0
- package/dist/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/dist/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/dist/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/dist/recipes/fullstack/template/tsconfig.json +14 -0
- package/dist/recipes/fullstack/template/vite.config.ts +15 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +200 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/scaffold.d.ts +35 -0
- package/dist/src/scaffold.d.ts.map +1 -0
- package/dist/src/scaffold.js +166 -0
- package/dist/src/scaffold.js.map +1 -0
- package/dist/src/scaffold.test.d.ts +2 -0
- package/dist/src/scaffold.test.d.ts.map +1 -0
- package/dist/src/scaffold.test.js +204 -0
- package/dist/src/scaffold.test.js.map +1 -0
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +28 -0
- package/recipes/11ty-blog/recipe.config.ts +11 -0
- package/recipes/11ty-blog/template/app.ts +18 -0
- package/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/recipes/11ty-blog/template/package.json +26 -0
- package/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/recipes/fullstack/recipe.config.ts +10 -0
- package/recipes/fullstack/template/app.ts +20 -0
- package/recipes/fullstack/template/nitro.config.ts +67 -0
- package/recipes/fullstack/template/package.json +26 -0
- package/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/recipes/fullstack/template/pages/index.ts +43 -0
- package/recipes/fullstack/template/public/.gitkeep +0 -0
- package/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/recipes/fullstack/template/tsconfig.json +14 -0
- package/recipes/fullstack/template/vite.config.ts +15 -0
- package/src/index.ts +229 -0
- package/src/scaffold.test.ts +228 -0
- package/src/scaffold.ts +202 -0
- package/src/types.ts +26 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hello, World!
|
|
3
|
+
date: 2026-01-15
|
|
4
|
+
description: Welcome to your new Litro blog — here's what you need to know to start writing.
|
|
5
|
+
tags:
|
|
6
|
+
- posts
|
|
7
|
+
- welcome
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Welcome to Your Litro Blog
|
|
11
|
+
|
|
12
|
+
You've just scaffolded a brand-new blog powered by Litro — a fullstack web framework that combines Lit web components with Nitro's battle-tested server engine. This post is here to help you get your bearings and understand how everything fits together.
|
|
13
|
+
|
|
14
|
+
Content lives in the `content/blog/` directory as plain Markdown files. Each file becomes a post. The frontmatter at the top of every file (the `---` fenced block) controls the post's title, date, description, and tags. The rest of the file is your post body, written in standard Markdown — headings, paragraphs, lists, links, code blocks, all of it.
|
|
15
|
+
|
|
16
|
+
## Writing Your First Post
|
|
17
|
+
|
|
18
|
+
Create a new `.md` file inside `content/blog/` and fill in the frontmatter:
|
|
19
|
+
|
|
20
|
+
```markdown
|
|
21
|
+
---
|
|
22
|
+
title: My Second Post
|
|
23
|
+
date: 2026-02-01
|
|
24
|
+
description: A short summary shown on the blog listing page.
|
|
25
|
+
tags:
|
|
26
|
+
- posts
|
|
27
|
+
- my-tag
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
Your post content goes here.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `tags` field accepts a list. Every post in `content/blog/` automatically inherits the `posts` tag from `blog.11tydata.json`, so you only need to list additional tags in the post's own frontmatter. Tag pages are generated automatically at `/tags/<tag>` — no extra configuration required.
|
|
34
|
+
|
|
35
|
+
## How Content Is Served
|
|
36
|
+
|
|
37
|
+
Litro reads your Markdown files at build time (or on-demand in dev mode) using its content layer. The `getPosts()` function returns all published posts sorted newest-first, with each post's Markdown rendered to HTML in the `body` field. The `getPost(slug)` function fetches a single post by its URL slug, derived from the filename. For example, this file — `hello-world.md` — is available at `/blog/hello-world`.
|
|
38
|
+
|
|
39
|
+
Run `litro dev` to start the development server and visit `http://localhost:3030` to see your blog live. Changes to content files are picked up immediately without restarting the server.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineNitroConfig } from 'nitropack/config';
|
|
2
|
+
import type { Nitro } from 'nitropack';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { ssrPreset, 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
|
+
const mode = process.env.LITRO_MODE ?? 'server';
|
|
10
|
+
|
|
11
|
+
export default defineNitroConfig({
|
|
12
|
+
...(mode === 'static' ? ssgPreset() : ssrPreset()),
|
|
13
|
+
|
|
14
|
+
srcDir: 'server',
|
|
15
|
+
|
|
16
|
+
publicAssets: [
|
|
17
|
+
{ dir: '../dist/client', baseURL: '/_litro/', maxAge: 31536000 },
|
|
18
|
+
{ dir: '../public', baseURL: '/', maxAge: 0 },
|
|
19
|
+
// Serve co-located content images at their natural paths
|
|
20
|
+
{ dir: '../content', baseURL: '/content/', maxAge: 86400 },
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
externals: { inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'] },
|
|
24
|
+
|
|
25
|
+
esbuild: {
|
|
26
|
+
options: {
|
|
27
|
+
tsconfigRaw: {
|
|
28
|
+
compilerOptions: {
|
|
29
|
+
experimentalDecorators: true,
|
|
30
|
+
useDefineForClassFields: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
ignore: ['**/middleware/vite-dev.ts'],
|
|
37
|
+
handlers: [
|
|
38
|
+
{
|
|
39
|
+
middleware: true,
|
|
40
|
+
handler: resolve('./server/middleware/vite-dev.ts'),
|
|
41
|
+
env: 'dev',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
|
|
45
|
+
hooks: {
|
|
46
|
+
'build:before': async (nitro: Nitro) => {
|
|
47
|
+
await contentPlugin(nitro);
|
|
48
|
+
await pagesPlugin(nitro);
|
|
49
|
+
await ssgPlugin(nitro);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
compatibilityDate: '2025-01-01',
|
|
54
|
+
|
|
55
|
+
routeRules: {
|
|
56
|
+
'/_litro/**': {
|
|
57
|
+
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -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.10.4",
|
|
23
|
+
"vite": "^5.4.11",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
3
|
+
import { customElement, state } 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 { getPost, getPosts } from 'litro:content';
|
|
9
|
+
|
|
10
|
+
export interface PostData {
|
|
11
|
+
post: Post;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
15
|
+
|
|
16
|
+
export const pageData = definePageData(async (event) => {
|
|
17
|
+
const slug = event.context.params?.slug ?? '';
|
|
18
|
+
const post = await getPost(slug);
|
|
19
|
+
if (!post) {
|
|
20
|
+
throw createError({ statusCode: 404, message: `Post not found: ${slug}` });
|
|
21
|
+
}
|
|
22
|
+
return { post } satisfies PostData;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
26
|
+
const posts = await getPosts();
|
|
27
|
+
return posts.map(post => post.url);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@customElement('page-blog-slug')
|
|
31
|
+
export class BlogPostPage extends LitroPage {
|
|
32
|
+
@state() declare serverData: PostData | null;
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
const { post } = this.serverData ?? {};
|
|
36
|
+
if (!post) return html`<p>Loading…</p>`;
|
|
37
|
+
return html`
|
|
38
|
+
<article>
|
|
39
|
+
<header>
|
|
40
|
+
<h1>${post.title}</h1>
|
|
41
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
42
|
+
${post.tags.length > 0 ? html`
|
|
43
|
+
<ul class="tags">
|
|
44
|
+
${post.tags.map(tag => html`
|
|
45
|
+
<li><a href="/tags/${tag}">#${tag}</a></li>
|
|
46
|
+
`)}
|
|
47
|
+
</ul>
|
|
48
|
+
` : ''}
|
|
49
|
+
</header>
|
|
50
|
+
<div class="post-body">
|
|
51
|
+
<!-- unsafeHTML renders the Markdown-generated HTML directly.
|
|
52
|
+
The content directory is trusted-author-only; do not place
|
|
53
|
+
user-submitted or untrusted content here without first
|
|
54
|
+
sanitizing with a library such as rehype-sanitize. -->
|
|
55
|
+
${unsafeHTML(post.body)}
|
|
56
|
+
</div>
|
|
57
|
+
<footer>
|
|
58
|
+
<a href="/blog">← Back to Blog</a>
|
|
59
|
+
</footer>
|
|
60
|
+
</article>
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default BlogPostPage;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } 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
|
+
|
|
8
|
+
export interface BlogIndexData {
|
|
9
|
+
posts: Post[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
13
|
+
|
|
14
|
+
export const pageData = definePageData(async (_event) => {
|
|
15
|
+
const posts = await getPosts();
|
|
16
|
+
return { posts } satisfies BlogIndexData;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
@customElement('page-blog')
|
|
20
|
+
export class BlogIndexPage extends LitroPage {
|
|
21
|
+
@state() declare serverData: BlogIndexData | null;
|
|
22
|
+
|
|
23
|
+
render() {
|
|
24
|
+
const { posts = [] } = this.serverData ?? {};
|
|
25
|
+
return html`
|
|
26
|
+
<main>
|
|
27
|
+
<h1>Blog</h1>
|
|
28
|
+
<ul>
|
|
29
|
+
${posts.map(post => html`
|
|
30
|
+
<li>
|
|
31
|
+
<a href="${post.url}">${post.title}</a>
|
|
32
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
33
|
+
${post.description ? html`<p>${post.description}</p>` : ''}
|
|
34
|
+
</li>
|
|
35
|
+
`)}
|
|
36
|
+
</ul>
|
|
37
|
+
<a href="/">← Home</a>
|
|
38
|
+
</main>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default BlogIndexPage;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } 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, getGlobalData } from 'litro:content';
|
|
7
|
+
|
|
8
|
+
export interface HomeData {
|
|
9
|
+
recentPosts: Post[];
|
|
10
|
+
siteTitle: string;
|
|
11
|
+
siteDescription: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
15
|
+
|
|
16
|
+
export const pageData = definePageData(async (_event) => {
|
|
17
|
+
const [recentPosts, metadata] = await Promise.all([
|
|
18
|
+
getPosts({ limit: 5 }),
|
|
19
|
+
getGlobalData(),
|
|
20
|
+
]);
|
|
21
|
+
return {
|
|
22
|
+
recentPosts,
|
|
23
|
+
siteTitle: String(metadata.title ?? '{{projectName}}'),
|
|
24
|
+
siteDescription: String(metadata.description ?? ''),
|
|
25
|
+
} satisfies HomeData;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
@customElement('page-home')
|
|
29
|
+
export class HomePage extends LitroPage {
|
|
30
|
+
@state() declare serverData: HomeData | null;
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
const { recentPosts = [], siteTitle = '{{projectName}}', siteDescription = '' } = this.serverData ?? {};
|
|
34
|
+
return html`
|
|
35
|
+
<main>
|
|
36
|
+
<header>
|
|
37
|
+
<h1>${siteTitle}</h1>
|
|
38
|
+
${siteDescription ? html`<p>${siteDescription}</p>` : ''}
|
|
39
|
+
</header>
|
|
40
|
+
<section>
|
|
41
|
+
<h2>Recent Posts</h2>
|
|
42
|
+
<ul>
|
|
43
|
+
${recentPosts.map(post => html`
|
|
44
|
+
<li>
|
|
45
|
+
<a href="${post.url}">${post.title}</a>
|
|
46
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
47
|
+
${post.description ? html`<p>${post.description}</p>` : ''}
|
|
48
|
+
</li>
|
|
49
|
+
`)}
|
|
50
|
+
</ul>
|
|
51
|
+
<p><a href="/blog">All Posts →</a></p>
|
|
52
|
+
</section>
|
|
53
|
+
</main>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default HomePage;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } 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, getTags } from 'litro:content';
|
|
7
|
+
|
|
8
|
+
export interface TagData {
|
|
9
|
+
tag: string;
|
|
10
|
+
posts: Post[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
14
|
+
|
|
15
|
+
export const pageData = definePageData(async (event) => {
|
|
16
|
+
const tag = event.context.params?.tag ?? '';
|
|
17
|
+
const posts = await getPosts({ tag });
|
|
18
|
+
return { tag, posts } satisfies TagData;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
22
|
+
const tags = await getTags();
|
|
23
|
+
return tags.map(tag => `/tags/${tag}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@customElement('page-tags-tag')
|
|
27
|
+
export class TagPage extends LitroPage {
|
|
28
|
+
@state() declare serverData: TagData | null;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
const { tag = '', posts = [] } = this.serverData ?? {};
|
|
32
|
+
return html`
|
|
33
|
+
<main>
|
|
34
|
+
<h1>Posts tagged: #${tag}</h1>
|
|
35
|
+
<ul>
|
|
36
|
+
${posts.map(post => html`
|
|
37
|
+
<li>
|
|
38
|
+
<a href="${post.url}">${post.title}</a>
|
|
39
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
40
|
+
</li>
|
|
41
|
+
`)}
|
|
42
|
+
</ul>
|
|
43
|
+
<p>
|
|
44
|
+
<a href="/blog">← All Posts</a>
|
|
45
|
+
|
|
|
46
|
+
<a href="/">← Home</a>
|
|
47
|
+
</p>
|
|
48
|
+
</main>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default TagPage;
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery } from 'h3';
|
|
2
|
+
import { getPosts } from 'litro:content';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const query = getQuery(event);
|
|
6
|
+
const tag = typeof query.tag === 'string' ? query.tag : undefined;
|
|
7
|
+
return getPosts({ tag });
|
|
8
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineEventHandler, fromNodeMiddleware } from 'h3';
|
|
2
|
+
|
|
3
|
+
let viteHandlerPromise: Promise<ReturnType<typeof fromNodeMiddleware>> | null = null;
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
if (!process.dev || !process.env.NITRO_DEV_WORKER_ID) return;
|
|
7
|
+
|
|
8
|
+
if (!viteHandlerPromise) {
|
|
9
|
+
const httpServer = (event.node.req.socket as import('node:net').Socket & {
|
|
10
|
+
server?: import('node:http').Server;
|
|
11
|
+
}).server;
|
|
12
|
+
|
|
13
|
+
viteHandlerPromise = import('vite')
|
|
14
|
+
.then(({ createServer }) =>
|
|
15
|
+
createServer({
|
|
16
|
+
server: {
|
|
17
|
+
middlewareMode: true,
|
|
18
|
+
hmr: httpServer ? { server: httpServer } : true,
|
|
19
|
+
},
|
|
20
|
+
appType: 'custom',
|
|
21
|
+
root: process.cwd(),
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
.then((server) => fromNodeMiddleware(server.middlewares));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const viteHandler = await viteHandlerPromise;
|
|
28
|
+
return viteHandler(event);
|
|
29
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader, getRequestURL } from 'h3';
|
|
2
|
+
import { createPageHandler } from '@beatzball/litro/runtime/create-page-handler.js';
|
|
3
|
+
import type { LitroRoute } from '@beatzball/litro';
|
|
4
|
+
import { routes, pageModules } from '#litro/page-manifest';
|
|
5
|
+
|
|
6
|
+
function matchRoute(
|
|
7
|
+
pathname: string,
|
|
8
|
+
): { route: LitroRoute; params: Record<string, string> } | undefined {
|
|
9
|
+
for (const route of routes) {
|
|
10
|
+
if (route.isCatchAll) return { route, params: {} };
|
|
11
|
+
|
|
12
|
+
if (!route.isDynamic) {
|
|
13
|
+
if (pathname === route.path) return { route, params: {} };
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const regexStr =
|
|
18
|
+
'^' +
|
|
19
|
+
route.path
|
|
20
|
+
.replace(/:([^/]+)\(\.\*\)\*/g, '(?<$1>.+)')
|
|
21
|
+
.replace(/:([^/?]+)\?/g, '(?<$1>[^/]*)?')
|
|
22
|
+
.replace(/:([^/]+)/g, '(?<$1>[^/]+)') +
|
|
23
|
+
'$';
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const match = pathname.match(new RegExp(regexStr));
|
|
27
|
+
if (match) return { route, params: (match.groups ?? {}) as Record<string, string> };
|
|
28
|
+
} catch {
|
|
29
|
+
// malformed pattern — skip
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default defineEventHandler(async (event) => {
|
|
36
|
+
const pathname = getRequestURL(event).pathname;
|
|
37
|
+
const result = matchRoute(pathname);
|
|
38
|
+
|
|
39
|
+
if (!result) {
|
|
40
|
+
setResponseHeader(event, 'content-type', 'text/html; charset=utf-8');
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en"><head><meta charset="UTF-8" /><title>404</title></head>
|
|
43
|
+
<body><h1>404 — Not Found</h1><p>No page matched <code>${pathname}</code>.</p></body>
|
|
44
|
+
</html>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { route: matched, params } = result;
|
|
48
|
+
event.context.params = { ...event.context.params, ...params };
|
|
49
|
+
|
|
50
|
+
const handler = createPageHandler({
|
|
51
|
+
route: matched,
|
|
52
|
+
pageModule: pageModules[matched.filePath],
|
|
53
|
+
});
|
|
54
|
+
return handler(event);
|
|
55
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"useDefineForClassFields": false
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
13
|
+
"exclude": ["node_modules", "dist", ".nitro"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import litroContentPlugin from '@beatzball/litro/vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [litroContentPlugin()],
|
|
6
|
+
base: '/_litro/',
|
|
7
|
+
resolve: {
|
|
8
|
+
conditions: ['source', 'browser', 'module', 'import', 'default'],
|
|
9
|
+
},
|
|
10
|
+
build: {
|
|
11
|
+
outDir: 'dist/client',
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: 'app.ts',
|
|
14
|
+
output: {
|
|
15
|
+
entryFileNames: '[name].js',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LitroRecipe } from '../../src/types.js';
|
|
2
|
+
|
|
3
|
+
const recipe: LitroRecipe = {
|
|
4
|
+
name: 'fullstack',
|
|
5
|
+
displayName: 'Fullstack App',
|
|
6
|
+
description: 'Full-stack Lit + Nitro app with SSR and blog example pages',
|
|
7
|
+
mode: 'both',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default recipe;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// CRITICAL: must be first — patches LitElement before any component is loaded.
|
|
2
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
|
|
3
|
+
|
|
4
|
+
// Client runtime: router outlet and link custom elements.
|
|
5
|
+
import 'litro/runtime/LitroOutlet.js';
|
|
6
|
+
import 'litro/runtime/LitroLink.js';
|
|
7
|
+
|
|
8
|
+
// Routes generated by the page scanner before each vite build.
|
|
9
|
+
// routes.generated.ts lives at the project root (not in dist/) so Vite's
|
|
10
|
+
// emptyOutDir does not delete it between builds.
|
|
11
|
+
import { routes } from './routes.generated.js';
|
|
12
|
+
|
|
13
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
14
|
+
const outlet = document.querySelector('litro-outlet') as (Element & { routes: unknown }) | null;
|
|
15
|
+
if (outlet) {
|
|
16
|
+
outlet.routes = routes;
|
|
17
|
+
} else {
|
|
18
|
+
console.warn('[litro] <litro-outlet> not found — router will not start.');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defineNitroConfig } from 'nitropack/config';
|
|
2
|
+
import type { Nitro } from 'nitropack';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { ssrPreset, ssgPreset } from '@beatzball/litro/config';
|
|
5
|
+
import pagesPlugin from '@beatzball/litro/plugins';
|
|
6
|
+
import ssgPlugin from '@beatzball/litro/plugins/ssg';
|
|
7
|
+
|
|
8
|
+
// LITRO_MODE controls the deployment target at build time:
|
|
9
|
+
// LITRO_MODE=server litro build (default — Node.js server)
|
|
10
|
+
// LITRO_MODE=static litro generate (SSG — static HTML for CDN)
|
|
11
|
+
const mode = process.env.LITRO_MODE ?? 'server';
|
|
12
|
+
|
|
13
|
+
export default defineNitroConfig({
|
|
14
|
+
...(mode === 'static' ? ssgPreset() : ssrPreset()),
|
|
15
|
+
|
|
16
|
+
// Nitro auto-discovers server/routes/, server/api/, server/middleware/
|
|
17
|
+
srcDir: 'server',
|
|
18
|
+
|
|
19
|
+
// publicAssets.dir is resolved relative to srcDir ('server/').
|
|
20
|
+
// Use '../dist/client' (not 'dist/client') to reach <rootDir>/dist/client.
|
|
21
|
+
publicAssets: [
|
|
22
|
+
{ dir: '../dist/client', baseURL: '/_litro/', maxAge: 31536000 },
|
|
23
|
+
{ dir: '../public', baseURL: '/', maxAge: 0 },
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
// Must be bundled for edge runtimes (Cloudflare Workers, Vercel Edge).
|
|
27
|
+
externals: { inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'] },
|
|
28
|
+
|
|
29
|
+
// Lit uses legacy experimental decorators.
|
|
30
|
+
esbuild: {
|
|
31
|
+
options: {
|
|
32
|
+
tsconfigRaw: {
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
experimentalDecorators: true,
|
|
35
|
+
useDefineForClassFields: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Exclude vite-dev.ts from auto-discovery so import('vite') never enters
|
|
42
|
+
// the production module graph. Re-register with env:'dev' so it only runs
|
|
43
|
+
// during development.
|
|
44
|
+
ignore: ['**/middleware/vite-dev.ts'],
|
|
45
|
+
handlers: [
|
|
46
|
+
{
|
|
47
|
+
middleware: true,
|
|
48
|
+
handler: resolve('./server/middleware/vite-dev.ts'),
|
|
49
|
+
env: 'dev',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
hooks: {
|
|
54
|
+
'build:before': async (nitro: Nitro) => {
|
|
55
|
+
await pagesPlugin(nitro);
|
|
56
|
+
await ssgPlugin(nitro);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
compatibilityDate: '2025-01-01',
|
|
61
|
+
|
|
62
|
+
routeRules: {
|
|
63
|
+
'/_litro/**': {
|
|
64
|
+
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -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.10.4",
|
|
23
|
+
"vite": "^5.4.11",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { definePageData } from '@beatzball/litro';
|
|
4
|
+
|
|
5
|
+
export interface PostData {
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Runs on the server; event.context.params contains the matched route params.
|
|
12
|
+
export const pageData = definePageData(async (event) => {
|
|
13
|
+
const slug = event.context.params?.slug ?? '';
|
|
14
|
+
return {
|
|
15
|
+
slug,
|
|
16
|
+
title: `Post: ${slug}`,
|
|
17
|
+
content: `This is the content for the "${slug}" post.`,
|
|
18
|
+
} satisfies PostData;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Tells the SSG which concrete paths to prerender when LITRO_MODE=static.
|
|
22
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
23
|
+
return ['/blog/hello-world', '/blog/getting-started', '/blog/about-litro'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@customElement('page-blog-slug')
|
|
27
|
+
export class BlogPostPage extends LitElement {
|
|
28
|
+
@state() declare serverData: PostData | null;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
return html`
|
|
32
|
+
<article>
|
|
33
|
+
<h1>${this.serverData?.title ?? 'Loading…'}</h1>
|
|
34
|
+
<p>${this.serverData?.content ?? ''}</p>
|
|
35
|
+
<litro-link href="/blog">← Back to Blog</litro-link>
|
|
36
|
+
|
|
|
37
|
+
<litro-link href="/">← Home</litro-link>
|
|
38
|
+
</article>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default BlogPostPage;
|