create-vexcms 0.1.0-alpha.16 → 0.1.0-alpha.19

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 (28) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +8 -1
  3. package/package.json +1 -1
  4. package/templates/base-nextjs/README.md +3 -2
  5. package/templates/base-nextjs/convex/auth/options.ts +18 -26
  6. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  7. package/templates/base-nextjs/convex/vex/globals.ts +1 -1
  8. package/templates/base-nextjs/convex/vex/media.ts +1 -1
  9. package/templates/base-nextjs/convex/vex.ts +1 -1
  10. package/templates/base-nextjs/package.json +6 -6
  11. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  12. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +1 -1
  13. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  14. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +1 -2
  15. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +1 -1
  16. package/templates/base-nextjs/src/auth/access.ts +14 -2
  17. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  18. package/templates/base-nextjs/src/vex.config.ts +14 -11
  19. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  20. package/templates/marketing-site/convex/seed.ts +14 -11
  21. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  22. package/templates/marketing-site/convex/theme.ts +1 -1
  23. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +1 -4
  24. package/templates/marketing-site/src/lib/metadata.ts +9 -2
  25. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  26. package/templates/marketing-site/src/vex.config.ts +12 -9
  27. package/templates/marketing-site/src/vexcms/api.ts +1 -1
  28. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +2 -1
package/README.md CHANGED
@@ -100,6 +100,17 @@ site:
100
100
  - `convex/seed.ts` — an idempotent `init` mutation seeding site settings, a header, a footer, the
101
101
  starter palettes, and a complete home page from the blocks' own defaults (`pnpm seed`)
102
102
 
103
+ ### Configuration
104
+
105
+ Every scaffold ships two config files: `src/vex.config.ts` — client-safe, holds collections,
106
+ globals, media collections, access rules, and the auth collections
107
+ `betterAuthCollections(authSchema)` derives from `src/auth/schema.ts` — and
108
+ `src/vex.config.server.ts`, which layers the Better Auth adapter and the storage adapter(s)
109
+ onto it via `defineServerConfig`. Nothing is generated for auth: `src/auth/schema.ts` is the
110
+ one source of truth for model names and fields (organizations on or off, per the `--orgs`
111
+ flag) — the client config calls `betterAuthCollections()` on it directly, and
112
+ `src/auth/options.ts` spreads it into the real Better Auth options.
113
+
103
114
  ## Getting started
104
115
 
105
116
  After scaffolding:
package/dist/index.js CHANGED
@@ -2339,7 +2339,8 @@ var VexFrameworkInstaller = class {
2339
2339
  }
2340
2340
  /**
2341
2341
  * Configure the Better Auth organizations plugin.
2342
- * When enabled, replaces placeholders with organization import and plugin,
2342
+ * When enabled, replaces placeholders with the organization import, plugin
2343
+ * instance, and schema descriptor,
2343
2344
  * and leaves every org-aware file (`createGetAuth` call sites, `access.ts`,
2344
2345
  * `AuthContext.tsx`, `hasPermission.ts`, `convex/auth/api.ts`,
2345
2346
  * `db/constants/index.ts`) untouched.
@@ -2369,6 +2370,12 @@ var VexFrameworkInstaller = class {
2369
2370
  }
2370
2371
  await fs5.writeFile(pluginsPath, content);
2371
2372
  }
2373
+ const schemaPath = path2.join(this.targetPath, "convex/auth/schema.ts");
2374
+ if (await fs5.pathExists(schemaPath)) {
2375
+ let content = await fs5.readFile(schemaPath, "utf-8");
2376
+ content = enabled ? content.replace(" // {{ORGANIZATIONS_SCHEMA}}", " organization: true,") : content.replace(/.*\/\/ \{\{ORGANIZATIONS_SCHEMA\}\}\n?/, "");
2377
+ await fs5.writeFile(schemaPath, content);
2378
+ }
2372
2379
  if (!enabled) {
2373
2380
  await this.stripOrganizationWiring();
2374
2381
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vexcms",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "Scaffold a new VexCMS project",
5
5
  "keywords": [
6
6
  "cms",
@@ -77,10 +77,11 @@ see `convex/vex/firstUser.ts`. From there, "Go to Admin Panel" opens `/admin`.
77
77
  │ ├── auth/ # Auth client + server helpers, access control
78
78
  │ ├── components/ # React components
79
79
  │ ├── vexcms/collections/ # Collection definitions
80
- │ ├── vex.config.ts # VexCMS config
80
+ │ ├── vex.config.ts # VexCMS client config (collections, globals, access)
81
+ │ ├── vex.config.server.ts # VexCMS server config (auth adapter, storage adapters)
81
82
  │ └── env.mjs # Typed environment variables
82
83
  ├── convex/ # Convex backend
83
- │ ├── auth/ # Better Auth config + plugins
84
+ │ ├── auth/ # Better Auth config + plugins (schema.ts, options.ts)
84
85
  │ ├── vex/ # Bootstrap, media, globals endpoints
85
86
  │ └── schema.ts # Database schema
86
87
  └── .env.local # Environment variables (not committed)
@@ -2,20 +2,28 @@ import type { BetterAuthOptions } from "better-auth"
2
2
 
3
3
  import { anonRoleDatabaseHook } from "@vexcms/better-auth"
4
4
 
5
- import {
6
- TABLE_SLUG_ACCOUNTS,
7
- TABLE_SLUG_SESSIONS,
8
- TABLE_SLUG_USERS,
9
- TABLE_SLUG_VERIFICATIONS,
10
- USER_ROLES,
11
- } from "~/db/constants"
5
+ import { USER_ROLES } from "~/db/constants"
12
6
 
13
7
  import { createPlugins } from "./plugins"
8
+ import { authSchema } from "./schema"
14
9
 
10
+ // `plugins` on the shared schema is a descriptor map for
11
+ // `betterAuthCollections`, not Better Auth plugin instances — dropped by name
12
+ // so the intent survives a reordering of the object literal below.
13
+ const { plugins: _pluginDescriptors, ...schemaOptions } = authSchema
14
+
15
+ /**
16
+ * Better Auth options for this project.
17
+ *
18
+ * Model names and `user.additionalFields` come from `./schema`, the one module
19
+ * both this file and `src/vex.config.ts` read, so a field added there reaches
20
+ * the database and the admin panel without being written twice. Everything
21
+ * below the spread is server-only.
22
+ *
23
+ * @see ./schema for the shared, client-safe half
24
+ */
15
25
  export const authOptions: BetterAuthOptions = {
16
- account: {
17
- modelName: TABLE_SLUG_ACCOUNTS,
18
- },
26
+ ...schemaOptions,
19
27
  baseURL: process.env.SITE_URL,
20
28
  databaseHooks: {
21
29
  // Ties Better Auth's anonymous-plugin users to `access.anonRole` (see
@@ -28,21 +36,5 @@ export const authOptions: BetterAuthOptions = {
28
36
  // {{OAUTH_PROVIDERS}}
29
37
  plugins: createPlugins(),
30
38
  secret: process.env.BETTER_AUTH_SECRET,
31
- session: {
32
- modelName: TABLE_SLUG_SESSIONS,
33
- },
34
39
  trustedOrigins: [process.env.SITE_URL!],
35
- user: {
36
- additionalFields: {
37
- roles: {
38
- type: "string[]",
39
- defaultValue: [USER_ROLES.user],
40
- required: true,
41
- },
42
- },
43
- modelName: TABLE_SLUG_USERS,
44
- },
45
- verification: {
46
- modelName: TABLE_SLUG_VERIFICATIONS,
47
- },
48
40
  }
@@ -0,0 +1,51 @@
1
+ import type { BetterAuthSchemaInput } from "@vexcms/better-auth/client"
2
+
3
+ import {
4
+ TABLE_SLUG_ACCOUNTS,
5
+ TABLE_SLUG_SESSIONS,
6
+ TABLE_SLUG_USERS,
7
+ TABLE_SLUG_VERIFICATIONS,
8
+ USER_ROLES,
9
+ } from "~/db/constants"
10
+
11
+ /**
12
+ * Everything about your Better Auth setup that determines which collections
13
+ * exist and which fields they carry — and nothing else.
14
+ *
15
+ * This is the one auth module the browser reads: `src/vex.config.ts` passes it
16
+ * to `betterAuthCollections()` to build the admin panel's auth collections,
17
+ * and `./options` spreads it so the same model names and fields reach the
18
+ * database. Add a `user.additionalFields` entry here and it shows up in both.
19
+ *
20
+ * Keep it inert. `./options` reads `process.env` and `./plugins` pulls
21
+ * `better-auth/next-js` (→ `next/headers`), which is why they are separate
22
+ * files — importing either from the client config would drag the server into
23
+ * your browser bundle. Runtime plugin options (admin roles, mailers, OAuth
24
+ * secrets) belong on the real plugin instances in `./plugins`; only options
25
+ * that change which TABLES exist belong here.
26
+ *
27
+ * @see betterAuthCollections in @vexcms/better-auth/client
28
+ * @see ./options for the server-side options that spread this
29
+ */
30
+ export const authSchema = {
31
+ account: { modelName: TABLE_SLUG_ACCOUNTS },
32
+ session: { modelName: TABLE_SLUG_SESSIONS },
33
+ user: {
34
+ modelName: TABLE_SLUG_USERS,
35
+ additionalFields: {
36
+ roles: {
37
+ type: "string[]",
38
+ defaultValue: [USER_ROLES.user],
39
+ required: true,
40
+ },
41
+ },
42
+ },
43
+ verification: { modelName: TABLE_SLUG_VERIFICATIONS },
44
+ plugins: {
45
+ admin: true,
46
+ anonymous: true,
47
+ apiKey: true,
48
+ convex: true,
49
+ // {{ORGANIZATIONS_SCHEMA}}
50
+ },
51
+ } satisfies BetterAuthSchemaInput
@@ -2,7 +2,7 @@ import { createGetAuth } from "@vexcms/better-auth";
2
2
  import { globalsApi } from "@vexcms/core/server";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
7
  import { mutation, query } from "../_generated/server";
8
8
 
@@ -2,7 +2,7 @@ import { createGetAuth } from "@vexcms/better-auth";
2
2
  import { mediaApi } from "@vexcms/core";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
7
  import { mutation, query } from "../_generated/server";
8
8
 
@@ -2,7 +2,7 @@ import { createGetAuth } from "@vexcms/better-auth";
2
2
  import { collectionsApi } from "@vexcms/core/server";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
7
  import { mutation, query } from "./_generated/server";
8
8
 
@@ -24,11 +24,11 @@
24
24
  "@t3-oss/env-nextjs": "0.13.11",
25
25
  "@tanstack/react-form": "1.33.1",
26
26
  "@tanstack/react-query": "5.101.2",
27
- "@vexcms/better-auth": "~0.1.0-alpha.16",
28
- "@vexcms/core": "~0.1.0-alpha.16",
29
- "@vexcms/file-storage-convex": "~0.1.0-alpha.16",
30
- "@vexcms/next": "~0.1.0-alpha.16",
31
- "@vexcms/react": "~0.1.0-alpha.16",
27
+ "@vexcms/better-auth": "~0.1.0-alpha.19",
28
+ "@vexcms/core": "~0.1.0-alpha.19",
29
+ "@vexcms/file-storage-convex": "~0.1.0-alpha.19",
30
+ "@vexcms/next": "~0.1.0-alpha.19",
31
+ "@vexcms/react": "~0.1.0-alpha.19",
32
32
  "better-auth": "1.6.23",
33
33
  "class-variance-authority": "0.7.1",
34
34
  "clsx": "2.1.1",
@@ -52,7 +52,7 @@
52
52
  "@types/node": "20.19.43",
53
53
  "@types/react": "19.2.17",
54
54
  "@types/react-dom": "19.2.3",
55
- "@vexcms/cli": "~0.1.0-alpha.16",
55
+ "@vexcms/cli": "~0.1.0-alpha.19",
56
56
  "babel-plugin-react-compiler": "1.0.0",
57
57
  "eslint": "9.39.5",
58
58
  "eslint-config-next": "15.5.9",
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { AuthView } from "@daveyplate/better-auth-ui";
4
- import { useSearchParams } from "next/navigation";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
5
  import { Activity } from "react";
6
6
 
7
7
  import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
8
8
 
9
9
  export default function AuthCard({ pathname }: { pathname: string }) {
10
+ const router = useRouter();
10
11
  const searchParams = useSearchParams();
11
12
  const isGuardRedirect = searchParams.has("redirectTo");
12
13
  if (isGuardRedirect) {
@@ -16,20 +17,31 @@ export default function AuthCard({ pathname }: { pathname: string }) {
16
17
  </main>
17
18
  );
18
19
  }
20
+ // No in-flow wrapper here on purpose. The Dialog portals into <body>, so a
21
+ // wrapping <main> would be an empty element sitting after the footer (the
22
+ // @auth slot renders last in the frontend layout). Next's post-navigation
23
+ // scroll handler targets the new segment's first DOM node and would scroll
24
+ // that empty element into view — i.e. jump the page to the bottom behind
25
+ // the modal. With only a portal rendered there is no node to target.
19
26
  return (
20
- <main>
21
- <Dialog open>
22
- <Activity mode="hidden">
23
- <DialogTitle />
24
- </Activity>
25
- <DialogContent
26
- aria-describedby={undefined}
27
- className="grid place-items-center bg-transparent shadow-none ring-transparent"
28
- showCloseButton={false}
29
- >
30
- <AuthView path={pathname} />
31
- </DialogContent>
32
- </Dialog>
33
- </main>
27
+ <Dialog
28
+ onOpenChange={(open) => {
29
+ if (!open) {
30
+ router.back();
31
+ }
32
+ }}
33
+ open
34
+ >
35
+ <Activity mode="hidden">
36
+ <DialogTitle />
37
+ </Activity>
38
+ <DialogContent
39
+ aria-describedby={undefined}
40
+ className="grid place-items-center bg-transparent shadow-none ring-transparent"
41
+ showCloseButton={false}
42
+ >
43
+ <AuthView path={pathname} />
44
+ </DialogContent>
45
+ </Dialog>
34
46
  );
35
47
  }
@@ -4,7 +4,7 @@ import { NextAdminPage } from "@vexcms/next/server";
4
4
  import { redirect } from "next/navigation";
5
5
 
6
6
  import { fetchAuthQuery, getToken } from "~/auth/server";
7
- import config from "~/vex.config";
7
+ import config from "~/vex.config.server";
8
8
 
9
9
  export const dynamic = "force-dynamic";
10
10
 
@@ -1,22 +1,17 @@
1
- "use client";
1
+ "use client"
2
2
 
3
- import { uploadFile } from "@vexcms/file-storage-convex";
4
- import { StorageAdapterContextProvider, VexAccessProvider } from "@vexcms/react";
3
+ import { VexConfigProvider } from "@vexcms/react"
5
4
 
6
- import { access } from "~/auth/access";
5
+ import config from "~/vex.config"
7
6
 
8
7
  /**
9
- * Provides client-side upload functions for VexCMS storage adapters.
8
+ * Mounts the single `VexConfigProvider` for the admin panel's client tree.
10
9
  *
11
- * This is a "use client" component so that uploadFile is imported and
12
- * owned on the client — it never crosses the RSC boundary as a prop.
13
- *
14
- * Add a new entry here when registering an additional storage adapter.
10
+ * This is a "use client" component so `vex.config` is imported and owned on
11
+ * the client — it never crosses the RSC boundary as a prop. `access` and
12
+ * `storage.clientUploads` (the two fields that used to need their own
13
+ * providers) are already part of the config this imports.
15
14
  */
16
15
  export function ClientProviders({ children }: { children: React.ReactNode }) {
17
- return (
18
- <StorageAdapterContextProvider adapterClients={{ convex: uploadFile }}>
19
- <VexAccessProvider access={access}>{children}</VexAccessProvider>
20
- </StorageAdapterContextProvider>
21
- );
16
+ return <VexConfigProvider config={config}>{children}</VexConfigProvider>
22
17
  }
@@ -4,7 +4,6 @@ import { NextAdminLayout } from "@vexcms/next/client"
4
4
 
5
5
  import { AuthServerProvider } from "~/components/providers/auth"
6
6
  import { getCurrentUser } from "~/auth/serverUtils"
7
- import config from "~/vex.config"
8
7
 
9
8
  import { ClientProviders } from "./clientProviders"
10
9
 
@@ -13,7 +12,7 @@ export default async function AdminLayout({ children }: { children: ReactNode })
13
12
  return (
14
13
  <AuthServerProvider>
15
14
  <ClientProviders>
16
- <NextAdminLayout config={config} user={user ?? undefined}>
15
+ <NextAdminLayout user={user ?? undefined}>
17
16
  {children}
18
17
  </NextAdminLayout>
19
18
  </ClientProviders>
@@ -2,7 +2,7 @@ import { api } from "@convex/_generated/api"
2
2
  import { createVexRevalidateRoute } from "@vexcms/next/cache"
3
3
 
4
4
  import { fetchAuthQuery, getToken } from "~/auth/server"
5
- import config from "~/vex.config"
5
+ import config from "~/vex.config.server"
6
6
 
7
7
  /**
8
8
  * `POST /api/vex/revalidate` — session-authorized path purge.
@@ -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: {
@@ -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
@@ -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
 
@@ -7,12 +7,15 @@ import {
7
7
  TABLE_SLUG_PAGES,
8
8
  TABLE_SLUG_THEMES,
9
9
  } from "~/db/constants"
10
- import config from "~/vex.config"
10
+ import config from "~/vex.config.server"
11
11
  import {
12
12
  BLOCK_SLUG_CODE_SHOWCASE,
13
13
  BLOCK_SLUG_CTA,
14
14
  BLOCK_SLUG_FAQ,
15
15
  BLOCK_SLUG_FEATURES,
16
+ BLOCK_SLUG_FOOTER,
17
+ BLOCK_SLUG_HEADER,
18
+ BLOCK_SLUG_HERO,
16
19
  BLOCK_SLUG_HOW_IT_WORKS,
17
20
  BLOCK_SLUG_ROADMAP,
18
21
  BLOCK_SLUG_STATS,
@@ -389,7 +392,7 @@ export const init = internalMutation({
389
392
  name: "Main Header",
390
393
  content: [
391
394
  {
392
- blockType: "header",
395
+ blockType: BLOCK_SLUG_HEADER,
393
396
  blockName: "Site Header",
394
397
  id: "main-header",
395
398
  logoText: "VexCMS",
@@ -427,7 +430,7 @@ export const init = internalMutation({
427
430
  name: "Main Footer",
428
431
  content: [
429
432
  {
430
- blockType: "footer",
433
+ blockType: BLOCK_SLUG_FOOTER,
431
434
  blockName: "Site Footer",
432
435
  id: "main-footer",
433
436
  logoText: "VexCMS",
@@ -461,7 +464,7 @@ export const init = internalMutation({
461
464
  slug: "home",
462
465
  blocks: [
463
466
  {
464
- blockType: "hero",
467
+ blockType: BLOCK_SLUG_HERO,
465
468
  blockName: "Hero",
466
469
  id: "home-hero",
467
470
  variant: ["full"],
@@ -507,7 +510,7 @@ export const init = internalMutation({
507
510
  ],
508
511
  },
509
512
  {
510
- blockType: "features",
513
+ blockType: BLOCK_SLUG_FEATURES,
511
514
  blockName: "Features",
512
515
  id: "home-features",
513
516
  heading: "Everything comes from one schema.",
@@ -618,7 +621,7 @@ export const posts = defineTable({
618
621
  ],
619
622
  },
620
623
  {
621
- blockType: "how_it_works",
624
+ blockType: BLOCK_SLUG_HOW_IT_WORKS,
622
625
  blockName: "How It Works",
623
626
  id: "home-how-it-works",
624
627
  heading: "From zero to a live CMS in four steps.",
@@ -651,7 +654,7 @@ export const posts = defineTable({
651
654
  ],
652
655
  },
653
656
  {
654
- blockType: "roadmap",
657
+ blockType: BLOCK_SLUG_ROADMAP,
655
658
  blockName: "Roadmap",
656
659
  id: "home-roadmap",
657
660
  heading: "Roadmap",
@@ -769,7 +772,7 @@ export const posts = defineTable({
769
772
  ],
770
773
  },
771
774
  {
772
- blockType: "faq",
775
+ blockType: BLOCK_SLUG_FAQ,
773
776
  blockName: "FAQ",
774
777
  id: "home-faq",
775
778
  heading: "Questions we keep getting.",
@@ -809,7 +812,7 @@ export const posts = defineTable({
809
812
  ],
810
813
  },
811
814
  {
812
- blockType: "cta",
815
+ blockType: BLOCK_SLUG_CTA,
813
816
  blockName: "CTA",
814
817
  id: "home-cta",
815
818
  heading: "Start with a schema. Ship in an hour.",
@@ -836,7 +839,7 @@ export const posts = defineTable({
836
839
  slug: "features",
837
840
  blocks: [
838
841
  {
839
- blockType: "hero",
842
+ blockType: BLOCK_SLUG_HERO,
840
843
  blockName: "Hero",
841
844
  id: "features-hero",
842
845
  variant: ["compact"],
@@ -937,7 +940,7 @@ export const posts = defineTable({
937
940
  slug: "roadmap",
938
941
  blocks: [
939
942
  {
940
- blockType: "hero",
943
+ blockType: BLOCK_SLUG_HERO,
941
944
  blockName: "Hero",
942
945
  id: "roadmap-hero",
943
946
  variant: ["compact"],
@@ -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"
@@ -6,7 +6,6 @@ import { AuthServerProvider } from "~/components/providers/auth";
6
6
  import { getCurrentUser } from "~/auth/serverUtils";
7
7
  import { ThemeLive } from "~/components/ThemeLive";
8
8
  import { ThemeStyle } from "~/components/ThemeStyle";
9
- import config from "~/vex.config";
10
9
 
11
10
  import { ClientProviders } from "./clientProviders";
12
11
 
@@ -26,9 +25,7 @@ export default async function AdminLayout({ children }: { children: ReactNode })
26
25
  <ClientProviders>
27
26
  <ThemeStyle scope="admin" />
28
27
  <ThemeLive scope="admin" />
29
- <NextAdminLayout config={config} user={user ?? undefined}>
30
- {children}
31
- </NextAdminLayout>
28
+ <NextAdminLayout user={user ?? undefined}>{children}</NextAdminLayout>
32
29
  </ClientProviders>
33
30
  </AuthServerProvider>
34
31
  );
@@ -75,8 +75,15 @@ export async function generatePageMetadata(props: { slug?: string }): Promise<Me
75
75
  imageUrl: ogImageUrl,
76
76
  })
77
77
 
78
- if (twitterHandle) {
79
- metadata.twitter = { card: "summary_large_image", site: twitterHandle }
78
+ // Discord, Slack and X pick the large-image layout from `twitter:card`, not
79
+ // from `og:image` alone — and Next auto-fills `card: "summary"` (a small
80
+ // thumbnail) whenever `twitter` is omitted. So the card type must be emitted
81
+ // whenever an image resolved, independently of whether a handle exists.
82
+ if (ogImageUrl || twitterHandle) {
83
+ metadata.twitter = {
84
+ card: ogImageUrl ? "summary_large_image" : "summary",
85
+ ...(twitterHandle ? { site: twitterHandle } : {}),
86
+ }
80
87
  }
81
88
 
82
89
  return metadata
@@ -0,0 +1,27 @@
1
+ import { authOptions } from "@convex/auth/options"
2
+ import { betterAuthAdapter } from "@vexcms/better-auth"
3
+ import { defineServerConfig } from "@vexcms/core"
4
+ import { convexFileStorage } from "@vexcms/file-storage-convex"
5
+
6
+ import vexConfig from "./vex.config"
7
+
8
+ /**
9
+ * VexCMS server-only configuration for the marketing site.
10
+ *
11
+ * Layers the Better Auth adapter and Convex file storage onto the
12
+ * client-safe `vexConfig`. You write this file once at scaffold time and
13
+ * rarely revisit it — day-to-day settings live in `vex.config.ts`.
14
+ *
15
+ * @see ./vex.config for the client-safe half this layers onto
16
+ */
17
+ const config = defineServerConfig({
18
+ config: vexConfig,
19
+ server: {
20
+ auth: { adapter: betterAuthAdapter({ config: authOptions }) },
21
+ storage: {
22
+ adapters: [convexFileStorage()],
23
+ },
24
+ },
25
+ })
26
+
27
+ export default config
@@ -1,33 +1,36 @@
1
- import { authOptions } from "@convex/auth/options"
2
- import { betterAuthAdapter } from "@vexcms/better-auth"
1
+ import { betterAuthCollections } from "@vexcms/better-auth/client"
3
2
  import { defineConfig } from "@vexcms/core"
4
- import { convexFileStorage } from "@vexcms/file-storage-convex"
3
+ import { uploadFile } from "@vexcms/file-storage-convex/client"
4
+
5
+ import { authSchema } from "@convex/auth/schema"
5
6
 
6
7
  import { access } from "~/auth/access"
7
8
  import { footers, headers, images, pages, themes, users } from "~/vexcms/collections"
8
9
  import { siteSettings } from "~/vexcms/globals"
9
10
 
10
11
  /**
11
- * VexCMS configuration for the marketing site.
12
+ * VexCMS client-safe configuration for the marketing site.
12
13
  *
13
14
  * Replaces `templates/base-nextjs`'s bare config wholesale (overlay copy is
14
15
  * file-level, not a merge): carries base's `users`/`images` forward unchanged
15
- * and adds the four marketing collections plus `siteSettings`. `vex dev`/
16
- * `vex generate` consume this to produce the Convex schema and TypeScript
17
- * types.
16
+ * and adds the four marketing collections plus `siteSettings`. Imported
17
+ * directly by the browser via `VexConfigProvider`.
18
+ *
19
+ * @see ./vex.config.server for the auth adapter and storage adapter wiring
18
20
  */
19
21
  const vexConfig = defineConfig({
20
22
  access,
23
+ authCollections: betterAuthCollections(authSchema),
21
24
  admin: {
22
25
  sidebar: {
23
26
  side: "right",
24
27
  },
25
28
  },
26
- authAdapter: betterAuthAdapter({ config: authOptions }),
27
29
  storage: {
28
- adapters: [convexFileStorage({ mediaCollections: [images] })],
30
+ clientUploads: { convex: uploadFile },
29
31
  },
30
32
  collections: [users, pages, headers, footers, themes],
33
+ mediaCollections: [images],
31
34
  globals: [siteSettings],
32
35
  routes: {
33
36
  map: ({ collection, doc }) => {
@@ -3,7 +3,7 @@ import { createGetAuth } from "@vexcms/better-auth"
3
3
  import { vexServerApi } from "@vexcms/core/server"
4
4
 
5
5
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants"
6
- import config from "~/vex.config"
6
+ import config from "~/vex.config.server"
7
7
 
8
8
  /**
9
9
  * Bound server API — `config`/`getAuth` are threaded once here so
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { BlockComponentProps } from "@vexcms/react"
4
4
 
5
- import { cn, ScrollArea, Tabs, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipTrigger } from "@vexcms/react"
5
+ import { cn, ScrollArea, ScrollBar, Tabs, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipTrigger } from "@vexcms/react"
6
6
  import { Check } from "lucide-react"
7
7
  import { useState } from "react"
8
8
 
@@ -88,6 +88,7 @@ export default function RoadmapBlock({ block }: BlockComponentProps) {
88
88
  </TabsTrigger>
89
89
  ))}
90
90
  </TabsList>
91
+ <ScrollBar orientation="horizontal" />
91
92
  </ScrollArea>
92
93
  </Tabs>
93
94
  ) : null}