create-vexcms 0.1.0-alpha.3 → 0.1.0-alpha.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vexcms",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Scaffold a new VexCMS project",
5
5
  "keywords": [
6
6
  "cms",
@@ -8,6 +8,13 @@ const nextConfig: NextConfig = {
8
8
  },
9
9
  allowedDevOrigins: ["127.0.01", "localhost"],
10
10
  reactCompiler: true,
11
+ // Pin the workspace root to THIS app. Without it, Next walks up and adopts
12
+ // any outer lockfile (e.g. when this project lives inside a monorepo),
13
+ // which shifts Turbopack's file-tracing root and Tailwind's source-scan
14
+ // root — producing wrong relative paths and scanning files outside the app.
15
+ turbopack: {
16
+ root: import.meta.dirname,
17
+ },
11
18
  images: {
12
19
  remotePatterns: [
13
20
  // Add your Convex deployment's hostname here once `npx convex dev` has
@@ -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.3",
28
- "@vexcms/core": "~0.1.0-alpha.3",
29
- "@vexcms/file-storage-convex": "~0.1.0-alpha.3",
30
- "@vexcms/next": "~0.1.0-alpha.3",
31
- "@vexcms/react": "~0.1.0-alpha.3",
27
+ "@vexcms/better-auth": "~0.1.0-alpha.4",
28
+ "@vexcms/core": "~0.1.0-alpha.4",
29
+ "@vexcms/file-storage-convex": "~0.1.0-alpha.4",
30
+ "@vexcms/next": "~0.1.0-alpha.4",
31
+ "@vexcms/react": "~0.1.0-alpha.4",
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.3",
55
+ "@vexcms/cli": "~0.1.0-alpha.4",
56
56
  "babel-plugin-react-compiler": "1.0.0",
57
57
  "eslint": "9.39.5",
58
58
  "eslint-config-next": "15.5.9",
@@ -25,15 +25,16 @@
25
25
  @import "shadcn/tailwind.css";
26
26
  @import "@vexcms/next/styles";
27
27
 
28
- /* Workspace source scan (belt-and-suspenders normal dev relies on tsup
29
- watchers rebuilding each package dist/, which Tailwind already scans via
30
- @vexcms/next/styles). These two lines also point Tailwind at the raw
31
- .tsx so classes appear immediately even if a tsup watcher dies. They
32
- are workspace-relative and resolve only inside this monorepo. */
33
- @source "../../../../packages/react/src/**/*.ts";
34
- @source "../../../../packages/react/src/**/*.tsx";
35
- @source "../../../../packages/next/src/**/*.ts";
36
- @source "../../../../packages/next/src/**/*.tsx";
28
+ /* Scan the installed @vexcms packages for class candidates (their dist output
29
+ ships alongside this app in node_modules; @vexcms/next/styles above pulls
30
+ the stylesheet, these make sure every utility used by the admin UI exists).
31
+ Globbed to `*.js` only the same `dist` directories also carry `.map`
32
+ sourcemaps and `.d.ts` declaration files, and Tailwind's scanner extracts
33
+ garbage "class candidates" from their base64/binary-ish content and emits
34
+ unparseable CSS, exactly like the raw `public/` binary assets below. */
35
+ @source "../../node_modules/@vexcms/react/dist/**/*.js";
36
+ @source "../../node_modules/@vexcms/next/dist/**/*.js";
37
+ @source not "../../public";
37
38
 
38
39
  @custom-variant dark (&:is(.dark *));
39
40
 
@@ -2,6 +2,7 @@ import type { Metadata } from "next"
2
2
 
3
3
  import "./globals.css"
4
4
 
5
+ import { ThemeScript } from "@vexcms/react"
5
6
  import { Geist, Geist_Mono } from "next/font/google"
6
7
 
7
8
  import ClientProviders from "~/components/providers/client"
@@ -30,6 +31,11 @@ export default function RootLayout({
30
31
  }>) {
31
32
  return (
32
33
  <html className={`${geistSans.variable} ${geistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
34
+ <head>
35
+ {/* Applies the persisted light/dark class before first paint —
36
+ without it the admin flashes light before the saved mode lands. */}
37
+ <ThemeScript />
38
+ </head>
33
39
  <body>
34
40
  <ServerProviders>
35
41
  <ClientProviders>{children}</ClientProviders>
@@ -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({ 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
  },
@@ -7,6 +7,13 @@ import {
7
7
  TABLE_SLUG_PAGES,
8
8
  TABLE_SLUG_THEMES,
9
9
  } from "~/db/constants"
10
+ import {
11
+ BLOCK_SLUG_CTA,
12
+ BLOCK_SLUG_FAQ,
13
+ BLOCK_SLUG_FEATURES,
14
+ BLOCK_SLUG_HOW_IT_WORKS,
15
+ BLOCK_SLUG_ROADMAP,
16
+ } from "~/vexcms/blocks/constants"
10
17
  import config from "~/vex.config"
11
18
 
12
19
  import { internalMutation } from "./_generated/server"
@@ -391,7 +398,7 @@ export const init = internalMutation({
391
398
  { label: "Docs", href: "/docs" },
392
399
  ],
393
400
  actionButtons: [
394
- { label: "GitHub", href: "https://github.com/vexcms/vex", variant: ["ghost"] },
401
+ { label: "GitHub", href: "https://github.com/ianyimi/vex", variant: ["ghost"] },
395
402
  { label: "Get Started", href: "/docs", variant: ["default"] },
396
403
  ],
397
404
  },
@@ -421,7 +428,7 @@ export const init = internalMutation({
421
428
  { label: "Roadmap", href: "/roadmap" },
422
429
  { label: "Documentation", href: "/docs" },
423
430
  ],
424
- socialLinks: [{ platform: "GitHub", href: "https://github.com/vexcms/vex", icon: "Github" }],
431
+ socialLinks: [{ platform: "GitHub", href: "https://github.com/ianyimi/vex", icon: "Github" }],
425
432
  },
426
433
  ],
427
434
  })
@@ -443,7 +450,7 @@ export const init = internalMutation({
443
450
  blockType: "hero",
444
451
  blockName: "Hero",
445
452
  id: "home-hero",
446
- badgeText: "Now in public beta",
453
+ badgeText: "Now in public alpha",
447
454
  badgeLink: "/docs",
448
455
  heading: "The CMS built for Convex",
449
456
  subheading:
@@ -451,7 +458,7 @@ export const init = internalMutation({
451
458
  primaryCtaLabel: "Get Started",
452
459
  primaryCtaHref: "/docs",
453
460
  secondaryCtaLabel: "View on GitHub",
454
- secondaryCtaHref: "https://github.com/vexcms/vex",
461
+ secondaryCtaHref: "https://github.com/ianyimi/vex",
455
462
  },
456
463
  {
457
464
  blockType: "features",
@@ -598,7 +605,7 @@ export const init = internalMutation({
598
605
  id: "home-faq",
599
606
  heading: "Frequently Asked Questions",
600
607
  subheading: "Everything you need to know about Vex CMS and building with Convex.",
601
- supportLink: "https://github.com/vexcms/vex/issues",
608
+ supportLink: "https://github.com/ianyimi/vex/issues",
602
609
  items: [
603
610
  {
604
611
  question: "What is Vex CMS?",
@@ -635,7 +642,7 @@ export const init = internalMutation({
635
642
  subheading: "Get started in minutes with create-vexcms. Real-time content management powered by Convex.",
636
643
  actions: [
637
644
  { label: "Get Started", href: "/docs" },
638
- { label: "View on GitHub", href: "https://github.com/vexcms/vex" },
645
+ { label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
639
646
  ],
640
647
  },
641
648
  ],
@@ -643,6 +650,240 @@ export const init = internalMutation({
643
650
  created.push("page:home")
644
651
  }
645
652
 
653
+ const existingFeatures = await ctx.db
654
+ .query(TABLE_SLUG_PAGES)
655
+ .withIndex("by_slug", (q) => q.eq("slug", "features"))
656
+ .first()
657
+ if (existingFeatures) {
658
+ skipped.push("page:features")
659
+ } else {
660
+ await ctx.db.insert(TABLE_SLUG_PAGES, {
661
+ title: "Features",
662
+ slug: "features",
663
+ blocks: [
664
+ {
665
+ blockType: BLOCK_SLUG_FEATURES,
666
+ blockName: "Features",
667
+ id: "features-features",
668
+ heading: "Everything you need to manage content",
669
+ subheading:
670
+ "Built on Convex's real-time infrastructure with a developer experience that doesn't compromise on power.",
671
+ features: [
672
+ {
673
+ title: "Real-Time by Default",
674
+ description:
675
+ "Every query is live. Content updates appear instantly across all connected clients — no polling, no webhooks.",
676
+ icon: "Zap",
677
+ },
678
+ {
679
+ title: "Type-Safe Schemas",
680
+ description:
681
+ "Define your collections with TypeScript. Vex generates Convex schemas, Zod validators, and typed queries automatically.",
682
+ icon: "Shield",
683
+ },
684
+ {
685
+ title: "Developer First",
686
+ description:
687
+ "Code-first configuration, CLI tooling, and a clean API. Build with the tools you already know and love.",
688
+ icon: "Code",
689
+ },
690
+ ],
691
+ },
692
+ {
693
+ blockType: BLOCK_SLUG_HOW_IT_WORKS,
694
+ blockName: "How It Works",
695
+ id: "features-how-it-works",
696
+ heading: "Get started in minutes",
697
+ subheading:
698
+ "From zero to a fully functional CMS in four steps. No boilerplate, no config files to wrestle with.",
699
+ steps: [
700
+ {
701
+ icon: "Terminal",
702
+ title: "Scaffold your project",
703
+ description:
704
+ "Run npx create-vexcms@latest to get a Next.js app with Convex, authentication, and the admin panel pre-configured.",
705
+ },
706
+ {
707
+ icon: "Code",
708
+ title: "Define your schema",
709
+ description:
710
+ "Use defineCollection() and field helpers to declare your content model in TypeScript. Vex generates your Convex schema, types, and queries automatically.",
711
+ },
712
+ {
713
+ icon: "LayoutGrid",
714
+ title: "Build with blocks",
715
+ description:
716
+ "Compose pages from reusable content blocks. Each block is a React component with a typed config — drag, drop, and edit inline from the admin panel.",
717
+ },
718
+ {
719
+ icon: "Rocket",
720
+ title: "Deploy and go live",
721
+ description:
722
+ "Push to Convex and deploy your Next.js app. Real-time content updates flow to every connected client instantly — no cache invalidation needed.",
723
+ },
724
+ ],
725
+ },
726
+ {
727
+ blockType: BLOCK_SLUG_CTA,
728
+ blockName: "CTA",
729
+ id: "features-cta",
730
+ heading: "Ready to build with Vex CMS?",
731
+ subheading:
732
+ "Get started in minutes with create-vexcms. Real-time content management powered by Convex.",
733
+ actions: [
734
+ { label: "Get Started", href: "/docs" },
735
+ { label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
736
+ ],
737
+ },
738
+ ],
739
+ })
740
+ created.push("page:features")
741
+ }
742
+
743
+ const existingRoadmapPage = await ctx.db
744
+ .query(TABLE_SLUG_PAGES)
745
+ .withIndex("by_slug", (q) => q.eq("slug", "roadmap"))
746
+ .first()
747
+ if (existingRoadmapPage) {
748
+ skipped.push("page:roadmap")
749
+ } else {
750
+ await ctx.db.insert(TABLE_SLUG_PAGES, {
751
+ title: "Roadmap",
752
+ slug: "roadmap",
753
+ blocks: [
754
+ {
755
+ blockType: BLOCK_SLUG_ROADMAP,
756
+ blockName: "Roadmap",
757
+ id: "roadmap-roadmap",
758
+ heading: "Roadmap",
759
+ subheading:
760
+ "What we've shipped and what's coming next. Vex CMS is actively developed — here's where we're headed.",
761
+ items: [
762
+ {
763
+ feature: "12 Field Types",
764
+ description:
765
+ "text, url, color, number, checkbox, date, select, relationship, array, group, blocks, and upload — no richtext, json, or tabs yet.",
766
+ status: ["shipped"],
767
+ },
768
+ {
769
+ feature: "Convex Schema Codegen",
770
+ description:
771
+ "vex dev / vex generate write your Convex schema, TypeScript types, and Zod validators from defineCollection() — no hand-written schema.ts.",
772
+ status: ["shipped"],
773
+ },
774
+ {
775
+ feature: "Real-Time Admin Panel",
776
+ description:
777
+ "DataTable with pagination, live totalDocs, and bulk operations — every list view is a Convex subscription.",
778
+ status: ["shipped"],
779
+ },
780
+ {
781
+ feature: "Media Library",
782
+ description:
783
+ "Convex file storage adapter with a searchable, paginated media picker built into every upload field.",
784
+ status: ["shipped"],
785
+ },
786
+ {
787
+ feature: "RBAC & Access Control",
788
+ description:
789
+ "Document-level access rules, indexed constraints that compile to withIndex ranges, per-call access.action/bypass overrides, and an anonRole fallback for public reads.",
790
+ status: ["shipped"],
791
+ },
792
+ {
793
+ feature: "Custom Theme System",
794
+ description:
795
+ "Database-driven themes with light/dark mode, 32 shadcn tokens per mode, and OKLCH color support — live-updates with zero page reload.",
796
+ status: ["shipped"],
797
+ },
798
+ {
799
+ feature: "Better Auth Integration",
800
+ description:
801
+ "Email/password and OAuth out of the box, with organizations and API keys as opt-in plugins.",
802
+ status: ["shipped"],
803
+ },
804
+ {
805
+ feature: "CLI & Scaffolder",
806
+ description:
807
+ "vex dev, vex generate, and create-vexcms for instant project setup — bare or full marketing-site templates.",
808
+ status: ["shipped"],
809
+ },
810
+ {
811
+ feature: "Versioning & Drafts",
812
+ description: "Draft/publish workflow with live preview — in active development.",
813
+ status: ["coming-soon"],
814
+ },
815
+ {
816
+ feature: "Richtext, JSON, Email & Textarea Fields",
817
+ description:
818
+ "Plate.js-powered rich text, plus structured JSON, email, and multi-line text inputs.",
819
+ status: ["planned"],
820
+ },
821
+ {
822
+ feature: "Form Builder & Lifecycle Hooks",
823
+ description:
824
+ "Composable form fields beyond content editing, plus beforeChange/afterChange hooks for custom side effects.",
825
+ status: ["planned"],
826
+ },
827
+ {
828
+ feature: "Team Management & API Keys",
829
+ description:
830
+ "Invite users, assign roles, and issue scoped read-only API tokens for external integrations.",
831
+ status: ["planned"],
832
+ },
833
+ ],
834
+ },
835
+ {
836
+ blockType: BLOCK_SLUG_FAQ,
837
+ blockName: "FAQ",
838
+ id: "roadmap-faq",
839
+ heading: "Frequently Asked Questions",
840
+ subheading: "Everything you need to know about Vex CMS and building with Convex.",
841
+ supportLink: "https://github.com/ianyimi/vex/issues",
842
+ items: [
843
+ {
844
+ question: "What is Vex CMS?",
845
+ answer:
846
+ "Vex CMS is a headless content management system built on Convex. It provides real-time data, type-safe schemas, RBAC, and a beautiful admin panel — all configured with TypeScript.",
847
+ },
848
+ {
849
+ question: "How is Vex different from other headless CMS platforms?",
850
+ answer:
851
+ "Vex is powered by Convex's real-time database, so content updates are instant across all clients — no polling, no webhooks. Your schema is defined in code, giving you full type safety from database to frontend.",
852
+ },
853
+ {
854
+ question: "Do I need to know Convex to use Vex CMS?",
855
+ answer:
856
+ "Basic familiarity helps, but Vex handles most of the complexity for you. The CLI generates your Convex schema, queries, and types automatically from your collection definitions.",
857
+ },
858
+ {
859
+ question: "Is Vex CMS free?",
860
+ answer:
861
+ "Yes — Vex CMS is open source and free to use. You only pay for your Convex usage, which has a generous free tier for most projects.",
862
+ },
863
+ {
864
+ question: "How do I get started?",
865
+ answer:
866
+ "Run npx create-vexcms@latest to scaffold a new project with Vex CMS pre-configured — Next.js app, Convex backend, authentication, and admin panel in under a minute.",
867
+ },
868
+ ],
869
+ },
870
+ {
871
+ blockType: BLOCK_SLUG_CTA,
872
+ blockName: "CTA",
873
+ id: "roadmap-cta",
874
+ heading: "Ready to build with Vex CMS?",
875
+ subheading:
876
+ "Get started in minutes with create-vexcms. Real-time content management powered by Convex.",
877
+ actions: [
878
+ { label: "Get Started", href: "/docs" },
879
+ { label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
880
+ ],
881
+ },
882
+ ],
883
+ })
884
+ created.push("page:roadmap")
885
+ }
886
+
646
887
  return { created, skipped }
647
888
  },
648
889
  })
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import { api } from "@convex/_generated/api"
4
+ import { fetchQuery } from "convex/nextjs"
5
+
6
+ import { SiteFooter } from "~/components/SiteFooter"
7
+ import { SiteHeader } from "~/components/SiteHeader"
8
+
9
+ /**
10
+ * Marketing chrome: header + footer around every site page. Auth routes live
11
+ * outside this group (directly under `(frontend)`), so they stay chrome-free.
12
+ */
13
+ export default async function SiteLayout({
14
+ children,
15
+ }: Readonly<{
16
+ children: ReactNode
17
+ }>) {
18
+ let headerData: null | Record<string, unknown> = null
19
+ let footerData: null | Record<string, unknown> = 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
+ return (
31
+ <>
32
+ <SiteHeader initialData={headerData} />
33
+ <main>{children}</main>
34
+ <SiteFooter initialData={footerData} />
35
+ </>
36
+ )
37
+ }
@@ -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 { getCurrentUser } from "~/auth/serverUtils";
6
+ import { ThemeLive } from "~/components/ThemeLive";
7
+ import { ThemeStyle } from "~/components/ThemeStyle";
8
+ import config from "~/vex.config";
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
+ <ClientProviders>
25
+ <ThemeStyle scope="admin" />
26
+ <ThemeLive scope="admin" />
27
+ <NextAdminLayout config={config} user={user ?? undefined}>
28
+ {children}
29
+ </NextAdminLayout>
30
+ </ClientProviders>
31
+ );
32
+ }
@@ -0,0 +1,58 @@
1
+ import type { Metadata } from "next"
2
+
3
+ import "./globals.css"
4
+
5
+ import { ThemeScript } from "@vexcms/react"
6
+ import { Geist, Geist_Mono } from "next/font/google"
7
+
8
+ import ClientProviders from "~/components/providers/client"
9
+ import ServerProviders from "~/components/providers/server"
10
+ import { ThemeLive } from "~/components/ThemeLive"
11
+ import { ThemeStyle } from "~/components/ThemeStyle"
12
+
13
+ const geistSans = Geist({
14
+ subsets: ["latin"],
15
+ variable: "--font-sans",
16
+ })
17
+
18
+ const geistMono = Geist_Mono({
19
+ subsets: ["latin"],
20
+ variable: "--font-geist-mono",
21
+ })
22
+
23
+ export const metadata: Metadata = {
24
+ description: "Built with VexCMS",
25
+ icons: { icon: "/favicons/favicon.ico" },
26
+ title: "VexCMS",
27
+ }
28
+
29
+ export default function RootLayout({
30
+ children,
31
+ }: Readonly<{
32
+ children: React.ReactNode
33
+ }>) {
34
+ return (
35
+ <html
36
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
37
+ lang="en"
38
+ suppressHydrationWarning
39
+ >
40
+ <head>
41
+ {/* ThemeScript applies the persisted light/dark class before first
42
+ paint (no flash); ThemeStyle server-renders the active site theme
43
+ once for the whole app — the admin layout re-emits its own scope
44
+ at higher specificity, so `siteSettings.adminTheme` opts out. */}
45
+ <ThemeScript />
46
+ <ThemeStyle />
47
+ </head>
48
+ <body>
49
+ <ServerProviders>
50
+ <ClientProviders>
51
+ <ThemeLive />
52
+ {children}
53
+ </ClientProviders>
54
+ </ServerProviders>
55
+ </body>
56
+ </html>
57
+ )
58
+ }
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ import { api } from "@convex/_generated/api"
4
+ import { useMutation, useQuery } from "convex/react"
5
+ import { useRouter } from "next/navigation"
6
+ import { useEffect, useState } from "react"
7
+
8
+ import { useSession } from "~/auth/client"
9
+
10
+ /**
11
+ * Promotes the first signed-in user to admin when no admin exists yet, then
12
+ * redirects to `/admin`. Headless — renders nothing.
13
+ *
14
+ * `WelcomePage`'s own trigger (`convex/vex/firstUser.ts`'s
15
+ * `promoteFirstAdmin`) only runs while `PageContent` is showing its
16
+ * no-page-found fallback — which stops rendering the instant a `home` page
17
+ * document exists. A fresh marketing scaffold gets a `home` document from
18
+ * `pnpm seed` on day one, so that fallback may never mount again and the
19
+ * bootstrap trigger would never fire. This mounts unconditionally in
20
+ * `(frontend)/layout.tsx` instead, so the trigger survives seeding.
21
+ *
22
+ * `promoteFirstAdmin` is a Convex mutation, so concurrent signups cannot
23
+ * both win — Convex serializes mutations, and the loser observes
24
+ * `isBootstrapped` already true.
25
+ */
26
+ export function FirstAdminBootstrap() {
27
+ const isBootstrapped = useQuery(api.vex.firstUser.isBootstrapped)
28
+ const promoteFirstAdmin = useMutation(api.vex.firstUser.promoteFirstAdmin)
29
+ const { data: session } = useSession()
30
+ const router = useRouter()
31
+ const [promoting, setPromoting] = useState(false)
32
+
33
+ useEffect(() => {
34
+ if (session?.user && isBootstrapped === false && !promoting) {
35
+ setPromoting(true)
36
+ promoteFirstAdmin()
37
+ .then((result) => {
38
+ if (result.promoted) {
39
+ router.push("/admin")
40
+ } else {
41
+ setPromoting(false)
42
+ }
43
+ })
44
+ .catch(() => {
45
+ setPromoting(false)
46
+ })
47
+ }
48
+ }, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
49
+
50
+ return null
51
+ }
@@ -26,7 +26,7 @@ export const ctaBlock = defineBlock({
26
26
  }),
27
27
  defaultValue: [
28
28
  { label: "Get Started", href: "/docs" },
29
- { label: "View on GitHub", href: "https://github.com/vexcms/vex" },
29
+ { label: "View on GitHub", href: "https://github.com/ianyimi/vex" },
30
30
  ],
31
31
  }),
32
32
  },
@@ -18,7 +18,7 @@ export const faqBlock = defineBlock({
18
18
  }),
19
19
  supportLink: text({
20
20
  label: "Support Link",
21
- defaultValue: "https://github.com/vexcms/vex/issues",
21
+ defaultValue: "https://github.com/ianyimi/vex/issues",
22
22
  description: "URL to contact support page",
23
23
  }),
24
24
  items: array({
@@ -32,7 +32,7 @@ export const footerBlock = defineBlock({
32
32
  { label: "Pricing", href: "/pricing" },
33
33
  { label: "Roadmap", href: "/roadmap" },
34
34
  { label: "Documentation", href: "/docs" },
35
- { label: "GitHub", href: "https://github.com/vexcms/vex" },
35
+ { label: "GitHub", href: "https://github.com/ianyimi/vex" },
36
36
  { label: "npm", href: "https://www.npmjs.com/package/@vexcms/core" },
37
37
  { label: "Convex", href: "https://convex.dev" },
38
38
  ],
@@ -50,7 +50,7 @@ export const footerBlock = defineBlock({
50
50
  },
51
51
  }),
52
52
  defaultValue: [
53
- { platform: "GitHub", href: "https://github.com/vexcms/vex", icon: "Github" },
53
+ { platform: "GitHub", href: "https://github.com/ianyimi/vex", icon: "Github" },
54
54
  { platform: "X", href: "https://x.com/vexcms", icon: "Twitter" },
55
55
  ],
56
56
  }),
@@ -54,7 +54,7 @@ export const headerBlock = defineBlock({
54
54
  },
55
55
  }),
56
56
  defaultValue: [
57
- { label: "GitHub", href: "https://github.com/vexcms/vex", variant: "ghost" },
57
+ { label: "GitHub", href: "https://github.com/ianyimi/vex", variant: "ghost" },
58
58
  { label: "Get Started", href: "/docs", variant: "default" },
59
59
  ],
60
60
  }),
@@ -8,7 +8,7 @@ export const heroBlock = defineBlock({
8
8
  fields: {
9
9
  badgeText: text({
10
10
  label: "Badge Text",
11
- defaultValue: "Now in public beta",
11
+ defaultValue: "Now in public alpha",
12
12
  description: "Small text shown in the announcement badge",
13
13
  }),
14
14
  badgeLink: text({
@@ -42,7 +42,7 @@ export const heroBlock = defineBlock({
42
42
  }),
43
43
  secondaryCtaHref: text({
44
44
  label: "Secondary CTA Link",
45
- defaultValue: "https://github.com/vexcms/vex",
45
+ defaultValue: "https://github.com/ianyimi/vex",
46
46
  }),
47
47
  },
48
48
  admin: {
@@ -21,6 +21,9 @@ export const pages = defineCollection({
21
21
  required: true,
22
22
  }),
23
23
  blocks: blocks({
24
+ admin: {
25
+ defaultCollapsed: true,
26
+ },
24
27
  blocks: pageBlocks,
25
28
  interfaceName: "PageBlock",
26
29
  label: "Content",