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
@@ -11,8 +11,16 @@ import { images, users } from "~/vexcms/collections"
11
11
  * Access control (RBAC) for the admin panel and every registered collection.
12
12
  *
13
13
  * `admin` gets unrestricted access. `user` can read and update only their own
14
- * profile row and cannot reach the admin panel. Add a resource here whenever
15
- * you register a new collection in `vex.config.ts`.
14
+ * profile row, cannot reach the admin panel, and can read `images`. Add a
15
+ * resource here whenever you register a new collection in `vex.config.ts`.
16
+ *
17
+ * `anonRole: user` also covers every unauthenticated public-site request,
18
+ * including the build-time metadata fetch. `images.read` is what lets the
19
+ * public site show uploaded media at all: `MediaImage` and
20
+ * `src/lib/metadata.ts` resolve uploads through the RBAC-gated
21
+ * `vex/media:getUrl`, so without it anonymous visitors get no images and the
22
+ * page ships no `og:image`. Narrow it with a `constraints` rule if some media
23
+ * must stay private; do not remove it.
16
24
  *
17
25
  * @see https://vexcms.dev/docs/access-control
18
26
  */
@@ -32,6 +40,10 @@ export const access = defineAccess({
32
40
  adminPanel: {
33
41
  access: false,
34
42
  },
43
+ images: {
44
+ "*": false,
45
+ read: true,
46
+ },
35
47
  user: {
36
48
  "*": false,
37
49
  read: {
@@ -2,11 +2,25 @@ import { api } from "@convex/_generated/api"
2
2
  import { fetchQuery } from "convex/nextjs"
3
3
  import { cookies } from "next/headers"
4
4
 
5
+ const SESSION_COOKIE = "better-auth.session_token"
6
+
7
+ /**
8
+ * Read the Better Auth session token from cookies.
9
+ *
10
+ * Better Auth prefixes its cookies with `__Secure-` whenever the resolved
11
+ * `baseURL` is https. Locally over http the cookie is
12
+ * `better-auth.session_token`; on a real domain it is
13
+ * `__Secure-better-auth.session_token`. Reading only the bare name works in
14
+ * development and silently returns `null` in production — no error, just an
15
+ * anonymous caller. Check the prefixed name first, as Better Auth's own reader
16
+ * does.
17
+ */
5
18
  export async function getSessionToken() {
6
19
  const cookieStore = await cookies()
7
- const sessionTokenCookie = cookieStore.get("better-auth.session_token")?.value
20
+ const sessionTokenCookie =
21
+ cookieStore.get(`__Secure-${SESSION_COOKIE}`)?.value ?? cookieStore.get(SESSION_COOKIE)?.value
8
22
 
9
- if (!sessionTokenCookie) return null
23
+ if (!sessionTokenCookie) {return null}
10
24
 
11
25
  // better-auth stores tokens as "<raw_token>.<hmac_signature>" (2 parts, not a JWT)
12
26
  // The raw token (first part) is what's indexed in the DB by_token
@@ -24,12 +38,12 @@ export async function getSessionToken() {
24
38
  export const getCurrentUser = async () => {
25
39
  try {
26
40
  const sessionToken = await getSessionToken()
27
- if (!sessionToken) return null
41
+ if (!sessionToken) {return null}
28
42
 
29
43
  const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
30
44
  sessionToken,
31
45
  })
32
- if (!session?.user) return null
46
+ if (!session?.user) {return null}
33
47
  return session.user
34
48
  } catch (error) {
35
49
  console.error("Error getting current user:", error)
@@ -43,12 +57,12 @@ export const getCurrentUser = async () => {
43
57
  export async function getSession() {
44
58
  try {
45
59
  const sessionToken = await getSessionToken()
46
- if (!sessionToken) return null
60
+ if (!sessionToken) {return null}
47
61
 
48
62
  const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
49
63
  sessionToken,
50
64
  })
51
- if (!session?.user) return null
65
+ if (!session?.user) {return null}
52
66
  return session
53
67
  } catch (error) {
54
68
  console.error("[getSession] Error:", error)
@@ -1,25 +1,12 @@
1
1
  "use client"
2
2
 
3
+ import { api } from "@convex/_generated/api"
3
4
  import { useMutation, useQuery } from "convex/react"
4
5
  import { usePathname, useRouter } from "next/navigation"
5
- import { useEffect, useState } from "react"
6
+ import { useEffect, useRef, useState } from "react"
6
7
 
7
- import { api } from "@convex/_generated/api"
8
8
  import { useSession } from "~/auth/client"
9
9
 
10
- function LoadingSpinner() {
11
- return (
12
- <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
13
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
14
- <path
15
- className="opacity-75"
16
- fill="currentColor"
17
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
18
- />
19
- </svg>
20
- )
21
- }
22
-
23
10
  /**
24
11
  * Landing UI for a project that has not been bootstrapped yet (no admin user),
25
12
  * and the "Go to Admin" / "Sign in" entry point once it has.
@@ -41,33 +28,44 @@ export function WelcomePage() {
41
28
  const [promoting, setPromoting] = useState(false)
42
29
  const [navigating, setNavigating] = useState(false)
43
30
 
44
- // Reset navigating state when the user comes back to the home page
45
- // (e.g. closing the intercepting auth dialog)
46
- useEffect(() => {
47
- if (pathname === "/") {
48
- setNavigating(false)
49
- }
50
- }, [pathname])
31
+ // Reset `navigating` when the user lands back on the home page (e.g. they
32
+ // dismissed the intercepting auth dialog). Adjusting state during render on
33
+ // a changed input is the pattern React documents for this; an effect would
34
+ // paint the stale spinner for a frame first.
35
+ const [lastPathname, setLastPathname] = useState(pathname)
36
+ if (lastPathname !== pathname) {
37
+ setLastPathname(pathname)
38
+ if (pathname === "/") {setNavigating(false)}
39
+ }
40
+
41
+ // Run-once latch. `promoting` and `navigating` both render, so they stay
42
+ // state; the guard does not, so it is a ref — that keeps it out of the
43
+ // dependency array and stops the effect re-running just to re-read it.
44
+ const promotingRef = useRef(false)
51
45
 
52
- // If the user is signed in and this is a fresh project, try to promote them
53
46
  useEffect(() => {
54
- if (session?.user && isBootstrapped === false && !promoting) {
55
- setPromoting(true)
56
- setNavigating(true)
57
- promoteFirstAdmin()
58
- .then((result) => {
59
- if (result.promoted) {
60
- router.push("/admin")
61
- } else {
62
- setNavigating(false)
63
- }
64
- })
65
- .catch(() => {
66
- setPromoting(false)
67
- setNavigating(false)
68
- })
69
- }
70
- }, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
47
+ if (!session?.user || isBootstrapped !== false || promotingRef.current) {return}
48
+
49
+ promotingRef.current = true
50
+ setPromoting(true)
51
+ setNavigating(true)
52
+
53
+ promoteFirstAdmin()
54
+ .then((result) => {
55
+ if (result.promoted) {
56
+ router.push("/admin")
57
+ return
58
+ }
59
+ promotingRef.current = false
60
+ setPromoting(false)
61
+ setNavigating(false)
62
+ })
63
+ .catch(() => {
64
+ promotingRef.current = false
65
+ setPromoting(false)
66
+ setNavigating(false)
67
+ })
68
+ }, [session, isBootstrapped, promoteFirstAdmin, router])
71
69
 
72
70
  // Treat undefined (query still loading) as not bootstrapped — show the welcome page immediately
73
71
  const bootstrapped = isBootstrapped === true
@@ -93,17 +91,17 @@ export function WelcomePage() {
93
91
 
94
92
  <div className="flex gap-4">
95
93
  {session?.user ? (
96
- <button onClick={() => handleNavigate("/admin")} disabled={navigating} className={buttonClass}>
94
+ <button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/admin")}>
97
95
  {navigating && <LoadingSpinner />}
98
96
  {navigating ? "Loading..." : "Go to Admin Panel"}
99
97
  </button>
100
98
  ) : bootstrapped ? (
101
- <button onClick={() => handleNavigate("/auth/sign-in")} disabled={navigating} className={buttonClass}>
99
+ <button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-in")}>
102
100
  {navigating && <LoadingSpinner />}
103
101
  {navigating ? "Loading..." : "Sign In"}
104
102
  </button>
105
103
  ) : (
106
- <button onClick={() => handleNavigate("/auth/sign-up")} disabled={navigating} className={buttonClass}>
104
+ <button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-up")}>
107
105
  {navigating && <LoadingSpinner />}
108
106
  {navigating ? "Loading..." : "Create Admin Account"}
109
107
  </button>
@@ -114,3 +112,16 @@ export function WelcomePage() {
114
112
  </div>
115
113
  )
116
114
  }
115
+
116
+ function LoadingSpinner() {
117
+ return (
118
+ <svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
119
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
120
+ <path
121
+ className="opacity-75"
122
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
123
+ fill="currentColor"
124
+ />
125
+ </svg>
126
+ )
127
+ }
@@ -59,7 +59,7 @@ export function ComponentExample() {
59
59
  </div>
60
60
 
61
61
  <code className="bg-muted text-muted-foreground rounded-lg px-4 py-2 font-mono text-sm">
62
- npx create-vexcms@latest
62
+ npx create-vexcms@alpha
63
63
  </code>
64
64
  </div>
65
65
  );
@@ -2,17 +2,25 @@ import { ThemeProvider } from "@vexcms/react";
2
2
  import { NuqsAdapter } from "nuqs/adapters/next/app";
3
3
  import { type PropsWithChildren } from "react";
4
4
 
5
- import { AuthServerProvider } from "./auth";
6
- import ConvexClientProvider from "./convex";
7
-
5
+ /**
6
+ * Server-side provider shell: theme context + the nuqs URL-state adapter.
7
+ *
8
+ * `AuthServerProvider` used to wrap `NuqsAdapter` here, putting a `getToken()`
9
+ * cookie read on every route's render path. It now mounts directly in
10
+ * `app/(vexcms)/admin/layout.tsx`, the only route group that needs it.
11
+ *
12
+ * Deliberately does **not** mount `ConvexClientProvider` — `ClientProviders`
13
+ * renders it, and `ClientProviders` is nested inside this component, so its
14
+ * copy is the one that actually reaches `children`. Mounting it here as well
15
+ * built a second `ConvexReactClient` + `QueryClient` pair on every server
16
+ * render (`providers/convex.tsx` intentionally creates fresh clients per call
17
+ * server-side to avoid cross-request leaks) whose only consumer was the
18
+ * discarded outer subtree.
19
+ */
8
20
  export default function ServerProviders({ children }: PropsWithChildren) {
9
21
  return (
10
22
  <ThemeProvider defaultTheme="system">
11
- <ConvexClientProvider>
12
- <AuthServerProvider>
13
- <NuqsAdapter>{children}</NuqsAdapter>
14
- </AuthServerProvider>
15
- </ConvexClientProvider>
23
+ <NuqsAdapter>{children}</NuqsAdapter>
16
24
  </ThemeProvider>
17
25
  );
18
26
  }
@@ -0,0 +1,30 @@
1
+ import { betterAuthAdapter } from "@vexcms/better-auth"
2
+ import { defineServerConfig } from "@vexcms/core"
3
+ import { convexFileStorage } from "@vexcms/file-storage-convex"
4
+
5
+ import { authOptions } from "@convex/auth/options"
6
+
7
+ import vexConfig from "./vex.config"
8
+
9
+ /**
10
+ * VexCMS server-only configuration for this project.
11
+ *
12
+ * Layers the Better Auth adapter and Convex file storage onto the
13
+ * client-safe `vexConfig`. You write this file once at scaffold time and
14
+ * rarely revisit it — day-to-day settings live in `vex.config.ts`.
15
+ *
16
+ * @see defineServerConfig in @vexcms/core
17
+ * @see betterAuthAdapter in @vexcms/better-auth
18
+ * @see ./vex.config for the client-safe half this layers onto
19
+ */
20
+ const config = defineServerConfig({
21
+ config: vexConfig,
22
+ server: {
23
+ auth: { adapter: betterAuthAdapter({ config: authOptions }) },
24
+ storage: {
25
+ adapters: [convexFileStorage()],
26
+ },
27
+ },
28
+ })
29
+
30
+ export default config
@@ -1,35 +1,38 @@
1
- import { betterAuthAdapter } from "@vexcms/better-auth"
1
+ import { betterAuthCollections } from "@vexcms/better-auth/client"
2
2
  import { defineConfig } from "@vexcms/core"
3
- import { convexFileStorage } from "@vexcms/file-storage-convex"
3
+ import { uploadFile } from "@vexcms/file-storage-convex/client"
4
4
 
5
- import { authOptions } from "@convex/auth/options"
5
+ import { authSchema } from "@convex/auth/schema"
6
6
 
7
7
  import { access } from "~/auth/access"
8
8
  import { images, users } from "~/vexcms/collections"
9
9
 
10
10
  /**
11
- * VexCMS configuration for this project.
11
+ * VexCMS client-safe configuration for this project.
12
12
  *
13
- * Registers the admin sidebar layout, the Better Auth adapter, Convex file
14
- * storage, and every collection. Add collections/globals here as you define
15
- * them — `vex dev` / `vex generate` derive the Convex schema and TypeScript
16
- * types from this file.
13
+ * Registers the admin sidebar layout and every collection, global, and
14
+ * media collection. Add collections/globals here as you define them —
15
+ * `vex dev` / `vex generate` derive the Convex schema and TypeScript types
16
+ * from this file, and the browser imports it directly via
17
+ * `VexConfigProvider`, so nothing here may reach a server SDK or an
18
+ * environment variable.
17
19
  *
18
20
  * @see defineConfig in @vexcms/core
19
- * @see betterAuthAdapter in @vexcms/better-auth
21
+ * @see ./vex.config.server for the auth adapter and storage adapter wiring
20
22
  */
21
23
  const vexConfig = defineConfig({
22
24
  access,
25
+ authCollections: betterAuthCollections(authSchema),
23
26
  admin: {
24
27
  sidebar: {
25
28
  side: "right",
26
29
  },
27
30
  },
28
- authAdapter: betterAuthAdapter({ config: authOptions }),
29
31
  storage: {
30
- adapters: [convexFileStorage({ mediaCollections: [images] })],
32
+ clientUploads: { convex: uploadFile },
31
33
  },
32
34
  collections: [users],
35
+ mediaCollections: [images],
33
36
  })
34
37
 
35
38
  export default vexConfig
@@ -135,6 +135,7 @@ export interface Image extends VexDocument {
135
135
  src: string
136
136
  width?: number
137
137
  height?: number
138
+ updatedAt?: number
138
139
  }
139
140
 
140
141
  export type CollectionSlug =
@@ -263,7 +264,7 @@ declare module "@vexcms/core" {
263
264
  }
264
265
  images: {
265
266
  text: "filename" | "alt" | "mimeType" | "storageId" | "src"
266
- number: "size" | "width" | "height"
267
+ number: "size" | "width" | "height" | "updatedAt"
267
268
  checkbox: "deleted"
268
269
  }
269
270
  }
@@ -1,4 +1,4 @@
1
- import { defineMediaCollection } from "@vexcms/file-storage-convex";
1
+ import { defineMediaCollection } from "@vexcms/file-storage-convex/client";
2
2
 
3
3
  import { TABLE_SLUG_IMAGES } from "~/db/constants";
4
4
 
@@ -1,10 +1,11 @@
1
- import { defineCollection } from "@vexcms/core"
1
+ import { defineCollection, text } from "@vexcms/core"
2
2
 
3
3
  import { TABLE_SLUG_USERS } from "~/db/constants"
4
4
 
5
5
  export const users = defineCollection({
6
6
  slug: TABLE_SLUG_USERS,
7
7
  admin: {
8
+ useAsTitle: "name",
8
9
  icon: "Users",
9
10
  },
10
11
  labels: {
@@ -12,6 +13,7 @@ export const users = defineCollection({
12
13
  plural: "Users",
13
14
  },
14
15
  fields: {
16
+ name: text({ label: "Name", required: true }),
15
17
  // Add custom user fields here — the auth adapter already contributes
16
18
  // name/email/roles/etc. from Better Auth's schema.
17
19
  },
@@ -1,7 +1,7 @@
1
1
  import { v } from "convex/values"
2
2
 
3
3
  import { TABLE_SLUG_PAGES } from "~/db/constants"
4
- import { find } from "~/vexcms/api"
4
+ import { find, publishedSlugs as readPublishedSlugs } from "~/vexcms/api"
5
5
 
6
6
  import { query } from "./_generated/server"
7
7
 
@@ -29,3 +29,22 @@ export const getBySlug = query({
29
29
  })
30
30
  },
31
31
  })
32
+
33
+ /**
34
+ * Returns `{ slug, createdAt, updatedAt? }` for every page document.
35
+ *
36
+ * Consumed by `app/sitemap.ts` and `[slug]/page.tsx`'s `generateStaticParams`.
37
+ * Access is bypassed for the same reason `getBySlug` bypasses it: both are
38
+ * read at build time and by anonymous crawlers, neither of which carries a
39
+ * session.
40
+ */
41
+ export const publishedSlugs = query({
42
+ args: {},
43
+ handler: async (ctx) => {
44
+ return await readPublishedSlugs({
45
+ access: { bypass: true },
46
+ collection: TABLE_SLUG_PAGES,
47
+ ctx,
48
+ })
49
+ },
50
+ })
@@ -5,14 +5,14 @@ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_USERS } from "~/db/constants"
5
5
 
6
6
  import {
7
7
  apikey,
8
+ footers,
9
+ headers,
8
10
  images,
9
11
  jwks,
10
- session,
11
- verification,
12
12
  pages,
13
- headers,
14
- footers,
13
+ session,
15
14
  themes,
15
+ verification,
16
16
  } from "./vex.schema"
17
17
 
18
18
  export default defineSchema({