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.
Files changed (115) hide show
  1. package/README.md +48 -14
  2. package/dist/index.js +1714 -1504
  3. package/package.json +2 -2
  4. package/templates/base-nextjs/README.md +18 -5
  5. package/templates/base-nextjs/convex/auth/index.ts +2 -2
  6. package/templates/base-nextjs/convex/auth/options.ts +27 -26
  7. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  8. package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
  9. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  10. package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
  11. package/templates/base-nextjs/convex/vex/globals.ts +3 -2
  12. package/templates/base-nextjs/convex/vex/media.ts +3 -2
  13. package/templates/base-nextjs/convex/vex.schema.ts +1 -0
  14. package/templates/base-nextjs/convex/vex.ts +17 -4
  15. package/templates/base-nextjs/next.config.ts +52 -6
  16. package/templates/base-nextjs/package.json +18 -7
  17. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  18. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  19. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
  20. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  21. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
  22. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
  23. package/templates/base-nextjs/src/app/globals.css +10 -9
  24. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  25. package/templates/base-nextjs/src/auth/access.ts +14 -2
  26. package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
  27. package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
  28. package/templates/base-nextjs/src/components/component-example.tsx +1 -1
  29. package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
  30. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  31. package/templates/base-nextjs/src/vex.config.ts +14 -11
  32. package/templates/base-nextjs/src/vex.types.ts +2 -1
  33. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  34. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  35. package/templates/marketing-site/convex/pages.ts +20 -1
  36. package/templates/marketing-site/convex/schema.ts +4 -4
  37. package/templates/marketing-site/convex/seed.ts +515 -70
  38. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  39. package/templates/marketing-site/convex/theme.ts +3 -3
  40. package/templates/marketing-site/convex/vex.schema.ts +75 -38
  41. package/templates/marketing-site/public/favicon.svg +14 -0
  42. package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
  43. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  44. package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
  45. package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
  46. package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
  47. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
  48. package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
  49. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  50. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  51. package/templates/marketing-site/src/app/globals.css +413 -0
  52. package/templates/marketing-site/src/app/layout.tsx +85 -0
  53. package/templates/marketing-site/src/app/robots.ts +13 -0
  54. package/templates/marketing-site/src/app/sitemap.ts +18 -0
  55. package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
  56. package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
  57. package/templates/marketing-site/src/components/CodePane.tsx +98 -0
  58. package/templates/marketing-site/src/components/Container.tsx +25 -0
  59. package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
  60. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
  61. package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
  62. package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
  63. package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
  64. package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
  65. package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
  66. package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
  67. package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
  68. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  69. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  70. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
  71. package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
  72. package/templates/marketing-site/src/lib/highlight.ts +134 -0
  73. package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
  74. package/templates/marketing-site/src/lib/metadata.ts +61 -33
  75. package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
  76. package/templates/marketing-site/src/lib/vex.ts +18 -0
  77. package/templates/marketing-site/src/proxy.ts +179 -0
  78. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  79. package/templates/marketing-site/src/vex.config.ts +27 -9
  80. package/templates/marketing-site/src/vex.types.ts +102 -76
  81. package/templates/marketing-site/src/vexcms/api.ts +2 -2
  82. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
  83. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
  84. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
  85. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
  86. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
  87. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
  88. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
  89. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
  90. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
  91. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
  92. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
  93. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
  94. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
  95. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
  96. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
  97. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
  98. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
  100. package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
  101. package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
  102. package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
  103. package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
  109. package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
  110. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
  111. package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
  112. package/dist/index.mjs +0 -30976
  113. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
  114. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
  115. 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 } from "~/db/constants"
4
- import config from "~/vex.config"
4
+ import config from "~/vex.config.server"
5
5
 
6
6
  import { query } from "./_generated/server"
7
7
 
@@ -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
- primaryCtaLabel: v.string(),
38
- primaryCtaHref: v.string(),
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(v.literal("shipped"), v.literal("coming-soon"), v.literal("planned"))
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("cta"),
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
- actions: v.optional(v.array(v.object({ label: v.string(), href: v.string() }))),
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("faq"),
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
- supportLink: v.optional(v.string()),
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>
@@ -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 { fetchQuery } from "convex/nextjs"
3
+ import { FirstAdminBootstrap } from "~/components/FirstAdminBootstrap"
4
4
 
5
- import { api } from "@convex/_generated/api"
6
-
7
- import { SiteFooter } from "~/components/SiteFooter"
8
- import { SiteHeader } from "~/components/SiteHeader"
9
- import { ThemeStyle } from "~/components/ThemeStyle"
10
-
11
- export default async function FrontendLayout({
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
- <ThemeStyle />
33
- <SiteHeader initialData={headerData} />
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
+ }