create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +48 -14
- package/dist/index.js +1714 -1504
- package/package.json +2 -2
- package/templates/base-nextjs/README.md +18 -5
- package/templates/base-nextjs/convex/auth/index.ts +2 -2
- package/templates/base-nextjs/convex/auth/options.ts +27 -26
- package/templates/base-nextjs/convex/auth/schema.ts +51 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
- package/templates/base-nextjs/convex/tsconfig.json +2 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
- package/templates/base-nextjs/convex/vex/globals.ts +3 -2
- package/templates/base-nextjs/convex/vex/media.ts +3 -2
- package/templates/base-nextjs/convex/vex.schema.ts +1 -0
- package/templates/base-nextjs/convex/vex.ts +17 -4
- package/templates/base-nextjs/next.config.ts +52 -6
- package/templates/base-nextjs/package.json +18 -7
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
- package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/auth/access.ts +14 -2
- package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
- package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
- package/templates/base-nextjs/src/components/component-example.tsx +1 -1
- package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
- package/templates/base-nextjs/src/vex.config.server.ts +30 -0
- package/templates/base-nextjs/src/vex.config.ts +14 -11
- package/templates/base-nextjs/src/vex.types.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/pages.ts +20 -1
- package/templates/marketing-site/convex/schema.ts +4 -4
- package/templates/marketing-site/convex/seed.ts +515 -70
- package/templates/marketing-site/convex/siteSettings.ts +1 -1
- package/templates/marketing-site/convex/theme.ts +3 -3
- package/templates/marketing-site/convex/vex.schema.ts +75 -38
- package/templates/marketing-site/public/favicon.svg +14 -0
- package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
- package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/globals.css +413 -0
- package/templates/marketing-site/src/app/layout.tsx +85 -0
- package/templates/marketing-site/src/app/robots.ts +13 -0
- package/templates/marketing-site/src/app/sitemap.ts +18 -0
- package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
- package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
- package/templates/marketing-site/src/components/CodePane.tsx +98 -0
- package/templates/marketing-site/src/components/Container.tsx +25 -0
- package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
- package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
- package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
- package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
- package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
- package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
- package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
- package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
- package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
- package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
- package/templates/marketing-site/src/lib/highlight.ts +134 -0
- package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
- package/templates/marketing-site/src/lib/metadata.ts +61 -33
- package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
- package/templates/marketing-site/src/lib/vex.ts +18 -0
- package/templates/marketing-site/src/proxy.ts +179 -0
- package/templates/marketing-site/src/vex.config.server.ts +27 -0
- package/templates/marketing-site/src/vex.config.ts +27 -9
- package/templates/marketing-site/src/vex.types.ts +102 -76
- package/templates/marketing-site/src/vexcms/api.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
- package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
- package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
- package/dist/index.mjs +0 -30976
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
- package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getGlobal } from "@vexcms/core/server"
|
|
2
2
|
|
|
3
3
|
import { GLOBAL_SLUG_SITE_SETTINGS, type TABLE_SLUG_THEMES } from "~/db/constants"
|
|
4
|
-
import config from "~/vex.config"
|
|
4
|
+
import config from "~/vex.config.server"
|
|
5
5
|
|
|
6
6
|
import type { Doc, Id } from "./_generated/dataModel"
|
|
7
7
|
import type { QueryCtx } from "./_generated/server"
|
|
@@ -36,11 +36,11 @@ async function resolveTheme(props: {
|
|
|
36
36
|
slug: GLOBAL_SLUG_SITE_SETTINGS,
|
|
37
37
|
access: { bypass: true },
|
|
38
38
|
})
|
|
39
|
-
if (!settings) return null
|
|
39
|
+
if (!settings) {return null}
|
|
40
40
|
|
|
41
41
|
const reference = settings[props.field] as string[] | undefined
|
|
42
42
|
const themeId = reference?.[0]
|
|
43
|
-
if (!themeId) return null
|
|
43
|
+
if (!themeId) {return null}
|
|
44
44
|
|
|
45
45
|
return await props.ctx.db.get(themeId as Id<typeof TABLE_SLUG_THEMES>)
|
|
46
46
|
}
|
|
@@ -30,15 +30,27 @@ export const pages = defineTable({
|
|
|
30
30
|
blockType: v.literal("hero"),
|
|
31
31
|
blockName: v.optional(v.string()),
|
|
32
32
|
id: v.string(),
|
|
33
|
+
variant: v.optional(v.array(v.union(v.literal("full"), v.literal("compact")))),
|
|
33
34
|
badgeText: v.optional(v.string()),
|
|
34
35
|
badgeLink: v.optional(v.string()),
|
|
35
36
|
heading: v.string(),
|
|
36
37
|
subheading: v.optional(v.string()),
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
installCommand: v.optional(v.string()),
|
|
39
|
+
primaryCtaLabel: v.optional(v.string()),
|
|
40
|
+
primaryCtaHref: v.optional(v.string()),
|
|
39
41
|
secondaryCtaLabel: v.optional(v.string()),
|
|
40
42
|
secondaryCtaHref: v.optional(v.string()),
|
|
41
43
|
}),
|
|
44
|
+
v.object({
|
|
45
|
+
blockType: v.literal("stats"),
|
|
46
|
+
blockName: v.optional(v.string()),
|
|
47
|
+
id: v.string(),
|
|
48
|
+
heading: v.optional(v.string()),
|
|
49
|
+
subheading: v.optional(v.string()),
|
|
50
|
+
items: v.array(
|
|
51
|
+
v.object({ value: v.string(), label: v.string(), description: v.optional(v.string()) })
|
|
52
|
+
),
|
|
53
|
+
}),
|
|
42
54
|
v.object({
|
|
43
55
|
blockType: v.literal("features"),
|
|
44
56
|
blockName: v.optional(v.string()),
|
|
@@ -49,6 +61,50 @@ export const pages = defineTable({
|
|
|
49
61
|
v.object({ title: v.string(), description: v.string(), icon: v.optional(v.string()) })
|
|
50
62
|
),
|
|
51
63
|
}),
|
|
64
|
+
v.object({
|
|
65
|
+
blockType: v.literal("code_showcase"),
|
|
66
|
+
blockName: v.optional(v.string()),
|
|
67
|
+
id: v.string(),
|
|
68
|
+
heading: v.string(),
|
|
69
|
+
subheading: v.optional(v.string()),
|
|
70
|
+
panes: v.array(
|
|
71
|
+
v.object({
|
|
72
|
+
label: v.string(),
|
|
73
|
+
filename: v.optional(v.string()),
|
|
74
|
+
language: v.optional(
|
|
75
|
+
v.array(
|
|
76
|
+
v.union(v.literal("ts"), v.literal("tsx"), v.literal("bash"), v.literal("json"))
|
|
77
|
+
)
|
|
78
|
+
),
|
|
79
|
+
authored: v.optional(v.array(v.union(v.literal("authored"), v.literal("generated")))),
|
|
80
|
+
code: v.string(),
|
|
81
|
+
caption: v.optional(v.string()),
|
|
82
|
+
})
|
|
83
|
+
),
|
|
84
|
+
}),
|
|
85
|
+
v.object({
|
|
86
|
+
blockType: v.literal("split"),
|
|
87
|
+
blockName: v.optional(v.string()),
|
|
88
|
+
id: v.string(),
|
|
89
|
+
eyebrow: v.optional(v.string()),
|
|
90
|
+
heading: v.string(),
|
|
91
|
+
body: v.string(),
|
|
92
|
+
bullets: v.optional(
|
|
93
|
+
v.array(v.object({ icon: v.optional(v.string()), text: v.string() }))
|
|
94
|
+
),
|
|
95
|
+
media: v.optional(
|
|
96
|
+
v.array(v.union(v.literal("code"), v.literal("image"), v.literal("none")))
|
|
97
|
+
),
|
|
98
|
+
code: v.optional(v.string()),
|
|
99
|
+
codeFilename: v.optional(v.string()),
|
|
100
|
+
codeLanguage: v.optional(
|
|
101
|
+
v.array(
|
|
102
|
+
v.union(v.literal("ts"), v.literal("tsx"), v.literal("bash"), v.literal("json"))
|
|
103
|
+
)
|
|
104
|
+
),
|
|
105
|
+
image: v.optional(v.array(v.id("images"))),
|
|
106
|
+
mediaPosition: v.optional(v.array(v.union(v.literal("right"), v.literal("left")))),
|
|
107
|
+
}),
|
|
52
108
|
v.object({
|
|
53
109
|
blockType: v.literal("how_it_works"),
|
|
54
110
|
blockName: v.optional(v.string()),
|
|
@@ -70,27 +126,33 @@ export const pages = defineTable({
|
|
|
70
126
|
feature: v.string(),
|
|
71
127
|
description: v.optional(v.string()),
|
|
72
128
|
status: v.array(
|
|
73
|
-
v.union(
|
|
129
|
+
v.union(
|
|
130
|
+
v.literal("shipped"),
|
|
131
|
+
v.literal("in-progress"),
|
|
132
|
+
v.literal("planned"),
|
|
133
|
+
v.literal("exploring"),
|
|
134
|
+
v.literal("future")
|
|
135
|
+
)
|
|
74
136
|
),
|
|
75
137
|
})
|
|
76
138
|
),
|
|
77
139
|
}),
|
|
78
140
|
v.object({
|
|
79
|
-
blockType: v.literal("
|
|
141
|
+
blockType: v.literal("faq"),
|
|
80
142
|
blockName: v.optional(v.string()),
|
|
81
143
|
id: v.string(),
|
|
82
144
|
heading: v.string(),
|
|
83
145
|
subheading: v.optional(v.string()),
|
|
84
|
-
|
|
146
|
+
supportLink: v.optional(v.string()),
|
|
147
|
+
items: v.array(v.object({ question: v.string(), answer: v.string() })),
|
|
85
148
|
}),
|
|
86
149
|
v.object({
|
|
87
|
-
blockType: v.literal("
|
|
150
|
+
blockType: v.literal("cta"),
|
|
88
151
|
blockName: v.optional(v.string()),
|
|
89
152
|
id: v.string(),
|
|
90
153
|
heading: v.string(),
|
|
91
154
|
subheading: v.optional(v.string()),
|
|
92
|
-
|
|
93
|
-
items: v.array(v.object({ question: v.string(), answer: v.string() })),
|
|
155
|
+
actions: v.optional(v.array(v.object({ label: v.string(), href: v.string() }))),
|
|
94
156
|
})
|
|
95
157
|
)
|
|
96
158
|
)
|
|
@@ -98,6 +160,7 @@ export const pages = defineTable({
|
|
|
98
160
|
metaTitle: v.optional(v.string()),
|
|
99
161
|
metaDescription: v.optional(v.string()),
|
|
100
162
|
ogImage: v.optional(v.array(v.id("images"))),
|
|
163
|
+
updatedAt: v.optional(v.number()),
|
|
101
164
|
}).index("by_slug", ["slug"])
|
|
102
165
|
|
|
103
166
|
export const headers = defineTable({
|
|
@@ -126,6 +189,7 @@ export const headers = defineTable({
|
|
|
126
189
|
})
|
|
127
190
|
)
|
|
128
191
|
),
|
|
192
|
+
updatedAt: v.optional(v.number()),
|
|
129
193
|
}).index("by_name", ["name"])
|
|
130
194
|
|
|
131
195
|
export const footers = defineTable({
|
|
@@ -148,6 +212,7 @@ export const footers = defineTable({
|
|
|
148
212
|
})
|
|
149
213
|
)
|
|
150
214
|
),
|
|
215
|
+
updatedAt: v.optional(v.number()),
|
|
151
216
|
}).index("by_name", ["name"])
|
|
152
217
|
|
|
153
218
|
export const themes = defineTable({
|
|
@@ -226,6 +291,7 @@ export const themes = defineTable({
|
|
|
226
291
|
sidebarRing: v.optional(v.string()),
|
|
227
292
|
})
|
|
228
293
|
),
|
|
294
|
+
updatedAt: v.optional(v.number()),
|
|
229
295
|
}).index("by_name", ["name"])
|
|
230
296
|
|
|
231
297
|
export const session = defineTable({
|
|
@@ -237,7 +303,6 @@ export const session = defineTable({
|
|
|
237
303
|
userAgent: v.optional(v.string()),
|
|
238
304
|
userId: v.string(),
|
|
239
305
|
impersonatedBy: v.optional(v.string()),
|
|
240
|
-
activeOrganizationId: v.optional(v.string()),
|
|
241
306
|
})
|
|
242
307
|
.index("by_token", ["token"])
|
|
243
308
|
.index("by_userId", ["userId"])
|
|
@@ -299,35 +364,6 @@ export const jwks = defineTable({
|
|
|
299
364
|
expiresAt: v.optional(v.number()),
|
|
300
365
|
})
|
|
301
366
|
|
|
302
|
-
export const organization = defineTable({
|
|
303
|
-
name: v.string(),
|
|
304
|
-
slug: v.string(),
|
|
305
|
-
logo: v.optional(v.string()),
|
|
306
|
-
createdAt: v.number(),
|
|
307
|
-
metadata: v.optional(v.string()),
|
|
308
|
-
}).index("by_slug", ["slug"])
|
|
309
|
-
|
|
310
|
-
export const member = defineTable({
|
|
311
|
-
organizationId: v.string(),
|
|
312
|
-
userId: v.string(),
|
|
313
|
-
role: v.string(),
|
|
314
|
-
createdAt: v.number(),
|
|
315
|
-
})
|
|
316
|
-
.index("by_organizationId", ["organizationId"])
|
|
317
|
-
.index("by_userId", ["userId"])
|
|
318
|
-
|
|
319
|
-
export const invitation = defineTable({
|
|
320
|
-
organizationId: v.string(),
|
|
321
|
-
email: v.string(),
|
|
322
|
-
role: v.optional(v.string()),
|
|
323
|
-
status: v.string(),
|
|
324
|
-
expiresAt: v.number(),
|
|
325
|
-
createdAt: v.number(),
|
|
326
|
-
inviterId: v.string(),
|
|
327
|
-
})
|
|
328
|
-
.index("by_organizationId", ["organizationId"])
|
|
329
|
-
.index("by_email", ["email"])
|
|
330
|
-
|
|
331
367
|
export const images = defineTable({
|
|
332
368
|
filename: v.string(),
|
|
333
369
|
alt: v.string(),
|
|
@@ -338,6 +374,7 @@ export const images = defineTable({
|
|
|
338
374
|
src: v.string(),
|
|
339
375
|
width: v.optional(v.number()),
|
|
340
376
|
height: v.optional(v.number()),
|
|
377
|
+
updatedAt: v.optional(v.number()),
|
|
341
378
|
})
|
|
342
379
|
.index("by_deleted", ["deleted"])
|
|
343
380
|
.searchIndex("search_filename", {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<!-- The VexCMS chevron, same geometry as src/components/BrandMark.tsx scaled
|
|
3
|
+
2x. Ember is hard-coded (#f87e28, the dark-mode primary token
|
|
4
|
+
oklch(72% 0.175 50)) rather than currentColor: a favicon renders outside
|
|
5
|
+
the document, so it has no colour to inherit, and this hue holds up on
|
|
6
|
+
both light and dark browser chrome. -->
|
|
7
|
+
<path
|
|
8
|
+
d="M5 6.5 14.5 25.5a1.7 1.7 0 0 0 3 0L27 6.5"
|
|
9
|
+
fill="none"
|
|
10
|
+
stroke="#f87e28"
|
|
11
|
+
stroke-linecap="square"
|
|
12
|
+
stroke-width="4.5"
|
|
13
|
+
/>
|
|
14
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { convexQuery } from "@convex-dev/react-query"
|
|
4
|
+
import { api } from "@convex/_generated/api"
|
|
5
|
+
import { RenderBlocks, useLivePreviewQuery } from "@vexcms/react"
|
|
6
|
+
|
|
7
|
+
import type { CodeHighlightMap } from "~/lib/highlight"
|
|
8
|
+
import type { PagesDocument } from "~/vex.types"
|
|
9
|
+
|
|
10
|
+
import { CodeHighlightProvider } from "~/components/CodeHighlightContext"
|
|
11
|
+
import { WelcomePage } from "~/components/WelcomePage"
|
|
12
|
+
import { blockComponents } from "~/vexcms/blocks"
|
|
13
|
+
|
|
14
|
+
export interface PageContentProps {
|
|
15
|
+
/**
|
|
16
|
+
* Server-highlighted code panes for `initialData`, keyed by
|
|
17
|
+
* `codeHighlightKey`. Built by `highlightPageBlocks`, because nothing below
|
|
18
|
+
* this client boundary can await shiki.
|
|
19
|
+
*/
|
|
20
|
+
codeHighlights?: CodeHighlightMap
|
|
21
|
+
/** Server-fetched `pages.getBySlug` result, hydrated as the query's initial data. */
|
|
22
|
+
initialData?: PagesDocument[]
|
|
23
|
+
/** URL slug to render. Omit (or empty) for the home page. */
|
|
24
|
+
slug?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renders one marketing page's blocks via `RenderBlocks` (Contract 1), or
|
|
29
|
+
* falls back to base's bootstrap `WelcomePage` when no `home` page document
|
|
30
|
+
* exists yet — a fresh scaffold before `pnpm seed` has run (Contract 3).
|
|
31
|
+
*
|
|
32
|
+
* `pages.getBySlug` always returns an array (empty when no match — the same
|
|
33
|
+
* shape every collection query returns); `useLivePreviewQuery` performs that
|
|
34
|
+
* narrowing itself and overlays any unsaved editor values for this document
|
|
35
|
+
* when the page is being rendered inside a live preview.
|
|
36
|
+
*/
|
|
37
|
+
export function PageContent({ codeHighlights, slug, initialData }: PageContentProps) {
|
|
38
|
+
const normalizedSlug = slug && slug.length > 0 ? slug : "home"
|
|
39
|
+
|
|
40
|
+
const { data: page, isPending } = useLivePreviewQuery(
|
|
41
|
+
{
|
|
42
|
+
...convexQuery(api.pages.getBySlug, { slug: normalizedSlug }),
|
|
43
|
+
initialData,
|
|
44
|
+
},
|
|
45
|
+
"pages",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (isPending && initialData === undefined) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!page) {
|
|
53
|
+
return <WelcomePage />
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<CodeHighlightProvider highlights={codeHighlights ?? {}}>
|
|
58
|
+
<RenderBlocks
|
|
59
|
+
blocks={page.blocks}
|
|
60
|
+
components={blockComponents}
|
|
61
|
+
/>
|
|
62
|
+
</CodeHighlightProvider>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
|
|
5
|
+
import { LivePreviewProvider } from "@vexcms/react"
|
|
6
|
+
|
|
7
|
+
import config from "~/vex.config"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mounts the live-preview listener for the public site's client tree.
|
|
11
|
+
*
|
|
12
|
+
* A `"use client"` component so `vex.config` is imported and owned on the
|
|
13
|
+
* client — it never crosses the RSC boundary as a prop (ADR-013; the config's
|
|
14
|
+
* `admin.livePreview.url` resolvers and field validators are functions, which
|
|
15
|
+
* React refuses to serialize). Mirrors `admin/clientProviders.tsx`.
|
|
16
|
+
*
|
|
17
|
+
* `LivePreviewProvider` gates itself on `?vexLivePreview=1` plus the
|
|
18
|
+
* session-verified `vex-live-preview` cookie `proxy.ts` sets, so wrapping the
|
|
19
|
+
* layout unconditionally costs a disabled provider and nothing else — and
|
|
20
|
+
* keeps every public route statically prerenderable.
|
|
21
|
+
*/
|
|
22
|
+
export function SiteLivePreviewProvider({ children }: { children: ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<LivePreviewProvider
|
|
25
|
+
allowedOrigins={config.livePreview.allowedOrigins}
|
|
26
|
+
collections={config.collections}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</LivePreviewProvider>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { api } from "@convex/_generated/api"
|
|
2
|
+
import { vexStaticParams } from "@vexcms/next/seo"
|
|
3
|
+
import { notFound } from "next/navigation"
|
|
4
|
+
|
|
5
|
+
import type { PagesDocument } from "~/vex.types"
|
|
6
|
+
|
|
7
|
+
import { generatePageMetadata } from "~/lib/metadata"
|
|
8
|
+
import { highlightPageBlocks } from "~/lib/highlight"
|
|
9
|
+
import { vex } from "~/lib/vex"
|
|
10
|
+
|
|
11
|
+
import { PageContent } from "../PageContent"
|
|
12
|
+
|
|
13
|
+
// Next requires this to be an inline literal — it is read by static analysis
|
|
14
|
+
// before any module executes, so neither `vexConfig.revalidate.revalidateSeconds`
|
|
15
|
+
// nor an imported constant is accepted ("Invalid segment configuration export").
|
|
16
|
+
export const revalidate = 3600
|
|
17
|
+
|
|
18
|
+
export async function generateStaticParams() {
|
|
19
|
+
const entries = await vexStaticParams({
|
|
20
|
+
client: vex,
|
|
21
|
+
query: api.pages.publishedSlugs,
|
|
22
|
+
paramName: "slug",
|
|
23
|
+
getSlug: (item) => item.slug,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return entries.filter((entry) => entry.slug !== "home")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
30
|
+
const { slug } = await params
|
|
31
|
+
return generatePageMetadata({ slug: slug && slug.length > 0 ? slug : "home" })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function PublicPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
35
|
+
const { slug } = await params
|
|
36
|
+
const normalized = slug && slug.length > 0 ? slug : "home"
|
|
37
|
+
|
|
38
|
+
// The two failure modes are deliberately NOT the same:
|
|
39
|
+
//
|
|
40
|
+
// - The query THROWS: the deployment is unreachable. A scaffold builds with
|
|
41
|
+
// placeholder env, so throwing here would fail `next build` outright.
|
|
42
|
+
// Render with no seed and let the client's live subscription hydrate.
|
|
43
|
+
// - The query SUCCEEDS and returns `[]`: the deployment answered, and this
|
|
44
|
+
// page genuinely does not exist. That must be a real 404, not an empty
|
|
45
|
+
// 200 — an empty 200 gets the URL indexed as a live, blank page.
|
|
46
|
+
let initialData: PagesDocument[] | undefined
|
|
47
|
+
let reachable = true
|
|
48
|
+
try {
|
|
49
|
+
initialData = await vex.query(api.pages.getBySlug, { slug: normalized })
|
|
50
|
+
} catch {
|
|
51
|
+
reachable = false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (reachable && (!initialData || initialData.length === 0)) {
|
|
55
|
+
notFound()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const codeHighlights = await highlightPageBlocks(initialData?.[0]?.blocks)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<PageContent codeHighlights={codeHighlights} initialData={initialData} slug={normalized} />
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
4
|
+
|
|
5
|
+
import type { FootersDocument, HeadersDocument } from "~/vex.types"
|
|
6
|
+
|
|
7
|
+
import { SiteFooter } from "~/components/SiteFooter"
|
|
8
|
+
import { SiteHeader } from "~/components/SiteHeader"
|
|
9
|
+
import { ThemeStyle } from "~/components/ThemeStyle"
|
|
10
|
+
import { vex } from "~/lib/vex"
|
|
11
|
+
import { SiteLivePreviewProvider } from "./SiteLivePreviewProvider"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Marketing chrome: header + footer around every site page. Auth routes live
|
|
15
|
+
* outside this group (directly under `(frontend)`), so they stay chrome-free.
|
|
16
|
+
*/
|
|
17
|
+
export default async function SiteLayout({
|
|
18
|
+
children,
|
|
19
|
+
}: Readonly<{
|
|
20
|
+
children: ReactNode
|
|
21
|
+
}>) {
|
|
22
|
+
let headerData: HeadersDocument | null = null
|
|
23
|
+
let footerData: FootersDocument | null = null
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
;[headerData, footerData] = await Promise.all([
|
|
27
|
+
vex.query(api.headers.getFirst),
|
|
28
|
+
vex.query(api.footers.getFirst),
|
|
29
|
+
])
|
|
30
|
+
} catch {
|
|
31
|
+
// Convex not available — fall back to client-only fetch
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<SiteLivePreviewProvider>
|
|
36
|
+
<ThemeStyle />
|
|
37
|
+
{/* Sits above the sticky header so it is the first tab stop on every
|
|
38
|
+
page. Visually hidden until focused. */}
|
|
39
|
+
<a
|
|
40
|
+
className="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-3 focus-visible:left-3 focus-visible:z-100 focus-visible:rounded-sm focus-visible:border focus-visible:border-border focus-visible:bg-card focus-visible:px-3 focus-visible:py-2 focus-visible:text-sm focus-visible:font-medium focus-visible:text-foreground"
|
|
41
|
+
href="#main"
|
|
42
|
+
>
|
|
43
|
+
Skip to content
|
|
44
|
+
</a>
|
|
45
|
+
<SiteHeader initialData={headerData} />
|
|
46
|
+
<main id="main">{children}</main>
|
|
47
|
+
<SiteFooter initialData={footerData} />
|
|
48
|
+
</SiteLivePreviewProvider>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { api } from "@convex/_generated/api"
|
|
2
|
+
|
|
3
|
+
import type { PagesDocument } from "~/vex.types"
|
|
4
|
+
|
|
5
|
+
import { generatePageMetadata } from "~/lib/metadata"
|
|
6
|
+
import { highlightPageBlocks } from "~/lib/highlight"
|
|
7
|
+
import { vex } from "~/lib/vex"
|
|
8
|
+
|
|
9
|
+
import { PageContent } from "./PageContent"
|
|
10
|
+
|
|
11
|
+
// Next requires this to be an inline literal — it is read by static analysis
|
|
12
|
+
// before any module executes, so neither `vexConfig.revalidate.revalidateSeconds`
|
|
13
|
+
// nor an imported constant is accepted ("Invalid segment configuration export").
|
|
14
|
+
export const revalidate = 3600
|
|
15
|
+
|
|
16
|
+
export async function generateMetadata() {
|
|
17
|
+
return generatePageMetadata({ slug: "home" })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function HomePage() {
|
|
21
|
+
let initialData: PagesDocument[] | undefined
|
|
22
|
+
try {
|
|
23
|
+
initialData = await vex.query(api.pages.getBySlug, { slug: "home" })
|
|
24
|
+
} catch {
|
|
25
|
+
// Fall back to client-only fetch
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const codeHighlights = await highlightPageBlocks(initialData?.[0]?.blocks)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<PageContent codeHighlights={codeHighlights} initialData={initialData} />
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -1,38 +1,24 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { FirstAdminBootstrap } from "~/components/FirstAdminBootstrap"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export default
|
|
5
|
+
/**
|
|
6
|
+
* Frontend shell: first-admin bootstrap + the `@auth` parallel slot only.
|
|
7
|
+
* The marketing chrome (header/footer) lives in the nested `(site)` group so
|
|
8
|
+
* auth routes (`/auth/sign-in`, sign-up, …) render standalone — themed via
|
|
9
|
+
* the root layout, but without the site navigation wrapped around the form.
|
|
10
|
+
*/
|
|
11
|
+
export default function FrontendLayout({
|
|
12
12
|
auth,
|
|
13
13
|
children,
|
|
14
14
|
}: Readonly<{
|
|
15
15
|
auth: ReactNode
|
|
16
16
|
children: ReactNode
|
|
17
17
|
}>) {
|
|
18
|
-
let headerData: Record<string, unknown> | null = null
|
|
19
|
-
let footerData: Record<string, unknown> | null = null
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
;[headerData, footerData] = await Promise.all([
|
|
23
|
-
fetchQuery(api.headers.getFirst),
|
|
24
|
-
fetchQuery(api.footers.getFirst),
|
|
25
|
-
])
|
|
26
|
-
} catch {
|
|
27
|
-
// Convex not available — fall back to client-only fetch
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
return (
|
|
31
19
|
<>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
<main>{children}</main>
|
|
35
|
-
<SiteFooter initialData={footerData} />
|
|
20
|
+
<FirstAdminBootstrap />
|
|
21
|
+
{children}
|
|
36
22
|
{auth}
|
|
37
23
|
</>
|
|
38
24
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { NextAdminLayout } from "@vexcms/next/client";
|
|
4
|
+
|
|
5
|
+
import { AuthServerProvider } from "~/components/providers/auth";
|
|
6
|
+
import { getCurrentUser } from "~/auth/serverUtils";
|
|
7
|
+
import { ThemeLive } from "~/components/ThemeLive";
|
|
8
|
+
import { ThemeStyle } from "~/components/ThemeStyle";
|
|
9
|
+
|
|
10
|
+
import { ClientProviders } from "./clientProviders";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Overlays `base-nextjs`'s admin layout to wire the theme system in: base
|
|
14
|
+
* ships no theme, so its admin layout has nothing to render here. `<ThemeStyle
|
|
15
|
+
* scope="admin" />` covers first paint; `<ThemeLive scope="admin" />` keeps it
|
|
16
|
+
* live after a save in the admin panel, with no full reload. Emitted at
|
|
17
|
+
* `:root:root`, so it outranks the site theme on admin routes only. Falls
|
|
18
|
+
* back to the site theme when `siteSettings.adminTheme` is empty — see
|
|
19
|
+
* `api.theme.getAdmin`.
|
|
20
|
+
*/
|
|
21
|
+
export default async function AdminLayout({ children }: { children: ReactNode }) {
|
|
22
|
+
const user = await getCurrentUser();
|
|
23
|
+
return (
|
|
24
|
+
<AuthServerProvider>
|
|
25
|
+
<ClientProviders>
|
|
26
|
+
<ThemeStyle scope="admin" />
|
|
27
|
+
<ThemeLive scope="admin" />
|
|
28
|
+
<NextAdminLayout user={user ?? undefined}>{children}</NextAdminLayout>
|
|
29
|
+
</ClientProviders>
|
|
30
|
+
</AuthServerProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|