create-velox-app 0.6.31 → 0.6.51
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 +120 -0
- package/GUIDE.md +230 -0
- package/dist/cli.js +1 -0
- package/dist/index.js +14 -4
- package/dist/templates/auth.js +10 -0
- package/dist/templates/index.js +30 -1
- package/dist/templates/placeholders.js +0 -3
- package/dist/templates/rsc-auth.d.ts +12 -0
- package/dist/templates/rsc-auth.js +208 -0
- package/dist/templates/rsc.js +40 -1
- package/dist/templates/shared/css-generator.d.ts +26 -0
- package/dist/templates/shared/css-generator.js +553 -0
- package/dist/templates/shared/index.d.ts +3 -0
- package/dist/templates/shared/index.js +3 -0
- package/dist/templates/shared/rsc-styles.d.ts +54 -0
- package/dist/templates/shared/rsc-styles.js +68 -0
- package/dist/templates/shared/theme.d.ts +133 -0
- package/dist/templates/shared/theme.js +141 -0
- package/dist/templates/spa.js +10 -0
- package/dist/templates/trpc.js +10 -0
- package/dist/templates/types.d.ts +2 -1
- package/dist/templates/types.js +6 -0
- package/package.json +6 -3
- package/src/templates/source/api/config/database.ts +13 -32
- package/src/templates/source/api/docker-compose.yml +21 -0
- package/src/templates/source/root/CLAUDE.auth.md +6 -0
- package/src/templates/source/root/CLAUDE.default.md +6 -0
- package/src/templates/source/rsc/CLAUDE.md +56 -2
- package/src/templates/source/rsc/app/actions/posts.ts +1 -1
- package/src/templates/source/rsc/app/actions/users.ts +111 -20
- package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
- package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
- package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
- package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
- package/src/templates/source/rsc/docker-compose.yml +21 -0
- package/src/templates/source/rsc/package.json +3 -3
- package/src/templates/source/rsc/src/api/database.ts +13 -32
- package/src/templates/source/rsc/src/api/handler.ts +1 -1
- package/src/templates/source/rsc/src/entry.client.tsx +65 -18
- package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
- package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
- package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
- package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
- package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
- package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
- package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
- package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
- package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
- package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
- package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
- package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
- package/src/templates/source/rsc-auth/app.config.ts +12 -0
- package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
- package/src/templates/source/rsc-auth/env.example +11 -0
- package/src/templates/source/rsc-auth/gitignore +34 -0
- package/src/templates/source/rsc-auth/package.json +44 -0
- package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
- package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
- package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
- package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
- package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
- package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
- package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
- package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
- package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
- package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
- package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
- package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
- package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
- package/src/templates/source/rsc-auth/tsconfig.json +24 -0
- package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* User Server Actions
|
|
5
5
|
*
|
|
6
|
-
* Type-safe server actions
|
|
6
|
+
* Type-safe server actions with built-in security using VeloxTS validated() helper.
|
|
7
7
|
* These can be called directly from client components with full type inference.
|
|
8
8
|
*
|
|
9
|
+
* The validated() helper provides:
|
|
10
|
+
* - Input validation via Zod schemas
|
|
11
|
+
* - Input size limits (DoS protection)
|
|
12
|
+
* - Input sanitization (prototype pollution prevention)
|
|
13
|
+
* - Rate limiting (optional)
|
|
14
|
+
* - Authentication checks (optional)
|
|
15
|
+
* - Authorization via custom callbacks (optional)
|
|
16
|
+
*
|
|
9
17
|
* @example
|
|
10
18
|
* ```tsx
|
|
11
19
|
* // In a client component
|
|
@@ -19,7 +27,7 @@
|
|
|
19
27
|
* ```
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
|
-
import {
|
|
30
|
+
import { validated, validatedMutation, validatedQuery } from '@veloxts/web/server';
|
|
23
31
|
import { z } from 'zod';
|
|
24
32
|
|
|
25
33
|
import { db } from '@/api/database';
|
|
@@ -36,6 +44,15 @@ const CreateUserSchema = z.object({
|
|
|
36
44
|
email: z.string().email('Invalid email address'),
|
|
37
45
|
});
|
|
38
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Schema for updating a user
|
|
49
|
+
*/
|
|
50
|
+
const UpdateUserSchema = z.object({
|
|
51
|
+
id: z.string().min(1, 'User ID is required'),
|
|
52
|
+
name: z.string().min(1).max(100).optional(),
|
|
53
|
+
email: z.string().email().optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Schema for deleting a user
|
|
41
58
|
*/
|
|
@@ -43,24 +60,90 @@ const DeleteUserSchema = z.object({
|
|
|
43
60
|
id: z.string().min(1, 'User ID is required'),
|
|
44
61
|
});
|
|
45
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Schema for searching users
|
|
65
|
+
*/
|
|
66
|
+
const SearchUsersSchema = z.object({
|
|
67
|
+
query: z.string().max(100).optional(),
|
|
68
|
+
limit: z.number().min(1).max(100).default(10),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Public Actions (no authentication required)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Search users - public query action
|
|
77
|
+
*
|
|
78
|
+
* Uses validatedQuery() which allows unauthenticated access by default.
|
|
79
|
+
* Includes input sanitization and validation.
|
|
80
|
+
*/
|
|
81
|
+
export const searchUsers = validatedQuery(SearchUsersSchema, async (input) => {
|
|
82
|
+
const users = await db.user.findMany({
|
|
83
|
+
where: input.query
|
|
84
|
+
? {
|
|
85
|
+
OR: [{ name: { contains: input.query } }, { email: { contains: input.query } }],
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
take: input.limit,
|
|
89
|
+
orderBy: { createdAt: 'desc' },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return users;
|
|
93
|
+
});
|
|
94
|
+
|
|
46
95
|
// ============================================================================
|
|
47
|
-
//
|
|
96
|
+
// Mutations (with security options)
|
|
48
97
|
// ============================================================================
|
|
49
98
|
|
|
50
99
|
/**
|
|
51
100
|
* Creates a new user.
|
|
52
101
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
102
|
+
* Uses validated() with explicit options:
|
|
103
|
+
* - Rate limiting: max 10 requests per minute
|
|
104
|
+
* - Input size limit: 10KB (prevent large payload DoS)
|
|
105
|
+
* - Input sanitization: enabled by default
|
|
106
|
+
*/
|
|
107
|
+
export const createUser = validated(
|
|
108
|
+
CreateUserSchema,
|
|
109
|
+
async (input) => {
|
|
110
|
+
const user = await db.user.create({
|
|
111
|
+
data: {
|
|
112
|
+
name: input.name.trim(),
|
|
113
|
+
email: input.email.toLowerCase().trim(),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return user;
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
// Rate limit: 10 requests per minute per IP
|
|
121
|
+
rateLimit: {
|
|
122
|
+
maxRequests: 10,
|
|
123
|
+
windowMs: 60_000,
|
|
124
|
+
},
|
|
125
|
+
// Limit input size to 10KB
|
|
126
|
+
maxInputSize: 10 * 1024,
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Updates a user.
|
|
132
|
+
*
|
|
133
|
+
* Uses validatedMutation() which requires authentication by default.
|
|
134
|
+
* When requireAuth is true, ctx.user is available and typed.
|
|
135
|
+
*
|
|
136
|
+
* Note: In a real app, add authorization to verify the user owns this record.
|
|
55
137
|
*/
|
|
56
|
-
export const
|
|
57
|
-
//
|
|
58
|
-
|
|
138
|
+
export const updateUser = validatedMutation(UpdateUserSchema, async (input, ctx) => {
|
|
139
|
+
// ctx.user is available because validatedMutation requires auth
|
|
140
|
+
console.log('User updating record:', ctx.user.id);
|
|
59
141
|
|
|
60
|
-
const user = await db.user.
|
|
142
|
+
const user = await db.user.update({
|
|
143
|
+
where: { id: input.id },
|
|
61
144
|
data: {
|
|
62
|
-
name: input.name.trim(),
|
|
63
|
-
email: input.email.toLowerCase().trim(),
|
|
145
|
+
...(input.name && { name: input.name.trim() }),
|
|
146
|
+
...(input.email && { email: input.email.toLowerCase().trim() }),
|
|
64
147
|
},
|
|
65
148
|
});
|
|
66
149
|
|
|
@@ -70,14 +153,22 @@ export const createUser = action(CreateUserSchema, async (input) => {
|
|
|
70
153
|
/**
|
|
71
154
|
* Deletes a user by ID.
|
|
72
155
|
*
|
|
73
|
-
*
|
|
156
|
+
* Uses validated() with role-based authorization.
|
|
157
|
+
* Only admins can delete users.
|
|
74
158
|
*/
|
|
75
|
-
export const deleteUser =
|
|
76
|
-
|
|
159
|
+
export const deleteUser = validated(
|
|
160
|
+
DeleteUserSchema,
|
|
161
|
+
async (input) => {
|
|
162
|
+
await db.user.delete({
|
|
163
|
+
where: { id: input.id },
|
|
164
|
+
});
|
|
77
165
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
166
|
+
return { deleted: true };
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
// Require authentication
|
|
170
|
+
requireAuth: true,
|
|
171
|
+
// Role-based authorization (requires 'admin' role)
|
|
172
|
+
requireRoles: ['admin'],
|
|
173
|
+
}
|
|
174
|
+
);
|
|
@@ -24,12 +24,12 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.dashboard-sidebar {
|
|
27
|
-
width:
|
|
27
|
+
width: 240px;
|
|
28
28
|
flex-shrink: 0;
|
|
29
|
-
background:
|
|
29
|
+
background: #111;
|
|
30
30
|
border-radius: 8px;
|
|
31
|
-
padding:
|
|
32
|
-
|
|
31
|
+
padding: 1.5rem;
|
|
32
|
+
border: 1px solid #222;
|
|
33
33
|
height: fit-content;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -37,8 +37,10 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
|
37
37
|
font-size: 0.75rem;
|
|
38
38
|
text-transform: uppercase;
|
|
39
39
|
color: #888;
|
|
40
|
-
margin-bottom: 0.
|
|
41
|
-
padding: 0 0.
|
|
40
|
+
margin-bottom: 0.75rem;
|
|
41
|
+
padding: 0 0.75rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: 0.05em;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
.sidebar-nav {
|
|
@@ -47,39 +49,42 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
|
47
49
|
|
|
48
50
|
.sidebar-nav a {
|
|
49
51
|
display: block;
|
|
50
|
-
padding: 0.
|
|
51
|
-
color: #
|
|
52
|
+
padding: 0.75rem;
|
|
53
|
+
color: #ededed;
|
|
52
54
|
text-decoration: none;
|
|
53
55
|
border-radius: 4px;
|
|
54
56
|
transition: background 0.2s;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
.sidebar-nav a:hover {
|
|
58
|
-
background: #
|
|
60
|
+
background: #1a1a1a;
|
|
61
|
+
color: #00d9ff;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
.sidebar-nav a.active {
|
|
62
|
-
background: #
|
|
63
|
-
color:
|
|
65
|
+
background: #00d9ff;
|
|
66
|
+
color: #000;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
.dashboard-content {
|
|
67
70
|
flex: 1;
|
|
68
|
-
background:
|
|
71
|
+
background: #111;
|
|
69
72
|
border-radius: 8px;
|
|
70
73
|
padding: 1.5rem;
|
|
71
|
-
|
|
74
|
+
border: 1px solid #222;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
.dashboard-badge {
|
|
75
78
|
display: inline-block;
|
|
76
|
-
background: #
|
|
77
|
-
color:
|
|
79
|
+
background: #00d9ff;
|
|
80
|
+
color: #000;
|
|
78
81
|
font-size: 0.625rem;
|
|
79
82
|
text-transform: uppercase;
|
|
80
|
-
padding: 0.
|
|
83
|
+
padding: 0.5rem 0.75rem;
|
|
81
84
|
border-radius: 4px;
|
|
82
85
|
margin-bottom: 1rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
letter-spacing: 0.05em;
|
|
83
88
|
}
|
|
84
89
|
`}</style>
|
|
85
90
|
|
|
@@ -15,6 +15,40 @@ interface MarketingLayoutProps {
|
|
|
15
15
|
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
|
16
16
|
return (
|
|
17
17
|
<div className="marketing-layout">
|
|
18
|
+
<style>{`
|
|
19
|
+
.marketing-layout {
|
|
20
|
+
position: relative;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.marketing-banner {
|
|
24
|
+
background: #111;
|
|
25
|
+
border: 1px solid #222;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
padding: 1rem 1.5rem;
|
|
28
|
+
margin-bottom: 1.5rem;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.marketing-banner .badge {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
background: #00d9ff;
|
|
37
|
+
color: #000;
|
|
38
|
+
font-size: 0.625rem;
|
|
39
|
+
text-transform: uppercase;
|
|
40
|
+
padding: 0.5rem 0.75rem;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: 0.05em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.marketing-banner span:not(.badge) {
|
|
47
|
+
color: #888;
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
}
|
|
50
|
+
`}</style>
|
|
51
|
+
|
|
18
52
|
<div className="marketing-banner">
|
|
19
53
|
<span className="badge">Marketing</span>
|
|
20
54
|
<span>This page uses the marketing layout</span>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Content Layout
|
|
3
|
+
*
|
|
4
|
+
* The content portion of the minimal layout, separate from the HTML document shell.
|
|
5
|
+
* This component is shared between server and client for proper hydration.
|
|
6
|
+
*
|
|
7
|
+
* Server renders: <div id="root"><MinimalContent><Page /></MinimalContent></div>
|
|
8
|
+
* Client hydrates: <MinimalContent><Page /></MinimalContent>
|
|
9
|
+
*
|
|
10
|
+
* This ensures the React tree matches on both sides, enabling proper hydration.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
|
|
15
|
+
interface MinimalContentProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function MinimalContent({ children }: MinimalContentProps) {
|
|
20
|
+
return <div className="minimal-content">{children}</div>;
|
|
21
|
+
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal Layout
|
|
2
|
+
* Minimal Layout (Document Shell)
|
|
3
3
|
*
|
|
4
4
|
* A stripped-down layout for pages that need minimal chrome,
|
|
5
5
|
* such as print-friendly pages, embedded widgets, or auth pages.
|
|
6
|
+
*
|
|
7
|
+
* This component provides the HTML document shell (server-only).
|
|
8
|
+
* The actual content wrapper is in MinimalContent (shared with client).
|
|
9
|
+
*
|
|
10
|
+
* Architecture for proper hydration:
|
|
11
|
+
* - Server renders: <MinimalLayout><Page /></MinimalLayout>
|
|
12
|
+
* Which produces: <html>...<div id="root"><MinimalContent><Page /></></div>...</html>
|
|
13
|
+
* - Client hydrates #root with: <MinimalContent><Page /></MinimalContent>
|
|
14
|
+
* This ensures the React tree matches exactly.
|
|
6
15
|
*/
|
|
7
16
|
|
|
8
17
|
import type { ReactNode } from 'react';
|
|
9
18
|
|
|
19
|
+
import MinimalContent from './minimal-content.tsx';
|
|
20
|
+
|
|
10
21
|
interface MinimalLayoutProps {
|
|
11
22
|
children: ReactNode;
|
|
12
23
|
params?: Record<string, string>;
|
|
@@ -19,11 +30,81 @@ export default function MinimalLayout({ children }: MinimalLayoutProps) {
|
|
|
19
30
|
<meta charSet="utf-8" />
|
|
20
31
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
21
32
|
<title>VeloxTS</title>
|
|
22
|
-
<
|
|
33
|
+
<style>{`
|
|
34
|
+
/* Global Reset & Dark Mode Base */
|
|
35
|
+
*,
|
|
36
|
+
*::before,
|
|
37
|
+
*::after {
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
font: inherit;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
html {
|
|
45
|
+
font-size: 16px;
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
-moz-osx-font-smoothing: grayscale;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
body {
|
|
51
|
+
background: #0a0a0a;
|
|
52
|
+
color: #ededed;
|
|
53
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
|
54
|
+
line-height: 1.6;
|
|
55
|
+
min-height: 100svh;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
h1, h2, h3, h4, h5, h6 {
|
|
59
|
+
text-wrap: balance;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
p, li, figcaption {
|
|
63
|
+
text-wrap: pretty;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
a {
|
|
67
|
+
color: #00d9ff;
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
transition: opacity 0.2s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
a:hover {
|
|
73
|
+
opacity: 0.8;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
code {
|
|
77
|
+
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Monaco, "Courier New", monospace;
|
|
78
|
+
background: #1a1a1a;
|
|
79
|
+
padding: 0.2em 0.4em;
|
|
80
|
+
border-radius: 4px;
|
|
81
|
+
font-size: 0.9em;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
::selection {
|
|
85
|
+
background: #00d9ff;
|
|
86
|
+
color: #000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.minimal-body {
|
|
90
|
+
min-height: 100vh;
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
padding: 2rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.minimal-content {
|
|
98
|
+
width: 100%;
|
|
99
|
+
max-width: 450px;
|
|
100
|
+
}
|
|
101
|
+
`}</style>
|
|
23
102
|
</head>
|
|
24
|
-
<body className="minimal-
|
|
25
|
-
|
|
26
|
-
|
|
103
|
+
<body className="minimal-body">
|
|
104
|
+
<div id="root">
|
|
105
|
+
<MinimalContent>{children}</MinimalContent>
|
|
106
|
+
</div>
|
|
107
|
+
<script src="/_build/src/entry.client.tsx" type="module" />
|
|
27
108
|
</body>
|
|
28
109
|
</html>
|
|
29
110
|
);
|