create-pxlr 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 (153) hide show
  1. package/README.md +160 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +264 -0
  4. package/package.json +51 -0
  5. package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
  6. package/templates/blog/frontend/app/blog/page.tsx +102 -0
  7. package/templates/blog/frontend/app/components/footer.tsx +21 -0
  8. package/templates/blog/frontend/app/components/header.tsx +45 -0
  9. package/templates/blog/frontend/app/globals.css +30 -0
  10. package/templates/blog/frontend/app/layout.tsx +38 -0
  11. package/templates/blog/frontend/app/lib/cms.ts +71 -0
  12. package/templates/blog/frontend/app/page.tsx +155 -0
  13. package/templates/blog/frontend/next.config.ts +16 -0
  14. package/templates/blog/frontend/package.json +24 -0
  15. package/templates/blog/frontend/postcss.config.mjs +7 -0
  16. package/templates/blog/frontend/tsconfig.json +23 -0
  17. package/templates/blog/pxlr-cms/README.md +188 -0
  18. package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
  19. package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
  20. package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
  21. package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
  22. package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
  23. package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  24. package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
  25. package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
  26. package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  27. package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  28. package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  29. package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  30. package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  31. package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  32. package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  33. package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  34. package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  35. package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  36. package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  37. package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  38. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  39. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  40. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  41. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  42. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  43. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  44. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  45. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  46. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  47. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  48. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  49. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  50. package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  51. package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  52. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  53. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  54. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  55. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  56. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  57. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  58. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  59. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  60. package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  61. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  62. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  63. package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  64. package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  65. package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  66. package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
  67. package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
  68. package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
  69. package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
  70. package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
  71. package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
  72. package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
  73. package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  74. package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
  75. package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
  76. package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  77. package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  78. package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  79. package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  80. package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  81. package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  82. package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
  83. package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
  84. package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  85. package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
  86. package/templates/clean/pxlr-cms/README.md +188 -0
  87. package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
  88. package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
  89. package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
  90. package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
  91. package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
  92. package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  93. package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
  94. package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
  95. package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  96. package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  97. package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  98. package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  99. package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  100. package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  101. package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  102. package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  103. package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  104. package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  105. package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  106. package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  107. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  108. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  109. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  110. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  111. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  112. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  113. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  114. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  115. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  116. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  117. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  118. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  119. package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  120. package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  121. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  122. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  123. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  124. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  125. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  126. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  127. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  128. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  129. package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  130. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  131. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  132. package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  133. package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  134. package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  135. package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
  136. package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
  137. package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
  138. package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
  139. package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
  140. package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
  141. package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
  142. package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  143. package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
  144. package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  145. package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  146. package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  147. package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  148. package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  149. package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  150. package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
  151. package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
  152. package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  153. package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # create-pxlr
2
+
3
+ 🚀 CLI tool to create PXLR CMS projects - a self-hosted Headless CMS.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # With pnpm (recommended)
9
+ pnpm create pxlr@latest
10
+
11
+ # With npm
12
+ npx create-pxlr@latest
13
+
14
+ # With yarn
15
+ yarn create pxlr
16
+ ```
17
+
18
+ ## Installation Types
19
+
20
+ ### 🐳 Docker (Recommended)
21
+ Complete self-contained setup with all services:
22
+ - PostgreSQL database
23
+ - MinIO S3 storage
24
+ - Redis cache
25
+ - API server
26
+ - Admin panel
27
+
28
+ Just run `docker-compose up -d` and you're ready!
29
+
30
+ ### ⚡ Standalone
31
+ For users with existing infrastructure:
32
+ - Connect to your own PostgreSQL database
33
+ - Use any S3-compatible storage (AWS S3, Cloudflare R2, MinIO, etc.)
34
+ - Optional Redis for caching
35
+ - Configure via `.env` files
36
+
37
+ ## Templates
38
+
39
+ ### 📦 Clean
40
+ - Empty admin panel
41
+ - No demo data
42
+ - Perfect for new projects
43
+
44
+ ### 📝 Blog
45
+ - Pre-configured blog schema
46
+ - Demo blog posts
47
+ - Optional Next.js frontend
48
+
49
+ ## Quick Start
50
+
51
+ ### Docker Installation
52
+
53
+ ```bash
54
+ # Create project
55
+ pnpm create pxlr@latest my-project
56
+
57
+ # Select: Docker installation
58
+ # Select: Blog template (or Clean)
59
+
60
+ # Start services
61
+ cd my-project/pxlr-cms
62
+ docker-compose up -d
63
+
64
+ # Access:
65
+ # Admin: http://localhost:3333
66
+ # API: http://localhost:4000
67
+ # MinIO: http://localhost:9011
68
+ ```
69
+
70
+ ### Standalone Installation
71
+
72
+ ```bash
73
+ # Create project
74
+ pnpm create pxlr@latest my-project
75
+
76
+ # Select: Standalone installation
77
+
78
+ # Configure database and S3
79
+ nano my-project/pxlr-cms/packages/api/.env
80
+
81
+ # Initialize database
82
+ psql -d your_database -f my-project/pxlr-cms/packages/api/src/database/init.sql
83
+
84
+ # Start API
85
+ cd my-project/pxlr-cms/packages/api
86
+ npm install && npm run dev
87
+
88
+ # Start Admin
89
+ cd my-project/pxlr-cms/packages/admin
90
+ npm install && npm run dev
91
+ ```
92
+
93
+ ## Default Credentials
94
+
95
+ - **Email:** admin@pxlr.local
96
+ - **Password:** admin123
97
+
98
+ ## Environment Variables
99
+
100
+ ### API (.env)
101
+
102
+ ```env
103
+ # Server
104
+ PORT=4000
105
+ NODE_ENV=development
106
+
107
+ # Database
108
+ DATABASE_URL=postgresql://user:pass@localhost:5432/pxlr
109
+
110
+ # Redis (optional)
111
+ REDIS_URL=redis://localhost:6379
112
+
113
+ # S3 Storage
114
+ S3_ENDPOINT=s3.amazonaws.com
115
+ S3_ACCESS_KEY=your-key
116
+ S3_SECRET_KEY=your-secret
117
+ S3_BUCKET=pxlr-media
118
+ S3_PUBLIC_URL=https://bucket.s3.amazonaws.com
119
+
120
+ # JWT
121
+ JWT_SECRET=your-secret-key
122
+ ```
123
+
124
+ ### Admin (.env)
125
+
126
+ ```env
127
+ NEXT_PUBLIC_API_URL=http://localhost:4000
128
+ ```
129
+
130
+ ## Project Structure
131
+
132
+ ```
133
+ my-project/
134
+ ├── pxlr-cms/
135
+ │ ├── packages/
136
+ │ │ ├── api/ # Fastify backend
137
+ │ │ └── admin/ # Next.js admin panel
138
+ │ ├── docker-compose.yml (Docker only)
139
+ │ └── nginx/ (Docker only)
140
+ └── frontend/ # Next.js frontend (blog template)
141
+ ```
142
+
143
+ ## Supported S3 Providers
144
+
145
+ - MinIO (self-hosted)
146
+ - AWS S3
147
+ - Cloudflare R2
148
+ - DigitalOcean Spaces
149
+ - Backblaze B2
150
+ - Any S3-compatible storage
151
+
152
+ ## Links
153
+
154
+ - [Documentation](https://github.com/pxlr-cms/pxlr)
155
+ - [GitHub](https://github.com/pxlr-cms/create-pxlr)
156
+ - [Issues](https://github.com/pxlr-cms/create-pxlr/issues)
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import inquirer from "inquirer";
6
+ import chalk from "chalk";
7
+ import ora from "ora";
8
+ import fs from "fs-extra";
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+ var __filename = fileURLToPath(import.meta.url);
12
+ var __dirname = path.dirname(__filename);
13
+ var TEMPLATES_DIR = path.join(__dirname, "..", "templates");
14
+ var program = new Command();
15
+ program.name("create-pxlr").description("\u{1F680} Create a new PXLR CMS project").version("1.0.0").argument("[project-name]", "Name of the project").action(async (projectName) => {
16
+ console.log();
17
+ console.log(chalk.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
18
+ console.log(chalk.bold.cyan(" \u2551") + chalk.bold.white(" \u{1F3A8} PXLR CMS Creator ") + chalk.bold.cyan("\u2551"));
19
+ console.log(chalk.bold.cyan(" \u2551") + chalk.gray(" Self-hosted Headless CMS ") + chalk.bold.cyan("\u2551"));
20
+ console.log(chalk.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
21
+ console.log();
22
+ try {
23
+ const answers = await inquirer.prompt([
24
+ {
25
+ type: "input",
26
+ name: "projectName",
27
+ message: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430:",
28
+ default: projectName || "my-pxlr-project",
29
+ validate: (input) => {
30
+ if (/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(input)) return true;
31
+ return "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u043D\u0430\u0447\u0438\u043D\u0430\u0442\u044C\u0441\u044F \u0441 \u0431\u0443\u043A\u0432\u044B \u0438 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0443\u043A\u0432\u044B, \u0446\u0438\u0444\u0440\u044B, - \u0438 _";
32
+ }
33
+ },
34
+ {
35
+ type: "list",
36
+ name: "installationType",
37
+ message: "\u0422\u0438\u043F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438:",
38
+ choices: [
39
+ {
40
+ name: "\u{1F433} Docker \u2014 \u041F\u043E\u043B\u043D\u0430\u044F \u0441\u0431\u043E\u0440\u043A\u0430 \u0441 PostgreSQL, MinIO, Redis (\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F)",
41
+ value: "docker"
42
+ },
43
+ {
44
+ name: "\u26A1 Standalone \u2014 \u0422\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0434, \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0447\u0435\u0440\u0435\u0437 .env (\u0441\u0432\u043E\u044F \u0411\u0414 \u0438 S3)",
45
+ value: "standalone"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ type: "list",
51
+ name: "template",
52
+ message: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u0430\u0431\u043B\u043E\u043D:",
53
+ choices: [
54
+ {
55
+ name: "\u{1F4E6} Clean \u2014 \u041F\u0443\u0441\u0442\u0430\u044F \u0430\u0434\u043C\u0438\u043D\u043A\u0430 \u0431\u0435\u0437 \u0434\u0435\u043C\u043E-\u0434\u0430\u043D\u043D\u044B\u0445",
56
+ value: "clean"
57
+ },
58
+ {
59
+ name: "\u{1F4DD} Blog \u2014 \u0413\u043E\u0442\u043E\u0432\u044B\u0439 \u0431\u043B\u043E\u0433 \u0441 \u0434\u0435\u043C\u043E-\u043A\u043E\u043D\u0442\u0435\u043D\u0442\u043E\u043C",
60
+ value: "blog"
61
+ }
62
+ ]
63
+ },
64
+ {
65
+ type: "confirm",
66
+ name: "installFrontend",
67
+ message: "\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C Next.js \u0444\u0440\u043E\u043D\u0442\u0435\u043D\u0434 \u0441 \u0448\u0430\u0431\u043B\u043E\u043D\u0430\u043C\u0438?",
68
+ default: true,
69
+ when: (answers2) => answers2.template === "blog"
70
+ }
71
+ ]);
72
+ const targetDir = path.resolve(process.cwd(), answers.projectName);
73
+ if (fs.existsSync(targetDir)) {
74
+ const { overwrite } = await inquirer.prompt([
75
+ {
76
+ type: "confirm",
77
+ name: "overwrite",
78
+ message: `\u0414\u0438\u0440\u0435\u043A\u0442\u043E\u0440\u0438\u044F ${answers.projectName} \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442. \u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C?`,
79
+ default: false
80
+ }
81
+ ]);
82
+ if (!overwrite) {
83
+ console.log(chalk.yellow("\n\u26A0\uFE0F \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430"));
84
+ process.exit(0);
85
+ }
86
+ await fs.remove(targetDir);
87
+ }
88
+ const spinner = ora("\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430...").start();
89
+ try {
90
+ const templateDir = path.join(TEMPLATES_DIR, answers.template);
91
+ await fs.copy(templateDir, targetDir);
92
+ await updateProjectName(targetDir, answers.projectName);
93
+ if (answers.installationType === "standalone") {
94
+ const dockerComposePath = path.join(targetDir, "pxlr-cms", "docker-compose.yml");
95
+ if (await fs.pathExists(dockerComposePath)) {
96
+ await fs.remove(dockerComposePath);
97
+ }
98
+ const nginxPath = path.join(targetDir, "pxlr-cms", "nginx");
99
+ if (await fs.pathExists(nginxPath)) {
100
+ await fs.remove(nginxPath);
101
+ }
102
+ await createStandaloneEnvFiles(targetDir);
103
+ }
104
+ if (!answers.installFrontend) {
105
+ const frontendDir = path.join(targetDir, "frontend");
106
+ if (await fs.pathExists(frontendDir)) {
107
+ await fs.remove(frontendDir);
108
+ }
109
+ }
110
+ spinner.succeed("\u041F\u0440\u043E\u0435\u043A\u0442 \u0441\u043E\u0437\u0434\u0430\u043D");
111
+ console.log();
112
+ console.log(chalk.green("\u2705 \u041F\u0440\u043E\u0435\u043A\u0442 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u043D!"));
113
+ console.log();
114
+ if (answers.installationType === "docker") {
115
+ printDockerInstructions(answers);
116
+ } else {
117
+ printStandaloneInstructions(answers);
118
+ }
119
+ } catch (error) {
120
+ spinner.fail("\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430");
121
+ throw error;
122
+ }
123
+ } catch (error) {
124
+ if (error instanceof Error && error.message.includes("User force closed")) {
125
+ console.log(chalk.yellow("\n\u{1F44B} \u0414\u043E \u0441\u0432\u0438\u0434\u0430\u043D\u0438\u044F!"));
126
+ process.exit(0);
127
+ }
128
+ console.error(chalk.red("\n\u274C \u041E\u0448\u0438\u0431\u043A\u0430:"), error);
129
+ process.exit(1);
130
+ }
131
+ });
132
+ program.parse();
133
+ function printDockerInstructions(answers) {
134
+ console.log(chalk.bold("\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 \u0448\u0430\u0433\u0438 (Docker):"));
135
+ console.log();
136
+ console.log(chalk.cyan(` cd ${answers.projectName}/pxlr-cms`));
137
+ console.log(chalk.cyan(" docker-compose up -d"));
138
+ console.log();
139
+ console.log(chalk.bold("\u0414\u043E\u0441\u0442\u0443\u043F:"));
140
+ console.log(chalk.gray(" \u2022 \u0410\u0434\u043C\u0438\u043D\u043A\u0430: ") + chalk.white("http://localhost:3333"));
141
+ console.log(chalk.gray(" \u2022 API: ") + chalk.white("http://localhost:4000"));
142
+ console.log(chalk.gray(" \u2022 MinIO: ") + chalk.white("http://localhost:9011"));
143
+ console.log();
144
+ console.log(chalk.bold("\u0410\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044F:"));
145
+ console.log(chalk.gray(" \u2022 Email: ") + chalk.white("admin@pxlr.local"));
146
+ console.log(chalk.gray(" \u2022 \u041F\u0430\u0440\u043E\u043B\u044C: ") + chalk.white("admin123"));
147
+ console.log();
148
+ if (answers.installFrontend) {
149
+ console.log(chalk.bold("\u0424\u0440\u043E\u043D\u0442\u0435\u043D\u0434 (Next.js):"));
150
+ console.log(chalk.cyan(` cd ${answers.projectName}/frontend`));
151
+ console.log(chalk.cyan(" npm install && npm run dev"));
152
+ console.log(chalk.gray(" \u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435: ") + chalk.white("http://localhost:3000"));
153
+ console.log();
154
+ }
155
+ }
156
+ function printStandaloneInstructions(answers) {
157
+ console.log(chalk.bold("\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 \u0448\u0430\u0433\u0438 (Standalone):"));
158
+ console.log();
159
+ console.log(chalk.yellow("1. \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u0442\u0435 \u0431\u0430\u0437\u0443 \u0434\u0430\u043D\u043D\u044B\u0445 PostgreSQL"));
160
+ console.log(chalk.gray(" \u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 SQL \u0438\u0437: ") + chalk.white("pxlr-cms/packages/api/src/database/init.sql"));
161
+ console.log();
162
+ console.log(chalk.yellow("2. \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u0442\u0435 S3 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 (AWS S3, MinIO, Cloudflare R2 \u0438 \u0442.\u0434.)"));
163
+ console.log();
164
+ console.log(chalk.yellow("3. \u041E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0439\u0442\u0435 .env \u0444\u0430\u0439\u043B\u044B:"));
165
+ console.log(chalk.cyan(` ${answers.projectName}/pxlr-cms/packages/api/.env`));
166
+ console.log(chalk.cyan(` ${answers.projectName}/pxlr-cms/packages/admin/.env`));
167
+ console.log();
168
+ console.log(chalk.yellow("4. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435:"));
169
+ console.log();
170
+ console.log(chalk.bold("API:"));
171
+ console.log(chalk.cyan(` cd ${answers.projectName}/pxlr-cms/packages/api`));
172
+ console.log(chalk.cyan(" npm install"));
173
+ console.log(chalk.cyan(" npm run dev"));
174
+ console.log();
175
+ console.log(chalk.bold("\u0410\u0434\u043C\u0438\u043D\u043A\u0430:"));
176
+ console.log(chalk.cyan(` cd ${answers.projectName}/pxlr-cms/packages/admin`));
177
+ console.log(chalk.cyan(" npm install"));
178
+ console.log(chalk.cyan(" npm run dev"));
179
+ console.log();
180
+ if (answers.installFrontend) {
181
+ console.log(chalk.bold("\u0424\u0440\u043E\u043D\u0442\u0435\u043D\u0434:"));
182
+ console.log(chalk.cyan(` cd ${answers.projectName}/frontend`));
183
+ console.log(chalk.cyan(" npm install && npm run dev"));
184
+ console.log();
185
+ }
186
+ console.log(chalk.bold("\u041F\u043E\u0440\u0442\u044B \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E:"));
187
+ console.log(chalk.gray(" \u2022 API: ") + chalk.white("http://localhost:4000"));
188
+ console.log(chalk.gray(" \u2022 \u0410\u0434\u043C\u0438\u043D\u043A\u0430: ") + chalk.white("http://localhost:3000"));
189
+ console.log();
190
+ }
191
+ async function createStandaloneEnvFiles(targetDir) {
192
+ const apiEnvContent = `# PXLR CMS API Configuration
193
+ # ===========================
194
+
195
+ # Server
196
+ PORT=4000
197
+ HOST=0.0.0.0
198
+ NODE_ENV=development
199
+
200
+ # JWT Secret (\u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u0435 \u043D\u0430 \u0441\u0432\u043E\u0439!)
201
+ JWT_SECRET=your-super-secret-jwt-key-change-this
202
+
203
+ # PostgreSQL Database
204
+ # \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432\u0430\u0448\u0435\u0439 \u0431\u0430\u0437\u044B \u0434\u0430\u043D\u043D\u044B\u0445
205
+ DATABASE_URL=postgresql://user:password@localhost:5432/pxlr_cms
206
+ # \u0418\u043B\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B:
207
+ # DB_HOST=localhost
208
+ # DB_PORT=5432
209
+ # DB_USER=your_user
210
+ # DB_PASSWORD=your_password
211
+ # DB_NAME=pxlr_cms
212
+
213
+ # Redis (\u043E\u043F\u0446\u0438\u043E\u043D\u0430\u043B\u044C\u043D\u043E, \u0434\u043B\u044F \u043A\u044D\u0448\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0438 real-time)
214
+ REDIS_URL=redis://localhost:6379
215
+ # \u0418\u043B\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B:
216
+ # REDIS_HOST=localhost
217
+ # REDIS_PORT=6379
218
+
219
+ # S3 Storage (MinIO, AWS S3, Cloudflare R2, \u0438 \u0442.\u0434.)
220
+ S3_ENDPOINT=s3.amazonaws.com
221
+ S3_PORT=443
222
+ S3_ACCESS_KEY=your-access-key
223
+ S3_SECRET_KEY=your-secret-key
224
+ S3_BUCKET=pxlr-media
225
+ S3_USE_SSL=true
226
+ S3_REGION=us-east-1
227
+ # \u041F\u0443\u0431\u043B\u0438\u0447\u043D\u044B\u0439 URL \u0434\u043B\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0444\u0430\u0439\u043B\u0430\u043C
228
+ S3_PUBLIC_URL=https://your-bucket.s3.amazonaws.com
229
+
230
+ # CORS
231
+ CORS_ORIGIN=http://localhost:3000,http://localhost:3333
232
+ `;
233
+ const adminEnvContent = `# PXLR CMS Admin Configuration
234
+ # =============================
235
+
236
+ # API URL (\u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0432\u0430\u0448\u0435\u0433\u043E API)
237
+ NEXT_PUBLIC_API_URL=http://localhost:4000
238
+ `;
239
+ const apiEnvPath = path.join(targetDir, "pxlr-cms", "packages", "api", ".env");
240
+ const adminEnvPath = path.join(targetDir, "pxlr-cms", "packages", "admin", ".env");
241
+ await fs.writeFile(apiEnvPath, apiEnvContent);
242
+ await fs.writeFile(adminEnvPath, adminEnvContent);
243
+ }
244
+ async function updateProjectName(targetDir, projectName) {
245
+ const dockerComposePath = path.join(targetDir, "pxlr-cms", "docker-compose.yml");
246
+ if (await fs.pathExists(dockerComposePath)) {
247
+ let content = await fs.readFile(dockerComposePath, "utf-8");
248
+ content = content.replace(/pxlr-/g, `${projectName}-`);
249
+ content = content.replace(/pxlr_/g, `${projectName}_`);
250
+ await fs.writeFile(dockerComposePath, content);
251
+ }
252
+ const packageJsonPaths = [
253
+ path.join(targetDir, "pxlr-cms", "packages", "api", "package.json"),
254
+ path.join(targetDir, "pxlr-cms", "packages", "admin", "package.json"),
255
+ path.join(targetDir, "frontend", "package.json")
256
+ ];
257
+ for (const pkgPath of packageJsonPaths) {
258
+ if (await fs.pathExists(pkgPath)) {
259
+ const pkg = await fs.readJson(pkgPath);
260
+ pkg.name = pkg.name.replace("@pxlr", `@${projectName}`);
261
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
262
+ }
263
+ }
264
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "create-pxlr",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to create PXLR CMS projects",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "create-pxlr": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts",
12
+ "dev": "tsup src/index.ts --format esm --watch",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "pxlr",
17
+ "cms",
18
+ "headless-cms",
19
+ "create",
20
+ "cli",
21
+ "docker",
22
+ "nextjs"
23
+ ],
24
+ "author": "PXLR Team",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/pxlr/create-pxlr"
29
+ },
30
+ "dependencies": {
31
+ "chalk": "^5.3.0",
32
+ "commander": "^12.1.0",
33
+ "fs-extra": "^11.2.0",
34
+ "inquirer": "^9.3.7",
35
+ "ora": "^8.1.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/fs-extra": "^11.0.4",
39
+ "@types/inquirer": "^9.0.7",
40
+ "@types/node": "^22.0.0",
41
+ "tsup": "^8.3.5",
42
+ "typescript": "^5.6.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "templates"
50
+ ]
51
+ }
@@ -0,0 +1,175 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+ import { getBlogPostBySlug, getBlogPosts } from '@/app/lib/cms';
4
+
5
+ export const revalidate = 60;
6
+
7
+ interface Props {
8
+ params: Promise<{ slug: string }>;
9
+ }
10
+
11
+ // Генерация статических путей
12
+ export async function generateStaticParams() {
13
+ const posts = await getBlogPosts();
14
+ return posts.map((post) => ({
15
+ slug: post.data.slug,
16
+ }));
17
+ }
18
+
19
+ // Метаданные страницы
20
+ export async function generateMetadata({ params }: Props) {
21
+ const { slug } = await params;
22
+ const post = await getBlogPostBySlug(slug);
23
+
24
+ if (!post) {
25
+ return { title: 'Запись не найдена' };
26
+ }
27
+
28
+ return {
29
+ title: post.data.title,
30
+ description: post.data.excerpt,
31
+ };
32
+ }
33
+
34
+ export default async function BlogPostPage({ params }: Props) {
35
+ const { slug } = await params;
36
+ const post = await getBlogPostBySlug(slug);
37
+
38
+ if (!post) {
39
+ notFound();
40
+ }
41
+
42
+ return (
43
+ <article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
44
+ {/* Навигация */}
45
+ <nav className="mb-8">
46
+ <Link
47
+ href="/blog"
48
+ className="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors"
49
+ >
50
+ <svg
51
+ className="mr-2 w-4 h-4"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ viewBox="0 0 24 24"
55
+ >
56
+ <path
57
+ strokeLinecap="round"
58
+ strokeLinejoin="round"
59
+ strokeWidth={2}
60
+ d="M15 19l-7-7 7-7"
61
+ />
62
+ </svg>
63
+ Назад к блогу
64
+ </Link>
65
+ </nav>
66
+
67
+ {/* Заголовок */}
68
+ <header className="mb-8">
69
+ <h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
70
+ {post.data.title}
71
+ </h1>
72
+
73
+ <div className="flex items-center gap-4 text-gray-500">
74
+ {post.data.publishedAt && (
75
+ <time dateTime={post.data.publishedAt}>
76
+ {new Date(post.data.publishedAt).toLocaleDateString('ru-RU', {
77
+ year: 'numeric',
78
+ month: 'long',
79
+ day: 'numeric',
80
+ })}
81
+ </time>
82
+ )}
83
+ {post.data.author && (
84
+ <>
85
+ <span>•</span>
86
+ <span className="font-medium">{post.data.author}</span>
87
+ </>
88
+ )}
89
+ </div>
90
+ </header>
91
+
92
+ {/* Изображение */}
93
+ {post.data.image?.url && (
94
+ <div className="mb-8 rounded-2xl overflow-hidden">
95
+ <img
96
+ src={post.data.image.url}
97
+ alt={post.data.image.alt || post.data.title}
98
+ className="w-full h-auto"
99
+ />
100
+ </div>
101
+ )}
102
+
103
+ {/* Краткое описание */}
104
+ {post.data.excerpt && (
105
+ <div className="mb-8 p-6 bg-gray-50 rounded-xl border-l-4 border-blue-500">
106
+ <p className="text-lg text-gray-700 italic">
107
+ {post.data.excerpt}
108
+ </p>
109
+ </div>
110
+ )}
111
+
112
+ {/* Контент */}
113
+ <div className="prose prose-lg max-w-none">
114
+ {post.data.content?.split('\n').map((paragraph, index) => {
115
+ // Обработка заголовков
116
+ if (paragraph.startsWith('## ')) {
117
+ return (
118
+ <h2 key={index} className="text-2xl font-bold text-gray-900 mt-8 mb-4">
119
+ {paragraph.replace('## ', '')}
120
+ </h2>
121
+ );
122
+ }
123
+ if (paragraph.startsWith('### ')) {
124
+ return (
125
+ <h3 key={index} className="text-xl font-semibold text-gray-900 mt-6 mb-3">
126
+ {paragraph.replace('### ', '')}
127
+ </h3>
128
+ );
129
+ }
130
+ // Обработка списков
131
+ if (paragraph.startsWith('- ')) {
132
+ return (
133
+ <li key={index} className="text-gray-700 ml-6">
134
+ {paragraph.replace('- ', '')}
135
+ </li>
136
+ );
137
+ }
138
+ // Пустые строки
139
+ if (!paragraph.trim()) {
140
+ return null;
141
+ }
142
+ // Обычные параграфы
143
+ return (
144
+ <p key={index} className="text-gray-700 mb-4 leading-relaxed">
145
+ {paragraph}
146
+ </p>
147
+ );
148
+ })}
149
+ </div>
150
+
151
+ {/* Навигация внизу */}
152
+ <div className="mt-12 pt-8 border-t border-gray-200">
153
+ <Link
154
+ href="/blog"
155
+ className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
156
+ >
157
+ <svg
158
+ className="mr-2 w-4 h-4"
159
+ fill="none"
160
+ stroke="currentColor"
161
+ viewBox="0 0 24 24"
162
+ >
163
+ <path
164
+ strokeLinecap="round"
165
+ strokeLinejoin="round"
166
+ strokeWidth={2}
167
+ d="M15 19l-7-7 7-7"
168
+ />
169
+ </svg>
170
+ Все записи блога
171
+ </Link>
172
+ </div>
173
+ </article>
174
+ );
175
+ }