strapi
This commit is contained in:
10
.env
10
.env
@@ -19,3 +19,13 @@ MAIL_USERNAME=postmaster@mg.mintel.me
|
|||||||
MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6
|
MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6
|
||||||
MAIL_FROM="KLZ Cables <postmaster@mg.mintel.me>"
|
MAIL_FROM="KLZ Cables <postmaster@mg.mintel.me>"
|
||||||
MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
|
MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
|
||||||
|
|
||||||
|
# Strapi
|
||||||
|
STRAPI_DATABASE_NAME=strapi
|
||||||
|
STRAPI_DATABASE_USERNAME=strapi
|
||||||
|
STRAPI_DATABASE_PASSWORD=strapi_password_change_me
|
||||||
|
APP_KEYS=toBeModified1,toBeModified2,toBeModified3,toBeModified4
|
||||||
|
API_TOKEN_SALT=tobemodified
|
||||||
|
ADMIN_JWT_SECRET=tobemodified
|
||||||
|
TRANSFER_TOKEN_SALT=tobemodified
|
||||||
|
JWT_SECRET=tobemodified
|
||||||
|
|||||||
13
.env.example
13
.env.example
@@ -45,6 +45,19 @@ LOG_LEVEL=info
|
|||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
VARNISH_CACHE_SIZE=256m
|
VARNISH_CACHE_SIZE=256m
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Strapi CMS
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
STRAPI_DATABASE_NAME=strapi
|
||||||
|
STRAPI_DATABASE_USERNAME=strapi
|
||||||
|
STRAPI_DATABASE_PASSWORD=strapi
|
||||||
|
STRAPI_URL=http://localhost:1337
|
||||||
|
APP_KEYS=toBeModified1,toBeModified2
|
||||||
|
API_TOKEN_SALT=tobemodified
|
||||||
|
ADMIN_JWT_SECRET=tobemodified
|
||||||
|
TRANSFER_TOKEN_SALT=tobemodified
|
||||||
|
JWT_SECRET=tobemodified
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# IMPORTANT NOTES
|
# IMPORTANT NOTES
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ MAIL_PASSWORD=
|
|||||||
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
|
MAIL_FROM=KLZ Cables <noreply@klz-cables.com>
|
||||||
MAIL_RECIPIENTS=info@klz-cables.com
|
MAIL_RECIPIENTS=info@klz-cables.com
|
||||||
|
|
||||||
|
# Strapi
|
||||||
|
STRAPI_DATABASE_NAME=strapi
|
||||||
|
STRAPI_DATABASE_USERNAME=strapi
|
||||||
|
STRAPI_DATABASE_PASSWORD=
|
||||||
|
APP_KEYS=
|
||||||
|
API_TOKEN_SALT=
|
||||||
|
ADMIN_JWT_SECRET=
|
||||||
|
TRANSFER_TOKEN_SALT=
|
||||||
|
JWT_SECRET=
|
||||||
|
|
||||||
# Varnish Cache Size (optional)
|
# Varnish Cache Size (optional)
|
||||||
VARNISH_CACHE_SIZE=256m
|
VARNISH_CACHE_SIZE=256m
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -29,6 +29,35 @@ npm run export
|
|||||||
|
|
||||||
# Or run development server
|
# Or run development server
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
### 🏗️ CMS (Strapi)
|
||||||
|
The CMS runs in Docker. Use the following npm scripts for local development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Strapi and its database
|
||||||
|
npm run cms:dev
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
npm run cms:logs
|
||||||
|
|
||||||
|
# Stop the CMS
|
||||||
|
npm run cms:stop
|
||||||
|
```
|
||||||
|
|
||||||
|
Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`.
|
||||||
|
|
||||||
|
### 🔄 Data & Migration
|
||||||
|
To sync data or migrate existing content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export local data
|
||||||
|
npm run cms:export -- my-data.tar.gz
|
||||||
|
|
||||||
|
# Import data
|
||||||
|
npm run cms:import -- my-data.tar.gz
|
||||||
|
|
||||||
|
# Migrate existing MDX data to Strapi
|
||||||
|
npm run cms:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
@@ -73,7 +102,8 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
|
|||||||
- **Framework**: Next.js 14 (App Router)
|
- **Framework**: Next.js 14 (App Router)
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
- **Styling**: SCSS
|
- **Styling**: SCSS
|
||||||
- **Data**: Static JSON (WordPress export)
|
- **CMS**: Strapi (Source of Truth)
|
||||||
|
- **Data**: Static JSON (WordPress export) & Strapi API
|
||||||
- **Email**: Resend
|
- **Email**: Resend
|
||||||
- **Analytics**: Vercel (consent-based)
|
- **Analytics**: Vercel (consent-based)
|
||||||
- **CAPTCHA**: Cloudflare Turnstile
|
- **CAPTCHA**: Cloudflare Turnstile
|
||||||
|
|||||||
16
cms/Dockerfile
Normal file
16
cms/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Installing libvips-dev for sharp Compatibility
|
||||||
|
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git > /dev/null 2>&1
|
||||||
|
|
||||||
|
WORKDIR /opt/
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install -g node-gyp
|
||||||
|
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install
|
||||||
|
ENV PATH /opt/node_modules/.bin:$PATH
|
||||||
|
|
||||||
|
WORKDIR /opt/app
|
||||||
|
COPY . .
|
||||||
|
RUN NODE_ENV=production npm run build
|
||||||
|
EXPOSE 1337
|
||||||
|
CMD ["npm", "run", "develop"]
|
||||||
13
cms/config/admin.ts
Normal file
13
cms/config/admin.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default ({ env }) => ({
|
||||||
|
auth: {
|
||||||
|
secret: env('ADMIN_JWT_SECRET'),
|
||||||
|
},
|
||||||
|
apiToken: {
|
||||||
|
salt: env('API_TOKEN_SALT'),
|
||||||
|
},
|
||||||
|
transfer: {
|
||||||
|
token: {
|
||||||
|
salt: env('TRANSFER_TOKEN_SALT'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
7
cms/config/api.ts
Normal file
7
cms/config/api.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
rest: {
|
||||||
|
defaultLimit: 25,
|
||||||
|
maxLimit: 100,
|
||||||
|
withCount: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
14
cms/config/database.ts
Normal file
14
cms/config/database.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default ({ env }) => ({
|
||||||
|
connection: {
|
||||||
|
client: 'postgres',
|
||||||
|
connection: {
|
||||||
|
host: env('DATABASE_HOST', '127.0.0.1'),
|
||||||
|
port: env.int('DATABASE_PORT', 5432),
|
||||||
|
database: env('DATABASE_NAME', 'strapi'),
|
||||||
|
user: env('DATABASE_USERNAME', 'strapi'),
|
||||||
|
password: env('DATABASE_PASSWORD', 'strapi'),
|
||||||
|
ssl: env.bool('DATABASE_SSL', false),
|
||||||
|
},
|
||||||
|
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
|
||||||
|
},
|
||||||
|
});
|
||||||
10
cms/config/server.ts
Normal file
10
cms/config/server.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default ({ env }) => ({
|
||||||
|
host: env('HOST', '0.0.0.0'),
|
||||||
|
port: env.int('PORT', 1337),
|
||||||
|
app: {
|
||||||
|
keys: env.array('APP_KEYS'),
|
||||||
|
},
|
||||||
|
webhooks: {
|
||||||
|
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
|
||||||
|
},
|
||||||
|
});
|
||||||
39
cms/package.json
Normal file
39
cms/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "klz-cms",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Strapi CMS for KLZ Cables",
|
||||||
|
"scripts": {
|
||||||
|
"develop": "strapi develop",
|
||||||
|
"start": "strapi start",
|
||||||
|
"build": "strapi build",
|
||||||
|
"strapi": "strapi"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strapi/strapi": "4.25.11",
|
||||||
|
"@strapi/plugin-users-permissions": "4.25.11",
|
||||||
|
"@strapi/plugin-i18n": "4.25.11",
|
||||||
|
"pg": "8.11.3",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"styled-components": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "A Strapi developer"
|
||||||
|
},
|
||||||
|
"strapi": {
|
||||||
|
"uuid": "klz-cms"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0 <=20.x.x",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "applications",
|
||||||
|
"info": {
|
||||||
|
"singularName": "application",
|
||||||
|
"pluralName": "applications",
|
||||||
|
"displayName": "Application"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"job": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToOne",
|
||||||
|
"target": "api::job.job"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "email",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"resume": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"allowedTypes": [
|
||||||
|
"files"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cms/src/api/application/controllers/application.ts
Normal file
2
cms/src/api/application/controllers/application.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::application.application');
|
||||||
2
cms/src/api/application/routes/application.ts
Normal file
2
cms/src/api/application/routes/application.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::application.application');
|
||||||
2
cms/src/api/application/services/application.ts
Normal file
2
cms/src/api/application/services/application.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::application.application');
|
||||||
39
cms/src/api/category/content-types/category/schema.json
Normal file
39
cms/src/api/category/content-types/category/schema.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "categories",
|
||||||
|
"info": {
|
||||||
|
"singularName": "category",
|
||||||
|
"pluralName": "categories",
|
||||||
|
"displayName": "Category"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "uid",
|
||||||
|
"targetField": "name",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"products": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToMany",
|
||||||
|
"target": "api::product.product",
|
||||||
|
"mappedBy": "categories"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cms/src/api/category/controllers/category.ts
Normal file
2
cms/src/api/category/controllers/category.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::category.category');
|
||||||
2
cms/src/api/category/routes/category.ts
Normal file
2
cms/src/api/category/routes/category.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::category.category');
|
||||||
2
cms/src/api/category/services/category.ts
Normal file
2
cms/src/api/category/services/category.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::category.category');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "contact_messages",
|
||||||
|
"info": {
|
||||||
|
"singularName": "contact-message",
|
||||||
|
"pluralName": "contact-messages",
|
||||||
|
"displayName": "Contact Message"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "email",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::contact-message.contact-message');
|
||||||
2
cms/src/api/contact-message/routes/contact-message.ts
Normal file
2
cms/src/api/contact-message/routes/contact-message.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::contact-message.contact-message');
|
||||||
2
cms/src/api/contact-message/services/contact-message.ts
Normal file
2
cms/src/api/contact-message/services/contact-message.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::contact-message.contact-message');
|
||||||
44
cms/src/api/job/content-types/job/schema.json
Normal file
44
cms/src/api/job/content-types/job/schema.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "jobs",
|
||||||
|
"info": {
|
||||||
|
"singularName": "job",
|
||||||
|
"pluralName": "jobs",
|
||||||
|
"displayName": "Job"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "richtext",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cms/src/api/job/controllers/job.ts
Normal file
2
cms/src/api/job/controllers/job.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::job.job');
|
||||||
2
cms/src/api/job/routes/job.ts
Normal file
2
cms/src/api/job/routes/job.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::job.job');
|
||||||
2
cms/src/api/job/services/job.ts
Normal file
2
cms/src/api/job/services/job.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::job.job');
|
||||||
80
cms/src/api/product/content-types/product/schema.json
Normal file
80
cms/src/api/product/content-types/product/schema.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "products",
|
||||||
|
"info": {
|
||||||
|
"singularName": "product",
|
||||||
|
"pluralName": "products",
|
||||||
|
"displayName": "Product",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sku": {
|
||||||
|
"type": "uid",
|
||||||
|
"targetField": "title",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "text",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application": {
|
||||||
|
"type": "text",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "richtext",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": true,
|
||||||
|
"required": false,
|
||||||
|
"allowedTypes": [
|
||||||
|
"images"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToMany",
|
||||||
|
"target": "api::category.category",
|
||||||
|
"inversedBy": "products"
|
||||||
|
},
|
||||||
|
"technicalData": {
|
||||||
|
"type": "json",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cms/src/api/product/controllers/product.ts
Normal file
2
cms/src/api/product/controllers/product.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::product.product');
|
||||||
2
cms/src/api/product/routes/product.ts
Normal file
2
cms/src/api/product/routes/product.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::product.product');
|
||||||
2
cms/src/api/product/services/product.ts
Normal file
2
cms/src/api/product/services/product.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::product.product');
|
||||||
42
cms/src/api/setting/content-types/setting/schema.json
Normal file
42
cms/src/api/setting/content-types/setting/schema.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"kind": "singleType",
|
||||||
|
"collectionName": "settings",
|
||||||
|
"info": {
|
||||||
|
"singularName": "setting",
|
||||||
|
"pluralName": "settings",
|
||||||
|
"displayName": "Setting"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"siteName": {
|
||||||
|
"type": "string",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"siteDescription": {
|
||||||
|
"type": "text",
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": false,
|
||||||
|
"allowedTypes": [
|
||||||
|
"images"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cms/src/api/setting/controllers/setting.ts
Normal file
2
cms/src/api/setting/controllers/setting.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreController('api::setting.setting');
|
||||||
2
cms/src/api/setting/routes/setting.ts
Normal file
2
cms/src/api/setting/routes/setting.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreRouter('api::setting.setting');
|
||||||
2
cms/src/api/setting/services/setting.ts
Normal file
2
cms/src/api/setting/services/setting.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
export default factories.createCoreService('api::setting.setting');
|
||||||
18
cms/src/index.ts
Normal file
18
cms/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* An asynchronous register function that runs before
|
||||||
|
* your application is initialized.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to extend code.
|
||||||
|
*/
|
||||||
|
register(/*{ strapi }*/) {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An asynchronous bootstrap function that runs before
|
||||||
|
* your application gets started.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to set up your data model,
|
||||||
|
* run jobs, or perform some special logic.
|
||||||
|
*/
|
||||||
|
bootstrap(/*{ strapi }*/) {},
|
||||||
|
};
|
||||||
22
cms/tsconfig.json
Normal file
22
cms/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "@strapi/typescript-utils/tsconfigs/server",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"config/**/*.ts",
|
||||||
|
"src/**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/",
|
||||||
|
"build/",
|
||||||
|
"dist/",
|
||||||
|
".cache/",
|
||||||
|
".tmp/",
|
||||||
|
"src/admin/",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"src/plugins/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -28,6 +28,54 @@ services:
|
|||||||
# Middlewares
|
# Middlewares
|
||||||
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
|
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
|
||||||
|
|
||||||
|
cms:
|
||||||
|
build:
|
||||||
|
context: ./cms
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- infra
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DATABASE_CLIENT: postgres
|
||||||
|
DATABASE_HOST: cms-db
|
||||||
|
DATABASE_PORT: 5432
|
||||||
|
DATABASE_NAME: ${STRAPI_DATABASE_NAME:-strapi}
|
||||||
|
DATABASE_USERNAME: ${STRAPI_DATABASE_USERNAME:-strapi}
|
||||||
|
DATABASE_PASSWORD: ${STRAPI_DATABASE_PASSWORD:-strapi}
|
||||||
|
NODE_ENV: ${NODE_ENV:-development}
|
||||||
|
STRAPI_URL: ${STRAPI_URL:-https://cms.klz-cables.com}
|
||||||
|
volumes:
|
||||||
|
- ./cms/config:/opt/app/config
|
||||||
|
- ./cms/src:/opt/app/src
|
||||||
|
- ./cms/package.json:/opt/app/package.json
|
||||||
|
- ./cms/public/uploads:/opt/app/public/uploads
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.klz-cms.rule=Host(`cms.klz-cables.com`) || Host(`cms-staging.klz-cables.com`)"
|
||||||
|
- "traefik.http.routers.klz-cms.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.klz-cms.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.klz-cms.tls=true"
|
||||||
|
- "traefik.http.services.klz-cms.loadbalancer.server.port=1337"
|
||||||
|
|
||||||
|
cms-db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- infra
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${STRAPI_DATABASE_NAME:-strapi}
|
||||||
|
POSTGRES_USER: ${STRAPI_DATABASE_USERNAME:-strapi}
|
||||||
|
POSTGRES_PASSWORD: ${STRAPI_DATABASE_PASSWORD:-strapi}
|
||||||
|
volumes:
|
||||||
|
- cms-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
infra:
|
infra:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cms-db-data:
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ These are loaded from the `.env` file at runtime and are only available on the s
|
|||||||
| `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails |
|
| `MAIL_RECIPIENTS` | ❌ No | Comma-separated list of recipient emails |
|
||||||
| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) |
|
| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) |
|
||||||
| `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) |
|
| `REDIS_KEY_PREFIX` | ❌ No | Redis key prefix (default: `klz:`) |
|
||||||
|
| `STRAPI_DATABASE_NAME` | ✅ Yes | Strapi database name |
|
||||||
|
| `STRAPI_DATABASE_USERNAME` | ✅ Yes | Strapi database username |
|
||||||
|
| `STRAPI_DATABASE_PASSWORD` | ✅ Yes | Strapi database password |
|
||||||
|
| `STRAPI_URL` | ✅ Yes | URL of the Strapi CMS |
|
||||||
|
| `APP_KEYS` | ✅ Yes | Strapi application keys (comma-separated) |
|
||||||
|
| `API_TOKEN_SALT` | ✅ Yes | Strapi API token salt |
|
||||||
|
| `ADMIN_JWT_SECRET` | ✅ Yes | Strapi admin JWT secret |
|
||||||
|
| `TRANSFER_TOKEN_SALT` | ✅ Yes | Strapi transfer token salt |
|
||||||
|
| `JWT_SECRET` | ✅ Yes | Strapi JWT secret |
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
|
|||||||
85
lib/strapi.ts
Normal file
85
lib/strapi.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
|
||||||
|
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
|
||||||
|
|
||||||
|
const strapi = axios.create({
|
||||||
|
baseURL: `${STRAPI_URL}/api`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${STRAPI_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface StrapiResponse<T> {
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
attributes: T;
|
||||||
|
}[];
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
pageCount: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
title: string;
|
||||||
|
sku: string;
|
||||||
|
description: string;
|
||||||
|
application: string;
|
||||||
|
content: string;
|
||||||
|
technicalData: any;
|
||||||
|
locale: string;
|
||||||
|
images?: {
|
||||||
|
data: {
|
||||||
|
attributes: {
|
||||||
|
url: string;
|
||||||
|
alternativeText: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProducts(locale: string = 'de') {
|
||||||
|
try {
|
||||||
|
const response = await strapi.get<StrapiResponse<Product>>('/products', {
|
||||||
|
params: {
|
||||||
|
locale,
|
||||||
|
populate: '*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data.data.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
...item.attributes,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching products from Strapi:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProductBySku(sku: string, locale: string = 'de') {
|
||||||
|
try {
|
||||||
|
const response = await strapi.get<StrapiResponse<Product>>('/products', {
|
||||||
|
params: {
|
||||||
|
filters: { sku: { $eq: sku } },
|
||||||
|
locale,
|
||||||
|
populate: '*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.data.data.length === 0) return null;
|
||||||
|
const item = response.data.data[0];
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
...item.attributes,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching product ${sku} from Strapi:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default strapi;
|
||||||
@@ -65,7 +65,13 @@
|
|||||||
"test": "vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"test:og": "vitest run tests/og-image.test.ts",
|
"test:og": "vitest run tests/og-image.test.ts",
|
||||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
||||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts"
|
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
||||||
|
"cms:dev": "docker-compose up -d cms cms-db",
|
||||||
|
"cms:stop": "docker-compose stop cms cms-db",
|
||||||
|
"cms:logs": "docker-compose logs -f cms",
|
||||||
|
"cms:export": "./scripts/strapi-sync.sh export",
|
||||||
|
"cms:import": "./scripts/strapi-sync.sh import",
|
||||||
|
"cms:migrate": "tsx ./scripts/migrate-to-strapi.ts"
|
||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
64
scripts/migrate-to-strapi.ts
Normal file
64
scripts/migrate-to-strapi.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
|
||||||
|
const STRAPI_TOKEN = process.env.STRAPI_ADMIN_TOKEN; // You'll need to generate this
|
||||||
|
|
||||||
|
async function migrateProducts() {
|
||||||
|
const productsDir = path.join(process.cwd(), 'data/products');
|
||||||
|
const locales = ['de', 'en'];
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
const localeDir = path.join(productsDir, locale);
|
||||||
|
if (!fs.existsSync(localeDir)) continue;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(localeDir).filter(f => f.endsWith('.mdx'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(localeDir, file);
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
|
console.log(`Migrating ${data.title} (${locale})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Check if product exists (by SKU)
|
||||||
|
const existing = await axios.get(`${STRAPI_URL}/api/products?filters[sku][$eq]=${data.sku}&locale=${locale}`, {
|
||||||
|
headers: { Authorization: `Bearer ${STRAPI_TOKEN}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
const productData = {
|
||||||
|
title: data.title,
|
||||||
|
sku: data.sku,
|
||||||
|
description: data.description,
|
||||||
|
application: data.application,
|
||||||
|
content: content,
|
||||||
|
technicalData: data.technicalData || {}, // This might need adjustment based on how it's stored in MDX
|
||||||
|
locale: locale,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existing.data.data.length > 0) {
|
||||||
|
// Update
|
||||||
|
const id = existing.data.data[0].id;
|
||||||
|
await axios.put(`${STRAPI_URL}/api/products/${id}`, { data: productData }, {
|
||||||
|
headers: { Authorization: `Bearer ${STRAPI_TOKEN}` }
|
||||||
|
});
|
||||||
|
console.log(`Updated ${data.title}`);
|
||||||
|
} else {
|
||||||
|
// Create
|
||||||
|
await axios.post(`${STRAPI_URL}/api/products`, { data: productData }, {
|
||||||
|
headers: { Authorization: `Bearer ${STRAPI_TOKEN}` }
|
||||||
|
});
|
||||||
|
console.log(`Created ${data.title}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error migrating ${data.title}:`, error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This script requires a running Strapi instance and an admin token.
|
||||||
|
// migrateProducts();
|
||||||
33
scripts/strapi-sync.sh
Executable file
33
scripts/strapi-sync.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to sync Strapi data between environments
|
||||||
|
# Usage: ./scripts/strapi-sync.sh [export|import] [filename]
|
||||||
|
|
||||||
|
COMMAND=$1
|
||||||
|
FILENAME=$2
|
||||||
|
|
||||||
|
if [ -z "$COMMAND" ]; then
|
||||||
|
echo "Usage: $0 [export|import] [filename]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$COMMAND" == "export" ]; then
|
||||||
|
if [ -z "$FILENAME" ]; then
|
||||||
|
FILENAME="strapi-export-$(date +%Y%m%d%H%M%S).tar.gz"
|
||||||
|
fi
|
||||||
|
echo "Exporting Strapi data to $FILENAME..."
|
||||||
|
docker-compose exec cms npm run strapi export -- --no-encrypt -f "$FILENAME"
|
||||||
|
docker cp $(docker-compose ps -q cms):/opt/app/$FILENAME .
|
||||||
|
echo "Export complete: $FILENAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$COMMAND" == "import" ]; then
|
||||||
|
if [ -z "$FILENAME" ]; then
|
||||||
|
echo "Please specify a filename to import"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Importing Strapi data from $FILENAME..."
|
||||||
|
docker cp $FILENAME $(docker-compose ps -q cms):/opt/app/$FILENAME
|
||||||
|
docker-compose exec cms npm run strapi import -- -f "$FILENAME" --force
|
||||||
|
echo "Import complete"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user