@emberkit/cli 0.5.2 → 0.6.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.
@@ -2,12 +2,13 @@ import { existsSync, mkdirSync, writeFileSync } from "fs";
2
2
  import { resolve, join } from "path";
3
3
  import { execSync } from "child_process";
4
4
  import { getPackageManager, getInstallCommand } from "../utils/filesystem.js";
5
- import { starterFiles, withUiTemplate } from "../templates/projects.js";
6
- import { minimalTemplate } from "../templates/minimal.js";
7
- import { blogTemplate } from "../templates/blog.js";
8
- import { saasTemplate } from "../templates/saas.js";
9
- import { dashboardTemplate } from "../templates/dashboard.js";
10
- import { apiTemplate } from "../templates/api.js";
5
+ import { starterFiles } from "../templates/projects/starter.js";
6
+ import { withUiTemplate } from "../templates/projects/with-ui.js";
7
+ import { minimalTemplate } from "../templates/minimal/minimal.js";
8
+ import { blogTemplate } from "../templates/blog/blog.js";
9
+ import { saasTemplate } from "../templates/saas/saas.js";
10
+ import { dashboardTemplate } from "../templates/dashboard/dashboard.js";
11
+ import { apiTemplate } from "../templates/api/api.js";
11
12
  const RESET = "\x1b[0m";
12
13
  const BOLD = "\x1b[1m";
13
14
  const DIM = "\x1b[2m";
@@ -0,0 +1,12 @@
1
+ export const actionTemplate = `import type { ActionFunction, LoaderResult } from '@emberkit/core';
2
+
3
+ export const action: ActionFunction = async ({ params, request }) => {
4
+ const formData = await request.formData();
5
+
6
+ return {
7
+ data: {
8
+ success: true,
9
+ },
10
+ } as LoaderResult<unknown>;
11
+ };
12
+ `;
@@ -0,0 +1,280 @@
1
+ export const apiTemplate = {
2
+ "package.json": `{
3
+ "name": "{{name}}",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "emberkit dev",
9
+ "build": "emberkit build",
10
+ "preview": "emberkit preview"
11
+ },
12
+ "dependencies": {
13
+ "@emberkit/core": "^0.2.4"
14
+ },
15
+ "devDependencies": {
16
+ "@emberkit/cli": "^0.2.4",
17
+ "typescript": "^5.7.0",
18
+ "vite": "^6.0.0"
19
+ }
20
+ }`,
21
+ "tsconfig.json": `{
22
+ "compilerOptions": {
23
+ "target": "ES2022",
24
+ "module": "ESNext",
25
+ "moduleResolution": "bundler",
26
+ "jsx": "react-jsx",
27
+ "jsxImportSource": "@emberkit/core",
28
+ "strict": true,
29
+ "esModuleInterop": true,
30
+ "skipLibCheck": true,
31
+ "forceConsistentCasingInFileNames": true,
32
+ "resolveJsonModule": true,
33
+ "isolatedModules": true,
34
+ "noEmit": true,
35
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
36
+ "paths": {
37
+ "@/*": ["./src/*"]
38
+ }
39
+ },
40
+ "include": ["src"],
41
+ "exclude": ["node_modules", "dist"]
42
+ }`,
43
+ "vite.config.ts": `import { defineConfig } from 'vite';
44
+ import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
45
+
46
+ export default defineConfig({
47
+ plugins: [emberkitVitePlugin()],
48
+ server: {
49
+ port: 3000,
50
+ host: 'localhost',
51
+ },
52
+ esbuild: {
53
+ jsxImportSource: '@emberkit/core',
54
+ },
55
+ });`,
56
+ "index.html": `<!DOCTYPE html>
57
+ <html lang="en">
58
+ <head>
59
+ <meta charset="UTF-8">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
+ <title>{{name}} API</title>
62
+ </head>
63
+ <body id="app">
64
+ <script type="module" src="/src/index.tsx"></script>
65
+ </body>
66
+ </html>`,
67
+ "src/index.tsx": `import { render } from '@emberkit/core';
68
+ import { routes } from 'virtual:emberkit-routes';
69
+ import App from './routes/index';
70
+
71
+ const root = document.getElementById('app');
72
+
73
+ if (root) {
74
+ try {
75
+ render(App, root, { routes });
76
+ } catch (error) {
77
+ console.error('[entry] Render error:', error);
78
+ }
79
+ }`,
80
+ "src/routes/index.tsx": `import type { RouteComponent } from '@emberkit/core';
81
+
82
+ const ApiHome: RouteComponent = () => {
83
+ const endpoints = [
84
+ { method: 'GET', path: '/api/users', desc: 'List all users' },
85
+ { method: 'POST', path: '/api/users', desc: 'Create a new user' },
86
+ { method: 'GET', path: '/api/users/:id', desc: 'Get a user by ID' },
87
+ { method: 'PUT', path: '/api/users/:id', desc: 'Update a user' },
88
+ { method: 'DELETE', path: '/api/users/:id', desc: 'Delete a user' },
89
+ { method: 'GET', path: '/api/health', desc: 'Health check' },
90
+ ];
91
+
92
+ const methodColor = (method: string) => {
93
+ switch (method) {
94
+ case 'GET': return 'bg-green-100 text-green-700';
95
+ case 'POST': return 'bg-blue-100 text-blue-700';
96
+ case 'PUT': return 'bg-yellow-100 text-yellow-700';
97
+ case 'DELETE': return 'bg-red-100 text-red-700';
98
+ default: return 'bg-gray-100 text-gray-700';
99
+ }
100
+ };
101
+
102
+ return (
103
+ <div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: '800px', margin: '2rem auto', padding: '0 1rem' }}>
104
+ <header className="mb-12">
105
+ <h1 className="text-3xl font-bold mb-2">{{name}} API</h1>
106
+ <p className="text-gray-600">RESTful API built with EmberKit</p>
107
+ <div className="mt-4 p-4 bg-gray-100 rounded-lg">
108
+ <code className="text-sm">Base URL: http://localhost:3000</code>
109
+ </div>
110
+ </header>
111
+
112
+ <section>
113
+ <h2 className="text-xl font-semibold mb-4">Endpoints</h2>
114
+ <div className="space-y-3">
115
+ {endpoints.map((ep) => (
116
+ <div key={ep.method + ep.path} className="flex items-center gap-4 p-4 border border-gray-200 rounded-lg">
117
+ <span className={\`px-2 py-1 rounded text-xs font-bold \${methodColor(ep.method)}\`}>
118
+ {ep.method}
119
+ </span>
120
+ <code className="text-sm font-mono">{ep.path}</code>
121
+ <span className="text-gray-500 text-sm ml-auto">{ep.desc}</span>
122
+ </div>
123
+ ))}
124
+ </div>
125
+ </section>
126
+
127
+ <section className="mt-12">
128
+ <h2 className="text-xl font-semibold mb-4">Example Response</h2>
129
+ <pre className="p-4 bg-gray-900 text-gray-100 rounded-lg overflow-x-auto text-sm">
130
+ {JSON.stringify({
131
+ data: [
132
+ { id: 1, name: 'John Doe', email: 'john@example.com' },
133
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
134
+ ],
135
+ meta: { page: 1, total: 2 },
136
+ }, null, 2)}
137
+ </pre>
138
+ </section>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default ApiHome;`,
144
+ "src/routes/_api/users.ts": `import type { LoaderFunction, LoaderResult, ActionFunction } from '@emberkit/core';
145
+
146
+ // In-memory store (replace with database in production)
147
+ const users = [
148
+ { id: 1, name: 'John Doe', email: 'john@example.com', createdAt: '2026-01-15' },
149
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', createdAt: '2026-02-20' },
150
+ { id: 3, name: 'Mike Johnson', email: 'mike@example.com', createdAt: '2026-03-10' },
151
+ ];
152
+
153
+ let nextId = 4;
154
+
155
+ export const GET: LoaderFunction = async ({ query }) => {
156
+ const page = parseInt(query.page as string) || 1;
157
+ const limit = parseInt(query.limit as string) || 10;
158
+ const start = (page - 1) * limit;
159
+ const end = start + limit;
160
+
161
+ return {
162
+ data: {
163
+ users: users.slice(start, end),
164
+ meta: {
165
+ page,
166
+ limit,
167
+ total: users.length,
168
+ },
169
+ },
170
+ } as LoaderResult<unknown>;
171
+ };
172
+
173
+ export const POST: ActionFunction = async ({ request }) => {
174
+ const body = await request.json();
175
+
176
+ if (!body.name || !body.email) {
177
+ return {
178
+ error: {
179
+ code: 'VALIDATION_ERROR',
180
+ message: 'Name and email are required',
181
+ status: 400,
182
+ },
183
+ } as LoaderResult<unknown>;
184
+ }
185
+
186
+ const newUser = {
187
+ id: nextId++,
188
+ name: body.name,
189
+ email: body.email,
190
+ createdAt: new Date().toISOString().split('T')[0],
191
+ };
192
+
193
+ users.push(newUser);
194
+
195
+ return {
196
+ data: newUser,
197
+ } as LoaderResult<unknown>;
198
+ };`,
199
+ "src/routes/_api/users/[id].ts": `import type { LoaderFunction, LoaderResult, ActionFunction } from '@emberkit/core';
200
+
201
+ // In-memory store (replace with database in production)
202
+ const users = [
203
+ { id: 1, name: 'John Doe', email: 'john@example.com', createdAt: '2026-01-15' },
204
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', createdAt: '2026-02-20' },
205
+ { id: 3, name: 'Mike Johnson', email: 'mike@example.com', createdAt: '2026-03-10' },
206
+ ];
207
+
208
+ export const GET: LoaderFunction = async ({ params }) => {
209
+ const id = parseInt(params.id);
210
+ const user = users.find((u) => u.id === id);
211
+
212
+ if (!user) {
213
+ return {
214
+ error: {
215
+ code: 'NOT_FOUND',
216
+ message: 'User not found',
217
+ status: 404,
218
+ },
219
+ } as LoaderResult<unknown>;
220
+ }
221
+
222
+ return {
223
+ data: user,
224
+ } as LoaderResult<unknown>;
225
+ };
226
+
227
+ export const PUT: ActionFunction = async ({ params, request }) => {
228
+ const id = parseInt(params.id);
229
+ const index = users.findIndex((u) => u.id === id);
230
+
231
+ if (index === -1) {
232
+ return {
233
+ error: {
234
+ code: 'NOT_FOUND',
235
+ message: 'User not found',
236
+ status: 404,
237
+ },
238
+ } as LoaderResult<unknown>;
239
+ }
240
+
241
+ const body = await request.json();
242
+ users[index] = { ...users[index], ...body };
243
+
244
+ return {
245
+ data: users[index],
246
+ } as LoaderResult<unknown>;
247
+ };
248
+
249
+ export const DELETE: ActionFunction = async ({ params }) => {
250
+ const id = parseInt(params.id);
251
+ const index = users.findIndex((u) => u.id === id);
252
+
253
+ if (index === -1) {
254
+ return {
255
+ error: {
256
+ code: 'NOT_FOUND',
257
+ message: 'User not found',
258
+ status: 404,
259
+ },
260
+ } as LoaderResult<unknown>;
261
+ }
262
+
263
+ users.splice(index, 1);
264
+
265
+ return {
266
+ data: { success: true },
267
+ } as LoaderResult<unknown>;
268
+ };`,
269
+ "src/routes/_api/health.ts": `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
270
+
271
+ export const GET: LoaderFunction = async () => {
272
+ return {
273
+ data: {
274
+ status: 'ok',
275
+ timestamp: new Date().toISOString(),
276
+ uptime: process.uptime(),
277
+ },
278
+ } as LoaderResult<unknown>;
279
+ };`,
280
+ };
@@ -0,0 +1,20 @@
1
+ export const apiRouteTemplate = `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
2
+
3
+ export const GET: LoaderFunction = async ({ params, query, request }) => {
4
+ return {
5
+ data: {
6
+ message: 'Hello from API',
7
+ },
8
+ } as LoaderResult<unknown>;
9
+ };
10
+
11
+ export const POST: LoaderFunction = async ({ request }) => {
12
+ const body = await request.json();
13
+
14
+ return {
15
+ data: {
16
+ received: body,
17
+ },
18
+ } as LoaderResult<unknown>;
19
+ };
20
+ `;
@@ -0,0 +1,358 @@
1
+ export const blogTemplate = {
2
+ "package.json": `{
3
+ "name": "{{name}}",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "emberkit dev",
9
+ "build": "emberkit build",
10
+ "preview": "emberkit preview"
11
+ },
12
+ "dependencies": {
13
+ "@emberkit/core": "^0.2.4"
14
+ },
15
+ "devDependencies": {
16
+ "@emberkit/cli": "^0.2.4",
17
+ "typescript": "^5.7.0",
18
+ "vite": "^6.0.0",
19
+ "tailwindcss": "^4.0.0",
20
+ "@tailwindcss/vite": "^4.0.0"
21
+ }
22
+ }`,
23
+ "tsconfig.json": `{
24
+ "compilerOptions": {
25
+ "target": "ES2022",
26
+ "module": "ESNext",
27
+ "moduleResolution": "bundler",
28
+ "jsx": "react-jsx",
29
+ "jsxImportSource": "@emberkit/core",
30
+ "strict": true,
31
+ "esModuleInterop": true,
32
+ "skipLibCheck": true,
33
+ "forceConsistentCasingInFileNames": true,
34
+ "resolveJsonModule": true,
35
+ "isolatedModules": true,
36
+ "noEmit": true,
37
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
38
+ "paths": {
39
+ "@/*": ["./src/*"]
40
+ }
41
+ },
42
+ "include": ["src"],
43
+ "exclude": ["node_modules", "dist"]
44
+ }`,
45
+ "vite.config.ts": `import { defineConfig } from 'vite';
46
+ import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
47
+ import tailwindcss from '@tailwindcss/vite';
48
+
49
+ export default defineConfig({
50
+ plugins: [emberkitVitePlugin(), tailwindcss()],
51
+ server: {
52
+ port: 3000,
53
+ host: 'localhost',
54
+ },
55
+ esbuild: {
56
+ jsxImportSource: '@emberkit/core',
57
+ },
58
+ });`,
59
+ "index.html": `<!DOCTYPE html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="UTF-8">
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
+ <title>{{name}}</title>
65
+ <link rel="preconnect" href="https://fonts.googleapis.com">
66
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
67
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Merriweather:wght@400;700&display=swap" rel="stylesheet">
68
+ </head>
69
+ <body id="app">
70
+ <script type="module" src="/src/index.tsx"></script>
71
+ </body>
72
+ </html>`,
73
+ "src/index.tsx": `import { render } from '@emberkit/core';
74
+ import { routes } from 'virtual:emberkit-routes';
75
+ import App from './routes/_layout';
76
+ import './styles.css';
77
+
78
+ const root = document.getElementById('app');
79
+
80
+ if (root) {
81
+ try {
82
+ render(App, root, { routes });
83
+ } catch (error) {
84
+ console.error('[entry] Render error:', error);
85
+ }
86
+ }`,
87
+ "src/styles.css": `@import "tailwindcss";
88
+
89
+ @theme {
90
+ --font-serif: 'Merriweather', Georgia, serif;
91
+ --font-sans: 'Inter', system-ui, sans-serif;
92
+ }
93
+
94
+ body {
95
+ @apply bg-white text-gray-900 font-sans;
96
+ }
97
+
98
+ .prose {
99
+ @apply max-w-none;
100
+ }
101
+
102
+ .prose h1, .prose h2, .prose h3 {
103
+ @apply font-serif font-bold;
104
+ }
105
+
106
+ .prose p {
107
+ @apply leading-relaxed;
108
+ }
109
+
110
+ .prose a {
111
+ @apply text-blue-600 no-underline hover:underline;
112
+ }
113
+
114
+ .prose code {
115
+ @apply bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono;
116
+ }
117
+
118
+ .prose pre {
119
+ @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto;
120
+ }
121
+
122
+ .prose pre code {
123
+ @apply bg-transparent p-0 text-sm;
124
+ }
125
+
126
+ .prose blockquote {
127
+ @apply border-l-4 border-gray-300 pl-4 italic text-gray-600;
128
+ }
129
+
130
+ .prose img {
131
+ @apply rounded-lg;
132
+ }
133
+
134
+ .prose ul {
135
+ @apply list-disc pl-6;
136
+ }
137
+
138
+ .prose ol {
139
+ @apply list-decimal pl-6;
140
+ }`,
141
+ "src/routes/_layout.tsx": `import type { RouteComponent } from '@emberkit/core';
142
+ import { Head } from '@emberkit/core';
143
+
144
+ const Layout: RouteComponent = ({ children }) => {
145
+ return (
146
+ <>
147
+ <Head>
148
+ <title>{{name}}</title>
149
+ <meta name="description" content="A minimal blog built with EmberKit" />
150
+ </Head>
151
+ <div className="min-h-screen flex flex-col">
152
+ <header className="border-b border-gray-200">
153
+ <div className="max-w-3xl mx-auto px-6 py-6 flex items-center justify-between">
154
+ <a href="/" className="text-xl font-bold font-serif">{{name}}</a>
155
+ <nav className="flex gap-6 text-sm text-gray-600">
156
+ <a href="/" className="hover:text-gray-900">Posts</a>
157
+ <a href="/about" className="hover:text-gray-900">About</a>
158
+ </nav>
159
+ </div>
160
+ </header>
161
+ <main className="flex-1">{children}</main>
162
+ <footer className="border-t border-gray-200 py-8 text-center text-sm text-gray-500">
163
+ <p>Built with <a href="https://emberkit.dev" className="text-gray-700 hover:underline">EmberKit</a></p>
164
+ </footer>
165
+ </div>
166
+ </>
167
+ );
168
+ };
169
+
170
+ export default Layout;`,
171
+ "src/routes/index.tsx": `import type { RouteComponent } from '@emberkit/core';
172
+
173
+ interface Post {
174
+ slug: string;
175
+ title: string;
176
+ excerpt: string;
177
+ date: string;
178
+ readTime: string;
179
+ }
180
+
181
+ const posts: Post[] = [
182
+ {
183
+ slug: 'getting-started',
184
+ title: 'Getting Started with EmberKit',
185
+ excerpt: 'Learn how to build your first project with EmberKit, a minimalist TypeScript-first JSX framework.',
186
+ date: '2026-05-14',
187
+ readTime: '5 min read',
188
+ },
189
+ {
190
+ slug: 'signals-deep-dive',
191
+ title: 'Signals: A Deep Dive',
192
+ excerpt: 'Understanding reactive signals and how they power the EmberKit runtime.',
193
+ date: '2026-05-10',
194
+ readTime: '8 min read',
195
+ },
196
+ {
197
+ slug: 'file-based-routing',
198
+ title: 'File-Based Routing Explained',
199
+ excerpt: 'How EmberKit automatically creates routes from your file structure.',
200
+ date: '2026-05-05',
201
+ readTime: '4 min read',
202
+ },
203
+ ];
204
+
205
+ const HomePage: RouteComponent = () => {
206
+ return (
207
+ <div className="max-w-3xl mx-auto px-6 py-12">
208
+ <div className="mb-12">
209
+ <h1 className="text-4xl font-bold font-serif mb-4">Latest Posts</h1>
210
+ <p className="text-gray-600 text-lg">Thoughts, tutorials, and updates.</p>
211
+ </div>
212
+ <div className="space-y-10">
213
+ {posts.map((post) => (
214
+ <article key={post.slug} className="group">
215
+ <a href={\`/posts/\${post.slug}\`} className="block">
216
+ <h2 className="text-xl font-semibold font-serif mb-2 group-hover:text-blue-600 transition-colors">
217
+ {post.title}
218
+ </h2>
219
+ <p className="text-gray-600 mb-3">{post.excerpt}</p>
220
+ <div className="flex gap-3 text-sm text-gray-500">
221
+ <time>{post.date}</time>
222
+ <span>&middot;</span>
223
+ <span>{post.readTime}</span>
224
+ </div>
225
+ </a>
226
+ </article>
227
+ ))}
228
+ </div>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ export default HomePage;`,
234
+ "src/routes/[slug].tsx": `import type { RouteComponent, RouteParams } from '@emberkit/core';
235
+ import { Head } from '@emberkit/core';
236
+
237
+ interface PostData {
238
+ title: string;
239
+ date: string;
240
+ author: string;
241
+ content: string;
242
+ }
243
+
244
+ const posts: Record<string, PostData> = {
245
+ 'getting-started': {
246
+ title: 'Getting Started with EmberKit',
247
+ date: 'May 14, 2026',
248
+ author: 'Author Name',
249
+ content: \`
250
+ <p>EmberKit is a minimalist TypeScript-first JSX framework built for speed and simplicity.</p>
251
+ <h2>Installation</h2>
252
+ <p>Get started by creating a new project:</p>
253
+ <pre><code>npm create emberkit@latest my-app</code></pre>
254
+ <h2>Project Structure</h2>
255
+ <p>Your routes live in the \`src/routes\` directory. Each file automatically becomes a route.</p>
256
+ <h2>Development</h2>
257
+ <p>Run the dev server with hot module replacement:</p>
258
+ <pre><code>emberkit dev</code></pre>
259
+ \`,
260
+ },
261
+ 'signals-deep-dive': {
262
+ title: 'Signals: A Deep Dive',
263
+ date: 'May 10, 2026',
264
+ author: 'Author Name',
265
+ content: \`
266
+ <p>Signals are the reactive primitive at the core of EmberKit.</p>
267
+ <h2>Creating Signals</h2>
268
+ <pre><code>const count = signal(0);</code></pre>
269
+ <h2>Computed Values</h2>
270
+ <pre><code>const doubled = computed(() => count.value * 2);</code></pre>
271
+ <h2>Side Effects</h2>
272
+ <pre><code>effect(() => console.log(count.value));</code></pre>
273
+ \`,
274
+ },
275
+ 'file-based-routing': {
276
+ title: 'File-Based Routing Explained',
277
+ date: 'May 5, 2026',
278
+ author: 'Author Name',
279
+ content: \`
280
+ <p>EmberKit uses file-based routing, meaning your file structure defines your routes.</p>
281
+ <h2>Basic Routes</h2>
282
+ <p>\`src/routes/index.tsx\` becomes \`/\`</p>
283
+ <p>\`src/routes/about.tsx\` becomes \`/about\`</p>
284
+ <h2>Dynamic Routes</h2>
285
+ <p>\`src/routes/[slug].tsx\` becomes \`/:slug\`</p>
286
+ \`,
287
+ },
288
+ };
289
+
290
+ interface Params {
291
+ slug: string;
292
+ }
293
+
294
+ const PostPage: RouteComponent<Params> = ({ params }) => {
295
+ const post = posts[params.slug];
296
+
297
+ if (!post) {
298
+ return (
299
+ <div className="max-w-3xl mx-auto px-6 py-12 text-center">
300
+ <h1 className="text-2xl font-bold mb-4">Post not found</h1>
301
+ <a href="/" className="text-blue-600 hover:underline">← Back to posts</a>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ return (
307
+ <>
308
+ <Head>
309
+ <title>{post.title} - {{name}}</title>
310
+ <meta name="description" content={post.title} />
311
+ </Head>
312
+ <article className="max-w-3xl mx-auto px-6 py-12">
313
+ <header className="mb-10">
314
+ <h1 className="text-4xl font-bold font-serif mb-4">{post.title}</h1>
315
+ <div className="flex gap-3 text-sm text-gray-500">
316
+ <time>{post.date}</time>
317
+ <span>&middot;</span>
318
+ <span>By {post.author}</span>
319
+ </div>
320
+ </header>
321
+ <div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
322
+ <div className="mt-12 pt-8 border-t border-gray-200">
323
+ <a href="/" className="text-blue-600 hover:underline">← Back to posts</a>
324
+ </div>
325
+ </article>
326
+ </>
327
+ );
328
+ };
329
+
330
+ export default PostPage;`,
331
+ "src/routes/about.tsx": `import type { RouteComponent } from '@emberkit/core';
332
+ import { Head } from '@emberkit/core';
333
+
334
+ const AboutPage: RouteComponent = () => {
335
+ return (
336
+ <>
337
+ <Head>
338
+ <title>About - {{name}}</title>
339
+ </Head>
340
+ <div className="max-w-3xl mx-auto px-6 py-12">
341
+ <h1 className="text-4xl font-bold font-serif mb-6">About</h1>
342
+ <div className="prose">
343
+ <p>Hi, I'm the author of this blog. I write about web development, frameworks, and building fast user interfaces.</p>
344
+ <p>This blog is built with <a href="https://emberkit.dev">EmberKit</a>, a minimalist TypeScript-first JSX framework.</p>
345
+ <h2>Tech Stack</h2>
346
+ <ul>
347
+ <li>EmberKit for routing and rendering</li>
348
+ <li>Tailwind CSS for styling</li>
349
+ <li>File-based routing</li>
350
+ </ul>
351
+ </div>
352
+ </div>
353
+ </>
354
+ );
355
+ };
356
+
357
+ export default AboutPage;`,
358
+ };