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_FROM="KLZ Cables <postmaster@mg.mintel.me>"
|
||||
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
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
# ============================================================================
|
||||
|
||||
@@ -26,6 +26,15 @@ MAIL_PASSWORD=
|
||||
MAIL_FROM=KLZ Cables <noreply@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=256m
|
||||
|
||||
32
README.md
32
README.md
@@ -29,6 +29,35 @@ npm run export
|
||||
|
||||
# Or run development server
|
||||
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
|
||||
@@ -73,7 +102,8 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
|
||||
- **Framework**: Next.js 14 (App Router)
|
||||
- **Language**: TypeScript
|
||||
- **Styling**: SCSS
|
||||
- **Data**: Static JSON (WordPress export)
|
||||
- **CMS**: Strapi (Source of Truth)
|
||||
- **Data**: Static JSON (WordPress export) & Strapi API
|
||||
- **Email**: Resend
|
||||
- **Analytics**: Vercel (consent-based)
|
||||
- **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
|
||||
- "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:
|
||||
infra:
|
||||
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 |
|
||||
| `REDIS_URL` | ❌ No | Redis connection URL (e.g., `redis://redis:6379/2`) |
|
||||
| `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
|
||||
|
||||
|
||||
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:og": "vitest run tests/og-image.test.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"
|
||||
}
|
||||
|
||||
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