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.
- package/README.md +41 -0
- package/bin/lib.js +97 -0
- package/bin/modern-cms.js +197 -0
- package/package.json +37 -0
- package/template/.env.example +48 -0
- package/template/README.md +210 -0
- package/template/apps/api/Dockerfile +26 -0
- package/template/apps/api/package.json +45 -0
- package/template/apps/api/prisma/backfill-search-text.ts +31 -0
- package/template/apps/api/prisma/migrations/20260701034530_init/migration.sql +86 -0
- package/template/apps/api/prisma/migrations/20260701081942_add_publish_at_and_revisions/migration.sql +19 -0
- package/template/apps/api/prisma/migrations/20260701085011_add_form_submissions/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20260702084049_add_global_sections_and_saved_blocks/migration.sql +24 -0
- package/template/apps/api/prisma/migrations/20260702085609_add_page_locale_and_translations/migration.sql +6 -0
- package/template/apps/api/prisma/migrations/20260702091914_add_collections/migration.sql +38 -0
- package/template/apps/api/prisma/migrations/20260702120000_platform_hardening/migration.sql +15 -0
- package/template/apps/api/prisma/migrations/20260705031230_add_collections_redirects_media_meta/migration.sql +17 -0
- package/template/apps/api/prisma/migrations/20260705033241_add_audit_log/migration.sql +18 -0
- package/template/apps/api/prisma/migrations/20260706031301_standard_cms_platform_pass/migration.sql +48 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/reset-admin.ts +19 -0
- package/template/apps/api/prisma/schema.prisma +251 -0
- package/template/apps/api/prisma/seed-homepage.ts +752 -0
- package/template/apps/api/prisma/seed-our-charities.ts +231 -0
- package/template/apps/api/prisma/seed-templates.ts +154 -0
- package/template/apps/api/prisma/seed.ts +191 -0
- package/template/apps/api/src/env.ts +61 -0
- package/template/apps/api/src/index.ts +66 -0
- package/template/apps/api/src/lib/revalidate.ts +28 -0
- package/template/apps/api/src/lib/storage/azure.ts +25 -0
- package/template/apps/api/src/lib/storage/cloudinary.ts +42 -0
- package/template/apps/api/src/lib/storage/index.ts +25 -0
- package/template/apps/api/src/lib/storage/s3.ts +39 -0
- package/template/apps/api/src/lib/storage/types.ts +8 -0
- package/template/apps/api/src/middleware/auditLog.ts +32 -0
- package/template/apps/api/src/middleware/auth.ts +53 -0
- package/template/apps/api/src/middleware/rateLimit.ts +31 -0
- package/template/apps/api/src/middleware/securityHeaders.ts +10 -0
- package/template/apps/api/src/prisma.ts +3 -0
- package/template/apps/api/src/routes/audit.routes.ts +26 -0
- package/template/apps/api/src/routes/auth.routes.ts +46 -0
- package/template/apps/api/src/routes/backup.routes.ts +216 -0
- package/template/apps/api/src/routes/blocks.routes.ts +38 -0
- package/template/apps/api/src/routes/collections.routes.ts +185 -0
- package/template/apps/api/src/routes/forms.routes.ts +114 -0
- package/template/apps/api/src/routes/globalSections.routes.ts +43 -0
- package/template/apps/api/src/routes/media.routes.ts +77 -0
- package/template/apps/api/src/routes/pages.routes.ts +319 -0
- package/template/apps/api/src/routes/redirects.routes.ts +52 -0
- package/template/apps/api/src/routes/search.routes.ts +83 -0
- package/template/apps/api/src/routes/templates.routes.ts +41 -0
- package/template/apps/api/src/routes/theme.routes.ts +75 -0
- package/template/apps/api/src/routes/users.routes.ts +39 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/web/Dockerfile +26 -0
- package/template/apps/web/app/[[...slug]]/page.tsx +254 -0
- package/template/apps/web/app/admin/(protected)/audit/page.tsx +80 -0
- package/template/apps/web/app/admin/(protected)/collections/item/[itemId]/page.tsx +375 -0
- package/template/apps/web/app/admin/(protected)/collections/page.tsx +224 -0
- package/template/apps/web/app/admin/(protected)/forms/page.tsx +88 -0
- package/template/apps/web/app/admin/(protected)/layout.tsx +5 -0
- package/template/apps/web/app/admin/(protected)/media/page.tsx +138 -0
- package/template/apps/web/app/admin/(protected)/page.tsx +186 -0
- package/template/apps/web/app/admin/(protected)/pages/[id]/page.tsx +560 -0
- package/template/apps/web/app/admin/(protected)/settings/backup/page.tsx +97 -0
- package/template/apps/web/app/admin/(protected)/settings/global-sections/page.tsx +333 -0
- package/template/apps/web/app/admin/(protected)/settings/navigation/page.tsx +192 -0
- package/template/apps/web/app/admin/(protected)/settings/redirects/page.tsx +108 -0
- package/template/apps/web/app/admin/(protected)/settings/theme/page.tsx +239 -0
- package/template/apps/web/app/admin/(protected)/users/page.tsx +94 -0
- package/template/apps/web/app/admin/login/page.tsx +76 -0
- package/template/apps/web/app/api/revalidate/route.ts +34 -0
- package/template/apps/web/app/globals.css +120 -0
- package/template/apps/web/app/layout.tsx +39 -0
- package/template/apps/web/app/preview/[token]/page.tsx +70 -0
- package/template/apps/web/app/robots.ts +11 -0
- package/template/apps/web/app/sitemap.ts +27 -0
- package/template/apps/web/components/admin/AdminShell.tsx +104 -0
- package/template/apps/web/components/builder/BuilderCanvasNode.tsx +546 -0
- package/template/apps/web/components/builder/BuilderDeviceContext.tsx +13 -0
- package/template/apps/web/components/builder/Canvas.tsx +76 -0
- package/template/apps/web/components/builder/CanvasOverlayContext.tsx +22 -0
- package/template/apps/web/components/builder/ColorPickerField.tsx +47 -0
- package/template/apps/web/components/builder/DroppableChildren.tsx +50 -0
- package/template/apps/web/components/builder/FieldRenderer.tsx +277 -0
- package/template/apps/web/components/builder/IconRenderer.tsx +7 -0
- package/template/apps/web/components/builder/Inspector.tsx +956 -0
- package/template/apps/web/components/builder/JsonEditor.tsx +71 -0
- package/template/apps/web/components/builder/KeyValueListEditor.tsx +76 -0
- package/template/apps/web/components/builder/MediaPickerModal.tsx +95 -0
- package/template/apps/web/components/builder/PageSettingsModal.tsx +363 -0
- package/template/apps/web/components/builder/Palette.tsx +106 -0
- package/template/apps/web/components/builder/RichTextEditor.tsx +140 -0
- package/template/apps/web/components/renderer/CmsImage.tsx +46 -0
- package/template/apps/web/components/renderer/NodeBackgroundLayers.tsx +35 -0
- package/template/apps/web/components/renderer/PublicPageView.tsx +56 -0
- package/template/apps/web/components/renderer/Renderer.tsx +146 -0
- package/template/apps/web/components/renderer/RendererContext.tsx +20 -0
- package/template/apps/web/components/renderer/registry.tsx +87 -0
- package/template/apps/web/components/renderer/sections/Accordion.tsx +39 -0
- package/template/apps/web/components/renderer/sections/BeforeAfter.tsx +75 -0
- package/template/apps/web/components/renderer/sections/Button.tsx +42 -0
- package/template/apps/web/components/renderer/sections/ButtonGroup.tsx +22 -0
- package/template/apps/web/components/renderer/sections/CardGrid.tsx +66 -0
- package/template/apps/web/components/renderer/sections/CollectionList.tsx +84 -0
- package/template/apps/web/components/renderer/sections/Column.tsx +26 -0
- package/template/apps/web/components/renderer/sections/ContactForm.tsx +96 -0
- package/template/apps/web/components/renderer/sections/Container.tsx +44 -0
- package/template/apps/web/components/renderer/sections/ContentLinks.tsx +38 -0
- package/template/apps/web/components/renderer/sections/Countdown.tsx +69 -0
- package/template/apps/web/components/renderer/sections/Cta.tsx +15 -0
- package/template/apps/web/components/renderer/sections/Divider.tsx +26 -0
- package/template/apps/web/components/renderer/sections/EmbedBlock.tsx +43 -0
- package/template/apps/web/components/renderer/sections/Footer.tsx +117 -0
- package/template/apps/web/components/renderer/sections/Gallery.tsx +27 -0
- package/template/apps/web/components/renderer/sections/GoToTop.tsx +54 -0
- package/template/apps/web/components/renderer/sections/Grid.tsx +54 -0
- package/template/apps/web/components/renderer/sections/GridCell.tsx +33 -0
- package/template/apps/web/components/renderer/sections/Header.tsx +132 -0
- package/template/apps/web/components/renderer/sections/Hero.tsx +73 -0
- package/template/apps/web/components/renderer/sections/ImageBackgroundSection.tsx +20 -0
- package/template/apps/web/components/renderer/sections/ImageBlock.tsx +55 -0
- package/template/apps/web/components/renderer/sections/LanguageSwitcher.tsx +51 -0
- package/template/apps/web/components/renderer/sections/Logo.tsx +24 -0
- package/template/apps/web/components/renderer/sections/LogoCloud.tsx +67 -0
- package/template/apps/web/components/renderer/sections/MapEmbed.tsx +24 -0
- package/template/apps/web/components/renderer/sections/Marquee.tsx +45 -0
- package/template/apps/web/components/renderer/sections/NavLinks.tsx +69 -0
- package/template/apps/web/components/renderer/sections/NavZone.tsx +19 -0
- package/template/apps/web/components/renderer/sections/PricingTable.tsx +65 -0
- package/template/apps/web/components/renderer/sections/RichText.tsx +7 -0
- package/template/apps/web/components/renderer/sections/SiteSearch.tsx +67 -0
- package/template/apps/web/components/renderer/sections/Slider.tsx +81 -0
- package/template/apps/web/components/renderer/sections/Spacer.tsx +8 -0
- package/template/apps/web/components/renderer/sections/Stats.tsx +69 -0
- package/template/apps/web/components/renderer/sections/Tabs.tsx +59 -0
- package/template/apps/web/components/renderer/sections/TeamGrid.tsx +49 -0
- package/template/apps/web/components/renderer/sections/Testimonial.tsx +79 -0
- package/template/apps/web/components/renderer/sections/ThreeBackground.tsx +43 -0
- package/template/apps/web/components/renderer/sections/Timeline.tsx +29 -0
- package/template/apps/web/components/renderer/sections/VideoBackgroundSection.tsx +20 -0
- package/template/apps/web/components/renderer/sections/VideoEmbed.tsx +70 -0
- package/template/apps/web/components/renderer/sections/types.ts +7 -0
- package/template/apps/web/components/seo/JsonLd.tsx +111 -0
- package/template/apps/web/components/theme/AnalyticsScripts.tsx +68 -0
- package/template/apps/web/components/theme/DarkModeToggle.tsx +39 -0
- package/template/apps/web/components/theme/SiteDataContext.tsx +39 -0
- package/template/apps/web/components/theme/ThemeProvider.tsx +58 -0
- package/template/apps/web/eslint.config.mjs +16 -0
- package/template/apps/web/lib/api.ts +45 -0
- package/template/apps/web/lib/responsiveCss.ts +42 -0
- package/template/apps/web/lib/sanitizeHtml.ts +16 -0
- package/template/apps/web/lib/serverApi.ts +25 -0
- package/template/apps/web/lib/style.ts +73 -0
- package/template/apps/web/lib/threeBackgroundScene.ts +298 -0
- package/template/apps/web/lib/useCurrentUser.ts +35 -0
- package/template/apps/web/lib/useEntranceAnimation.ts +245 -0
- package/template/apps/web/next-env.d.ts +6 -0
- package/template/apps/web/next.config.mjs +32 -0
- package/template/apps/web/package.json +46 -0
- package/template/apps/web/postcss.config.mjs +6 -0
- package/template/apps/web/proxy.ts +23 -0
- package/template/apps/web/tailwind.config.ts +24 -0
- package/template/apps/web/tsconfig.json +41 -0
- package/template/docker-compose.yml +60 -0
- package/template/package.json +29 -0
- package/template/packages/shared/package.json +21 -0
- package/template/packages/shared/src/components.ts +1626 -0
- package/template/packages/shared/src/index.ts +9 -0
- package/template/packages/shared/src/schema/collections.ts +70 -0
- package/template/packages/shared/src/schema/fields.ts +26 -0
- package/template/packages/shared/src/schema/media.ts +13 -0
- package/template/packages/shared/src/schema/pageNode.ts +255 -0
- package/template/packages/shared/src/schema/templates.ts +20 -0
- package/template/packages/shared/src/schema/theme.ts +69 -0
- package/template/packages/shared/src/schema/user.ts +25 -0
- package/template/packages/shared/src/tree.test.ts +65 -0
- package/template/packages/shared/src/tree.ts +303 -0
- package/template/packages/shared/tsconfig.json +8 -0
- 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"]
|