From be9f9cf483f81edb4119058a19893b4681521544 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 29 Jan 2026 19:47:55 +0100 Subject: [PATCH] strapi --- .env | 10 +++ .env.example | 13 +++ .env.production | 9 ++ README.md | 32 ++++++- cms/Dockerfile | 16 ++++ cms/config/admin.ts | 13 +++ cms/config/api.ts | 7 ++ cms/config/database.ts | 14 +++ cms/config/server.ts | 10 +++ cms/package.json | 39 +++++++++ .../content-types/application/schema.json | 38 +++++++++ .../application/controllers/application.ts | 2 + cms/src/api/application/routes/application.ts | 2 + .../api/application/services/application.ts | 2 + .../content-types/category/schema.json | 39 +++++++++ cms/src/api/category/controllers/category.ts | 2 + cms/src/api/category/routes/category.ts | 2 + cms/src/api/category/services/category.ts | 2 + .../content-types/contact-message/schema.json | 29 +++++++ .../controllers/contact-message.ts | 2 + .../contact-message/routes/contact-message.ts | 2 + .../services/contact-message.ts | 2 + cms/src/api/job/content-types/job/schema.json | 44 ++++++++++ cms/src/api/job/controllers/job.ts | 2 + cms/src/api/job/routes/job.ts | 2 + cms/src/api/job/services/job.ts | 2 + .../product/content-types/product/schema.json | 80 +++++++++++++++++ cms/src/api/product/controllers/product.ts | 2 + cms/src/api/product/routes/product.ts | 2 + cms/src/api/product/services/product.ts | 2 + .../setting/content-types/setting/schema.json | 42 +++++++++ cms/src/api/setting/controllers/setting.ts | 2 + cms/src/api/setting/routes/setting.ts | 2 + cms/src/api/setting/services/setting.ts | 2 + cms/src/index.ts | 18 ++++ cms/tsconfig.json | 22 +++++ docker-compose.yml | 48 +++++++++++ docs/DEPLOYMENT.md | 9 ++ lib/strapi.ts | 85 +++++++++++++++++++ package.json | 8 +- scripts/migrate-to-strapi.ts | 64 ++++++++++++++ scripts/strapi-sync.sh | 33 +++++++ 42 files changed, 756 insertions(+), 2 deletions(-) create mode 100644 cms/Dockerfile create mode 100644 cms/config/admin.ts create mode 100644 cms/config/api.ts create mode 100644 cms/config/database.ts create mode 100644 cms/config/server.ts create mode 100644 cms/package.json create mode 100644 cms/src/api/application/content-types/application/schema.json create mode 100644 cms/src/api/application/controllers/application.ts create mode 100644 cms/src/api/application/routes/application.ts create mode 100644 cms/src/api/application/services/application.ts create mode 100644 cms/src/api/category/content-types/category/schema.json create mode 100644 cms/src/api/category/controllers/category.ts create mode 100644 cms/src/api/category/routes/category.ts create mode 100644 cms/src/api/category/services/category.ts create mode 100644 cms/src/api/contact-message/content-types/contact-message/schema.json create mode 100644 cms/src/api/contact-message/controllers/contact-message.ts create mode 100644 cms/src/api/contact-message/routes/contact-message.ts create mode 100644 cms/src/api/contact-message/services/contact-message.ts create mode 100644 cms/src/api/job/content-types/job/schema.json create mode 100644 cms/src/api/job/controllers/job.ts create mode 100644 cms/src/api/job/routes/job.ts create mode 100644 cms/src/api/job/services/job.ts create mode 100644 cms/src/api/product/content-types/product/schema.json create mode 100644 cms/src/api/product/controllers/product.ts create mode 100644 cms/src/api/product/routes/product.ts create mode 100644 cms/src/api/product/services/product.ts create mode 100644 cms/src/api/setting/content-types/setting/schema.json create mode 100644 cms/src/api/setting/controllers/setting.ts create mode 100644 cms/src/api/setting/routes/setting.ts create mode 100644 cms/src/api/setting/services/setting.ts create mode 100644 cms/src/index.ts create mode 100644 cms/tsconfig.json create mode 100644 lib/strapi.ts create mode 100644 scripts/migrate-to-strapi.ts create mode 100755 scripts/strapi-sync.sh diff --git a/.env b/.env index 4037fea9..3052178f 100644 --- a/.env +++ b/.env @@ -19,3 +19,13 @@ MAIL_USERNAME=postmaster@mg.mintel.me MAIL_PASSWORD=4592fcb94599ee1a45b4ac2386fd0a64-102c75d8-ca2870e6 MAIL_FROM="KLZ Cables " 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 diff --git a/.env.example b/.env.example index b281a558..1b09ed08 100644 --- a/.env.example +++ b/.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 # ============================================================================ diff --git a/.env.production b/.env.production index 0d0b5cf0..0d41ad03 100644 --- a/.env.production +++ b/.env.production @@ -26,6 +26,15 @@ MAIL_PASSWORD= MAIL_FROM=KLZ Cables 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 diff --git a/README.md b/README.md index 7dda3a0f..af1ed032 100644 --- a/README.md +++ b/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 diff --git a/cms/Dockerfile b/cms/Dockerfile new file mode 100644 index 00000000..dc2a5eeb --- /dev/null +++ b/cms/Dockerfile @@ -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"] diff --git a/cms/config/admin.ts b/cms/config/admin.ts new file mode 100644 index 00000000..0362175d --- /dev/null +++ b/cms/config/admin.ts @@ -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'), + }, + }, +}); diff --git a/cms/config/api.ts b/cms/config/api.ts new file mode 100644 index 00000000..37f7c14a --- /dev/null +++ b/cms/config/api.ts @@ -0,0 +1,7 @@ +export default { + rest: { + defaultLimit: 25, + maxLimit: 100, + withCount: true, + }, +}; diff --git a/cms/config/database.ts b/cms/config/database.ts new file mode 100644 index 00000000..01667956 --- /dev/null +++ b/cms/config/database.ts @@ -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) }, + }, +}); diff --git a/cms/config/server.ts b/cms/config/server.ts new file mode 100644 index 00000000..a54a2414 --- /dev/null +++ b/cms/config/server.ts @@ -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), + }, +}); diff --git a/cms/package.json b/cms/package.json new file mode 100644 index 00000000..df4f23a5 --- /dev/null +++ b/cms/package.json @@ -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" +} diff --git a/cms/src/api/application/content-types/application/schema.json b/cms/src/api/application/content-types/application/schema.json new file mode 100644 index 00000000..61cbc60b --- /dev/null +++ b/cms/src/api/application/content-types/application/schema.json @@ -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" + } + } +} diff --git a/cms/src/api/application/controllers/application.ts b/cms/src/api/application/controllers/application.ts new file mode 100644 index 00000000..95301770 --- /dev/null +++ b/cms/src/api/application/controllers/application.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::application.application'); diff --git a/cms/src/api/application/routes/application.ts b/cms/src/api/application/routes/application.ts new file mode 100644 index 00000000..6dacd94c --- /dev/null +++ b/cms/src/api/application/routes/application.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::application.application'); diff --git a/cms/src/api/application/services/application.ts b/cms/src/api/application/services/application.ts new file mode 100644 index 00000000..42629e3e --- /dev/null +++ b/cms/src/api/application/services/application.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::application.application'); diff --git a/cms/src/api/category/content-types/category/schema.json b/cms/src/api/category/content-types/category/schema.json new file mode 100644 index 00000000..237bd5d9 --- /dev/null +++ b/cms/src/api/category/content-types/category/schema.json @@ -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" + } + } +} diff --git a/cms/src/api/category/controllers/category.ts b/cms/src/api/category/controllers/category.ts new file mode 100644 index 00000000..9ef86005 --- /dev/null +++ b/cms/src/api/category/controllers/category.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::category.category'); diff --git a/cms/src/api/category/routes/category.ts b/cms/src/api/category/routes/category.ts new file mode 100644 index 00000000..272b9bba --- /dev/null +++ b/cms/src/api/category/routes/category.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::category.category'); diff --git a/cms/src/api/category/services/category.ts b/cms/src/api/category/services/category.ts new file mode 100644 index 00000000..583f34f7 --- /dev/null +++ b/cms/src/api/category/services/category.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::category.category'); diff --git a/cms/src/api/contact-message/content-types/contact-message/schema.json b/cms/src/api/contact-message/content-types/contact-message/schema.json new file mode 100644 index 00000000..755c914e --- /dev/null +++ b/cms/src/api/contact-message/content-types/contact-message/schema.json @@ -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 + } + } +} diff --git a/cms/src/api/contact-message/controllers/contact-message.ts b/cms/src/api/contact-message/controllers/contact-message.ts new file mode 100644 index 00000000..518b3b8d --- /dev/null +++ b/cms/src/api/contact-message/controllers/contact-message.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::contact-message.contact-message'); diff --git a/cms/src/api/contact-message/routes/contact-message.ts b/cms/src/api/contact-message/routes/contact-message.ts new file mode 100644 index 00000000..8a0a5a2a --- /dev/null +++ b/cms/src/api/contact-message/routes/contact-message.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::contact-message.contact-message'); diff --git a/cms/src/api/contact-message/services/contact-message.ts b/cms/src/api/contact-message/services/contact-message.ts new file mode 100644 index 00000000..be654396 --- /dev/null +++ b/cms/src/api/contact-message/services/contact-message.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::contact-message.contact-message'); diff --git a/cms/src/api/job/content-types/job/schema.json b/cms/src/api/job/content-types/job/schema.json new file mode 100644 index 00000000..9faf0d59 --- /dev/null +++ b/cms/src/api/job/content-types/job/schema.json @@ -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 + } + } + } + } +} diff --git a/cms/src/api/job/controllers/job.ts b/cms/src/api/job/controllers/job.ts new file mode 100644 index 00000000..d571b5cf --- /dev/null +++ b/cms/src/api/job/controllers/job.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::job.job'); diff --git a/cms/src/api/job/routes/job.ts b/cms/src/api/job/routes/job.ts new file mode 100644 index 00000000..2ff64a3f --- /dev/null +++ b/cms/src/api/job/routes/job.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::job.job'); diff --git a/cms/src/api/job/services/job.ts b/cms/src/api/job/services/job.ts new file mode 100644 index 00000000..c840c6a5 --- /dev/null +++ b/cms/src/api/job/services/job.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::job.job'); diff --git a/cms/src/api/product/content-types/product/schema.json b/cms/src/api/product/content-types/product/schema.json new file mode 100644 index 00000000..4eda0575 --- /dev/null +++ b/cms/src/api/product/content-types/product/schema.json @@ -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 + } + } + } + } +} diff --git a/cms/src/api/product/controllers/product.ts b/cms/src/api/product/controllers/product.ts new file mode 100644 index 00000000..8283c75a --- /dev/null +++ b/cms/src/api/product/controllers/product.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::product.product'); diff --git a/cms/src/api/product/routes/product.ts b/cms/src/api/product/routes/product.ts new file mode 100644 index 00000000..8b354a47 --- /dev/null +++ b/cms/src/api/product/routes/product.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::product.product'); diff --git a/cms/src/api/product/services/product.ts b/cms/src/api/product/services/product.ts new file mode 100644 index 00000000..a4c24197 --- /dev/null +++ b/cms/src/api/product/services/product.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::product.product'); diff --git a/cms/src/api/setting/content-types/setting/schema.json b/cms/src/api/setting/content-types/setting/schema.json new file mode 100644 index 00000000..b6ab3a23 --- /dev/null +++ b/cms/src/api/setting/content-types/setting/schema.json @@ -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" + ] + } + } +} diff --git a/cms/src/api/setting/controllers/setting.ts b/cms/src/api/setting/controllers/setting.ts new file mode 100644 index 00000000..f42d4d15 --- /dev/null +++ b/cms/src/api/setting/controllers/setting.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreController('api::setting.setting'); diff --git a/cms/src/api/setting/routes/setting.ts b/cms/src/api/setting/routes/setting.ts new file mode 100644 index 00000000..d49b9327 --- /dev/null +++ b/cms/src/api/setting/routes/setting.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreRouter('api::setting.setting'); diff --git a/cms/src/api/setting/services/setting.ts b/cms/src/api/setting/services/setting.ts new file mode 100644 index 00000000..e4a71d83 --- /dev/null +++ b/cms/src/api/setting/services/setting.ts @@ -0,0 +1,2 @@ +import { factories } from '@strapi/strapi'; +export default factories.createCoreService('api::setting.setting'); diff --git a/cms/src/index.ts b/cms/src/index.ts new file mode 100644 index 00000000..9fca261c --- /dev/null +++ b/cms/src/index.ts @@ -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 }*/) {}, +}; diff --git a/cms/tsconfig.json b/cms/tsconfig.json new file mode 100644 index 00000000..e2ef75ca --- /dev/null +++ b/cms/tsconfig.json @@ -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/**" + ] +} diff --git a/docker-compose.yml b/docker-compose.yml index d8b0f825..7e3b7579 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 06269c1b..e5150d10 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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 diff --git a/lib/strapi.ts b/lib/strapi.ts new file mode 100644 index 00000000..d6c87e00 --- /dev/null +++ b/lib/strapi.ts @@ -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 { + 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>('/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>('/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; diff --git a/package.json b/package.json index 24449db4..2aa383fc 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/scripts/migrate-to-strapi.ts b/scripts/migrate-to-strapi.ts new file mode 100644 index 00000000..101cabdd --- /dev/null +++ b/scripts/migrate-to-strapi.ts @@ -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(); diff --git a/scripts/strapi-sync.sh b/scripts/strapi-sync.sh new file mode 100755 index 00000000..17385c10 --- /dev/null +++ b/scripts/strapi-sync.sh @@ -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