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.
- package/README.md +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- 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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|