modern-cms 1.0.0

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 (180) hide show
  1. package/README.md +41 -0
  2. package/bin/lib.js +97 -0
  3. package/bin/modern-cms.js +197 -0
  4. package/package.json +37 -0
  5. package/template/.env.example +48 -0
  6. package/template/README.md +210 -0
  7. package/template/apps/api/Dockerfile +26 -0
  8. package/template/apps/api/package.json +45 -0
  9. package/template/apps/api/prisma/backfill-search-text.ts +31 -0
  10. package/template/apps/api/prisma/migrations/20260701034530_init/migration.sql +86 -0
  11. package/template/apps/api/prisma/migrations/20260701081942_add_publish_at_and_revisions/migration.sql +19 -0
  12. package/template/apps/api/prisma/migrations/20260701085011_add_form_submissions/migration.sql +13 -0
  13. package/template/apps/api/prisma/migrations/20260702084049_add_global_sections_and_saved_blocks/migration.sql +24 -0
  14. package/template/apps/api/prisma/migrations/20260702085609_add_page_locale_and_translations/migration.sql +6 -0
  15. package/template/apps/api/prisma/migrations/20260702091914_add_collections/migration.sql +38 -0
  16. package/template/apps/api/prisma/migrations/20260702120000_platform_hardening/migration.sql +15 -0
  17. package/template/apps/api/prisma/migrations/20260705031230_add_collections_redirects_media_meta/migration.sql +17 -0
  18. package/template/apps/api/prisma/migrations/20260705033241_add_audit_log/migration.sql +18 -0
  19. package/template/apps/api/prisma/migrations/20260706031301_standard_cms_platform_pass/migration.sql +48 -0
  20. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  21. package/template/apps/api/prisma/reset-admin.ts +19 -0
  22. package/template/apps/api/prisma/schema.prisma +251 -0
  23. package/template/apps/api/prisma/seed-homepage.ts +752 -0
  24. package/template/apps/api/prisma/seed-our-charities.ts +231 -0
  25. package/template/apps/api/prisma/seed-templates.ts +154 -0
  26. package/template/apps/api/prisma/seed.ts +191 -0
  27. package/template/apps/api/src/env.ts +61 -0
  28. package/template/apps/api/src/index.ts +66 -0
  29. package/template/apps/api/src/lib/revalidate.ts +28 -0
  30. package/template/apps/api/src/lib/storage/azure.ts +25 -0
  31. package/template/apps/api/src/lib/storage/cloudinary.ts +42 -0
  32. package/template/apps/api/src/lib/storage/index.ts +25 -0
  33. package/template/apps/api/src/lib/storage/s3.ts +39 -0
  34. package/template/apps/api/src/lib/storage/types.ts +8 -0
  35. package/template/apps/api/src/middleware/auditLog.ts +32 -0
  36. package/template/apps/api/src/middleware/auth.ts +53 -0
  37. package/template/apps/api/src/middleware/rateLimit.ts +31 -0
  38. package/template/apps/api/src/middleware/securityHeaders.ts +10 -0
  39. package/template/apps/api/src/prisma.ts +3 -0
  40. package/template/apps/api/src/routes/audit.routes.ts +26 -0
  41. package/template/apps/api/src/routes/auth.routes.ts +46 -0
  42. package/template/apps/api/src/routes/backup.routes.ts +216 -0
  43. package/template/apps/api/src/routes/blocks.routes.ts +38 -0
  44. package/template/apps/api/src/routes/collections.routes.ts +185 -0
  45. package/template/apps/api/src/routes/forms.routes.ts +114 -0
  46. package/template/apps/api/src/routes/globalSections.routes.ts +43 -0
  47. package/template/apps/api/src/routes/media.routes.ts +77 -0
  48. package/template/apps/api/src/routes/pages.routes.ts +319 -0
  49. package/template/apps/api/src/routes/redirects.routes.ts +52 -0
  50. package/template/apps/api/src/routes/search.routes.ts +83 -0
  51. package/template/apps/api/src/routes/templates.routes.ts +41 -0
  52. package/template/apps/api/src/routes/theme.routes.ts +75 -0
  53. package/template/apps/api/src/routes/users.routes.ts +39 -0
  54. package/template/apps/api/tsconfig.json +11 -0
  55. package/template/apps/web/Dockerfile +26 -0
  56. package/template/apps/web/app/[[...slug]]/page.tsx +254 -0
  57. package/template/apps/web/app/admin/(protected)/audit/page.tsx +80 -0
  58. package/template/apps/web/app/admin/(protected)/collections/item/[itemId]/page.tsx +375 -0
  59. package/template/apps/web/app/admin/(protected)/collections/page.tsx +224 -0
  60. package/template/apps/web/app/admin/(protected)/forms/page.tsx +88 -0
  61. package/template/apps/web/app/admin/(protected)/layout.tsx +5 -0
  62. package/template/apps/web/app/admin/(protected)/media/page.tsx +138 -0
  63. package/template/apps/web/app/admin/(protected)/page.tsx +186 -0
  64. package/template/apps/web/app/admin/(protected)/pages/[id]/page.tsx +560 -0
  65. package/template/apps/web/app/admin/(protected)/settings/backup/page.tsx +97 -0
  66. package/template/apps/web/app/admin/(protected)/settings/global-sections/page.tsx +333 -0
  67. package/template/apps/web/app/admin/(protected)/settings/navigation/page.tsx +192 -0
  68. package/template/apps/web/app/admin/(protected)/settings/redirects/page.tsx +108 -0
  69. package/template/apps/web/app/admin/(protected)/settings/theme/page.tsx +239 -0
  70. package/template/apps/web/app/admin/(protected)/users/page.tsx +94 -0
  71. package/template/apps/web/app/admin/login/page.tsx +76 -0
  72. package/template/apps/web/app/api/revalidate/route.ts +34 -0
  73. package/template/apps/web/app/globals.css +120 -0
  74. package/template/apps/web/app/layout.tsx +39 -0
  75. package/template/apps/web/app/preview/[token]/page.tsx +70 -0
  76. package/template/apps/web/app/robots.ts +11 -0
  77. package/template/apps/web/app/sitemap.ts +27 -0
  78. package/template/apps/web/components/admin/AdminShell.tsx +104 -0
  79. package/template/apps/web/components/builder/BuilderCanvasNode.tsx +546 -0
  80. package/template/apps/web/components/builder/BuilderDeviceContext.tsx +13 -0
  81. package/template/apps/web/components/builder/Canvas.tsx +76 -0
  82. package/template/apps/web/components/builder/CanvasOverlayContext.tsx +22 -0
  83. package/template/apps/web/components/builder/ColorPickerField.tsx +47 -0
  84. package/template/apps/web/components/builder/DroppableChildren.tsx +50 -0
  85. package/template/apps/web/components/builder/FieldRenderer.tsx +277 -0
  86. package/template/apps/web/components/builder/IconRenderer.tsx +7 -0
  87. package/template/apps/web/components/builder/Inspector.tsx +956 -0
  88. package/template/apps/web/components/builder/JsonEditor.tsx +71 -0
  89. package/template/apps/web/components/builder/KeyValueListEditor.tsx +76 -0
  90. package/template/apps/web/components/builder/MediaPickerModal.tsx +95 -0
  91. package/template/apps/web/components/builder/PageSettingsModal.tsx +363 -0
  92. package/template/apps/web/components/builder/Palette.tsx +106 -0
  93. package/template/apps/web/components/builder/RichTextEditor.tsx +140 -0
  94. package/template/apps/web/components/renderer/CmsImage.tsx +46 -0
  95. package/template/apps/web/components/renderer/NodeBackgroundLayers.tsx +35 -0
  96. package/template/apps/web/components/renderer/PublicPageView.tsx +56 -0
  97. package/template/apps/web/components/renderer/Renderer.tsx +146 -0
  98. package/template/apps/web/components/renderer/RendererContext.tsx +20 -0
  99. package/template/apps/web/components/renderer/registry.tsx +87 -0
  100. package/template/apps/web/components/renderer/sections/Accordion.tsx +39 -0
  101. package/template/apps/web/components/renderer/sections/BeforeAfter.tsx +75 -0
  102. package/template/apps/web/components/renderer/sections/Button.tsx +42 -0
  103. package/template/apps/web/components/renderer/sections/ButtonGroup.tsx +22 -0
  104. package/template/apps/web/components/renderer/sections/CardGrid.tsx +66 -0
  105. package/template/apps/web/components/renderer/sections/CollectionList.tsx +84 -0
  106. package/template/apps/web/components/renderer/sections/Column.tsx +26 -0
  107. package/template/apps/web/components/renderer/sections/ContactForm.tsx +96 -0
  108. package/template/apps/web/components/renderer/sections/Container.tsx +44 -0
  109. package/template/apps/web/components/renderer/sections/ContentLinks.tsx +38 -0
  110. package/template/apps/web/components/renderer/sections/Countdown.tsx +69 -0
  111. package/template/apps/web/components/renderer/sections/Cta.tsx +15 -0
  112. package/template/apps/web/components/renderer/sections/Divider.tsx +26 -0
  113. package/template/apps/web/components/renderer/sections/EmbedBlock.tsx +43 -0
  114. package/template/apps/web/components/renderer/sections/Footer.tsx +117 -0
  115. package/template/apps/web/components/renderer/sections/Gallery.tsx +27 -0
  116. package/template/apps/web/components/renderer/sections/GoToTop.tsx +54 -0
  117. package/template/apps/web/components/renderer/sections/Grid.tsx +54 -0
  118. package/template/apps/web/components/renderer/sections/GridCell.tsx +33 -0
  119. package/template/apps/web/components/renderer/sections/Header.tsx +132 -0
  120. package/template/apps/web/components/renderer/sections/Hero.tsx +73 -0
  121. package/template/apps/web/components/renderer/sections/ImageBackgroundSection.tsx +20 -0
  122. package/template/apps/web/components/renderer/sections/ImageBlock.tsx +55 -0
  123. package/template/apps/web/components/renderer/sections/LanguageSwitcher.tsx +51 -0
  124. package/template/apps/web/components/renderer/sections/Logo.tsx +24 -0
  125. package/template/apps/web/components/renderer/sections/LogoCloud.tsx +67 -0
  126. package/template/apps/web/components/renderer/sections/MapEmbed.tsx +24 -0
  127. package/template/apps/web/components/renderer/sections/Marquee.tsx +45 -0
  128. package/template/apps/web/components/renderer/sections/NavLinks.tsx +69 -0
  129. package/template/apps/web/components/renderer/sections/NavZone.tsx +19 -0
  130. package/template/apps/web/components/renderer/sections/PricingTable.tsx +65 -0
  131. package/template/apps/web/components/renderer/sections/RichText.tsx +7 -0
  132. package/template/apps/web/components/renderer/sections/SiteSearch.tsx +67 -0
  133. package/template/apps/web/components/renderer/sections/Slider.tsx +81 -0
  134. package/template/apps/web/components/renderer/sections/Spacer.tsx +8 -0
  135. package/template/apps/web/components/renderer/sections/Stats.tsx +69 -0
  136. package/template/apps/web/components/renderer/sections/Tabs.tsx +59 -0
  137. package/template/apps/web/components/renderer/sections/TeamGrid.tsx +49 -0
  138. package/template/apps/web/components/renderer/sections/Testimonial.tsx +79 -0
  139. package/template/apps/web/components/renderer/sections/ThreeBackground.tsx +43 -0
  140. package/template/apps/web/components/renderer/sections/Timeline.tsx +29 -0
  141. package/template/apps/web/components/renderer/sections/VideoBackgroundSection.tsx +20 -0
  142. package/template/apps/web/components/renderer/sections/VideoEmbed.tsx +70 -0
  143. package/template/apps/web/components/renderer/sections/types.ts +7 -0
  144. package/template/apps/web/components/seo/JsonLd.tsx +111 -0
  145. package/template/apps/web/components/theme/AnalyticsScripts.tsx +68 -0
  146. package/template/apps/web/components/theme/DarkModeToggle.tsx +39 -0
  147. package/template/apps/web/components/theme/SiteDataContext.tsx +39 -0
  148. package/template/apps/web/components/theme/ThemeProvider.tsx +58 -0
  149. package/template/apps/web/eslint.config.mjs +16 -0
  150. package/template/apps/web/lib/api.ts +45 -0
  151. package/template/apps/web/lib/responsiveCss.ts +42 -0
  152. package/template/apps/web/lib/sanitizeHtml.ts +16 -0
  153. package/template/apps/web/lib/serverApi.ts +25 -0
  154. package/template/apps/web/lib/style.ts +73 -0
  155. package/template/apps/web/lib/threeBackgroundScene.ts +298 -0
  156. package/template/apps/web/lib/useCurrentUser.ts +35 -0
  157. package/template/apps/web/lib/useEntranceAnimation.ts +245 -0
  158. package/template/apps/web/next-env.d.ts +6 -0
  159. package/template/apps/web/next.config.mjs +32 -0
  160. package/template/apps/web/package.json +46 -0
  161. package/template/apps/web/postcss.config.mjs +6 -0
  162. package/template/apps/web/proxy.ts +23 -0
  163. package/template/apps/web/tailwind.config.ts +24 -0
  164. package/template/apps/web/tsconfig.json +41 -0
  165. package/template/docker-compose.yml +60 -0
  166. package/template/package.json +29 -0
  167. package/template/packages/shared/package.json +21 -0
  168. package/template/packages/shared/src/components.ts +1626 -0
  169. package/template/packages/shared/src/index.ts +9 -0
  170. package/template/packages/shared/src/schema/collections.ts +70 -0
  171. package/template/packages/shared/src/schema/fields.ts +26 -0
  172. package/template/packages/shared/src/schema/media.ts +13 -0
  173. package/template/packages/shared/src/schema/pageNode.ts +255 -0
  174. package/template/packages/shared/src/schema/templates.ts +20 -0
  175. package/template/packages/shared/src/schema/theme.ts +69 -0
  176. package/template/packages/shared/src/schema/user.ts +25 -0
  177. package/template/packages/shared/src/tree.test.ts +65 -0
  178. package/template/packages/shared/src/tree.ts +303 -0
  179. package/template/packages/shared/tsconfig.json +8 -0
  180. package/template/packages/shared/vitest.config.ts +8 -0
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # modern-cms
2
+
3
+ A self-hosted, drag-and-drop website CMS — Next.js (App Router) front end, Express + Prisma API, PostgreSQL or MySQL, and pluggable file storage (AWS S3, Azure Blob Storage, or Cloudinary).
4
+
5
+ ## Quick start
6
+
7
+ ```
8
+ npx modern-cms
9
+ ```
10
+
11
+ You'll be asked for:
12
+
13
+ - Where to create the project
14
+ - Database engine (PostgreSQL or MySQL) — host, database name, username, password
15
+ - File storage provider (AWS S3 / any S3-compatible service, Azure Blob Storage, or Cloudinary) and its credentials
16
+ - Site URLs and an admin login
17
+
18
+ The installer scaffolds the full project into your target directory and writes working `.env` files for the API and web app. It does **not** run `npm install` or touch your database for you — it prints the exact next commands to run, so nothing happens on your machine without you seeing it first.
19
+
20
+ ## What you get
21
+
22
+ - A visual, drag-and-drop page builder (containers, grids, 30+ content blocks, responsive breakpoints, animations, dark mode)
23
+ - Collections (blog/news/etc.), full-text search, SEO (sitemap, JSON-LD, redirects), page revisions and templates
24
+ - Role-based auth (Admin / Editor / Author), audit log, form submissions
25
+ - Media library backed by whichever storage provider you chose
26
+
27
+ ## After setup
28
+
29
+ ```
30
+ cd <your-project-dir>
31
+ npm install
32
+ npm run db:migrate # PostgreSQL
33
+ # or, for MySQL (the bundled migration history is Postgres-specific SQL):
34
+ npx prisma db push --schema apps/api/prisma/schema.prisma
35
+ npm run db:seed
36
+ npm run dev
37
+ ```
38
+
39
+ ## License
40
+
41
+ MIT
package/bin/lib.js ADDED
@@ -0,0 +1,97 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export function randomSecret() {
4
+ return crypto.randomBytes(48).toString("hex");
5
+ }
6
+
7
+ export function buildDatabaseUrl(db) {
8
+ const protocol = db.engine === "mysql" ? "mysql" : "postgresql";
9
+ return `${protocol}://${encodeURIComponent(db.user)}:${encodeURIComponent(db.password)}@${db.host}:${db.port}/${db.name}`;
10
+ }
11
+
12
+ export function rewriteSchemaProvider(schemaContent, engine) {
13
+ return schemaContent.replace(/provider\s*=\s*"postgresql"/, `provider = "${engine}"`);
14
+ }
15
+
16
+ /** MySQL can't run the bundled migration history — it's raw SQL generated for
17
+ * PostgreSQL. There's no existing data on a fresh install, so dropping the history and
18
+ * using `prisma db push` instead of `prisma migrate dev` is the correct, simplest path. */
19
+ export function usesDbPush(engine) {
20
+ return engine === "mysql";
21
+ }
22
+
23
+ export function buildStorageEnvLines(provider, values) {
24
+ if (provider === "s3") {
25
+ return [
26
+ `S3_ENDPOINT=${values.endpoint}`,
27
+ `S3_REGION=${values.region}`,
28
+ `S3_ACCESS_KEY_ID=${values.accessKeyId}`,
29
+ `S3_SECRET_ACCESS_KEY=${values.secretAccessKey}`,
30
+ `S3_BUCKET=${values.bucket}`,
31
+ `S3_PUBLIC_URL=${values.publicUrl}`,
32
+ `S3_FORCE_PATH_STYLE=${String(values.forcePathStyle)}`,
33
+ ];
34
+ }
35
+ if (provider === "azure") {
36
+ return [
37
+ `AZURE_STORAGE_CONNECTION_STRING=${values.connectionString}`,
38
+ `AZURE_STORAGE_CONTAINER=${values.container}`,
39
+ ...(values.publicUrl ? [`AZURE_STORAGE_PUBLIC_URL=${values.publicUrl}`] : []),
40
+ ];
41
+ }
42
+ return [
43
+ `CLOUDINARY_CLOUD_NAME=${values.cloudName}`,
44
+ `CLOUDINARY_API_KEY=${values.apiKey}`,
45
+ `CLOUDINARY_API_SECRET=${values.apiSecret}`,
46
+ `CLOUDINARY_FOLDER=${values.folder}`,
47
+ ];
48
+ }
49
+
50
+ export function buildApiEnv({ databaseUrl, jwtSecret, adminEmail, adminPassword, storageProvider, storageEnvLines, siteUrl, revalidateSecret }) {
51
+ return [
52
+ `DATABASE_URL=${databaseUrl}`,
53
+ "",
54
+ "# --- Auth ---",
55
+ `JWT_SECRET=${jwtSecret}`,
56
+ "JWT_EXPIRES_IN=7d",
57
+ "COOKIE_NAME=pgcms_token",
58
+ "",
59
+ "# --- Seed admin user ---",
60
+ `SEED_ADMIN_EMAIL=${adminEmail}`,
61
+ `SEED_ADMIN_PASSWORD=${adminPassword}`,
62
+ "",
63
+ `# --- File storage (${storageProvider}) ---`,
64
+ `STORAGE_PROVIDER=${storageProvider}`,
65
+ ...storageEnvLines,
66
+ "",
67
+ "# --- API ---",
68
+ "API_PORT=4000",
69
+ `WEB_ORIGIN=${siteUrl}`,
70
+ "",
71
+ "# --- Cache revalidation ---",
72
+ `REVALIDATE_URL=${siteUrl}`,
73
+ `REVALIDATE_SECRET=${revalidateSecret}`,
74
+ "",
75
+ "# --- Form integrations (optional) ---",
76
+ "FORMS_WEBHOOK_URL=",
77
+ "",
78
+ ].join("\n");
79
+ }
80
+
81
+ export function buildWebEnv({ apiUrl, siteUrl }) {
82
+ return [`API_URL=${apiUrl}`, `SITE_URL=${siteUrl}`, ""].join("\n");
83
+ }
84
+
85
+ export function buildRootEnv({ db, databaseUrl, storageProvider, storageEnvLines }) {
86
+ const s3Line = (prefix) => storageEnvLines.find((l) => l.startsWith(prefix));
87
+ return [
88
+ `POSTGRES_USER=${db.user}`,
89
+ `POSTGRES_PASSWORD=${db.password}`,
90
+ `POSTGRES_DB=${db.name}`,
91
+ `DATABASE_URL=${databaseUrl}`,
92
+ ...(storageProvider === "s3"
93
+ ? [s3Line("S3_ACCESS_KEY_ID="), s3Line("S3_SECRET_ACCESS_KEY="), s3Line("S3_BUCKET=")].filter(Boolean)
94
+ : []),
95
+ "",
96
+ ].join("\n");
97
+ }
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ import prompts from "prompts";
7
+ import {
8
+ randomSecret,
9
+ buildDatabaseUrl,
10
+ rewriteSchemaProvider,
11
+ usesDbPush,
12
+ buildStorageEnvLines,
13
+ buildApiEnv,
14
+ buildWebEnv,
15
+ buildRootEnv,
16
+ } from "./lib.js";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const TEMPLATE_DIR = path.join(__dirname, "..", "template");
20
+
21
+ function copyDir(src, dest) {
22
+ fs.mkdirSync(dest, { recursive: true });
23
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
24
+ const srcPath = path.join(src, entry.name);
25
+ const destPath = path.join(dest, entry.name);
26
+ if (entry.isDirectory()) copyDir(srcPath, destPath);
27
+ else fs.copyFileSync(srcPath, destPath);
28
+ }
29
+ }
30
+
31
+ function onCancel() {
32
+ console.log("\nSetup cancelled — nothing was written.");
33
+ process.exit(1);
34
+ }
35
+
36
+ async function main() {
37
+ console.log("\nmodern-cms — self-hosted drag-and-drop website CMS\n");
38
+
39
+ const { targetDir } = await prompts(
40
+ { type: "text", name: "targetDir", message: "Where should the project be created?", initial: "./modern-cms-app" },
41
+ { onCancel }
42
+ );
43
+ const dest = path.resolve(process.cwd(), targetDir);
44
+ if (fs.existsSync(dest) && fs.readdirSync(dest).length > 0) {
45
+ console.error(`\n"${dest}" already exists and isn't empty. Choose an empty or new directory.`);
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log(`\nCopying project files into ${dest} ...`);
50
+ copyDir(TEMPLATE_DIR, dest);
51
+
52
+ // --- Database ---
53
+ const db = await prompts(
54
+ [
55
+ {
56
+ type: "select",
57
+ name: "engine",
58
+ message: "Database",
59
+ choices: [
60
+ { title: "PostgreSQL", value: "postgresql" },
61
+ { title: "MySQL", value: "mysql" },
62
+ ],
63
+ },
64
+ { type: "text", name: "host", message: "Database host", initial: "localhost" },
65
+ {
66
+ type: "text",
67
+ name: "port",
68
+ message: "Database port",
69
+ initial: (_, values) => (values.engine === "mysql" ? "3306" : "5432"),
70
+ },
71
+ { type: "text", name: "name", message: "Database name", initial: "modern_cms" },
72
+ { type: "text", name: "user", message: "Database username", initial: (_, values) => (values.engine === "mysql" ? "root" : "postgres") },
73
+ { type: "password", name: "password", message: "Database password" },
74
+ ],
75
+ { onCancel }
76
+ );
77
+
78
+ const databaseUrl = buildDatabaseUrl(db);
79
+
80
+ const schemaPath = path.join(dest, "apps/api/prisma/schema.prisma");
81
+ fs.writeFileSync(schemaPath, rewriteSchemaProvider(fs.readFileSync(schemaPath, "utf8"), db.engine));
82
+
83
+ if (usesDbPush(db.engine)) {
84
+ const migrationsDir = path.join(dest, "apps/api/prisma/migrations");
85
+ if (fs.existsSync(migrationsDir)) fs.rmSync(migrationsDir, { recursive: true, force: true });
86
+ }
87
+
88
+ // --- Storage ---
89
+ const { provider } = await prompts(
90
+ {
91
+ type: "select",
92
+ name: "provider",
93
+ message: "File storage",
94
+ choices: [
95
+ { title: "AWS S3 (or any S3-compatible: MinIO, R2, Spaces...)", value: "s3" },
96
+ { title: "Azure Blob Storage", value: "azure" },
97
+ { title: "Cloudinary", value: "cloudinary" },
98
+ ],
99
+ },
100
+ { onCancel }
101
+ );
102
+
103
+ let storageValues;
104
+ if (provider === "s3") {
105
+ storageValues = await prompts(
106
+ [
107
+ { type: "text", name: "endpoint", message: "S3 endpoint", initial: "https://s3.amazonaws.com" },
108
+ { type: "text", name: "region", message: "S3 region", initial: "us-east-1" },
109
+ { type: "text", name: "accessKeyId", message: "S3 access key ID" },
110
+ { type: "password", name: "secretAccessKey", message: "S3 secret access key" },
111
+ { type: "text", name: "bucket", message: "S3 bucket name" },
112
+ { type: "text", name: "publicUrl", message: "Public base URL for uploaded files (e.g. https://<bucket>.s3.amazonaws.com)" },
113
+ {
114
+ type: "toggle",
115
+ name: "forcePathStyle",
116
+ message: "Force path-style URLs? (needed for MinIO/most non-AWS S3-compatible services)",
117
+ initial: false,
118
+ active: "yes",
119
+ inactive: "no",
120
+ },
121
+ ],
122
+ { onCancel }
123
+ );
124
+ } else if (provider === "azure") {
125
+ storageValues = await prompts(
126
+ [
127
+ { type: "password", name: "connectionString", message: "Azure Storage connection string" },
128
+ { type: "text", name: "container", message: "Blob container name", initial: "media" },
129
+ { type: "text", name: "publicUrl", message: "Public base URL (leave blank to use the storage account's own blob URL)", initial: "" },
130
+ ],
131
+ { onCancel }
132
+ );
133
+ } else {
134
+ storageValues = await prompts(
135
+ [
136
+ { type: "text", name: "cloudName", message: "Cloudinary cloud name" },
137
+ { type: "text", name: "apiKey", message: "Cloudinary API key" },
138
+ { type: "password", name: "apiSecret", message: "Cloudinary API secret" },
139
+ { type: "text", name: "folder", message: "Upload folder", initial: "pgcms" },
140
+ ],
141
+ { onCancel }
142
+ );
143
+ }
144
+ const storageEnvLines = buildStorageEnvLines(provider, storageValues);
145
+
146
+ // --- Site / admin ---
147
+ const site = await prompts(
148
+ [
149
+ { type: "text", name: "siteUrl", message: "Public site URL", initial: "http://localhost:3000" },
150
+ { type: "text", name: "apiUrl", message: "API URL", initial: "http://localhost:4000" },
151
+ { type: "text", name: "adminEmail", message: "Admin login email", initial: "admin@example.com" },
152
+ { type: "password", name: "adminPassword", message: "Admin login password (leave blank to generate one)" },
153
+ ],
154
+ { onCancel }
155
+ );
156
+ const adminPassword = site.adminPassword || crypto.randomBytes(9).toString("base64url");
157
+
158
+ const apiEnv = buildApiEnv({
159
+ databaseUrl,
160
+ jwtSecret: randomSecret(),
161
+ adminEmail: site.adminEmail,
162
+ adminPassword,
163
+ storageProvider: provider,
164
+ storageEnvLines,
165
+ siteUrl: site.siteUrl,
166
+ revalidateSecret: randomSecret(),
167
+ });
168
+ const webEnv = buildWebEnv({ apiUrl: site.apiUrl, siteUrl: site.siteUrl });
169
+ const rootEnv = buildRootEnv({ db, databaseUrl, storageProvider: provider, storageEnvLines });
170
+
171
+ fs.writeFileSync(path.join(dest, "apps/api/.env"), apiEnv);
172
+ fs.writeFileSync(path.join(dest, "apps/web/.env.local"), webEnv);
173
+ fs.writeFileSync(path.join(dest, ".env"), rootEnv);
174
+
175
+ if (!site.adminPassword) {
176
+ console.log(`\nGenerated admin password: ${adminPassword}\n(save this — it's only shown once)`);
177
+ }
178
+
179
+ console.log("\nDone! Generated .env files for apps/api and apps/web.\n");
180
+ console.log("Next steps:");
181
+ console.log(` cd ${targetDir}`);
182
+ console.log(" npm install");
183
+ if (usesDbPush(db.engine)) {
184
+ console.log(" npx prisma generate --schema apps/api/prisma/schema.prisma");
185
+ console.log(" npx prisma db push --schema apps/api/prisma/schema.prisma");
186
+ console.log(" (MySQL installs use db push instead of migrate — the bundled migration history is Postgres-specific SQL)");
187
+ } else {
188
+ console.log(" npm run db:migrate");
189
+ }
190
+ console.log(" npm run db:seed");
191
+ console.log(" npm run dev\n");
192
+ }
193
+
194
+ main().catch((err) => {
195
+ console.error(err);
196
+ process.exit(1);
197
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "modern-cms",
3
+ "version": "1.0.0",
4
+ "description": "Self-hosted, drag-and-drop website CMS (Next.js + Express + Prisma). Interactive installer scaffolds a full project with your choice of PostgreSQL/MySQL and AWS S3/Azure Blob/Cloudinary storage.",
5
+ "type": "module",
6
+ "bin": {
7
+ "modern-cms": "./bin/modern-cms.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "cms",
16
+ "nextjs",
17
+ "prisma",
18
+ "page-builder",
19
+ "drag-and-drop",
20
+ "postgresql",
21
+ "mysql",
22
+ "s3",
23
+ "azure",
24
+ "cloudinary"
25
+ ],
26
+ "author": {
27
+ "name": "Md Isahaq (+880 1852376598)",
28
+ "email": "hmisahaq01@gmail.com"
29
+ },
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "prompts": "^2.4.2"
36
+ }
37
+ }
@@ -0,0 +1,48 @@
1
+ # --- Postgres ---
2
+ POSTGRES_USER=pgcms
3
+ POSTGRES_PASSWORD=pgcms_dev_password
4
+ POSTGRES_DB=pgcms
5
+ # Note: mapped to host port 5434 (not 5432) to avoid clashing with any locally installed
6
+ # Postgres or other projects' containers — check `docker ps` for conflicts before reusing 5433.
7
+ DATABASE_URL=postgresql://pgcms:pgcms_dev_password@localhost:5434/pgcms
8
+
9
+ # --- Auth ---
10
+ JWT_SECRET=change_me_to_a_long_random_string
11
+ JWT_EXPIRES_IN=7d
12
+ COOKIE_NAME=pgcms_token
13
+
14
+ # --- Seed admin user ---
15
+ SEED_ADMIN_EMAIL=admin@example.com
16
+ SEED_ADMIN_PASSWORD=change_me_admin_password
17
+
18
+ # --- MinIO / S3 ---
19
+ # Mapped to host ports 9010/9011 (not the MinIO defaults 9000/9001) to avoid clashing
20
+ # with other projects' containers — check `docker ps` for conflicts before reusing 9000.
21
+ S3_ENDPOINT=http://localhost:9010
22
+ S3_REGION=us-east-1
23
+ S3_ACCESS_KEY_ID=pgcms_minio
24
+ S3_SECRET_ACCESS_KEY=pgcms_minio_secret
25
+ S3_BUCKET=pgcms-media
26
+ S3_FORCE_PATH_STYLE=true
27
+ # Public base URL used to build media URLs served back to the browser
28
+ S3_PUBLIC_URL=http://localhost:9010/pgcms-media
29
+
30
+ # --- API ---
31
+ API_PORT=4000
32
+ WEB_ORIGIN=http://localhost:3000
33
+
34
+ # --- Web (Next.js) ---
35
+ # Server-side only: used by next.config.mjs (API proxy/rewrites) and server components.
36
+ # Not prefixed with NEXT_PUBLIC_ on purpose — the browser always calls the relative /api
37
+ # path, which Next.js proxies to this URL, keeping the auth cookie first-party.
38
+ API_URL=http://localhost:4000
39
+ SITE_URL=http://localhost:3000
40
+
41
+ # --- Cache revalidation (production) ---
42
+ # API calls POST {REVALIDATE_URL}/api/revalidate after publish to bust ISR cache.
43
+ REVALIDATE_URL=http://localhost:3000
44
+ REVALIDATE_SECRET=change_me_revalidate_secret
45
+
46
+ # --- Form integrations (optional) ---
47
+ # POST JSON payload on every form submission: { event, id, formName, pageSlug, data, createdAt }
48
+ FORMS_WEBHOOK_URL=
@@ -0,0 +1,210 @@
1
+ # pg-cms
2
+
3
+ A self-hosted, drag-and-drop website CMS. Build every page of a site visually — drag sections onto a canvas, edit their content and style in an inspector panel, animate them, and publish. The public site and the builder canvas render through the same React components, so what you see while editing is exactly what visitors get.
4
+
5
+ Built as an npm-workspaces monorepo:
6
+
7
+ | Workspace | What it is |
8
+ |---|---|
9
+ | `apps/web` | Next.js 14 (App Router) — the public site **and** the `/admin` builder UI |
10
+ | `apps/api` | Express + Prisma + PostgreSQL — auth, pages, media, theme, forms |
11
+ | `packages/shared` | Zod schemas, the component registry, and page-tree helpers shared by both |
12
+
13
+ ---
14
+
15
+ ## Quick start
16
+
17
+ ### Prerequisites
18
+
19
+ - Node.js 18+
20
+ - Docker Desktop (for PostgreSQL + MinIO)
21
+
22
+ ### 1. Start the databases
23
+
24
+ ```bash
25
+ npm run docker:up
26
+ ```
27
+
28
+ This starts:
29
+
30
+ | Service | Host port | Notes |
31
+ |---|---|---|
32
+ | PostgreSQL 16 | **5434** | container port 5432; 5434 chosen to avoid clashing with other local Postgres instances |
33
+ | MinIO (S3 API) | **9010** | media file storage |
34
+ | MinIO console | **9011** | web UI for browsing the bucket |
35
+
36
+ A one-shot init container creates the `pgcms-media` bucket automatically.
37
+
38
+ > **Port conflicts:** if `docker compose up` fails with "port is already allocated", another container or service owns that port — run `docker ps` to find it, then either stop it or change the host-side port in `docker-compose.yml` **and** the matching URLs in `apps/api/.env`.
39
+
40
+ ### 2. Environment files
41
+
42
+ Two env files are used:
43
+
44
+ - **Repo root `.env`** — read by `docker-compose.yml` (Postgres user/password/db name, MinIO keys).
45
+ - **`apps/api/.env`** — read by the API and Prisma (`DATABASE_URL`, JWT, S3, seed admin).
46
+
47
+ Copy the example and adjust if needed:
48
+
49
+ ```bash
50
+ cp .env.example .env
51
+ cp .env.example apps/api/.env # then trim to the API-relevant keys, or keep all
52
+ ```
53
+
54
+ **Important:** the password inside `DATABASE_URL` in `apps/api/.env` must match `POSTGRES_PASSWORD` in the root `.env`. Postgres sets its password the *first* time its data volume is created — changing `.env` later does not change the database password.
55
+
56
+ ### 3. Install, migrate, seed
57
+
58
+ ```bash
59
+ npm install
60
+ npm run db:migrate # applies Prisma migrations
61
+ npm run db:seed # creates the admin user, default theme, and a sample home page
62
+ ```
63
+
64
+ Optional demo content (a full multi-section homepage and an extra page):
65
+
66
+ ```bash
67
+ cd apps/api
68
+ npx tsx prisma/seed-homepage.ts
69
+ npx tsx prisma/seed-our-charities.ts
70
+ ```
71
+
72
+ ### 4. Run the dev servers
73
+
74
+ ```bash
75
+ npm run dev # API (port 4000) + web (port 3000) together
76
+ # or separately:
77
+ npm run dev:api
78
+ npm run dev:web
79
+ ```
80
+
81
+ ### 5. Log in
82
+
83
+ | URL | What |
84
+ |---|---|
85
+ | http://localhost:3000 | Public site |
86
+ | http://localhost:3000/admin/login | Admin builder |
87
+
88
+ **Default example credentials** (from `SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD` in `apps/api/.env`):
89
+
90
+ ```
91
+ Email: admin@example.com
92
+ Password: ChangeMe123!
93
+ ```
94
+
95
+ > Change these before deploying anywhere real. The seed only sets the password when the user is first created — to change it later, update the env values, delete the user row, and re-run `npm run db:seed` (or update the password through the admin Users page).
96
+
97
+ ---
98
+
99
+ ## Features
100
+
101
+ ### Page builder
102
+
103
+ - **Drag & drop** — drag components from the palette onto the canvas, reorder by dragging, nest inside containers/columns.
104
+ - **Live canvas** — pages render with the same components as the public site; select any section to edit it.
105
+ - **Inspector panel** — per-component **Content** tab (fields auto-generated from the component registry) and **Style** tab organized into collapsible groups: Basics, Border, Shadow & Opacity, Animation, Modern Effects, Advanced.
106
+ - **Drag-to-resize** — pull the right/bottom edge handles of a selected section to set its width/height; columns resize their real CSS Grid tracks.
107
+ - **Breadcrumb selection** — when a container is fully covered by its children, use the breadcrumb at the top of the Inspector to select the parent.
108
+ - **Undo / redo** — Ctrl+Z / Ctrl+Y, with rapid edits coalesced into single history steps.
109
+ - **Autosave & draft recovery** — periodic local backup with a restore prompt if the browser closes mid-edit.
110
+ - **Revisions** — every save snapshots the previous version (last 30 kept); restore from Page Settings → History.
111
+ - **Scheduled publishing** — set a future publish date; the page stays hidden until then.
112
+ - **Show/hide** — non-destructively hide any section from the live site while keeping it in the builder.
113
+ - **Developer mode** — the `</>` toggle in the Inspector reveals a raw JSON tab for editing any node's props/style directly.
114
+
115
+ ### Components (22+)
116
+
117
+ Layout: Header (3-zone with logo/nav/buttons), Footer (link columns, contact info, socials), Columns/Container.
118
+ Content: Hero, Rich Text (full toolbar: headings, lists, alignment, colors, links, quotes), Card Grid (image or icon tiles), Accordion/FAQ, Tabs (animated switching), Stats (count-up numbers), CTA, Countdown, Marquee/Ticker.
119
+ Media: Image, Image Slider, Image/Video Background sections, 3D Background, Gallery.
120
+ Interactive: Buttons, Map, Contact Form (submissions stored and viewable in the admin).
121
+
122
+ ### Animation system (GSAP)
123
+
124
+ Every section of any type can animate — configured entirely in the Inspector:
125
+
126
+ - **12 entrance effects** — fade, slide (4 directions), zoom, rotate, pulse, scroll-reveal, blur, flip, bounce.
127
+ - **5 triggers** — on load, on scroll into view, on hover (reverses on leave), on focus, on click (plays a full tap gesture).
128
+ - **Customization** — duration, delay, easing presets (smooth / overshoot / elastic / bounce / custom GSAP string), continuous loop.
129
+ - **Stagger** — reveal a section's inner items (cards, paragraphs, buttons) one after another.
130
+ - **Parallax** — sections drift against the scroll at a configurable speed.
131
+
132
+ ### Modern effects
133
+
134
+ - **Hover effects** — lift, 3D tilt (follows the pointer), glow, scale.
135
+ - **Glassmorphism** — one-click frosted-glass panels.
136
+ - **Gradient headings** — fill a section's headings with a two-color gradient.
137
+ - **Animated gradients** — hero backgrounds that slowly sweep their colors.
138
+
139
+ ### 3D backgrounds (Three.js)
140
+
141
+ Six scene presets usable in the Hero or the dedicated 3D Background component: **Particles, Waves, Floating Shapes, Starfield, Network/Constellation, Orbit Rings** — each with two colors, density, speed, opacity, an on/off switch, and optional pointer-follow camera parallax. Three.js is lazy-loaded only on pages that actually use it, and all motion respects `prefers-reduced-motion`.
142
+
143
+ ### Site-wide
144
+
145
+ - **Theme settings** — brand colors, fonts, logo applied via CSS variables everywhere.
146
+ - **Navigation editor** — menu items with dropdown submenus.
147
+ - **Media library** — uploads stored in MinIO/S3; image/video pickers throughout the builder.
148
+ - **Form submissions** — public forms POST to the API; view/delete entries in the admin.
149
+ - **SEO** — per-page title, description, and OG image.
150
+ - **Escape hatches** — arbitrary custom CSS properties and HTML attributes (`data-*`, `aria-*`, `id`) on any section.
151
+
152
+ ---
153
+
154
+ ## Scripts reference
155
+
156
+ | Command | What it does |
157
+ |---|---|
158
+ | `npm run dev` | API + web dev servers together |
159
+ | `npm run dev:api` / `npm run dev:web` | Each dev server alone |
160
+ | `npm run build` | Build shared → api → web for production |
161
+ | `npm run docker:up` / `npm run docker:down` | Start / stop Postgres + MinIO |
162
+ | `npm run db:migrate` | Apply Prisma migrations (`prisma migrate dev`) |
163
+ | `npm run db:seed` | Seed admin user, theme, sample page |
164
+
165
+ ---
166
+
167
+ ## Project structure
168
+
169
+ ```
170
+ pg-cms/
171
+ docker-compose.yml # Postgres (5434) + MinIO (9010/9011)
172
+ .env / .env.example # compose-level env
173
+ packages/shared/src/
174
+ schema/ # Zod schemas: PageNode, NodeStyle, User, Theme, fields
175
+ components.ts # THE component registry — one entry per block type
176
+ tree.ts # pure page-tree helpers (insert/move/remove/create)
177
+ apps/api/
178
+ .env # DATABASE_URL, JWT, S3, seed admin credentials
179
+ prisma/ # schema, migrations, seed scripts
180
+ src/routes/ # auth, pages, media, theme, users, forms
181
+ apps/web/
182
+ app/[[...slug]]/ # public site — renders published pages
183
+ app/admin/ # login, dashboard, builder, media, settings
184
+ components/renderer/ # Renderer + one component per block type
185
+ components/builder/ # Palette, Canvas, Inspector, field editors
186
+ lib/ # style mapping, GSAP hooks, Three.js scenes
187
+ ```
188
+
189
+ ### Adding a new component type
190
+
191
+ 1. Add a `ComponentDefinition` to `packages/shared/src/components.ts` (type, label, icon, default props/style, Inspector fields).
192
+ 2. Create the React component in `apps/web/components/renderer/sections/`.
193
+ 3. Register it in `apps/web/components/renderer/registry.tsx`.
194
+
195
+ No builder-chrome changes needed — the palette, inspector, and drag-and-drop pick it up from the registry.
196
+
197
+ ---
198
+
199
+ ## Troubleshooting
200
+
201
+ **`P1010: User was denied access` on migrate** — the `DATABASE_URL` password doesn't match what Postgres was initialized with, *or* you're accidentally connecting to a different Postgres on the same port (check `docker ps` for other projects' containers).
202
+
203
+ **`port is already allocated` from Docker** — another container owns the host port. `docker ps`, then stop it or change the port mapping in `docker-compose.yml` + the URLs in `apps/api/.env`.
204
+
205
+ **"Invalid email or password"** — credentials are case-sensitive; watch for browser autofill inserting a stale saved password. The current values live in `apps/api/.env`.
206
+
207
+ **Prisma `EPERM ... query_engine ... .dll.node` on Windows** — the running API dev server is holding the engine file. Stop it (`netstat -ano | findstr :4000`, then kill that PID), run the migration, restart.
208
+
209
+ **Dev server "running" but serving stale code** — an orphaned process may still own the port. Verify with `netstat -ano | findstr :3000` (or `:4000`) and kill the listed PID before restarting.
210
+ "# Modern-CMS"
@@ -0,0 +1,26 @@
1
+ FROM node:24-alpine AS base
2
+ WORKDIR /app
3
+
4
+ FROM base AS deps
5
+ COPY package.json package-lock.json ./
6
+ COPY packages/shared/package.json ./packages/shared/
7
+ COPY apps/api/package.json ./apps/api/
8
+ RUN npm ci --workspace=apps/api --workspace=packages/shared --include-workspace-root
9
+
10
+ FROM base AS build
11
+ COPY --from=deps /app/node_modules ./node_modules
12
+ COPY . .
13
+ RUN npm run db:generate -w apps/api
14
+ RUN npm run build -w packages/shared && npm run build -w apps/api
15
+
16
+ FROM base AS runner
17
+ ENV NODE_ENV=production
18
+ WORKDIR /app
19
+ COPY --from=build /app/node_modules ./node_modules
20
+ COPY --from=build /app/package.json ./
21
+ COPY --from=build /app/packages/shared ./packages/shared
22
+ COPY --from=build /app/apps/api/package.json ./apps/api/
23
+ COPY --from=build /app/apps/api/dist ./apps/api/dist
24
+ COPY --from=build /app/apps/api/prisma ./apps/api/prisma
25
+ EXPOSE 4000
26
+ CMD ["node", "apps/api/dist/index.js"]