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 +1 -1
- package/templates/base-nextjs/next.config.ts +7 -0
- package/templates/base-nextjs/package.json +6 -6
- package/templates/base-nextjs/src/app/globals.css +10 -9
- package/templates/base-nextjs/src/app/layout.tsx +6 -0
- package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
- package/templates/marketing-site/convex/seed.ts +247 -6
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +37 -0
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
- package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
- package/templates/marketing-site/src/app/layout.tsx +58 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +51 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +2 -2
- package/templates/marketing-site/src/vexcms/collections/pages.ts +3 -0
- /package/templates/marketing-site/src/app/(frontend)/{PageContent.tsx → (site)/PageContent.tsx} +0 -0
- /package/templates/marketing-site/src/app/(frontend)/{[slug] → (site)/[slug]}/page.tsx +0 -0
- /package/templates/marketing-site/src/app/(frontend)/{page.tsx → (site)/page.tsx} +0 -0
package/package.json
CHANGED
|
@@ -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.
|
|
28
|
-
"@vexcms/core": "~0.1.0-alpha.
|
|
29
|
-
"@vexcms/file-storage-convex": "~0.1.0-alpha.
|
|
30
|
-
"@vexcms/next": "~0.1.0-alpha.
|
|
31
|
-
"@vexcms/react": "~0.1.0-alpha.
|
|
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.
|
|
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
|
-
/*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@source "
|
|
36
|
-
@source "
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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 {
|
|
3
|
+
import { FirstAdminBootstrap } from "~/components/FirstAdminBootstrap"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export default
|
|
5
|
+
/**
|
|
6
|
+
* Frontend shell: first-admin bootstrap + the `@auth` parallel slot only.
|
|
7
|
+
* The marketing chrome (header/footer) lives in the nested `(site)` group so
|
|
8
|
+
* auth routes (`/auth/sign-in`, sign-up, …) render standalone — themed via
|
|
9
|
+
* the root layout, but without the site navigation wrapped around the form.
|
|
10
|
+
*/
|
|
11
|
+
export default function FrontendLayout({
|
|
12
12
|
auth,
|
|
13
13
|
children,
|
|
14
14
|
}: Readonly<{
|
|
15
15
|
auth: ReactNode
|
|
16
16
|
children: ReactNode
|
|
17
17
|
}>) {
|
|
18
|
-
let headerData: Record<string, unknown> | null = null
|
|
19
|
-
let footerData: Record<string, unknown> | null = null
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
;[headerData, footerData] = await Promise.all([
|
|
23
|
-
fetchQuery(api.headers.getFirst),
|
|
24
|
-
fetchQuery(api.footers.getFirst),
|
|
25
|
-
])
|
|
26
|
-
} catch {
|
|
27
|
-
// Convex not available — fall back to client-only fetch
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
return (
|
|
31
19
|
<>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
<main>{children}</main>
|
|
35
|
-
<SiteFooter initialData={footerData} />
|
|
20
|
+
<FirstAdminBootstrap />
|
|
21
|
+
{children}
|
|
36
22
|
{auth}
|
|
37
23
|
</>
|
|
38
24
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { NextAdminLayout } from "@vexcms/next/client";
|
|
4
|
+
|
|
5
|
+
import { 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
45
|
+
defaultValue: "https://github.com/ianyimi/vex",
|
|
46
46
|
}),
|
|
47
47
|
},
|
|
48
48
|
admin: {
|
/package/templates/marketing-site/src/app/(frontend)/{PageContent.tsx → (site)/PageContent.tsx}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|