diff --git a/.gitignore b/.gitignore index 96ff69f..d40bbcb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist/ .next/ out/ .contentlayer/ +.pnpm-store # generated types .astro/ diff --git a/Dockerfile b/Dockerfile index c4ef7e5..a326e9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,12 @@ WORKDIR /app # Arguments for build-time configuration ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_TARGET -ARG DIRECTUS_URL ARG UMAMI_API_ENDPOINT ARG NPM_TOKEN # Environment variables for Next.js build ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET -ENV DIRECTUS_URL=$DIRECTUS_URL ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT ENV SKIP_RUNTIME_ENV_VALIDATION=true ENV CI=true diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..4936254 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,20 @@ +FROM node:20-alpine + +# Install essential build tools if needed (e.g., for node-gyp) +RUN apk add --no-cache libc6-compat python3 make g++ + +WORKDIR /app + +# Enable corepack for pnpm +RUN corepack enable + +# Pre-set the pnpm store directory to a location we can volume-mount +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +# Set up pnpm store configuration +RUN pnpm config set store-dir /pnpm/store + +# Note: Dependency installation happens at runtime to support linked packages +# and named volumes, but the base image is now optimized for the stack. +EXPOSE 3000 diff --git a/Posts.ts.tmp b/Posts.ts.tmp new file mode 100644 index 0000000..f730150 --- /dev/null +++ b/Posts.ts.tmp @@ -0,0 +1,95 @@ +import type { CollectionConfig } from "payload"; +import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical"; +import { allBlocks } from "../blocks/allBlocks"; + +export const Posts: CollectionConfig = { + slug: "posts", + admin: { + useAsTitle: "title", + }, + access: { + read: () => true, // Publicly readable API + }, + fields: [ + { + name: "aiOptimizer", + type: "ui", + admin: { + position: "sidebar", + components: { + Field: "@/src/payload/components/OptimizeButton#OptimizeButton", + }, + }, + }, + { + name: "title", + type: "text", + required: true, + }, + { + name: "slug", + type: "text", + required: true, + unique: true, + admin: { + position: "sidebar", + }, + hooks: { + beforeValidate: [ + ({ value, data }) => { + if (value) return value; + if (data?.title) { + return data.title + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w-]+/g, ""); + } + return value; + }, + ], + }, + }, + { + name: "description", + type: "text", + required: true, + }, + { + name: "date", + type: "date", + required: true, + }, + { + name: "tags", + type: "array", + required: true, + fields: [ + { + name: "tag", + type: "text", + }, + ], + }, + { + name: "featuredImage", + type: "upload", + relationTo: "media", + admin: { + description: "The main hero image for the blog post.", + position: "sidebar", + }, + }, + { + name: "content", + type: "richText", + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + BlocksFeature({ + blocks: allBlocks, + }), + ], + }), + }, + ], +}; diff --git a/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-1024x572.png b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-1024x572.png new file mode 100644 index 0000000..2a57e52 Binary files /dev/null and b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-1024x572.png differ diff --git a/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-400x300.png b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-400x300.png new file mode 100644 index 0000000..c597ad3 Binary files /dev/null and b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-400x300.png differ diff --git a/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-768x1024.png b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-768x1024.png new file mode 100644 index 0000000..590c6f9 Binary files /dev/null and b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung-768x1024.png differ diff --git a/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.png b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.png new file mode 100644 index 0000000..b002313 Binary files /dev/null and b/apps/public/media/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.png differ diff --git a/apps/public/media/build-first-digital-architecture-1024x572.png b/apps/public/media/build-first-digital-architecture-1024x572.png new file mode 100644 index 0000000..5d31d6b Binary files /dev/null and b/apps/public/media/build-first-digital-architecture-1024x572.png differ diff --git a/apps/public/media/build-first-digital-architecture-400x300.png b/apps/public/media/build-first-digital-architecture-400x300.png new file mode 100644 index 0000000..e204c66 Binary files /dev/null and b/apps/public/media/build-first-digital-architecture-400x300.png differ diff --git a/apps/public/media/build-first-digital-architecture-768x1024.png b/apps/public/media/build-first-digital-architecture-768x1024.png new file mode 100644 index 0000000..a69be7e Binary files /dev/null and b/apps/public/media/build-first-digital-architecture-768x1024.png differ diff --git a/apps/public/media/build-first-digital-architecture.png b/apps/public/media/build-first-digital-architecture.png new file mode 100644 index 0000000..7548c6b Binary files /dev/null and b/apps/public/media/build-first-digital-architecture.png differ diff --git a/apps/public/media/builder-systems-threaten-independence-1024x572.png b/apps/public/media/builder-systems-threaten-independence-1024x572.png new file mode 100644 index 0000000..7b00825 Binary files /dev/null and b/apps/public/media/builder-systems-threaten-independence-1024x572.png differ diff --git a/apps/public/media/builder-systems-threaten-independence-400x300.png b/apps/public/media/builder-systems-threaten-independence-400x300.png new file mode 100644 index 0000000..9559af1 Binary files /dev/null and b/apps/public/media/builder-systems-threaten-independence-400x300.png differ diff --git a/apps/public/media/builder-systems-threaten-independence-768x1024.png b/apps/public/media/builder-systems-threaten-independence-768x1024.png new file mode 100644 index 0000000..3aa9ca0 Binary files /dev/null and b/apps/public/media/builder-systems-threaten-independence-768x1024.png differ diff --git a/apps/public/media/builder-systems-threaten-independence.png b/apps/public/media/builder-systems-threaten-independence.png new file mode 100644 index 0000000..2c16bab Binary files /dev/null and b/apps/public/media/builder-systems-threaten-independence.png differ diff --git a/apps/public/media/clean-code-for-business-value-1024x572.png b/apps/public/media/clean-code-for-business-value-1024x572.png new file mode 100644 index 0000000..c2bdd31 Binary files /dev/null and b/apps/public/media/clean-code-for-business-value-1024x572.png differ diff --git a/apps/public/media/clean-code-for-business-value-400x300.png b/apps/public/media/clean-code-for-business-value-400x300.png new file mode 100644 index 0000000..10d3409 Binary files /dev/null and b/apps/public/media/clean-code-for-business-value-400x300.png differ diff --git a/apps/public/media/clean-code-for-business-value-768x1024.png b/apps/public/media/clean-code-for-business-value-768x1024.png new file mode 100644 index 0000000..d815722 Binary files /dev/null and b/apps/public/media/clean-code-for-business-value-768x1024.png differ diff --git a/apps/public/media/clean-code-for-business-value.png b/apps/public/media/clean-code-for-business-value.png new file mode 100644 index 0000000..f42050b Binary files /dev/null and b/apps/public/media/clean-code-for-business-value.png differ diff --git a/apps/public/media/crm-synchronization-headless-1024x572.png b/apps/public/media/crm-synchronization-headless-1024x572.png new file mode 100644 index 0000000..f455e43 Binary files /dev/null and b/apps/public/media/crm-synchronization-headless-1024x572.png differ diff --git a/apps/public/media/crm-synchronization-headless-400x300.png b/apps/public/media/crm-synchronization-headless-400x300.png new file mode 100644 index 0000000..85501cc Binary files /dev/null and b/apps/public/media/crm-synchronization-headless-400x300.png differ diff --git a/apps/public/media/crm-synchronization-headless-768x1024.png b/apps/public/media/crm-synchronization-headless-768x1024.png new file mode 100644 index 0000000..d989d63 Binary files /dev/null and b/apps/public/media/crm-synchronization-headless-768x1024.png differ diff --git a/apps/public/media/crm-synchronization-headless.png b/apps/public/media/crm-synchronization-headless.png new file mode 100644 index 0000000..4ea8438 Binary files /dev/null and b/apps/public/media/crm-synchronization-headless.png differ diff --git a/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-1024x585.png b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-1024x585.png new file mode 100644 index 0000000..1c10f38 Binary files /dev/null and b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-1024x585.png differ diff --git a/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-400x300.png b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-400x300.png new file mode 100644 index 0000000..5344141 Binary files /dev/null and b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-400x300.png differ diff --git a/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-768x1024.png b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-768x1024.png new file mode 100644 index 0000000..51a49f6 Binary files /dev/null and b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist-768x1024.png differ diff --git a/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist.png b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist.png new file mode 100644 index 0000000..541745d Binary files /dev/null and b/apps/public/media/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist.png differ diff --git a/apps/public/media/digital-longevity-architecture-1024x572.png b/apps/public/media/digital-longevity-architecture-1024x572.png new file mode 100644 index 0000000..e4debc7 Binary files /dev/null and b/apps/public/media/digital-longevity-architecture-1024x572.png differ diff --git a/apps/public/media/digital-longevity-architecture-400x300.png b/apps/public/media/digital-longevity-architecture-400x300.png new file mode 100644 index 0000000..129be45 Binary files /dev/null and b/apps/public/media/digital-longevity-architecture-400x300.png differ diff --git a/apps/public/media/digital-longevity-architecture-768x1024.png b/apps/public/media/digital-longevity-architecture-768x1024.png new file mode 100644 index 0000000..dd0aa55 Binary files /dev/null and b/apps/public/media/digital-longevity-architecture-768x1024.png differ diff --git a/apps/public/media/digital-longevity-architecture.png b/apps/public/media/digital-longevity-architecture.png new file mode 100644 index 0000000..406cadb Binary files /dev/null and b/apps/public/media/digital-longevity-architecture.png differ diff --git a/apps/public/media/fixed-price-digital-projects-1024x572.png b/apps/public/media/fixed-price-digital-projects-1024x572.png new file mode 100644 index 0000000..625d4aa Binary files /dev/null and b/apps/public/media/fixed-price-digital-projects-1024x572.png differ diff --git a/apps/public/media/fixed-price-digital-projects-400x300.png b/apps/public/media/fixed-price-digital-projects-400x300.png new file mode 100644 index 0000000..5a4c7ef Binary files /dev/null and b/apps/public/media/fixed-price-digital-projects-400x300.png differ diff --git a/apps/public/media/fixed-price-digital-projects-768x1024.png b/apps/public/media/fixed-price-digital-projects-768x1024.png new file mode 100644 index 0000000..19c28d1 Binary files /dev/null and b/apps/public/media/fixed-price-digital-projects-768x1024.png differ diff --git a/apps/public/media/fixed-price-digital-projects.png b/apps/public/media/fixed-price-digital-projects.png new file mode 100644 index 0000000..3cdf599 Binary files /dev/null and b/apps/public/media/fixed-price-digital-projects.png differ diff --git a/apps/public/media/gdpr-conformity-system-approach-1024x572.png b/apps/public/media/gdpr-conformity-system-approach-1024x572.png new file mode 100644 index 0000000..2b12a19 Binary files /dev/null and b/apps/public/media/gdpr-conformity-system-approach-1024x572.png differ diff --git a/apps/public/media/gdpr-conformity-system-approach-400x300.png b/apps/public/media/gdpr-conformity-system-approach-400x300.png new file mode 100644 index 0000000..09c5251 Binary files /dev/null and b/apps/public/media/gdpr-conformity-system-approach-400x300.png differ diff --git a/apps/public/media/gdpr-conformity-system-approach-768x1024.png b/apps/public/media/gdpr-conformity-system-approach-768x1024.png new file mode 100644 index 0000000..03a40e7 Binary files /dev/null and b/apps/public/media/gdpr-conformity-system-approach-768x1024.png differ diff --git a/apps/public/media/gdpr-conformity-system-approach.png b/apps/public/media/gdpr-conformity-system-approach.png new file mode 100644 index 0000000..f348d07 Binary files /dev/null and b/apps/public/media/gdpr-conformity-system-approach.png differ diff --git a/apps/public/media/green-it-sustainable-web-1024x572.png b/apps/public/media/green-it-sustainable-web-1024x572.png new file mode 100644 index 0000000..1998e60 Binary files /dev/null and b/apps/public/media/green-it-sustainable-web-1024x572.png differ diff --git a/apps/public/media/green-it-sustainable-web-400x300.png b/apps/public/media/green-it-sustainable-web-400x300.png new file mode 100644 index 0000000..5d31c87 Binary files /dev/null and b/apps/public/media/green-it-sustainable-web-400x300.png differ diff --git a/apps/public/media/green-it-sustainable-web-768x1024.png b/apps/public/media/green-it-sustainable-web-768x1024.png new file mode 100644 index 0000000..1b2632c Binary files /dev/null and b/apps/public/media/green-it-sustainable-web-768x1024.png differ diff --git a/apps/public/media/green-it-sustainable-web.png b/apps/public/media/green-it-sustainable-web.png new file mode 100644 index 0000000..f807618 Binary files /dev/null and b/apps/public/media/green-it-sustainable-web.png differ diff --git a/apps/public/media/hidden-costs-of-wordpress-plugins-1024x572.png b/apps/public/media/hidden-costs-of-wordpress-plugins-1024x572.png new file mode 100644 index 0000000..2821183 Binary files /dev/null and b/apps/public/media/hidden-costs-of-wordpress-plugins-1024x572.png differ diff --git a/apps/public/media/hidden-costs-of-wordpress-plugins-400x300.png b/apps/public/media/hidden-costs-of-wordpress-plugins-400x300.png new file mode 100644 index 0000000..2a781cd Binary files /dev/null and b/apps/public/media/hidden-costs-of-wordpress-plugins-400x300.png differ diff --git a/apps/public/media/hidden-costs-of-wordpress-plugins-768x1024.png b/apps/public/media/hidden-costs-of-wordpress-plugins-768x1024.png new file mode 100644 index 0000000..c557a52 Binary files /dev/null and b/apps/public/media/hidden-costs-of-wordpress-plugins-768x1024.png differ diff --git a/apps/public/media/hidden-costs-of-wordpress-plugins.png b/apps/public/media/hidden-costs-of-wordpress-plugins.png new file mode 100644 index 0000000..3fe92d8 Binary files /dev/null and b/apps/public/media/hidden-costs-of-wordpress-plugins.png differ diff --git a/apps/public/media/mintel-thumb-1771868198228-1024x572.png b/apps/public/media/mintel-thumb-1771868198228-1024x572.png new file mode 100644 index 0000000..aa798de Binary files /dev/null and b/apps/public/media/mintel-thumb-1771868198228-1024x572.png differ diff --git a/apps/public/media/mintel-thumb-1771868198228-400x300.png b/apps/public/media/mintel-thumb-1771868198228-400x300.png new file mode 100644 index 0000000..d095900 Binary files /dev/null and b/apps/public/media/mintel-thumb-1771868198228-400x300.png differ diff --git a/apps/public/media/mintel-thumb-1771868198228-768x1024.png b/apps/public/media/mintel-thumb-1771868198228-768x1024.png new file mode 100644 index 0000000..8fb5331 Binary files /dev/null and b/apps/public/media/mintel-thumb-1771868198228-768x1024.png differ diff --git a/apps/public/media/mintel-thumb-1771868198228.png b/apps/public/media/mintel-thumb-1771868198228.png new file mode 100644 index 0000000..c5329a0 Binary files /dev/null and b/apps/public/media/mintel-thumb-1771868198228.png differ diff --git a/apps/public/media/mintel-thumb-1771870270443-1024x572.png b/apps/public/media/mintel-thumb-1771870270443-1024x572.png new file mode 100644 index 0000000..7554d3c Binary files /dev/null and b/apps/public/media/mintel-thumb-1771870270443-1024x572.png differ diff --git a/apps/public/media/mintel-thumb-1771870270443-400x300.png b/apps/public/media/mintel-thumb-1771870270443-400x300.png new file mode 100644 index 0000000..4c16ce0 Binary files /dev/null and b/apps/public/media/mintel-thumb-1771870270443-400x300.png differ diff --git a/apps/public/media/mintel-thumb-1771870270443-768x1024.png b/apps/public/media/mintel-thumb-1771870270443-768x1024.png new file mode 100644 index 0000000..5ef8d3f Binary files /dev/null and b/apps/public/media/mintel-thumb-1771870270443-768x1024.png differ diff --git a/apps/public/media/mintel-thumb-1771870270443.png b/apps/public/media/mintel-thumb-1771870270443.png new file mode 100644 index 0000000..8d98988 Binary files /dev/null and b/apps/public/media/mintel-thumb-1771870270443.png differ diff --git a/apps/public/media/no-us-cloud-platforms-1024x572.png b/apps/public/media/no-us-cloud-platforms-1024x572.png new file mode 100644 index 0000000..76a74ee Binary files /dev/null and b/apps/public/media/no-us-cloud-platforms-1024x572.png differ diff --git a/apps/public/media/no-us-cloud-platforms-400x300.png b/apps/public/media/no-us-cloud-platforms-400x300.png new file mode 100644 index 0000000..6ca9996 Binary files /dev/null and b/apps/public/media/no-us-cloud-platforms-400x300.png differ diff --git a/apps/public/media/no-us-cloud-platforms-768x1024.png b/apps/public/media/no-us-cloud-platforms-768x1024.png new file mode 100644 index 0000000..7bc5d6e Binary files /dev/null and b/apps/public/media/no-us-cloud-platforms-768x1024.png differ diff --git a/apps/public/media/no-us-cloud-platforms.png b/apps/public/media/no-us-cloud-platforms.png new file mode 100644 index 0000000..7e03906 Binary files /dev/null and b/apps/public/media/no-us-cloud-platforms.png differ diff --git a/apps/public/media/professional-hosting-operations-1024x572.png b/apps/public/media/professional-hosting-operations-1024x572.png new file mode 100644 index 0000000..f09fdca Binary files /dev/null and b/apps/public/media/professional-hosting-operations-1024x572.png differ diff --git a/apps/public/media/professional-hosting-operations-400x300.png b/apps/public/media/professional-hosting-operations-400x300.png new file mode 100644 index 0000000..4843890 Binary files /dev/null and b/apps/public/media/professional-hosting-operations-400x300.png differ diff --git a/apps/public/media/professional-hosting-operations-768x1024.png b/apps/public/media/professional-hosting-operations-768x1024.png new file mode 100644 index 0000000..1defba2 Binary files /dev/null and b/apps/public/media/professional-hosting-operations-768x1024.png differ diff --git a/apps/public/media/professional-hosting-operations.png b/apps/public/media/professional-hosting-operations.png new file mode 100644 index 0000000..cf4c332 Binary files /dev/null and b/apps/public/media/professional-hosting-operations.png differ diff --git a/apps/public/media/responsive-design-high-fidelity-1024x572.png b/apps/public/media/responsive-design-high-fidelity-1024x572.png new file mode 100644 index 0000000..fbcad16 Binary files /dev/null and b/apps/public/media/responsive-design-high-fidelity-1024x572.png differ diff --git a/apps/public/media/responsive-design-high-fidelity-400x300.png b/apps/public/media/responsive-design-high-fidelity-400x300.png new file mode 100644 index 0000000..60d01a7 Binary files /dev/null and b/apps/public/media/responsive-design-high-fidelity-400x300.png differ diff --git a/apps/public/media/responsive-design-high-fidelity-768x1024.png b/apps/public/media/responsive-design-high-fidelity-768x1024.png new file mode 100644 index 0000000..8d388fd Binary files /dev/null and b/apps/public/media/responsive-design-high-fidelity-768x1024.png differ diff --git a/apps/public/media/responsive-design-high-fidelity.png b/apps/public/media/responsive-design-high-fidelity.png new file mode 100644 index 0000000..6aca2bb Binary files /dev/null and b/apps/public/media/responsive-design-high-fidelity.png differ diff --git a/apps/public/media/slow-loading-costs-customers-1024x572.png b/apps/public/media/slow-loading-costs-customers-1024x572.png new file mode 100644 index 0000000..4384979 Binary files /dev/null and b/apps/public/media/slow-loading-costs-customers-1024x572.png differ diff --git a/apps/public/media/slow-loading-costs-customers-400x300.png b/apps/public/media/slow-loading-costs-customers-400x300.png new file mode 100644 index 0000000..f81635e Binary files /dev/null and b/apps/public/media/slow-loading-costs-customers-400x300.png differ diff --git a/apps/public/media/slow-loading-costs-customers-768x1024.png b/apps/public/media/slow-loading-costs-customers-768x1024.png new file mode 100644 index 0000000..9a86cf6 Binary files /dev/null and b/apps/public/media/slow-loading-costs-customers-768x1024.png differ diff --git a/apps/public/media/slow-loading-costs-customers.png b/apps/public/media/slow-loading-costs-customers.png new file mode 100644 index 0000000..ccc43ff Binary files /dev/null and b/apps/public/media/slow-loading-costs-customers.png differ diff --git a/apps/public/media/website-without-cookie-banners-1024x572.png b/apps/public/media/website-without-cookie-banners-1024x572.png new file mode 100644 index 0000000..f856c38 Binary files /dev/null and b/apps/public/media/website-without-cookie-banners-1024x572.png differ diff --git a/apps/public/media/website-without-cookie-banners-400x300.png b/apps/public/media/website-without-cookie-banners-400x300.png new file mode 100644 index 0000000..169a626 Binary files /dev/null and b/apps/public/media/website-without-cookie-banners-400x300.png differ diff --git a/apps/public/media/website-without-cookie-banners-768x1024.png b/apps/public/media/website-without-cookie-banners-768x1024.png new file mode 100644 index 0000000..d60746e Binary files /dev/null and b/apps/public/media/website-without-cookie-banners-768x1024.png differ diff --git a/apps/public/media/website-without-cookie-banners.png b/apps/public/media/website-without-cookie-banners.png new file mode 100644 index 0000000..467d415 Binary files /dev/null and b/apps/public/media/website-without-cookie-banners.png differ diff --git a/apps/public/media/why-agencies-are-slow-1024x572.png b/apps/public/media/why-agencies-are-slow-1024x572.png new file mode 100644 index 0000000..8b95660 Binary files /dev/null and b/apps/public/media/why-agencies-are-slow-1024x572.png differ diff --git a/apps/public/media/why-agencies-are-slow-400x300.png b/apps/public/media/why-agencies-are-slow-400x300.png new file mode 100644 index 0000000..5f32413 Binary files /dev/null and b/apps/public/media/why-agencies-are-slow-400x300.png differ diff --git a/apps/public/media/why-agencies-are-slow-768x1024.png b/apps/public/media/why-agencies-are-slow-768x1024.png new file mode 100644 index 0000000..ed93027 Binary files /dev/null and b/apps/public/media/why-agencies-are-slow-768x1024.png differ diff --git a/apps/public/media/why-agencies-are-slow.png b/apps/public/media/why-agencies-are-slow.png new file mode 100644 index 0000000..75d788e Binary files /dev/null and b/apps/public/media/why-agencies-are-slow.png differ diff --git a/apps/public/media/why-no-templates-matter-1024x572.png b/apps/public/media/why-no-templates-matter-1024x572.png new file mode 100644 index 0000000..b122854 Binary files /dev/null and b/apps/public/media/why-no-templates-matter-1024x572.png differ diff --git a/apps/public/media/why-no-templates-matter-400x300.png b/apps/public/media/why-no-templates-matter-400x300.png new file mode 100644 index 0000000..27b5a20 Binary files /dev/null and b/apps/public/media/why-no-templates-matter-400x300.png differ diff --git a/apps/public/media/why-no-templates-matter-768x1024.png b/apps/public/media/why-no-templates-matter-768x1024.png new file mode 100644 index 0000000..77a5484 Binary files /dev/null and b/apps/public/media/why-no-templates-matter-768x1024.png differ diff --git a/apps/public/media/why-no-templates-matter.png b/apps/public/media/why-no-templates-matter.png new file mode 100644 index 0000000..f7f8788 Binary files /dev/null and b/apps/public/media/why-no-templates-matter.png differ diff --git a/apps/public/media/why-websites-break-after-updates-1024x572.png b/apps/public/media/why-websites-break-after-updates-1024x572.png new file mode 100644 index 0000000..a3fb527 Binary files /dev/null and b/apps/public/media/why-websites-break-after-updates-1024x572.png differ diff --git a/apps/public/media/why-websites-break-after-updates-400x300.png b/apps/public/media/why-websites-break-after-updates-400x300.png new file mode 100644 index 0000000..8ae785d Binary files /dev/null and b/apps/public/media/why-websites-break-after-updates-400x300.png differ diff --git a/apps/public/media/why-websites-break-after-updates-768x1024.png b/apps/public/media/why-websites-break-after-updates-768x1024.png new file mode 100644 index 0000000..e859d63 Binary files /dev/null and b/apps/public/media/why-websites-break-after-updates-768x1024.png differ diff --git a/apps/public/media/why-websites-break-after-updates.png b/apps/public/media/why-websites-break-after-updates.png new file mode 100644 index 0000000..9e73397 Binary files /dev/null and b/apps/public/media/why-websites-break-after-updates.png differ diff --git a/apps/web/app/(payload)/actions.ts b/apps/web/app/(payload)/actions.ts index ad9da86..8ce1382 100644 --- a/apps/web/app/(payload)/actions.ts +++ b/apps/web/app/(payload)/actions.ts @@ -1,4 +1,12 @@ "use server"; import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts"; +import config from "@payload-config"; +import { importMap } from "./admin/importMap"; -export const handleServerFunctions = payloadHandleServerFunctions; +export const handleServerFunctions = async (args: any) => { + return payloadHandleServerFunctions({ + ...args, + config, + importMap, + }); +}; diff --git a/apps/web/app/(payload)/admin/importMap.js b/apps/web/app/(payload)/admin/importMap.js index b105d7e..da5a80c 100644 --- a/apps/web/app/(payload)/admin/importMap.js +++ b/apps/web/app/(payload)/admin/importMap.js @@ -1,6 +1,99 @@ +import { OptimizeButton as OptimizeButton_a629b3460534b7aa208597fdc5e30aec } from "@/src/payload/components/OptimizeButton"; +import { GenerateSlugButton as GenerateSlugButton_63aadb132a046b3f001fac7a715e5717 } from "@/src/payload/components/FieldGenerators/GenerateSlugButton"; +import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector"; +import { GenerateThumbnailButton as GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e } from "@/src/payload/components/FieldGenerators/GenerateThumbnailButton"; +import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc"; +import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc"; +import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc"; +import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { AiFieldButton as AiFieldButton_da42292f87769a8025025b774910be6d } from "@/src/payload/components/FieldGenerators/AiFieldButton"; +import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client"; +import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker"; +import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector"; import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc"; export const importMap = { + "@/src/payload/components/OptimizeButton#OptimizeButton": + OptimizeButton_a629b3460534b7aa208597fdc5e30aec, + "@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton": + GenerateSlugButton_63aadb132a046b3f001fac7a715e5717, + "@/src/payload/components/TagSelector#default": + default_76cec558bd86098fa1dab70b12eb818f, + "@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton": + GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e, + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": + RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": + RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": + LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/client#BlocksFeatureClient": + BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton": + AiFieldButton_da42292f87769a8025025b774910be6d, + "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": + InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": + HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UploadFeatureClient": + UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": + BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": + RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#LinkFeatureClient": + LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": + ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": + OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": + UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#IndentFeatureClient": + IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#AlignFeatureClient": + AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HeadingFeatureClient": + HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": + ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": + InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": + SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": + SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": + StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": + UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BoldFeatureClient": + BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ItalicFeatureClient": + ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@/src/payload/components/ColorPicker#default": + default_2ebf44fdf8ebc607cf0de30cff485248, + "@/src/payload/components/IconSelector#default": + default_a1c6da8fb7dd9846a8b07123ff256d09, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1, }; diff --git a/apps/web/app/(site)/about/page.tsx b/apps/web/app/(site)/about/page.tsx index f2abb28..a873fcd 100644 --- a/apps/web/app/(site)/about/page.tsx +++ b/apps/web/app/(site)/about/page.tsx @@ -31,7 +31,7 @@ import { CodeSnippet, AbstractCircuit, } from "@/src/components/Effects"; -import { getImgproxyUrl } from "@/src/utils/imgproxy"; + import { Marker } from "@/src/components/Marker"; export default function AboutPage() { @@ -51,12 +51,7 @@ export default function AboutPage() {
Marc Mintel diff --git a/apps/web/app/(site)/blog/[slug]/page.tsx b/apps/web/app/(site)/blog/[slug]/page.tsx index 096099c..98cdba0 100644 --- a/apps/web/app/(site)/blog/[slug]/page.tsx +++ b/apps/web/app/(site)/blog/[slug]/page.tsx @@ -1,6 +1,8 @@ import * as React from "react"; import type { Metadata } from "next"; -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; +import { getPayloadHMR } from "@payloadcms/next/utilities"; +import configPromise from "@payload-config"; import { getAllPosts } from "@/src/lib/posts"; import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader"; import { Section } from "@/src/components/Section"; @@ -9,6 +11,8 @@ import { BlogPostClient } from "@/src/components/BlogPostClient"; import { TextSelectionShare } from "@/src/components/TextSelectionShare"; import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar"; import { MDXContent } from "@/src/components/MDXContent"; +import { PayloadRichText } from "@/src/components/PayloadRichText"; +import { TableOfContents } from "@/src/components/TableOfContents"; export async function generateStaticParams() { const allPosts = await getAllPosts(); @@ -54,6 +58,18 @@ export default async function BlogPostPage({ const post = allPosts.find((p) => p.slug === slug); if (!post) { + const payload = await getPayloadHMR({ config: configPromise }); + const redirectDoc = await payload.find({ + collection: "redirects", + where: { + from: { equals: slug }, + }, + }); + + if (redirectDoc.docs.length > 0) { + redirect(`/blog/${redirectDoc.docs[0].to}`); + } + notFound(); } @@ -102,7 +118,12 @@ export default async function BlogPostPage({ )}
- + + {post.lexicalContent ? ( + + ) : ( + + )}
diff --git a/apps/web/check-db.ts b/apps/web/check-db.ts new file mode 100644 index 0000000..9b8998c --- /dev/null +++ b/apps/web/check-db.ts @@ -0,0 +1,12 @@ +export const run = async ({ payload }) => { + const docs = await payload.find({ + collection: "context-files", + limit: 100, + }); + console.log(`--- DB CHECK ---`); + console.log(`Found ${docs.totalDocs} context files.`); + docs.docs.forEach((doc) => { + console.log(`- ${doc.filename}`); + }); + process.exit(0); +}; diff --git a/apps/web/context/about.md b/apps/web/context/about.md deleted file mode 100644 index a974021..0000000 --- a/apps/web/context/about.md +++ /dev/null @@ -1,64 +0,0 @@ -# Marc — digital problem solver - -## Identity -- Name: Marc Mintel -- Mail: marc@mintel.me -- Location: Vulkaneifel, Germany -- Role: Independent digital problem solver -- Mode: Solo -- Focus: Understanding problems and building practical solutions - -## What I do -I work on digital problems and build tools, scripts, and systems to solve them. -Sometimes that means code, sometimes automation, sometimes AI, sometimes something else. -The tool is secondary. The problem comes first. - -## How I work -- I try things -- I break things -- I fix things -- I write down what I learned - -## What this blog is -A public notebook of: -- things I figured out -- mistakes I made -- tools I tested -- small insights that might be useful later - -Mostly short entries. -Mostly practical. - -## Why no portfolio -Finished projects get outdated. -Understanding doesn’t. - -This blog shows how I approach problems, not how pretty something looked last year. - -## Topics -- Vibe coding with AI -- Debugging and problem solving -- Mac tools and workflows -- Automation -- Small scripts and systems -- Learning notes -- FOSS - -## Audience -People who: -- build things -- work with computers -- solve problems -- and don’t need marketing talk - -## Tone -- calm -- factual -- direct -- no hype -- no self-promotion - -## Core idea -Write things down. -So I don’t forget. -And so others might find them useful. \ No newline at end of file diff --git a/apps/web/docs/PRINCIPLES.md b/apps/web/docs/PRINCIPLES.md deleted file mode 100644 index d8a8371..0000000 --- a/apps/web/docs/PRINCIPLES.md +++ /dev/null @@ -1,43 +0,0 @@ -Prinzipien - -Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren. - -⸻ - -1. Volle Preis-Transparenz -Alle Kosten sind offen und nachvollziehbar. -Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. -Jeder Kunde sieht genau, wofür er bezahlt. - -⸻ - -2. Quellcode & Projektzugang -Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur. -Damit kann jeder andere Entwickler problemlos weiterarbeiten. -Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar. - -⸻ - -3. Best Practices & saubere Technik -Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein. -Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben – langfristig. - -⸻ - -4. Verantwortung & Fairness -Ich übernehme die technische Verantwortung für die Website. -Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse – nur saubere Umsetzung und stabile Systeme. -Wenn etwas nicht sinnvoll ist, sage ich es ehrlich. - -⸻ - -5. Langfristiger Wert -Eine Website ist ein Investment. -Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind. -Das schützt Ihre Investition und vermeidet teure Neuaufbauten. - -⸻ - -6. Zusammenarbeit ohne Tricks -Keine künstlichen Deadlines, kein unnötiger Overhead. -Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert. \ No newline at end of file diff --git a/apps/web/migrate-docs.ts b/apps/web/migrate-docs.ts new file mode 100644 index 0000000..e8c8705 --- /dev/null +++ b/apps/web/migrate-docs.ts @@ -0,0 +1,54 @@ +import { getPayload } from "payload"; +import configPromise from "./payload.config"; +import fs from "fs"; +import path from "path"; + +async function run() { + try { + const payload = await getPayload({ config: configPromise }); + console.log("Payload initialized."); + + const docsDir = path.resolve(process.cwd(), "docs"); + + if (!fs.existsSync(docsDir)) { + console.log(`Docs directory not found at ${docsDir}`); + process.exit(0); + } + + const files = fs.readdirSync(docsDir); + let count = 0; + + for (const file of files) { + if (file.endsWith(".md")) { + const content = fs.readFileSync(path.join(docsDir, file), "utf8"); + + // Check if already exists + const existing = await payload.find({ + collection: "context-files", + where: { filename: { equals: file } }, + }); + + if (existing.totalDocs === 0) { + await payload.create({ + collection: "context-files", + data: { + filename: file, + content: content, + }, + }); + count++; + } + } + } + + console.log( + `Migration successful! Added ${count} new context files to the database.`, + ); + process.exit(0); + } catch (e) { + console.error("Migration failed:", e); + process.exit(1); + } +} + +run(); diff --git a/apps/web/migrate-drafts.ts b/apps/web/migrate-drafts.ts new file mode 100644 index 0000000..f83a479 --- /dev/null +++ b/apps/web/migrate-drafts.ts @@ -0,0 +1,34 @@ +import { getPayload } from "payload"; +import configPromise from "./payload.config"; + +async function run() { + const payload = await getPayload({ config: configPromise }); + const { docs } = await payload.find({ + collection: "posts", + limit: 1000, + }); + + console.log(`Found ${docs.length} posts. Checking status...`); + + for (const doc of docs) { + if (doc._status !== "published") { + try { + await payload.update({ + collection: "posts", + id: doc.id, + data: { + _status: "published", + }, + }); + console.log(`Updated "${doc.title}" to published.`); + } catch (e) { + console.error(`Failed to update ${doc.title}:`, e.message); + } + } + } + + console.log("Migration complete."); + process.exit(0); +} + +run(); diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ecdde76..ce495ae 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,17 +1,15 @@ import withMintelConfig from "@mintel/next-config"; import { withPayload } from '@payloadcms/next/withPayload'; - import createMDX from '@next/mdx'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); /** @type {import('next').NextConfig} */ const nextConfig = { - pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], - reactStrictMode: true, - output: 'standalone', - images: { - loader: 'custom', - loaderFile: './src/utils/imgproxy-loader.ts', - }, + serverExternalPackages: ['@mintel/content-engine'], async rewrites() { return [ // Umami proxy rewrite handled in app/stats/api/send/route.ts @@ -27,6 +25,13 @@ const nextConfig = { }, ]; }, + webpack: (config) => { + config.resolve.alias = { + ...config.resolve.alias, + '@mintel/content-engine': path.resolve(dirname, 'node_modules/@mintel/content-engine'), + }; + return config; + }, }; const withMDX = createMDX({ diff --git a/apps/web/package.json b/apps/web/package.json index 527875b..807bbd4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,8 @@ "version": "0.1.0", "description": "Technical problem solver's blog - practical insights and learning notes", "scripts": { - "dev": "rm -rf .next && next dev", + "dev": "pnpm run seed:context && next dev --turbo", + "seed:context": "tsx ./seed-context.ts", "build": "next build --webpack", "start": "next start", "lint": "eslint app src scripts video", @@ -40,6 +41,7 @@ "@payloadcms/email-nodemailer": "^3.77.0", "@payloadcms/next": "^3.77.0", "@payloadcms/richtext-lexical": "^3.77.0", + "@payloadcms/ui": "^3.77.0", "@react-pdf/renderer": "^4.3.2", "@remotion/bundler": "^4.0.414", "@remotion/cli": "^4.0.414", diff --git a/apps/web/payload-types.ts b/apps/web/payload-types.ts index a2f9dcf..c89fb10 100644 --- a/apps/web/payload-types.ts +++ b/apps/web/payload-types.ts @@ -70,6 +70,9 @@ export interface Config { users: User; media: Media; posts: Post; + inquiries: Inquiry; + redirects: Redirect; + "context-files": ContextFile; "payload-kv": PayloadKv; "payload-locked-documents": PayloadLockedDocument; "payload-preferences": PayloadPreference; @@ -80,6 +83,9 @@ export interface Config { users: UsersSelect | UsersSelect; media: MediaSelect | MediaSelect; posts: PostsSelect | PostsSelect; + inquiries: InquiriesSelect | InquiriesSelect; + redirects: RedirectsSelect | RedirectsSelect; + "context-files": ContextFilesSelect | ContextFilesSelect; "payload-kv": PayloadKvSelect | PayloadKvSelect; "payload-locked-documents": | PayloadLockedDocumentsSelect @@ -95,8 +101,12 @@ export interface Config { defaultIDType: number; }; fallbackLocale: null; - globals: {}; - globalsSelect: {}; + globals: { + "ai-settings": AiSetting; + }; + globalsSelect: { + "ai-settings": AiSettingsSelect | AiSettingsSelect; + }; locale: null; user: User; jobs: { @@ -201,12 +211,99 @@ export interface Post { title: string; slug: string; description: string; + /** + * Set a future date and save as 'Published' to schedule this post. It will not appear on the frontend until this date is reached. + */ date: string; tags: { + /** + * Kategorisiere diesen Post mit einem eindeutigen Tag + */ tag?: string | null; id?: string | null; }[]; - thumbnail?: string | null; + /** + * The main hero image for the blog post. + */ + featuredImage?: (number | null) | Media; + content?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ("ltr" | "rtl") | null; + format: "left" | "start" | "center" | "right" | "end" | "justify" | ""; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + updatedAt: string; + createdAt: string; + _status?: ("draft" | "published") | null; +} +/** + * Contact form leads and inquiries. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "inquiries". + */ +export interface Inquiry { + id: number; + name: string; + email: string; + companyName?: string | null; + projectType?: string | null; + message?: string | null; + isFreeText?: boolean | null; + /** + * The JSON data from the configurator. + */ + config?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "redirects". + */ +export interface Redirect { + id: number; + /** + * The old URL slug that should be redirected (e.g. 'old-post-name') + */ + from: string; + /** + * The new URL slug to redirect to (e.g. 'new-awesome-post') + */ + to: string; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "context-files". + */ +export interface ContextFile { + id: number; + /** + * Exact filename (e.g. 'strategy.md'). The system uses this to identify the document during prompt generation. + */ + filename: string; + /** + * The raw markdown/text content of the document. + */ content: string; updatedAt: string; createdAt: string; @@ -246,6 +343,18 @@ export interface PayloadLockedDocument { | ({ relationTo: "posts"; value: number | Post; + } | null) + | ({ + relationTo: "inquiries"; + value: number | Inquiry; + } | null) + | ({ + relationTo: "redirects"; + value: number | Redirect; + } | null) + | ({ + relationTo: "context-files"; + value: number | ContextFile; } | null); globalSlug?: string | null; user: { @@ -378,7 +487,43 @@ export interface PostsSelect { tag?: T; id?: T; }; - thumbnail?: T; + featuredImage?: T; + content?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "inquiries_select". + */ +export interface InquiriesSelect { + name?: T; + email?: T; + companyName?: T; + projectType?: T; + message?: T; + isFreeText?: T; + config?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "redirects_select". + */ +export interface RedirectsSelect { + from?: T; + to?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "context-files_select". + */ +export interface ContextFilesSelect { + filename?: T; content?: T; updatedAt?: T; createdAt?: T; @@ -423,6 +568,39 @@ export interface PayloadMigrationsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ai-settings". + */ +export interface AiSetting { + id: number; + /** + * List of trusted B2B/Tech sources (e.g. 'Vercel Blog', 'Fireship', 'Theo - t3.gg') the AI should prioritize when researching facts or videos. This overrides the hardcoded defaults. + */ + customSources?: + | { + sourceName: string; + id?: string | null; + }[] + | null; + updatedAt?: string | null; + createdAt?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ai-settings_select". + */ +export interface AiSettingsSelect { + customSources?: + | T + | { + sourceName?: T; + id?: T; + }; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". diff --git a/apps/web/payload.config.ts b/apps/web/payload.config.ts index f7e0cb3..950ff56 100644 --- a/apps/web/payload.config.ts +++ b/apps/web/payload.config.ts @@ -1,6 +1,8 @@ import { buildConfig } from "payload"; +// Triggering config re-analysis for blocks visibility - V4 import { postgresAdapter } from "@payloadcms/db-postgres"; -import { lexicalEditor } from "@payloadcms/richtext-lexical"; +import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical"; +import { payloadBlocks } from "./src/payload/blocks/allBlocks"; import { nodemailerAdapter } from "@payloadcms/email-nodemailer"; import path from "path"; import { fileURLToPath } from "url"; @@ -9,6 +11,11 @@ import sharp from "sharp"; import { Users } from "./src/payload/collections/Users"; import { Media } from "./src/payload/collections/Media"; import { Posts } from "./src/payload/collections/Posts"; +import { Inquiries } from "./src/payload/collections/Inquiries"; +import { Redirects } from "./src/payload/collections/Redirects"; +import { ContextFiles } from "./src/payload/collections/ContextFiles"; + +import { AiSettings } from "./src/payload/globals/AiSettings"; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -20,7 +27,8 @@ export default buildConfig({ baseDir: path.resolve(dirname), }, }, - collections: [Users, Media, Posts], + collections: [Users, Media, Posts, Inquiries, Redirects, ContextFiles], + globals: [AiSettings], ...(process.env.MAIL_HOST ? { email: nodemailerAdapter({ @@ -37,7 +45,14 @@ export default buildConfig({ }), } : {}), - editor: lexicalEditor(), + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + BlocksFeature({ + blocks: payloadBlocks, + }), + ], + }), secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev", typescript: { outputFile: path.resolve(dirname, "payload-types.ts"), diff --git a/apps/web/remove-toc.ts b/apps/web/remove-toc.ts new file mode 100644 index 0000000..eb44f0d --- /dev/null +++ b/apps/web/remove-toc.ts @@ -0,0 +1,87 @@ +import { getPayload } from "payload"; +import configPromise from "./payload.config"; + +async function run() { + const payload = await getPayload({ config: configPromise }); + const { docs } = await payload.find({ + collection: "posts", + limit: 1000, + }); + + console.log( + `Found ${docs.length} posts. Checking for ...`, + ); + + let updatedCount = 0; + + const removeTOC = (node: any): boolean => { + let modified = false; + if (node.children && Array.isArray(node.children)) { + // Filter out raw text nodes or paragraph nodes that are exactly TableOfContents + const originalLength = node.children.length; + node.children = node.children.filter((child: any) => { + if ( + child.type === "text" && + child.text && + child.text.includes("") + ) { + return false; + } + if ( + child.type === "paragraph" && + child.children && + child.children.length === 1 && + child.children[0].text === "" + ) { + return false; + } + return true; + }); + if (node.children.length !== originalLength) { + modified = true; + } + + // Also clean up any substrings in remaining text nodes + for (const child of node.children) { + if ( + child.type === "text" && + child.text && + child.text.includes("") + ) { + child.text = child.text.replace("", "").trim(); + modified = true; + } + if (removeTOC(child)) { + modified = true; + } + } + } + return modified; + }; + + for (const doc of docs) { + if (doc.content?.root) { + const isModified = removeTOC(doc.content.root); + if (isModified) { + try { + await payload.update({ + collection: "posts", + id: doc.id, + data: { + content: doc.content, + }, + }); + console.log(`Cleaned up TOC in "${doc.title}".`); + updatedCount++; + } catch (e) { + console.error(`Failed to update ${doc.title}:`, e.message); + } + } + } + } + + console.log(`Cleanup complete. Modified ${updatedCount} posts.`); + process.exit(0); +} + +run(); diff --git a/apps/web/scripts/migrate-posts.ts b/apps/web/scripts/migrate-posts.ts index e357843..8a0e590 100644 --- a/apps/web/scripts/migrate-posts.ts +++ b/apps/web/scripts/migrate-posts.ts @@ -2,6 +2,7 @@ import { getPayload } from "payload"; import configPromise from "../payload.config"; import fs from "fs"; import path from "path"; +import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser"; function parseMatter(content: string) { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); @@ -39,29 +40,112 @@ async function run() { const slug = file.replace(/\.mdx$/, ""); console.log(`Migrating ${slug}...`); - const existing = await payload.find({ - collection: "posts", - where: { slug: { equals: slug } }, - }); - - if (existing.docs.length === 0) { - await payload.create({ + try { + const existing = await payload.find({ collection: "posts", - data: { - title: data.title || slug, - slug, - description: data.description || "", - date: data.date - ? new Date(data.date).toISOString() - : new Date().toISOString(), - tags: (data.tags || []).map((t: string) => ({ tag: t })), - thumbnail: data.thumbnail || "", - content: body, - }, + where: { slug: { equals: slug } }, }); - console.log(`✔ Inserted ${slug}`); - } else { - console.log(`⚠ Skipped ${slug} (already exists)`); + + const lexicalBlocks = parseMarkdownToLexical(body); + const lexicalAST = { + root: { + type: "root", + format: "", + indent: 0, + version: 1, + children: lexicalBlocks, + direction: "ltr", + }, + }; + + // Handle thumbnail mapping + let featuredImageId = null; + if (data.thumbnail) { + try { + // Remove leading slash and find local file + const localPath = path.join( + process.cwd(), + "public", + data.thumbnail.replace(/^\//, ""), + ); + const fileName = path.basename(localPath); + + if (fs.existsSync(localPath)) { + // Check if media already exists in Payload + const existingMedia = await payload.find({ + collection: "media", + where: { filename: { equals: fileName } }, + }); + + if (existingMedia.docs.length > 0) { + featuredImageId = existingMedia.docs[0].id; + } else { + // Upload new media item + const fileData = fs.readFileSync(localPath); + const { size } = fs.statSync(localPath); + + const newMedia = await payload.create({ + collection: "media", + data: { + alt: data.title || fileName, + }, + file: { + data: fileData, + name: fileName, + mimetype: fileName.endsWith(".png") + ? "image/png" + : fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") + ? "image/jpeg" + : "image/webp", + size, + }, + }); + featuredImageId = newMedia.id; + console.log(` ↑ Uploaded thumbnail: ${fileName}`); + } + } + } catch (e) { + console.warn( + ` ⚠ Warning: Could not process thumbnail ${data.thumbnail}`, + ); + } + } + + if (existing.docs.length === 0) { + await payload.create({ + collection: "posts", + data: { + title: data.title || slug, + slug, + description: data.description || "", + date: data.date + ? new Date(data.date).toISOString() + : new Date().toISOString(), + tags: (data.tags || []).map((t: string) => ({ tag: t })), + content: lexicalAST as any, + featuredImage: featuredImageId, + }, + }); + console.log(`✔ Inserted ${slug}`); + } else { + await payload.update({ + collection: "posts", + id: existing.docs[0].id, + data: { + content: lexicalAST as any, + featuredImage: featuredImageId, + }, + }); + console.log(`✔ Updated AST and thumbnail for ${slug}`); + } + } catch (err: any) { + console.error(`✘ FAILED ${slug}: ${err.message}`); + if (err.data?.errors) { + console.error( + ` Validation errors:`, + JSON.stringify(err.data.errors, null, 2), + ); + } } } diff --git a/apps/web/seed-context.ts b/apps/web/seed-context.ts new file mode 100644 index 0000000..a073abc --- /dev/null +++ b/apps/web/seed-context.ts @@ -0,0 +1,56 @@ +import { getPayload } from "payload"; +import configPromise from "./payload.config"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function run() { + try { + const payload = await getPayload({ config: configPromise }); + + const existing = await payload.find({ + collection: "context-files", + limit: 0, + }); + + if (existing.totalDocs > 0) { + console.log("Context collection already populated. Skipping seed."); + process.exit(0); + } + + const seedDir = path.resolve( + __dirname, + "src/payload/collections/ContextFiles/seed", + ); + if (!fs.existsSync(seedDir)) { + console.log(`Seed directory not found at ${seedDir}`); + process.exit(0); + } + + const files = fs.readdirSync(seedDir).filter((f) => f.endsWith(".md")); + let count = 0; + + for (const file of files) { + const content = fs.readFileSync(path.join(seedDir, file), "utf8"); + await payload.create({ + collection: "context-files", + data: { + filename: file, + content: content, + }, + }); + count++; + } + + console.log(`Seeded ${count} context files.`); + process.exit(0); + } catch (e) { + console.error("Seeding failed:", e); + process.exit(1); + } +} + +run(); diff --git a/apps/web/src/actions/contact.ts b/apps/web/src/actions/contact.ts index 885d7b5..ca7969a 100644 --- a/apps/web/src/actions/contact.ts +++ b/apps/web/src/actions/contact.ts @@ -5,6 +5,8 @@ import { getInquiryEmailHtml, getConfirmationEmailHtml, } from "../components/ContactForm/EmailTemplates"; +import { getPayload } from "payload"; +import configPromise from "@payload-config"; export async function sendContactInquiry(data: { name: string; @@ -16,7 +18,22 @@ export async function sendContactInquiry(data: { config?: any; }) { try { - // 1. Send Inquiry to Marc + // 1. Save to Payload CMS (Replaces Directus) + const payload = await getPayload({ config: configPromise }); + await payload.create({ + collection: "inquiries", + data: { + name: data.name, + email: data.email, + companyName: data.companyName, + projectType: data.projectType, + message: data.message, + isFreeText: data.isFreeText, + config: data.config || null, + }, + }); + + // 2. Send Inquiry to Marc const inquiryResult = await sendEmail({ subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`, html: getInquiryEmailHtml(data), diff --git a/apps/web/src/components/IframeSection.tsx b/apps/web/src/components/IframeSection.tsx index 9d02e8d..c010b55 100644 --- a/apps/web/src/components/IframeSection.tsx +++ b/apps/web/src/components/IframeSection.tsx @@ -1,6 +1,5 @@ "use client"; - import * as React from "react"; import { cn } from "../utils/cn"; import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react"; @@ -31,8 +30,6 @@ interface IframeSectionProps { desktopHeight?: string; } -import PropTypes from "prop-types"; - /** * Reusable Browser UI components to maintain consistency */ @@ -102,11 +99,6 @@ const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({ ); }; -BrowserChromeComponent.propTypes = { - url: PropTypes.string.isRequired, - minimal: PropTypes.bool, -}; - const BrowserChrome = React.memo(BrowserChromeComponent); BrowserChrome.displayName = "BrowserChrome"; @@ -212,7 +204,7 @@ export const IframeSection: React.FC = ({ const isScrollable = doc.scrollHeight > doc.clientHeight + 10; setScrollState({ atTop, atBottom, isScrollable }); } - } catch (_e) { } + } catch (_e) {} }, []); // Ambilight effect (sampled from iframe if same-origin) @@ -257,7 +249,7 @@ export const IframeSection: React.FC = ({ ); updateScrollState(); - } catch (_e) { } + } catch (_e) {} }, [dynamicGlow, offsetY, updateScrollState]); // Height parse helper @@ -376,9 +368,9 @@ export const IframeSection: React.FC = ({ "w-full relative flex flex-col z-10", minimal ? "bg-transparent" : "bg-slate-50", !minimal && - "rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]", + "rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]", perspective && - "hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]", + "hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]", "overflow-hidden", )} style={chassisStyle} diff --git a/apps/web/src/components/MemeCard.tsx b/apps/web/src/components/MemeCard.tsx index 68109c3..7bbf6bd 100644 --- a/apps/web/src/components/MemeCard.tsx +++ b/apps/web/src/components/MemeCard.tsx @@ -1,256 +1,355 @@ -'use client'; +"use client"; -import React from 'react'; -import { ComponentShareButton } from './ComponentShareButton'; -import { Reveal } from './Reveal'; +import React from "react"; +import { ComponentShareButton } from "./ComponentShareButton"; +import { Reveal } from "./Reveal"; interface MemeCardProps { - /** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */ - template: string; - /** Pipe-delimited captions */ - captions: string; - /** Optional local image path. If provided, overrides the text-based template. */ - image?: string; - className?: string; + /** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */ + template: string; + /** Pipe-delimited captions */ + captions: string; + /** Optional local image path. If provided, overrides the text-based template. */ + image?: string; + className?: string; } /** * Premium text-based meme cards with dedicated layouts per template. * Uses emoji + typography instead of images for on-brand aesthetics. */ -export const MemeCard: React.FC = ({ template, captions, image, className = '' }) => { - const captionList = (captions || '').split('|').map(s => s.trim()).filter(Boolean); - const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`; - - if (image) { - return ( - -
-
- -
-
- -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`Meme: -
-
- - ); - } +export const MemeCard: React.FC = ({ + template, + captions, + image, + className = "", +}) => { + // Also replace literal `\n` (slash-n) strings from AI output with actual newlines + const processedCaptions = (captions || "").replace(/\\n/g, "\n"); + const captionList = processedCaptions + .split("|") + .map((s) => s.trim()) + .filter(Boolean); + const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`; + if (image) { return ( - -
-
+ +
+
-
-
- -
- - {template === 'drake' && } - {template === 'ds' && } - {template === 'gru' && } - {template === 'fine' && } - {template === 'clown' && } - {template === 'expanding' && } - {template === 'distracted' && } - -
+
+
+
- + {/* eslint-disable-next-line @next/next/no-img-element */} + {`Meme: +
+
+ ); + } + + return ( + +
+
+ +
+
+ +
+ + {template === "drake" && } + {template === "ds" && } + {template === "gru" && } + {template === "fine" && } + {template === "clown" && } + {template === "expanding" && ( + + )} + {template === "distracted" && ( + + )} + +
+
+ + ); }; function DrakeMeme({ captions }: { captions: string[] }) { - return ( -
-
-
- 🙅 -
-
-

{captions[0]}

-
-
-
-
- 😎 -
-
-

{captions[1]}

-
-
+ return ( +
+
+
+ + 🙅 +
- ); +
+

+ {captions[0]} +

+
+
+
+
+ + 😎 + +
+
+

+ {captions[1]} +

+
+
+
+ ); } function DailyStruggleMeme({ captions }: { captions: string[] }) { - return ( -
-
😰
-

Daily Struggle

-
-
-
-

{captions[0]}

-
-
-
-

{captions[1]}

-
-
+ return ( +
+
+ 😰 +
+

+ Daily Struggle +

+
+
+
+

+ {captions[0]} +

- ); +
+
+

+ {captions[1]} +

+
+
+
+ ); } function GruMeme({ captions }: { captions: string[] }) { - const steps = captions.slice(0, 4); - return ( -
- {(steps || []).map((caption, i) => { - const isLast = i >= 2; - return ( -
- - {isLast ? '😱' : '😏'} - -

- {caption} -

-
- ); - })} -
- ); + const steps = captions.slice(0, 4); + return ( +
+ {(steps || []).map((caption, i) => { + const isLast = i >= 2; + return ( +
+ + {isLast ? "😱" : "😏"} + +

+ {caption} +

+
+ ); + })} +
+ ); } function FineMeme({ captions }: { captions: string[] }) { - return ( -
-
-
- 🔥 -

This is Fine

-
-

{captions[0]}

-
-
-
- -

- “{captions[1] || 'Alles im grünen Bereich.'}” -

-
-
+ return ( +
+
+
+ 🔥 +

+ This is Fine +

- ); +

+ {captions[0]} +

+
+
+
+ + ☕ + +

+ “{captions[1] || "Alles im grünen Bereich."}” +

+
+
+
+ ); } function ClownMeme({ captions }: { captions: string[] }) { - const steps = captions.slice(0, 4); - const emojis = ['😐', '🤡', '💀', '🎪']; - return ( -
-
-

Clown Progression

-
- {steps.map((caption, i) => ( -
- {emojis[i] || '🤡'} -

- {caption} -

-
- ))} + const steps = captions.slice(0, 4); + const emojis = ["😐", "🤡", "💀", "🎪"]; + return ( +
+
+

+ Clown Progression +

+
+ {steps.map((caption, i) => ( +
+ + {emojis[i] || "🤡"} + +

+ {caption} +

- ); + ))} +
+ ); } function ExpandingBrainMeme({ captions }: { captions: string[] }) { - const steps = captions.slice(0, 4); - const emojis = ['🧠', '🧠✨', '🧠💡', '🧠🚀']; - const shadows = [ - '', - 'shadow-[0_0_15px_rgba(59,130,246,0.1)]', - 'shadow-[0_0_20px_rgba(99,102,241,0.2)]', - 'shadow-[0_0_25px_rgba(168,85,247,0.3)]', - ]; - return ( -
-
-

Expanding Intelligence

-
- {steps.map((caption, i) => ( -
- {emojis[i]} -

- {caption} -

-
- ))} + const steps = captions.slice(0, 4); + const emojis = ["🧠", "🧠✨", "🧠💡", "🧠🚀"]; + const shadows = [ + "", + "shadow-[0_0_15px_rgba(59,130,246,0.1)]", + "shadow-[0_0_20px_rgba(99,102,241,0.2)]", + "shadow-[0_0_25px_rgba(168,85,247,0.3)]", + ]; + return ( +
+
+

+ Expanding Intelligence +

+
+ {steps.map((caption, i) => ( +
+ + {emojis[i]} + +

+ {caption} +

- ); + ))} +
+ ); } function DistractedMeme({ captions }: { captions: string[] }) { - return ( -
-
-

The Distraction

-
-
-
- 👤 -

Subject

-

{captions[0]}

-
-
- -

Temptation

-

{captions[1]}

-
-
- 😤 -

Reality

-

{captions[2]}

-
-
+ return ( +
+
+

+ The Distraction +

+
+
+
+ 👤 +

+ Subject +

+

+ {captions[0]} +

- ); +
+ + ✨ + +

+ Temptation +

+

+ {captions[1]} +

+
+
+ 😤 +

+ Reality +

+

+ {captions[2]} +

+
+
+
+ ); } -function GenericMeme({ captions, template }: { captions: string[]; template: string }) { - return ( -
-

{template}

-
- {(captions || []).map((caption, i) => ( -
-

- {caption} -

-
- ))} -
-
- ); +function GenericMeme({ + captions, + template, +}: { + captions: string[]; + template: string; +}) { + return ( +
+

+ {template} +

+
+ {(captions || []).map((caption, i) => ( +
+

+ {caption} +

+
+ ))} +
+
+ ); } diff --git a/apps/web/src/components/PayloadRichText.tsx b/apps/web/src/components/PayloadRichText.tsx new file mode 100644 index 0000000..82378d6 --- /dev/null +++ b/apps/web/src/components/PayloadRichText.tsx @@ -0,0 +1,315 @@ +import { RichText } from "@payloadcms/richtext-lexical/react"; +import type { JSXConverters } from "@payloadcms/richtext-lexical/react"; +import { MemeCard } from "@/src/components/MemeCard"; +import { Mermaid } from "@/src/components/Mermaid"; +import { LeadMagnet } from "@/src/components/LeadMagnet"; +import { ComparisonRow } from "@/src/components/Landing/ComparisonRow"; +import { mdxComponents } from "../content-engine/components"; + +const jsxConverters: JSXConverters = { + blocks: { + memeCard: ({ node }: any) => ( +
+ +
+ ), + mermaid: ({ node }: any) => ( +
+ + {node.fields.chartDefinition} + +
+ ), + leadMagnet: ({ node }: any) => ( +
+ +
+ ), + comparisonRow: ({ node }: any) => ( + + ), + // --- MDX Registry Injections --- + leadParagraph: ({ node }: any) => ( + + {node.fields.text} + + ), + articleBlockquote: ({ node }: any) => ( + + {node.fields.quote} + {node.fields.author && ` - ${node.fields.author}`} + + ), + mintelH2: ({ node }: any) => ( + {node.fields.text} + ), + mintelH3: ({ node }: any) => ( + {node.fields.text} + ), + mintelHeading: ({ node }: any) => { + const displayLevel = node.fields.displayLevel || "h2"; + if (displayLevel === "h3") + return {node.fields.text}; + return {node.fields.text}; + }, + statsDisplay: ({ node }: any) => ( + + ), + diagramState: ({ node }: any) => ( + + ), + diagramTimeline: ({ node }: any) => ( + + ), + diagramGantt: ({ node }: any) => ( + + ), + diagramPie: ({ node }: any) => ( + + ), + diagramSequence: ({ node }: any) => ( + + ), + diagramFlow: ({ node }: any) => ( + + ), + + waterfallChart: ({ node }: any) => ( + + ), + premiumComparisonChart: ({ node }: any) => ( + + ), + iconList: ({ node }: any) => ( + + {node.fields.items?.map((item: any, i: number) => ( + // @ts-ignore + + {item.description} + + ))} + + ), + statsGrid: ({ node }: any) => { + const rawStats = node.fields.stats || []; + let statsStr = ""; + if (Array.isArray(rawStats)) { + statsStr = rawStats + .map((s: any) => `${s.value || ""}|${s.label || ""}`) + .join("~"); + } else if (typeof rawStats === "string") { + statsStr = rawStats; + } + return ; + }, + metricBar: ({ node }: any) => ( + + ), + carousel: ({ node }: any) => ( + ({ + title: s.caption || "Image", + content: "", + icon: undefined, + })) || [] + } + /> + ), + imageText: ({ node }: any) => ( + + {node.fields.text} + + ), + revenueLossCalculator: ({ node }: any) => ( + + ), + performanceChart: ({ node }: any) => , + performanceROICalculator: ({ node }: any) => ( +
+ +
+ ), + loadTimeSimulator: ({ node }: any) => ( +
+ +
+ ), + architectureBuilder: ({ node }: any) => ( +
+ +
+ ), + digitalAssetVisualizer: ({ node }: any) => ( +
+ +
+ ), + + twitterEmbed: ({ node }: any) => ( + + ), + youTubeEmbed: ({ node }: any) => ( + + ), + linkedInEmbed: ({ node }: any) => ( + + ), + externalLink: ({ node }: any) => ( + + {node.fields.label} + + ), + trackedLink: ({ node }: any) => ( + + {node.fields.label} + + ), + articleMeme: ({ node }: any) => ( + + ), + marker: ({ node }: any) => ( + + {node.fields.text} + + ), + boldNumber: ({ node }: any) => ( + + ), + webVitalsScore: ({ node }: any) => ( + + ), + buttonBlock: ({ node }: any) => ( + + {node.fields.label} + + ), + articleQuote: ({ node }: any) => ( + + ), + reveal: ({ node }: any) => ( + + {/* Reveal component takes children, which in MDX is nested content */} + + + ), + section: ({ node }: any) => ( + + + + ), + tableOfContents: () => , + faqSection: ({ node }: any) => ( + + + + ), + }, +}; + +export function PayloadRichText({ data }: { data: any }) { + if (!data) return null; + + return ( +
+ +
+ ); +} diff --git a/apps/web/src/components/TLDR.tsx b/apps/web/src/components/TLDR.tsx new file mode 100644 index 0000000..e18099d --- /dev/null +++ b/apps/web/src/components/TLDR.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +interface TLDRProps { + children?: React.ReactNode; + content?: string; + className?: string; +} + +export const TLDR: React.FC = ({ + children, + content, + className = "", +}) => { + return ( +
+
+
+ + + +
+

+ TL;DR +

+
+
+ {children || content} +
+
+ ); +}; diff --git a/apps/web/src/components/blog/BlogThumbnailSVG.tsx b/apps/web/src/components/blog/BlogThumbnailSVG.tsx index a2010f3..8ea2019 100644 --- a/apps/web/src/components/blog/BlogThumbnailSVG.tsx +++ b/apps/web/src/components/blog/BlogThumbnailSVG.tsx @@ -1,7 +1,4 @@ -/* eslint-disable react/prop-types */ -import type { - ThumbnailIcon, -} from "./blogThumbnails"; +import type { ThumbnailIcon } from "./blogThumbnails"; import { blogThumbnails } from "./blogThumbnails"; interface BlogThumbnailSVGProps { diff --git a/apps/web/src/content-engine/components.ts b/apps/web/src/content-engine/components.ts index 965da49..0b88239 100644 --- a/apps/web/src/content-engine/components.ts +++ b/apps/web/src/content-engine/components.ts @@ -1,40 +1,39 @@ +import { LeadParagraph } from "../components/ArticleParagraph"; +import { H1, H2, H3 } from "../components/ArticleHeading"; +import { Paragraph } from "../components/ArticleParagraph"; +import { ArticleBlockquote } from "../components/ArticleBlockquote"; +import { Marker } from "../components/Marker"; +import { ComparisonRow } from "../components/Landing/ComparisonRow"; +import { StatsDisplay } from "../components/StatsDisplay"; +import { Mermaid } from "../components/Mermaid"; +import { DiagramState } from "../components/DiagramState"; +import { DiagramTimeline } from "../components/DiagramTimeline"; +import { DiagramGantt } from "../components/DiagramGantt"; +import { DiagramPie } from "../components/DiagramPie"; +import { DiagramSequence } from "../components/DiagramSequence"; +import { DiagramFlow } from "../components/DiagramFlow"; +import { IconList, IconListItem } from "../components/IconList"; +import { ArticleMeme } from "../components/ArticleMeme"; +import { MemeCard } from "../components/MemeCard"; +import { ExternalLink } from "../components/ExternalLink"; +import { StatsGrid } from "../components/StatsGrid"; +import { MetricBar } from "../components/MetricBar"; +import { ArticleQuote } from "../components/ArticleQuote"; +import { BoldNumber } from "../components/BoldNumber"; +import { WebVitalsScore } from "../components/WebVitalsScore"; +import { WaterfallChart } from "../components/WaterfallChart"; +import { Button } from "../components/Button"; +import { LeadMagnet } from "../components/LeadMagnet"; +import { TrackedLink } from "../components/analytics/TrackedLink"; +import { FAQSection } from "../components/FAQSection"; -import { LeadParagraph } from '../components/ArticleParagraph'; -import { H1, H2, H3 } from '../components/ArticleHeading'; -import { Paragraph } from '../components/ArticleParagraph'; -import { ArticleBlockquote } from '../components/ArticleBlockquote'; -import { Marker } from '../components/Marker'; -import { ComparisonRow } from '../components/Landing/ComparisonRow'; -import { StatsDisplay } from '../components/StatsDisplay'; -import { Mermaid } from '../components/Mermaid'; -import { DiagramState } from '../components/DiagramState'; -import { DiagramTimeline } from '../components/DiagramTimeline'; -import { DiagramGantt } from '../components/DiagramGantt'; -import { DiagramPie } from '../components/DiagramPie'; -import { DiagramSequence } from '../components/DiagramSequence'; -import { DiagramFlow } from '../components/DiagramFlow'; -import { IconList, IconListItem } from '../components/IconList'; -import { ArticleMeme } from '../components/ArticleMeme'; -import { MemeCard } from '../components/MemeCard'; -import { ExternalLink } from '../components/ExternalLink'; -import { StatsGrid } from '../components/StatsGrid'; -import { MetricBar } from '../components/MetricBar'; -import { ArticleQuote } from '../components/ArticleQuote'; -import { BoldNumber } from '../components/BoldNumber'; -import { WebVitalsScore } from '../components/WebVitalsScore'; -import { WaterfallChart } from '../components/WaterfallChart'; -import { Button } from '../components/Button'; -import { LeadMagnet } from '../components/LeadMagnet'; -import { TrackedLink } from '../components/analytics/TrackedLink'; -import { FAQSection } from '../components/FAQSection'; +import { PremiumComparisonChart } from "../components/PremiumComparisonChart"; +import { ImageText } from "../components/ImageText"; +import { Carousel } from "../components/Carousel"; -import { PremiumComparisonChart } from '../components/PremiumComparisonChart'; -import { ImageText } from '../components/ImageText'; -import { Carousel } from '../components/Carousel'; - -import { Section } from '../components/Section'; -import { Reveal } from '../components/Reveal'; -import { TableOfContents } from '../components/TableOfContents'; +import { Section } from "../components/Section"; +import { Reveal } from "../components/Reveal"; +import { TableOfContents } from "../components/TableOfContents"; import { RevenueLossCalculator } from "../components/RevenueLossCalculator"; import { PerformanceChart } from "../components/PerformanceChart"; @@ -43,56 +42,61 @@ import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator"; import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder"; import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer"; -import { TwitterEmbed } from '../components/TwitterEmbed'; -import { YouTubeEmbed } from '../components/YouTubeEmbed'; -import { LinkedInEmbed } from '../components/LinkedInEmbed'; +import { TwitterEmbed } from "../components/TwitterEmbed"; +import { YouTubeEmbed } from "../components/YouTubeEmbed"; +import { LinkedInEmbed } from "../components/LinkedInEmbed"; +import { TLDR } from "../components/TLDR"; +/** + * Single Source of Truth for MDX component rendering. + * Handled separately from Payload blocks to avoid SVG import issues in Node.js. + */ export const mdxComponents = { - // Named exports for explicit MDX usage - LeadParagraph, - H1, - H2, - H3, - Paragraph, - ArticleBlockquote, - Marker, - ComparisonRow, - StatsDisplay, - Mermaid, - DiagramState, - DiagramTimeline, - DiagramGantt, - DiagramPie, - DiagramSequence, - DiagramFlow, - IconList, - IconListItem, - ArticleMeme, - MemeCard, - ExternalLink, - StatsGrid, - MetricBar, - ArticleQuote, - BoldNumber, - WebVitalsScore, - WaterfallChart, - PremiumComparisonChart, - ImageText, - Carousel, - Section, - Reveal, - TableOfContents, - RevenueLossCalculator, - PerformanceChart, - PerformanceROICalculator, - LoadTimeSimulator, - ArchitectureBuilder, - DigitalAssetVisualizer, - TwitterEmbed, - YouTubeEmbed, - LinkedInEmbed, - Button, - LeadMagnet, - TrackedLink, - FAQSection + LeadParagraph, + H1, + H2, + H3, + Paragraph, + ArticleBlockquote, + Marker, + ComparisonRow, + StatsDisplay, + Mermaid, + DiagramState, + DiagramTimeline, + DiagramGantt, + DiagramPie, + DiagramSequence, + DiagramFlow, + IconList, + IconListItem, + ArticleMeme, + MemeCard, + ExternalLink, + StatsGrid, + MetricBar, + ArticleQuote, + BoldNumber, + WebVitalsScore, + WaterfallChart, + PremiumComparisonChart, + ImageText, + Carousel, + Section, + Reveal, + TableOfContents, + RevenueLossCalculator, + PerformanceChart, + PerformanceROICalculator, + LoadTimeSimulator, + ArchitectureBuilder, + DigitalAssetVisualizer, + TwitterEmbed, + YouTubeEmbed, + LinkedInEmbed, + Button, + LeadMagnet, + TrackedLink, + FAQSection, + TLDR, }; diff --git a/apps/web/src/content-engine/definitions.ts b/apps/web/src/content-engine/definitions.ts index 7fb2424..6128774 100644 --- a/apps/web/src/content-engine/definitions.ts +++ b/apps/web/src/content-engine/definitions.ts @@ -1,296 +1,9 @@ - -import { ComponentDefinition } from '@mintel/content-engine'; +import { ComponentDefinition } from "@mintel/content-engine"; +import { allComponentDefinitions } from "../payload/blocks/allBlocks"; /** * Single Source of Truth for all MDX component definitions. - * Used by: - * - content-engine.config.ts (for the optimization script) - * - The AI content pipeline (for component injection) - * - * Keep in sync with: src/content-engine/components.ts (the MDX runtime registry) + * Now dynamically generated from individual Payload block definitions. */ -export const componentDefinitions: ComponentDefinition[] = [ - { - name: 'LeadParagraph', - description: 'Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.', - usageExample: '\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n' - }, - { - name: 'H2', - description: 'Main section heading. Used for top-level content sections.', - usageExample: '

Der wirtschaftliche Case

' - }, - { - name: 'BoldNumber', - description: 'Large centerpiece number with label for primary statistics.', - usageExample: '' - }, - { - name: 'PremiumComparisonChart', - description: 'Advanced chart for comparing performance metrics with industrial aesthetics.', - usageExample: '' - }, - { - name: 'ImageText', - description: 'Layout component for image next to explanatory text.', - usageExample: 'Erklärung...' - }, - { - name: 'Carousel', - description: 'Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).', - usageExample: '' - }, - { - name: 'H3', - description: 'Subsection heading. Used within H2 sections.', - usageExample: '

Die drei Säulen meiner Umsetzung

' - }, - { - name: 'Paragraph', - description: 'Standard body text paragraph. All body text must be wrapped in this.', - usageExample: '\n Mein System ist kein Kostenfaktor, sondern ein ROI-Beschleuniger.\n' - }, - { - name: 'ArticleBlockquote', - description: 'Styled blockquote for expert quotes or key statements.', - usageExample: '\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n' - }, - { - name: 'Marker', - description: 'Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.', - usageExample: 'entscheidender Wettbewerbsvorteil' - }, - { - name: 'ComparisonRow', - description: 'Side-by-side comparison: negative "Standard" approach vs positive "Mintel" approach. Props include showShare boolean.', - usageExample: `` - }, - { - name: 'StatsDisplay', - description: 'A single large stat card with prominent value, label, and optional subtext.', - usageExample: '' - }, - { - name: 'Mermaid', - description: 'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".', - usageExample: `
- -graph TD - A["Request"] --> B["CDN Edge"] - B --> C["Static HTML"] - -
` - }, - { - name: 'DiagramFlow', - description: 'Structured flowchart diagram. Use for process flows, architecture diagrams, etc. Supports structured nodes/edges. direction defaults to LR.', - usageExample: `` - }, - { - name: 'DiagramPie', - description: 'Pie chart with structured data props.', - usageExample: `` - }, - { - name: 'DiagramGantt', - description: 'Gantt timeline chart comparing durations of tasks.', - usageExample: `` - }, - { - name: 'DiagramState', - description: 'State diagram showing states and transitions.', - usageExample: `` - }, - { - name: 'DiagramSequence', - description: 'Sequence diagram (uses raw Mermaid sequence syntax as children).', - usageExample: ` -sequenceDiagram - Browser->>CDN: GET /page - CDN->>Browser: Static HTML (< 50ms) -` - }, - { - name: 'DiagramTimeline', - description: 'Timeline diagram (uses raw Mermaid timeline syntax as children).', - usageExample: ` -timeline - 2024 : Planung - 2025 : Entwicklung - 2026 : Launch -` - }, - { - name: 'IconList', - description: 'Checklist with check/cross icons. Wrap IconListItem children inside.', - usageExample: ` - - Zero-Computation: Statische Seiten, kein Serverwarten. - - - Legacy CMS: Datenbankabfragen bei jedem Request. - -` - }, - { - name: 'ArticleMeme', - description: 'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".', - usageExample: `
- -
` - }, - - { - name: 'Section', - description: 'Wraps a thematic section block with optional heading.', - usageExample: '
\n

Section Title

\n

Content here.

\n
' - }, - { - name: 'Reveal', - description: 'Scroll-triggered reveal animation wrapper. Wrap any content to animate on scroll.', - usageExample: '\n \n' - }, - { - name: 'StatsGrid', - description: 'Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.', - usageExample: '' - }, - { - name: 'MetricBar', - description: 'Animated horizontal progress bar. Use multiple in sequence to compare metrics. IMPORTANT: value MUST be a real number > 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).', - usageExample: ` -` - }, - { - name: 'ArticleQuote', - description: 'Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).', - usageExample: '' - }, - { - name: 'BoldNumber', - description: 'Full-width hero number card with dark gradient, animated count-up, and share button. Use for the most impactful single statistics. IMPORTANT: Always provide source and sourceUrl. Numbers without comparison context should use PremiumComparisonChart or paired MetricBar instead. Props: value (string like "53%" or "2.5M€"), label (short description), source (REQUIRED), sourceUrl (REQUIRED).', - usageExample: '' - }, - { - name: 'WebVitalsScore', - description: 'Displays Core Web Vitals (LCP, INP, CLS) in a premium card layout with automatic traffic light coloring (Good/Needs Improvement/Poor). Use for performance audits or comparisons.', - usageExample: '' - }, - { - name: 'WaterfallChart', - description: 'A timeline visualization of network requests (waterfall). Use to show loading sequences or bottlenecks. Labels auto-color coded by type (JS, HTML, IMG).', - usageExample: `` - }, - { - name: 'ExternalLink', - description: 'Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.', - usageExample: 'Google Core Web Vitals' - }, - { - name: 'TwitterEmbed', - description: 'Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.', - usageExample: '' - }, - { - name: 'YouTubeEmbed', - description: 'Embeds a YouTube video to visualize concepts or provide deep dives. Use the 11-character videoId.', - usageExample: '' - }, - { - name: 'LinkedInEmbed', - description: 'Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).', - usageExample: '' - }, - { - name: 'TrackedLink', - description: 'A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.', - usageExample: 'Jetzt anfragen' - }, - { - name: 'Button', - description: 'DEPRECATED: Use instead for main CTAs. Only use for small secondary links.', - usageExample: '' - }, - { - name: 'LeadMagnet', - description: 'Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).', - usageExample: '' - }, - { - name: 'PerformanceROICalculator', - description: 'Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.', - usageExample: '' - }, - { - name: 'LoadTimeSimulator', - description: 'Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.', - usageExample: '' - }, - { - name: 'FAQSection', - description: 'Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.', - usageExample: '\n

Frage 1

\n Antwort 1\n
' - }, - { - name: 'ArchitectureBuilder', - description: 'Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.', - usageExample: '' - }, - { - name: 'DigitalAssetVisualizer', - description: 'Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.', - usageExample: '' - } -]; +export const componentDefinitions: ComponentDefinition[] = + allComponentDefinitions; diff --git a/apps/web/src/lib/posts.ts b/apps/web/src/lib/posts.ts index 61b7e1f..f05e6ed 100644 --- a/apps/web/src/lib/posts.ts +++ b/apps/web/src/lib/posts.ts @@ -9,22 +9,50 @@ export async function getAllPosts() { return []; } - const payload = await getPayload({ config: configPromise }); - const { docs } = await payload.find({ - collection: "posts", - limit: 1000, - sort: "-date", - }); + try { + const payload = await getPayload({ config: configPromise }); + const { docs } = await payload.find({ + collection: "posts", + limit: 1000, + sort: "-date", + where: { + and: [ + { + _status: { + equals: "published", + }, + }, + { + date: { + less_than_equal: new Date(), + }, + }, + ], + }, + }); - return docs.map((doc) => ({ - title: doc.title as string, - description: doc.description as string, - date: doc.date as string, - tags: (doc.tags || []).map((t) => - typeof t === "object" && t !== null ? t.tag : t, - ) as string[], - slug: doc.slug as string, - thumbnail: doc.thumbnail as string, - body: { code: doc.content as string }, - })); + return docs.map((doc) => ({ + title: doc.title as string, + description: doc.description as string, + date: doc.date as string, + tags: (doc.tags || []).map((t) => + typeof t === "object" && t !== null ? t.tag : t, + ) as string[], + slug: doc.slug as string, + thumbnail: + (doc.featuredImage && + typeof doc.featuredImage === "object" && + doc.featuredImage.url + ? doc.featuredImage.url + : "") || "", + body: { code: "" as string }, + lexicalContent: doc.content || null, + })); + } catch (error) { + console.warn( + "⚠️ Bypassing Payload fetch during build: Database connection refused.", + error, + ); + return []; + } } diff --git a/apps/web/src/payload/actions/generateField.ts b/apps/web/src/payload/actions/generateField.ts new file mode 100644 index 0000000..41c7845 --- /dev/null +++ b/apps/web/src/payload/actions/generateField.ts @@ -0,0 +1,191 @@ +"use server"; + +import { config } from "../../../content-engine.config"; +import { getPayloadHMR } from "@payloadcms/next/utilities"; +import configPromise from "@payload-config"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import * as os from "node:os"; + +async function getOrchestrator() { + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + + if (!OPENROUTER_KEY) { + throw new Error( + "Missing OPENROUTER_API_KEY in .env (Required for AI generation)", + ); + } + + const importDynamic = new Function("modulePath", "return import(modulePath)"); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + + return new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); +} + +export async function generateSlugAction( + title: string, + draftContent: string, + oldSlug?: string, + instructions?: string, +) { + try { + const orchestrator = await getOrchestrator(); + const newSlug = await orchestrator.generateSlug( + draftContent, + title, + instructions, + ); + + if (oldSlug && oldSlug !== newSlug) { + const payload = await getPayloadHMR({ config: configPromise }); + await payload.create({ + collection: "redirects", + data: { + from: oldSlug, + to: newSlug, + }, + }); + } + + return { success: true, slug: newSlug }; + } catch (e: any) { + return { success: false, error: e.message }; + } +} + +export async function generateThumbnailAction( + draftContent: string, + title?: string, + instructions?: string, +) { + try { + const payload = await getPayloadHMR({ config: configPromise }); + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + + if (!OPENROUTER_KEY) { + throw new Error("Missing OPENROUTER_API_KEY in .env"); + } + if (!REPLICATE_KEY) { + throw new Error( + "Missing REPLICATE_API_KEY in .env (Required for Thumbnails)", + ); + } + + const importDynamic = new Function( + "modulePath", + "return import(modulePath)", + ); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + const { ThumbnailGenerator } = await importDynamic( + "@mintel/thumbnail-generator", + ); + + const orchestrator = new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); + + const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY }); + + const prompt = await orchestrator.generateVisualPrompt( + draftContent || title || "Technology", + instructions, + ); + + const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`); + await tg.generateImage(prompt, tmpPath); + + const fileData = await fs.readFile(tmpPath); + const stat = await fs.stat(tmpPath); + const fileName = path.basename(tmpPath); + + const newMedia = await payload.create({ + collection: "media", + data: { + alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail", + }, + file: { + data: fileData, + name: fileName, + mimetype: "image/png", + size: stat.size, + }, + }); + + // Cleanup temp file + await fs.unlink(tmpPath).catch(() => {}); + + return { success: true, mediaId: newMedia.id }; + } catch (e: any) { + return { success: false, error: e.message }; + } +} +export async function generateSingleFieldAction( + documentTitle: string, + documentContent: string, + fieldName: string, + fieldDescription: string, + instructions?: string, +) { + try { + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY"); + + const payload = await getPayloadHMR({ config: configPromise }); + + // Fetch context documents from DB + const contextDocsData = await payload.find({ + collection: "context-files", + limit: 100, + }); + const projectContext = contextDocsData.docs + .map((doc) => `--- ${doc.filename} ---\n${doc.content}`) + .join("\n\n"); + + const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components. +PROJECT STRATEGY & CONTEXT: +${projectContext} + +DOCUMENT TITLE: ${documentTitle} +DOCUMENT DRAFT:\n${documentContent}\n +YOUR TASK: Generate the exact value for a specific field named "${fieldName}". +${fieldDescription ? `FIELD DESCRIPTION / CONSTRAINTS: ${fieldDescription}\n` : ""} +${instructions ? `EDITOR INSTRUCTIONS for this field: ${instructions}\n` : ""} +CRITICAL RULES: +1. Respond ONLY with the requested content value. +2. NO markdown wrapping blocks (like \`\`\`mermaid or \`\`\`html) around the output! Just the raw code or text. +3. If the field implies a diagram or flow, output RAW Mermaid.js code. +4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`; + + const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${OPENROUTER_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "google/gemini-3-flash-preview", + messages: [{ role: "user", content: prompt }], + }), + }); + const data = await res.json(); + const text = data.choices?.[0]?.message?.content?.trim() || ""; + return { success: true, text }; + } catch (e: any) { + return { success: false, error: e.message }; + } +} diff --git a/apps/web/src/payload/actions/optimizePost.ts b/apps/web/src/payload/actions/optimizePost.ts new file mode 100644 index 0000000..6019883 --- /dev/null +++ b/apps/web/src/payload/actions/optimizePost.ts @@ -0,0 +1,88 @@ +"use server"; + +import { config } from "../../../content-engine.config"; +import { revalidatePath } from "next/cache"; +import { parseMarkdownToLexical } from "../utils/lexicalParser"; +import { getPayloadHMR } from "@payloadcms/next/utilities"; +import configPromise from "@payload-config"; + +export async function optimizePostText( + draftContent: string, + instructions?: string, +) { + try { + const payload = await getPayloadHMR({ config: configPromise }); + const globalAiSettings = await payload.findGlobal({ slug: "ai-settings" }); + const customSources = + globalAiSettings?.customSources?.map((s: any) => s.sourceName) || []; + + const OPENROUTER_KEY = + process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY; + const REPLICATE_KEY = process.env.REPLICATE_API_KEY; + + if (!OPENROUTER_KEY) { + throw new Error( + "OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.", + ); + } + + const importDynamic = new Function( + "modulePath", + "return import(modulePath)", + ); + const { AiBlogPostOrchestrator } = await importDynamic( + "@mintel/content-engine", + ); + + const orchestrator = new AiBlogPostOrchestrator({ + apiKey: OPENROUTER_KEY, + replicateApiKey: REPLICATE_KEY, + model: "google/gemini-3-flash-preview", + }); + + // Fetch context documents purely from DB + const contextDocsData = await payload.find({ + collection: "context-files", + limit: 100, + }); + const projectContext = contextDocsData.docs.map((doc) => doc.content); + + const optimizedMarkdown = await orchestrator.optimizeDocument({ + content: draftContent, + projectContext, + availableComponents: config.components, + instructions, + internalLinks: [], + customSources, + }); + + // The orchestrator currently returns Markdown + JSX tags. + // We convert this mixed string into a basic Lexical AST map. + + if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") { + throw new Error("AI returned invalid markup."); + } + + const blocks = parseMarkdownToLexical(optimizedMarkdown); + + return { + success: true, + lexicalAST: { + root: { + type: "root", + format: "", + indent: 0, + version: 1, + children: blocks, + direction: "ltr", + }, + }, + }; + } catch (error: any) { + console.error("Failed to optimize post:", error); + return { + success: false, + error: error.message || "An unknown error occurred during optimization.", + }; + } +} diff --git a/apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts b/apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts new file mode 100644 index 0000000..cab7ffe --- /dev/null +++ b/apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts @@ -0,0 +1,28 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ArchitectureBuilderBlock: MintelBlock = { + slug: "architectureBuilder", + labels: { + singular: "Architecture Builder", + plural: "Architecture Builders", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ArchitectureBuilder", + description: + "Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.", + usageExample: "''", + }, + fields: [ + { + name: "preset", + type: "text", + defaultValue: "standard", + admin: { description: "Geben Sie den Text für preset ein." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts b/apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts new file mode 100644 index 0000000..20f7649 --- /dev/null +++ b/apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts @@ -0,0 +1,59 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ArticleBlockquoteBlock: MintelBlock = { + slug: "articleBlockquote", + labels: { + singular: "Article Blockquote", + plural: "Article Blockquotes", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ArticleBlockquote", + description: "Styled blockquote for expert quotes or key statements.", + usageExample: + "'\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n'", + }, + fields: [ + { + name: "quote", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für quote ein.", + }, + }, + { + name: "author", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für author ein.", + }, + }, + { + name: "role", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für role ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ArticleMemeBlock.ts b/apps/web/src/payload/blocks/ArticleMemeBlock.ts new file mode 100644 index 0000000..c4a38ce --- /dev/null +++ b/apps/web/src/payload/blocks/ArticleMemeBlock.ts @@ -0,0 +1,54 @@ +import { MintelBlock } from "./types"; +import type { Block } from "payload"; + +export const ArticleMemeBlock: MintelBlock = { + slug: "articleMeme", + labels: { + singular: "Article Meme", + plural: "Article Memes", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ArticleMeme", + description: + "Real image-based meme from the media library. Use for static screenshots or custom memes that are not available via memegen.link.", + usageExample: + '', + }, + fields: [ + { + name: "image", + type: "upload", + relationTo: "media", + required: true, + admin: { description: "Laden Sie die Datei für image hoch." }, + }, + { + name: "alt", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für alt ein.", + }, + }, + { + name: "caption", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für caption ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ArticleQuoteBlock.ts b/apps/web/src/payload/blocks/ArticleQuoteBlock.ts new file mode 100644 index 0000000..099d22d --- /dev/null +++ b/apps/web/src/payload/blocks/ArticleQuoteBlock.ts @@ -0,0 +1,97 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ArticleQuoteBlock: MintelBlock = { + slug: "articleQuote", + labels: { + singular: "Article Quote", + plural: "Article Quotes", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ArticleQuote", + description: + "Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).", + usageExample: + '\'\'', + }, + fields: [ + { + name: "value", + type: "text", + required: true, + admin: { + description: "e.g. 53% or 2.5M€", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + { + name: "label", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + { + name: "source", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für source ein.", + }, + }, + { + name: "sourceUrl", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für sourceUrl ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ButtonBlock.ts b/apps/web/src/payload/blocks/ButtonBlock.ts new file mode 100644 index 0000000..86ee4f9 --- /dev/null +++ b/apps/web/src/payload/blocks/ButtonBlock.ts @@ -0,0 +1,69 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ButtonBlock: MintelBlock = { + slug: "buttonBlock", + labels: { + singular: "Button Block", + plural: "Button Blocks", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Button", + description: + "DEPRECATED: Use instead for main CTAs. Only use for small secondary links.", + usageExample: + '', + }, + fields: [ + { + name: "label", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + { + name: "href", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für href ein." }, + }, + { + name: "variant", + type: "select", + options: [ + { label: "Primary", value: "primary" }, + { label: "Outline", value: "outline" }, + { label: "Ghost", value: "ghost" }, + ], + defaultValue: "primary", + admin: { description: "Wählen Sie eine Option für variant aus." }, + }, + { + name: "size", + type: "select", + options: [ + { label: "Normal", value: "normal" }, + { label: "Large", value: "large" }, + ], + defaultValue: "normal", + admin: { description: "Wählen Sie eine Option für size aus." }, + }, + { + name: "showArrow", + type: "checkbox", + defaultValue: true, + admin: { description: "Wert für showArrow eingeben." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/CarouselBlock.ts b/apps/web/src/payload/blocks/CarouselBlock.ts new file mode 100644 index 0000000..bf78daa --- /dev/null +++ b/apps/web/src/payload/blocks/CarouselBlock.ts @@ -0,0 +1,48 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const CarouselBlock: MintelBlock = { + slug: "carousel", + labels: { + singular: "Carousel", + plural: "Carousels", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Carousel", + description: + "Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).", + usageExample: + '\' B[End]", + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DiagramGanttBlock.ts b/apps/web/src/payload/blocks/DiagramGanttBlock.ts new file mode 100644 index 0000000..16c6190 --- /dev/null +++ b/apps/web/src/payload/blocks/DiagramGanttBlock.ts @@ -0,0 +1,35 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DiagramGanttBlock: MintelBlock = { + slug: "diagramGantt", + labels: { + singular: "Diagram Gantt", + plural: "Diagram Gantts", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DiagramGantt", + description: "Mermaid Gantt timeline chart. MUST output raw mermaid code.", + usageExample: + "gantt\\n title Project Roadmap\\n dateFormat YYYY-MM-DD\\n Section Design\\n Draft UI :a1, 2024-01-01, 7d", + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DiagramPieBlock.ts b/apps/web/src/payload/blocks/DiagramPieBlock.ts new file mode 100644 index 0000000..e6b5481 --- /dev/null +++ b/apps/web/src/payload/blocks/DiagramPieBlock.ts @@ -0,0 +1,34 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DiagramPieBlock: MintelBlock = { + slug: "diagramPie", + labels: { + singular: "Diagram Pie", + plural: "Diagram Pies", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DiagramPie", + description: "Mermaid pie chart diagram. MUST output raw mermaid code.", + usageExample: 'pie title Market Share\\n "Chrome" : 60\\n "Safari" : 20', + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DiagramSequenceBlock.ts b/apps/web/src/payload/blocks/DiagramSequenceBlock.ts new file mode 100644 index 0000000..c35836f --- /dev/null +++ b/apps/web/src/payload/blocks/DiagramSequenceBlock.ts @@ -0,0 +1,36 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DiagramSequenceBlock: MintelBlock = { + slug: "diagramSequence", + labels: { + singular: "Diagram Sequence", + plural: "Diagram Sequences", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DiagramSequence", + description: + "Mermaid sequence diagram showing actor interactions. MUST output raw mermaid code.", + usageExample: + "sequenceDiagram\\n Client->>Server: GET /api\\n Server-->>Client: 200 OK", + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DiagramStateBlock.ts b/apps/web/src/payload/blocks/DiagramStateBlock.ts new file mode 100644 index 0000000..4a90258 --- /dev/null +++ b/apps/web/src/payload/blocks/DiagramStateBlock.ts @@ -0,0 +1,35 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DiagramStateBlock: MintelBlock = { + slug: "diagramState", + labels: { + singular: "Diagram State", + plural: "Diagram States", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DiagramState", + description: + "Mermaid state diagram showing states and transitions. MUST output raw mermaid code.", + usageExample: "stateDiagram-v2\\n [*] --> Idle\\n Idle --> Loading", + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DiagramTimelineBlock.ts b/apps/web/src/payload/blocks/DiagramTimelineBlock.ts new file mode 100644 index 0000000..b79d7ca --- /dev/null +++ b/apps/web/src/payload/blocks/DiagramTimelineBlock.ts @@ -0,0 +1,36 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DiagramTimelineBlock: MintelBlock = { + slug: "diagramTimeline", + labels: { + singular: "Diagram Timeline", + plural: "Diagram Timelines", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DiagramTimeline", + description: + "Mermaid timeline or journey diagram. MUST output raw mermaid code.", + usageExample: + "timeline\\n title Project Timeline\\n 2024\\n : Q1 : Planning\\n : Q2 : Execution", + }, + fields: [ + { + name: "definition", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für definition ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts b/apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts new file mode 100644 index 0000000..bc9f382 --- /dev/null +++ b/apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts @@ -0,0 +1,27 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const DigitalAssetVisualizerBlock: MintelBlock = { + slug: "digitalAssetVisualizer", + labels: { + singular: "Digital Asset Visualizer", + plural: "Digital Asset Visualizers", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "DigitalAssetVisualizer", + description: + "Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.", + usageExample: "''", + }, + fields: [ + { + name: "assetId", + type: "text", + admin: { description: "Geben Sie den Text für assetId ein." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ExternalLinkBlock.ts b/apps/web/src/payload/blocks/ExternalLinkBlock.ts new file mode 100644 index 0000000..071ce4d --- /dev/null +++ b/apps/web/src/payload/blocks/ExternalLinkBlock.ts @@ -0,0 +1,42 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ExternalLinkBlock: MintelBlock = { + slug: "externalLink", + labels: { + singular: "External Link", + plural: "External Links", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ExternalLink", + description: + "Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.", + usageExample: + "'Google Core Web Vitals'", + }, + fields: [ + { + name: "href", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für href ein." }, + }, + { + name: "label", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/FAQSectionBlock.ts b/apps/web/src/payload/blocks/FAQSectionBlock.ts new file mode 100644 index 0000000..61bfdb6 --- /dev/null +++ b/apps/web/src/payload/blocks/FAQSectionBlock.ts @@ -0,0 +1,47 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; +import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical"; +import { HeadingBlock } from "./HeadingBlock"; +import { ParagraphBlock } from "./ParagraphBlock"; +import { ExternalLinkBlock } from "./ExternalLinkBlock"; +import { TrackedLinkBlock } from "./TrackedLinkBlock"; + +export const FAQSectionBlock: MintelBlock = { + slug: "faqSection", + labels: { + singular: "Faq Section", + plural: "Faq Sections", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "FAQSection", + description: + "Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.", + usageExample: + "'\n

Frage 1

\n Antwort 1\n
'", + }, + fields: [ + { + name: "content", + type: "richText", + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + BlocksFeature({ + blocks: [ + HeadingBlock, + ParagraphBlock, + ExternalLinkBlock, + TrackedLinkBlock, + ].map(({ ai, render, ...b }) => b), + }), + ], + }), + required: true, + admin: { description: "Formatierter Textbereich für content." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/H2Block.ts b/apps/web/src/payload/blocks/H2Block.ts new file mode 100644 index 0000000..3b4bc70 --- /dev/null +++ b/apps/web/src/payload/blocks/H2Block.ts @@ -0,0 +1,24 @@ +import type { MintelBlock } from "./types"; + +export const H2Block: MintelBlock = { + slug: "mintelH2", + labels: { + singular: "Heading 2", + plural: "Headings 2", + }, + fields: [ + { + name: "text", + type: "text", + required: true, + admin: { + description: "Geben Sie den Text für die H2-Überschrift ein.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/H3Block.ts b/apps/web/src/payload/blocks/H3Block.ts new file mode 100644 index 0000000..4e85bcb --- /dev/null +++ b/apps/web/src/payload/blocks/H3Block.ts @@ -0,0 +1,24 @@ +import type { MintelBlock } from "./types"; + +export const H3Block: MintelBlock = { + slug: "mintelH3", + labels: { + singular: "Heading 3", + plural: "Headings 3", + }, + fields: [ + { + name: "text", + type: "text", + required: true, + admin: { + description: "Geben Sie den Text für die H3-Überschrift ein.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/HeadingBlock.ts b/apps/web/src/payload/blocks/HeadingBlock.ts new file mode 100644 index 0000000..a2d4105 --- /dev/null +++ b/apps/web/src/payload/blocks/HeadingBlock.ts @@ -0,0 +1,50 @@ +import { MintelBlock } from "./types"; + +export const HeadingBlock: MintelBlock = { + slug: "mintelHeading", + labels: { + singular: "Heading", + plural: "Headings", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Heading", + description: + "Flexible heading component with separated SEO and visual display levels.", + usageExample: + '\'Titel\'', + }, + fields: [ + { + name: "text", + type: "text", + required: true, + admin: { + description: "Der Text der Überschrift.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + { + name: "seoLevel", + type: "select", + options: ["h1", "h2", "h3", "h4", "h5", "h6"], + defaultValue: "h2", + admin: { description: "Das semantische HTML-Tag für SEO." }, + }, + { + name: "displayLevel", + type: "select", + options: ["h1", "h2", "h3", "h4", "h5", "h6"], + defaultValue: "h2", + admin: { + description: "Die visuelle Größe der Überschrift (unabhängig von SEO).", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/IconListBlock.ts b/apps/web/src/payload/blocks/IconListBlock.ts new file mode 100644 index 0000000..b60b990 --- /dev/null +++ b/apps/web/src/payload/blocks/IconListBlock.ts @@ -0,0 +1,69 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const IconListBlock: MintelBlock = { + slug: "iconList", + labels: { + singular: "Icon List", + plural: "Icon Lists", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "IconList", + description: + "Checklist with check/cross icons. Wrap IconListItem children inside.", + usageExample: ` + + Zero-Computation: Statische Seiten, kein Serverwarten. + + + Legacy CMS: Datenbankabfragen bei jedem Request. + +`, + }, + fields: [ + { + name: "items", + type: "array", + fields: [ + { + name: "icon", + type: "text", + admin: { + description: "Lucide icon", + components: { Field: "@/src/payload/components/IconSelector" }, + }, + }, + { + name: "title", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für title ein.", + }, + }, + { + name: "description", + type: "textarea", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für description ein.", + }, + }, + ], + admin: { description: "Fügen Sie Elemente zur Liste items hinzu." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/ImageTextBlock.ts b/apps/web/src/payload/blocks/ImageTextBlock.ts new file mode 100644 index 0000000..5a996bf --- /dev/null +++ b/apps/web/src/payload/blocks/ImageTextBlock.ts @@ -0,0 +1,52 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const ImageTextBlock: MintelBlock = { + slug: "imageText", + labels: { + singular: "Image Text", + plural: "Image Texts", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "ImageText", + description: "Layout component for image next to explanatory text.", + usageExample: + '\'Erklärung...\'', + }, + fields: [ + { + name: "image", + type: "upload", + relationTo: "media", + required: true, + admin: { description: "Laden Sie die Datei für image hoch." }, + }, + { + name: "text", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für text ein.", + }, + }, + { + name: "alignment", + type: "select", + options: [ + { label: "Left", value: "left" }, + { label: "Right", value: "right" }, + ], + defaultValue: "left", + admin: { description: "Wählen Sie eine Option für alignment aus." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/LeadMagnetBlock.ts b/apps/web/src/payload/blocks/LeadMagnetBlock.ts new file mode 100644 index 0000000..e748b2d --- /dev/null +++ b/apps/web/src/payload/blocks/LeadMagnetBlock.ts @@ -0,0 +1,81 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const LeadMagnetBlock: MintelBlock = { + slug: "leadMagnet", + labels: { + singular: "Lead Magnet CTA", + plural: "Lead Magnet CTAs", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "LeadMagnet", + description: + "Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).", + usageExample: + '\'\'', + }, + fields: [ + { + name: "title", + type: "text", + required: true, + admin: { + description: "The strong headline for the Call-to-Action", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + { + name: "description", + type: "text", + required: true, + admin: { + description: "The value proposition text.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + { + name: "buttonText", + type: "text", + required: true, + defaultValue: "Jetzt anfragen", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für buttonText ein.", + }, + }, + { + name: "href", + type: "text", + required: true, + defaultValue: "/contact", + admin: { description: "Geben Sie den Text für href ein." }, + }, + { + name: "variant", + type: "select", + options: [ + { label: "Performance", value: "performance" }, + { label: "Security", value: "security" }, + { label: "Standard", value: "standard" }, + ], + defaultValue: "standard", + admin: { description: "Wählen Sie eine Option für variant aus." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/LeadParagraphBlock.ts b/apps/web/src/payload/blocks/LeadParagraphBlock.ts new file mode 100644 index 0000000..ecb9e6d --- /dev/null +++ b/apps/web/src/payload/blocks/LeadParagraphBlock.ts @@ -0,0 +1,36 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const LeadParagraphBlock: MintelBlock = { + slug: "leadParagraph", + labels: { + singular: "Lead Paragraph", + plural: "Lead Paragraphs", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "LeadParagraph", + description: + "Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.", + usageExample: + "'\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n'", + }, + fields: [ + { + name: "text", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für text ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/LinkedInEmbedBlock.ts b/apps/web/src/payload/blocks/LinkedInEmbedBlock.ts new file mode 100644 index 0000000..76c9036 --- /dev/null +++ b/apps/web/src/payload/blocks/LinkedInEmbedBlock.ts @@ -0,0 +1,29 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const LinkedInEmbedBlock: MintelBlock = { + slug: "linkedInEmbed", + labels: { + singular: "Linked In Embed", + plural: "Linked In Embeds", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "LinkedInEmbed", + description: + "Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).", + usageExample: + "''", + }, + fields: [ + { + name: "url", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für url ein." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts b/apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts new file mode 100644 index 0000000..6dbce1c --- /dev/null +++ b/apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts @@ -0,0 +1,31 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const LoadTimeSimulatorBlock: MintelBlock = { + slug: "loadTimeSimulator", + labels: { + singular: "Load Time Simulator", + plural: "Load Time Simulators", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "LoadTimeSimulator", + description: + "Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.", + usageExample: "''", + }, + fields: [ + { + name: "initialLoadTime", + type: "number", + defaultValue: 3.5, + admin: { + description: + "Tragen Sie einen numerischen Wert für initialLoadTime ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/MarkerBlock.ts b/apps/web/src/payload/blocks/MarkerBlock.ts new file mode 100644 index 0000000..f5f2ab5 --- /dev/null +++ b/apps/web/src/payload/blocks/MarkerBlock.ts @@ -0,0 +1,51 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const MarkerBlock: MintelBlock = { + slug: "marker", + labels: { + singular: "Marker", + plural: "Markers", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Marker", + description: + "Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.", + usageExample: "'entscheidender Wettbewerbsvorteil'", + }, + fields: [ + { + name: "text", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für text ein.", + }, + }, + { + name: "color", + type: "text", + admin: { + description: "Hex or rgba color", + components: { Field: "@/src/payload/components/ColorPicker" }, + }, + }, + { + name: "delay", + type: "number", + defaultValue: 0, + admin: { + description: "Tragen Sie einen numerischen Wert für delay ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/MemeCardBlock.ts b/apps/web/src/payload/blocks/MemeCardBlock.ts new file mode 100644 index 0000000..c194a89 --- /dev/null +++ b/apps/web/src/payload/blocks/MemeCardBlock.ts @@ -0,0 +1,51 @@ +import { MintelBlock } from "./types"; +import type { Block } from "payload"; + +export const MemeCardBlock: MintelBlock = { + slug: "memeCard", + labels: { + singular: "Meme Card", + plural: "Meme Cards", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "MemeCard", + description: + 'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".', + usageExample: `
+ +
`, + }, + fields: [ + { + name: "template", + type: "text", + required: true, + admin: { + description: + "The template ID from memegen.link (e.g. 'drake', 'disastergirl')", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + { + name: "captions", + type: "textarea", + required: true, + admin: { + description: + "Pipe-separated captions for the meme (e.g. 'Legacy Code|Mintel Stack'). Maximum 6 words per line.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/MermaidBlock.ts b/apps/web/src/payload/blocks/MermaidBlock.ts new file mode 100644 index 0000000..9590548 --- /dev/null +++ b/apps/web/src/payload/blocks/MermaidBlock.ts @@ -0,0 +1,68 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const MermaidBlock: MintelBlock = { + slug: "mermaid", + labels: { + singular: "Mermaid", + plural: "Mermaids", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Mermaid", + description: + 'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".', + usageExample: `
+ 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).", + usageExample: '\n Mein System ist kein Kostenfaktor, sondern ein ROI-Beschleuniger.\n'", + }, + fields: [ + { + name: "text", + type: "textarea", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den mehrzeiligen Text für text ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/PerformanceChartBlock.ts b/apps/web/src/payload/blocks/PerformanceChartBlock.ts new file mode 100644 index 0000000..3310a69 --- /dev/null +++ b/apps/web/src/payload/blocks/PerformanceChartBlock.ts @@ -0,0 +1,36 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const PerformanceChartBlock: MintelBlock = { + slug: "performanceChart", + labels: { + singular: "Performance Chart", + plural: "Performance Charts", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "PerformanceChart", + description: + "A visual chart illustrating performance metrics (e.g. PageSpeed, TTFB) over time or in comparison. Use to emphasize technical improvements.", + usageExample: + '', + }, + fields: [ + { + name: "title", + type: "text", + defaultValue: "Website Performance", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für title ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts b/apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts new file mode 100644 index 0000000..1de5a5f --- /dev/null +++ b/apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts @@ -0,0 +1,40 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const PerformanceROICalculatorBlock: MintelBlock = { + slug: "performanceROICalculator", + labels: { + singular: "Performance R O I Calculator", + plural: "Performance R O I Calculators", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "PerformanceROICalculator", + description: + "Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.", + usageExample: "''", + }, + fields: [ + { + name: "baseConversionRate", + type: "number", + defaultValue: 2.5, + admin: { + description: + "Tragen Sie einen numerischen Wert für baseConversionRate ein.", + }, + }, + { + name: "monthlyVisitors", + type: "number", + defaultValue: 50000, + admin: { + description: + "Tragen Sie einen numerischen Wert für monthlyVisitors ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts b/apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts new file mode 100644 index 0000000..b3be1b1 --- /dev/null +++ b/apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts @@ -0,0 +1,115 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const PremiumComparisonChartBlock: MintelBlock = { + slug: "premiumComparisonChart", + labels: { + singular: "Premium Comparison Chart", + plural: "Premium Comparison Charts", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "PremiumComparisonChart", + description: + "Advanced chart for comparing performance metrics with industrial aesthetics.", + usageExample: + '\'\n \n\'', + }, + fields: [ + { + name: "direction", + type: "select", + options: [ + { label: "Up", value: "up" }, + { label: "Down", value: "down" }, + { label: "Left", value: "left" }, + { label: "Right", value: "right" }, + ], + defaultValue: "up", + admin: { description: "Wählen Sie eine Option für direction aus." }, + }, + { + name: "delay", + type: "number", + defaultValue: 0.1, + admin: { + description: "Tragen Sie einen numerischen Wert für delay ein.", + }, + }, + { + name: "content", + type: "richText", + editor: lexicalEditor({}), + required: true, + admin: { description: "Formatierter Textbereich für content." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts b/apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts new file mode 100644 index 0000000..35d7838 --- /dev/null +++ b/apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts @@ -0,0 +1,35 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const RevenueLossCalculatorBlock: MintelBlock = { + slug: "revenueLossCalculator", + labels: { + singular: "Revenue Loss Calculator", + plural: "Revenue Loss Calculators", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "RevenueLossCalculator", + description: + "Interactive calculator that estimates financial loss due to slow page load times. Use to build a business case for performance optimization.", + usageExample: "", + }, + fields: [ + { + name: "title", + type: "text", + defaultValue: "Performance Revenue Simulator", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für title ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/SectionBlock.ts b/apps/web/src/payload/blocks/SectionBlock.ts new file mode 100644 index 0000000..849f546 --- /dev/null +++ b/apps/web/src/payload/blocks/SectionBlock.ts @@ -0,0 +1,42 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; +import { lexicalEditor } from "@payloadcms/richtext-lexical"; + +export const SectionBlock: MintelBlock = { + slug: "mintelSection", + labels: { + singular: "Section Wrap", + plural: "Section Wraps", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "Section", + description: "Wraps a thematic section block with optional heading.", + usageExample: + "'
\n

Section Title

\n

Content here.

\n
'", + }, + fields: [ + { + name: "title", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für title ein.", + }, + }, + { + name: "content", + type: "richText", + editor: lexicalEditor({}), + required: true, + admin: { description: "Formatierter Textbereich für content." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/StatsDisplayBlock.ts b/apps/web/src/payload/blocks/StatsDisplayBlock.ts new file mode 100644 index 0000000..0b93c54 --- /dev/null +++ b/apps/web/src/payload/blocks/StatsDisplayBlock.ts @@ -0,0 +1,60 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const StatsDisplayBlock: MintelBlock = { + slug: "statsDisplay", + labels: { + singular: "Stats Display", + plural: "Stats Displays", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "StatsDisplay", + description: + "A single large stat card with prominent value, label, and optional subtext.", + usageExample: + '\'\'', + }, + fields: [ + { + name: "label", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + { + name: "value", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für value ein.", + }, + }, + { + name: "subtext", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für subtext ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/StatsGridBlock.ts b/apps/web/src/payload/blocks/StatsGridBlock.ts new file mode 100644 index 0000000..d132110 --- /dev/null +++ b/apps/web/src/payload/blocks/StatsGridBlock.ts @@ -0,0 +1,56 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const StatsGridBlock: MintelBlock = { + slug: "statsGrid", + labels: { + singular: "Stats Grid", + plural: "Stats Grids", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "StatsGrid", + description: + "Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.", + usageExample: + "''", + }, + fields: [ + { + name: "stats", + type: "array", + fields: [ + { + name: "label", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + { + name: "value", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für value ein.", + }, + }, + ], + admin: { description: "Fügen Sie Elemente zur Liste stats hinzu." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/TLDRBlock.ts b/apps/web/src/payload/blocks/TLDRBlock.ts new file mode 100644 index 0000000..43350d9 --- /dev/null +++ b/apps/web/src/payload/blocks/TLDRBlock.ts @@ -0,0 +1,35 @@ +import { MintelBlock } from "./types"; +import type { Block } from "payload"; + +export const TLDRBlock: MintelBlock = { + slug: "mintelTldr", + labels: { + singular: "TL;DR Block", + plural: "TL;DR Blocks", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "TLDR", + description: + "Presents a bite-sized summary of the article in a premium dark card. Use exactly once at the very beginning.", + usageExample: + "'\n Stabilität ist kein Zufall, sondern das Ergebnis einer Clean Code Strategie.\n'", + }, + fields: [ + { + name: "content", + type: "textarea", + required: true, + admin: { + description: "The summary content for the TLDR box.", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/TrackedLinkBlock.ts b/apps/web/src/payload/blocks/TrackedLinkBlock.ts new file mode 100644 index 0000000..d43ebc2 --- /dev/null +++ b/apps/web/src/payload/blocks/TrackedLinkBlock.ts @@ -0,0 +1,48 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const TrackedLinkBlock: MintelBlock = { + slug: "trackedLink", + labels: { + singular: "Tracked Link", + plural: "Tracked Links", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "TrackedLink", + description: + "A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.", + usageExample: + '\'Jetzt anfragen\'', + }, + fields: [ + { + name: "href", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für href ein." }, + }, + { + name: "label", + type: "text", + required: true, + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für label ein.", + }, + }, + { + name: "eventName", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für eventName ein." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/TwitterEmbedBlock.ts b/apps/web/src/payload/blocks/TwitterEmbedBlock.ts new file mode 100644 index 0000000..387ad85 --- /dev/null +++ b/apps/web/src/payload/blocks/TwitterEmbedBlock.ts @@ -0,0 +1,29 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const TwitterEmbedBlock: MintelBlock = { + slug: "twitterEmbed", + labels: { + singular: "Twitter (X) Embed", + plural: "Twitter (X) Embeds", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "TwitterEmbed", + description: + "Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.", + usageExample: + '\'\'', + }, + fields: [ + { + name: "url", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für url ein." }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/WaterfallChartBlock.ts b/apps/web/src/payload/blocks/WaterfallChartBlock.ts new file mode 100644 index 0000000..d182235 --- /dev/null +++ b/apps/web/src/payload/blocks/WaterfallChartBlock.ts @@ -0,0 +1,73 @@ +import { MintelBlock } from "./types"; + +import type { Block } from "payload"; + +export const WaterfallChartBlock: MintelBlock = { + slug: "waterfallChart", + labels: { + singular: "Waterfall Chart", + plural: "Waterfall Charts", + }, + admin: { + group: "MDX Components", + }, + ai: { + name: "WaterfallChart", + description: + "A timeline visualization of network requests (waterfall). Use to show loading sequences or bottlenecks. Labels auto-color coded by type (JS, HTML, IMG).", + usageExample: `\'', + }, + fields: [ + { + name: "videoId", + type: "text", + required: true, + admin: { description: "Geben Sie den Text für videoId ein." }, + }, + { + name: "title", + type: "text", + admin: { + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton", + ], + }, + description: "Geben Sie den Text für title ein.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/blocks/allBlocks.ts b/apps/web/src/payload/blocks/allBlocks.ts new file mode 100644 index 0000000..a35e82f --- /dev/null +++ b/apps/web/src/payload/blocks/allBlocks.ts @@ -0,0 +1,104 @@ +import { MintelBlock } from "./types"; +import { MemeCardBlock } from "./MemeCardBlock"; +import { MermaidBlock } from "./MermaidBlock"; +import { LeadMagnetBlock } from "./LeadMagnetBlock"; +import { ComparisonRowBlock } from "./ComparisonRowBlock"; +import { LeadParagraphBlock } from "./LeadParagraphBlock"; +import { ArticleBlockquoteBlock } from "./ArticleBlockquoteBlock"; +import { FAQSectionBlock } from "./FAQSectionBlock"; +import { StatsDisplayBlock } from "./StatsDisplayBlock"; +import { DiagramStateBlock } from "./DiagramStateBlock"; +import { DiagramTimelineBlock } from "./DiagramTimelineBlock"; +import { DiagramGanttBlock } from "./DiagramGanttBlock"; +import { DiagramPieBlock } from "./DiagramPieBlock"; +import { DiagramSequenceBlock } from "./DiagramSequenceBlock"; +import { DiagramFlowBlock } from "./DiagramFlowBlock"; +import { WaterfallChartBlock } from "./WaterfallChartBlock"; +import { PremiumComparisonChartBlock } from "./PremiumComparisonChartBlock"; +import { IconListBlock } from "./IconListBlock"; +import { StatsGridBlock } from "./StatsGridBlock"; +import { MetricBarBlock } from "./MetricBarBlock"; +import { CarouselBlock } from "./CarouselBlock"; +import { ImageTextBlock } from "./ImageTextBlock"; +import { RevenueLossCalculatorBlock } from "./RevenueLossCalculatorBlock"; +import { PerformanceChartBlock } from "./PerformanceChartBlock"; +import { PerformanceROICalculatorBlock } from "./PerformanceROICalculatorBlock"; +import { LoadTimeSimulatorBlock } from "./LoadTimeSimulatorBlock"; +import { ArchitectureBuilderBlock } from "./ArchitectureBuilderBlock"; +import { DigitalAssetVisualizerBlock } from "./DigitalAssetVisualizerBlock"; +import { TwitterEmbedBlock } from "./TwitterEmbedBlock"; +import { YouTubeEmbedBlock } from "./YouTubeEmbedBlock"; +import { LinkedInEmbedBlock } from "./LinkedInEmbedBlock"; +import { ExternalLinkBlock } from "./ExternalLinkBlock"; +import { TrackedLinkBlock } from "./TrackedLinkBlock"; +import { ArticleMemeBlock } from "./ArticleMemeBlock"; +import { MarkerBlock } from "./MarkerBlock"; +import { BoldNumberBlock } from "./BoldNumberBlock"; +import { WebVitalsScoreBlock } from "./WebVitalsScoreBlock"; +import { ButtonBlock } from "./ButtonBlock"; +import { ArticleQuoteBlock } from "./ArticleQuoteBlock"; +import { RevealBlock } from "./RevealBlock"; +import { SectionBlock } from "./SectionBlock"; +import { TLDRBlock } from "./TLDRBlock"; +import { HeadingBlock } from "./HeadingBlock"; +import { ParagraphBlock } from "./ParagraphBlock"; +import { H2Block } from "./H2Block"; +import { H3Block } from "./H3Block"; + +export const allBlocks: MintelBlock[] = [ + TLDRBlock, + HeadingBlock, + H2Block, + H3Block, + ParagraphBlock, + MemeCardBlock, + MermaidBlock, + LeadMagnetBlock, + ComparisonRowBlock, + LeadParagraphBlock, + ArticleBlockquoteBlock, + FAQSectionBlock, + StatsDisplayBlock, + DiagramStateBlock, + DiagramTimelineBlock, + DiagramGanttBlock, + DiagramPieBlock, + DiagramSequenceBlock, + DiagramFlowBlock, + WaterfallChartBlock, + PremiumComparisonChartBlock, + IconListBlock, + StatsGridBlock, + MetricBarBlock, + CarouselBlock, + ImageTextBlock, + RevenueLossCalculatorBlock, + PerformanceChartBlock, + PerformanceROICalculatorBlock, + LoadTimeSimulatorBlock, + ArchitectureBuilderBlock, + DigitalAssetVisualizerBlock, + TwitterEmbedBlock, + YouTubeEmbedBlock, + LinkedInEmbedBlock, + ExternalLinkBlock, + TrackedLinkBlock, + ArticleMemeBlock, + MarkerBlock, + BoldNumberBlock, + WebVitalsScoreBlock, + ButtonBlock, + ArticleQuoteBlock, + RevealBlock, + SectionBlock, +]; + +/** + * Payload 3.x silently drops blocks containing unknown properties. + * We strip `ai` and `render` so Payload gets clean Block objects. + */ +export const payloadBlocks = allBlocks.map(({ ai, render, ...block }) => block); + +export const allComponentDefinitions = allBlocks + .filter((block) => !!block.ai) + .map((block) => block.ai!); diff --git a/apps/web/src/payload/blocks/index.ts b/apps/web/src/payload/blocks/index.ts new file mode 100644 index 0000000..5148e53 --- /dev/null +++ b/apps/web/src/payload/blocks/index.ts @@ -0,0 +1,45 @@ +export * from "./ArchitectureBuilderBlock"; +export * from "./ArticleBlockquoteBlock"; +export * from "./ArticleMemeBlock"; +export * from "./ArticleQuoteBlock"; +export * from "./BoldNumberBlock"; +export * from "./ButtonBlock"; +export * from "./CarouselBlock"; +export * from "./ComparisonRowBlock"; +export * from "./DiagramFlowBlock"; +export * from "./DiagramGanttBlock"; +export * from "./DiagramPieBlock"; +export * from "./DiagramSequenceBlock"; +export * from "./DiagramStateBlock"; +export * from "./DiagramTimelineBlock"; +export * from "./DigitalAssetVisualizerBlock"; +export * from "./ExternalLinkBlock"; +export * from "./FAQSectionBlock"; +export * from "./IconListBlock"; +export * from "./ImageTextBlock"; +export * from "./LeadMagnetBlock"; +export * from "./LeadParagraphBlock"; +export * from "./LinkedInEmbedBlock"; +export * from "./LoadTimeSimulatorBlock"; +export * from "./MarkerBlock"; +export * from "./MemeCardBlock"; +export * from "./MermaidBlock"; +export * from "./MetricBarBlock"; +export * from "./PerformanceChartBlock"; +export * from "./PerformanceROICalculatorBlock"; +export * from "./PremiumComparisonChartBlock"; +export * from "./RevealBlock"; +export * from "./RevenueLossCalculatorBlock"; +export * from "./SectionBlock"; +export * from "./StatsDisplayBlock"; +export * from "./StatsGridBlock"; +export * from "./TrackedLinkBlock"; +export * from "./TwitterEmbedBlock"; +export * from "./WaterfallChartBlock"; +export * from "./WebVitalsScoreBlock"; +export * from "./YouTubeEmbedBlock"; +export * from "./HeadingBlock"; +export * from "./H2Block"; +export * from "./H3Block"; +export * from "./ParagraphBlock"; +export * from "./TLDRBlock"; diff --git a/apps/web/src/payload/blocks/types.ts b/apps/web/src/payload/blocks/types.ts new file mode 100644 index 0000000..858986a --- /dev/null +++ b/apps/web/src/payload/blocks/types.ts @@ -0,0 +1,8 @@ +import type { Block } from "payload"; +import type { ComponentDefinition } from "@mintel/content-engine"; +import type { ComponentType } from "react"; + +export type MintelBlock = Block & { + ai?: ComponentDefinition; + render?: ComponentType; +}; diff --git a/apps/web/src/payload/collections/ContextFiles.ts b/apps/web/src/payload/collections/ContextFiles.ts new file mode 100644 index 0000000..42b588a --- /dev/null +++ b/apps/web/src/payload/collections/ContextFiles.ts @@ -0,0 +1,55 @@ +import type { CollectionConfig } from "payload"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const ContextFiles: CollectionConfig = { + slug: "context-files", + labels: { + singular: "Context File", + plural: "Context Files", + }, + admin: { + useAsTitle: "filename", + defaultColumns: ["filename", "updatedAt"], + }, + access: { + read: () => true, // Needed for server actions to fetch context + create: () => true, + update: () => true, + delete: () => true, + }, + hooks: { + afterChange: [ + ({ doc, operation }) => { + // Potential future: sync back to disk? + // For now, let's keep it simple as a CMS-first feature. + }, + ], + }, + fields: [ + { + name: "filename", + type: "text", + required: true, + unique: true, + admin: { + description: + "Exact filename (e.g. 'strategy.md'). The system uses this to identify the document during prompt generation.", + }, + }, + { + name: "content", + type: "textarea", + required: true, + admin: { + rows: 25, + description: "The raw markdown/text content of the document.", + style: { fontFamily: "monospace" }, + }, + }, + ], +}; diff --git a/apps/web/docs/ABOUT.md b/apps/web/src/payload/collections/ContextFiles/seed/ABOUT.md similarity index 55% rename from apps/web/docs/ABOUT.md rename to apps/web/src/payload/collections/ContextFiles/seed/ABOUT.md index 0e93225..2d4ead9 100644 --- a/apps/web/docs/ABOUT.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/ABOUT.md @@ -4,12 +4,12 @@ Ich baue Websites und Systeme seit über 15 Jahren. Nicht weil ich Websites so liebe – sondern weil ich es hasse, wenn Dinge nicht funktionieren. In diesen 15 Jahren habe ich: - • Agenturen von innen gesehen - • Konzerne erlebt - • Startups aufgebaut - • Marketingversprechen zerlegt - • Systeme repariert, die „fertig“ waren - • und gelernt, wie man Dinge baut, die einfach laufen +• Agenturen von innen gesehen +• Konzerne erlebt +• Startups aufgebaut +• Marketingversprechen zerlegt +• Systeme repariert, die „fertig“ waren +• und gelernt, wie man Dinge baut, die einfach laufen Heute mache ich das ohne Agentur-Zwischenschichten. Direkt. Sauber. Verantwortlich. @@ -35,35 +35,35 @@ Also habe ich mir angewöhnt, Verantwortung zu übernehmen. Warum ich Websites wie Systeme baue Ich war viele Jahre Senior Developer in Firmen, in denen: - • Millionenumsätze dranhingen - • Fehler teuer waren - • Performance nicht optional war - • Sicherheit kein Nice-to-Have war - • „kurz mal ändern“ trotzdem passieren musste +• Millionenumsätze dranhingen +• Fehler teuer waren +• Performance nicht optional war +• Sicherheit kein Nice-to-Have war +• „kurz mal ändern“ trotzdem passieren musste Das prägt. Deshalb sind meine Websites: - • schnell - • stabil - • boring (im besten Sinne) - • erweiterbar - • wartungsarm - • und nicht abhängig von Plugins oder Agenturen +• schnell +• stabil +• boring (im besten Sinne) +• erweiterbar +• wartungsarm +• und nicht abhängig von Plugins oder Agenturen ⸻ Ich habe beide Seiten gesehen Ich war: - • Webdesigner - • Entwickler - • Marketing - • Vertrieb - • Agentur - • Inhouse - • Dienstleister - • Unternehmer +• Webdesigner +• Entwickler +• Marketing +• Vertrieb +• Agentur +• Inhouse +• Dienstleister +• Unternehmer Das heißt: @@ -77,36 +77,36 @@ und was sie nicht brauchen. Was Kunden davon haben Sie bekommen: - • keinen Projektmanager - • keinen Prozess - • kein Team - • kein Ticket - • kein CMS-Drama +• keinen Projektmanager +• keinen Prozess +• kein Team +• kein Ticket +• kein CMS-Drama Sie bekommen: - • eine Person - • eine Verantwortung - • ein Ergebnis +• eine Person +• eine Verantwortung +• ein Ergebnis ⸻ Ein kurzer Überblick (ohne Lebenslauf-Gefühl) Ich habe u. a. gearbeitet bei: - • Agenturen - • E-Commerce-Plattformen - • SaaS-Firmen - • Marketing-Teams - • internationalen Unternehmen - • Mittelständlern - • und Konzernen +• Agenturen +• E-Commerce-Plattformen +• SaaS-Firmen +• Marketing-Teams +• internationalen Unternehmen +• Mittelständlern +• und Konzernen Als: - • Web Designer - • Frontend Developer - • Software Developer - • Senior Developer - • und später Gründer +• Web Designer +• Frontend Developer +• Software Developer +• Senior Developer +• und später Gründer Das Ergebnis daraus ist nicht ein Titel. Sondern eine Arbeitsweise. diff --git a/apps/web/docs/AGBS.md b/apps/web/src/payload/collections/ContextFiles/seed/AGBS.md similarity index 66% rename from apps/web/docs/AGBS.md rename to apps/web/src/payload/collections/ContextFiles/seed/AGBS.md index 36bfe66..690ae5f 100644 --- a/apps/web/docs/AGBS.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/AGBS.md @@ -13,16 +13,16 @@ Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertrags 2. Vertragsgegenstand Der Auftragnehmer erbringt Dienstleistungen im Bereich: - • Webentwicklung - • technische Umsetzung digitaler Systeme - • Funktionen, Schnittstellen und Automatisierungen - • Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart +• Webentwicklung +• technische Umsetzung digitaler Systeme +• Funktionen, Schnittstellen und Automatisierungen +• Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch: - • einen wirtschaftlichen Erfolg - • bestimmte Umsätze, Conversions oder Reichweiten - • Suchmaschinen-Rankings - • rechtliche oder geschäftliche Ergebnisse +• einen wirtschaftlichen Erfolg +• bestimmte Umsätze, Conversions oder Reichweiten +• Suchmaschinen-Rankings +• rechtliche oder geschäftliche Ergebnisse ⸻ @@ -31,10 +31,10 @@ Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere: - • Texte, Bilder, Videos, Produktdaten - • Freigaben und Feedback - • Zugangsdaten - • rechtlich erforderliche Inhalte (z. B. Impressum, Datenschutzerklärung) +• Texte, Bilder, Videos, Produktdaten +• Freigaben und Feedback +• Zugangsdaten +• rechtlich erforderliche Inhalte (z. B. Impressum, Datenschutzerklärung) Verzögerungen oder Unterlassungen der Mitwirkung führen zu einer entsprechenden Verschiebung aller Termine. Hieraus entstehen keine Schadensersatz- oder Minderungsansprüche. @@ -52,8 +52,8 @@ Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als v 5. Abnahme Die Leistung gilt als abgenommen, wenn: - • der Auftraggeber sie produktiv nutzt oder - • innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. +• der Auftraggeber sie produktiv nutzt oder +• innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar. @@ -64,11 +64,11 @@ Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für: - • entgangenen Gewinn - • Umsatzausfälle - • Datenverlust - • Betriebsunterbrechungen - • mittelbare oder Folgeschäden +• entgangenen Gewinn +• Umsatzausfälle +• Datenverlust +• Betriebsunterbrechungen +• mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig. @@ -85,18 +85,18 @@ Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen (z. B. Die Betriebs- und Pflegeleistung ist fester Bestandteil der laufenden Leistungen des Auftragnehmers. Sie umfasst ausschließlich: - • Sicherstellung des technischen Betriebs der Website - • Wartung, Updates und Fehlerbehebung der bestehenden Systeme - • Austausch, Korrektur oder Aktualisierung bereits vorhandener Inhalte - • Pflege bestehender Datensätze ohne Änderung oder Erweiterung der Datenstruktur +• Sicherstellung des technischen Betriebs der Website +• Wartung, Updates und Fehlerbehebung der bestehenden Systeme +• Austausch, Korrektur oder Aktualisierung bereits vorhandener Inhalte +• Pflege bestehender Datensätze ohne Änderung oder Erweiterung der Datenstruktur Nicht Bestandteil der Betriebs- und Pflegeleistung sind insbesondere: - • regelmäßige oder fortlaufende Erstellung neuer Inhalte +• regelmäßige oder fortlaufende Erstellung neuer Inhalte (z. B. Blogartikel, News, Produkte, Seiten) - • redaktionelle Tätigkeiten oder Content-Produktion - • strategische Inhaltsplanung oder Marketingmaßnahmen - • Aufbau neuer Seiten, Features, Funktionen oder Datenmodelle - • Serien-, Massen- oder Dauerpflege +• redaktionelle Tätigkeiten oder Content-Produktion +• strategische Inhaltsplanung oder Marketingmaßnahmen +• Aufbau neuer Seiten, Features, Funktionen oder Datenmodelle +• Serien-, Massen- oder Dauerpflege (z. B. tägliche oder wiederkehrende Inhaltserstellung) Die Betriebs- und Pflegeleistung dient ausschließlich der Instandhaltung, Sicherheit und Funktionsfähigkeit der bestehenden Website. @@ -108,9 +108,9 @@ Leistungen, die darüber hinausgehen, gelten als Neuentwicklung oder Inhaltserst 8. Drittanbieter & externe Systeme Der Auftragnehmer übernimmt keine Verantwortung für: - • Leistungen, Ausfälle oder Änderungen externer Dienste - • APIs, Schnittstellen oder Plattformen Dritter - • rechtliche oder technische Änderungen fremder Systeme +• Leistungen, Ausfälle oder Änderungen externer Dienste +• APIs, Schnittstellen oder Plattformen Dritter +• rechtliche oder technische Änderungen fremder Systeme Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden. @@ -119,9 +119,9 @@ Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schni 9. Inhalte & Rechtliches Der Auftraggeber ist allein verantwortlich für: - • Inhalte der Website - • rechtliche Konformität (DSGVO, Urheberrecht, Wettbewerbsrecht etc.) - • bereitgestellte Daten und Medien +• Inhalte der Website +• rechtliche Konformität (DSGVO, Urheberrecht, Wettbewerbsrecht etc.) +• bereitgestellte Daten und Medien Der Auftragnehmer übernimmt keine rechtliche Prüfung. @@ -134,9 +134,9 @@ Alle Preise verstehen sich netto zuzüglich gesetzlicher Umsatzsteuer. Rechnungen sind, sofern nicht anders vereinbart, innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt: - • Leistungen auszusetzen - • Systeme offline zu nehmen - • laufende Arbeiten zu stoppen +• Leistungen auszusetzen +• Systeme offline zu nehmen +• laufende Arbeiten zu stoppen ⸻ @@ -151,4 +151,4 @@ Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Woch Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist – soweit zulässig – der Sitz des Auftragnehmers. -Sollte eine Bestimmung dieser AGB unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt. \ No newline at end of file +Sollte eine Bestimmung dieser AGB unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt. diff --git a/apps/web/docs/AUDIENCE.md b/apps/web/src/payload/collections/ContextFiles/seed/AUDIENCE.md similarity index 100% rename from apps/web/docs/AUDIENCE.md rename to apps/web/src/payload/collections/ContextFiles/seed/AUDIENCE.md diff --git a/apps/web/docs/AUTOMATION.md b/apps/web/src/payload/collections/ContextFiles/seed/AUTOMATION.md similarity index 83% rename from apps/web/docs/AUTOMATION.md rename to apps/web/src/payload/collections/ContextFiles/seed/AUTOMATION.md index c5f07ac..6ad4ac8 100644 --- a/apps/web/docs/AUTOMATION.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/AUTOMATION.md @@ -1,11 +1,13 @@ -# Routine Automation -*Kleine Helfer, die den Alltag deutlich entlasten* +# Routine Automation -In vielen mittelständischen Unternehmen fressen wiederkehrende Aufgaben Monat für Monat unzählige Stunden: -- Daten aus Dokumenten abtippen -- Formulare von Hand ausfüllen -- Angebote, Berichte oder Bestätigungen manuell anpassen -- Eingehende Anfragen immer wieder neu prüfen und bearbeiten +_Kleine Helfer, die den Alltag deutlich entlasten_ + +In vielen mittelständischen Unternehmen fressen wiederkehrende Aufgaben Monat für Monat unzählige Stunden: + +- Daten aus Dokumenten abtippen +- Formulare von Hand ausfüllen +- Angebote, Berichte oder Bestätigungen manuell anpassen +- Eingehende Anfragen immer wieder neu prüfen und bearbeiten Das ist keine wertschöpfende Arbeit. Das ist Routine, die teuer ist, Fehler produziert und gute Mitarbeiter davon abhält, sich um das zu kümmern, was wirklich Umsatz bringt. @@ -34,10 +36,10 @@ Kein großes Projekt. Kein monatliches Tool-Abo. Kein „lernen Sie das neue Sys ### Der echte Wert für Sie -- 30–80 % weniger Zeit bei Routineaufgaben → Ihre Teams konzentrieren sich aufs Wesentliche -- Weniger Fehler & Rückfragen → einheitlicher, professioneller Output -- Schnellere Reaktion auf Kunden → Konfiguratoren & KI-Einlesen liefern sofort Infos -- Amortisation oft schon nach wenigen Wochen oder Dutzend Nutzungen +- 30–80 % weniger Zeit bei Routineaufgaben → Ihre Teams konzentrieren sich aufs Wesentliche +- Weniger Fehler & Rückfragen → einheitlicher, professioneller Output +- Schnellere Reaktion auf Kunden → Konfiguratoren & KI-Einlesen liefern sofort Infos +- Amortisation oft schon nach wenigen Wochen oder Dutzend Nutzungen - Nutzt, was Sie bereits haben: Website, Excel, Mail, Scanner-App ### Was ich **nicht** mache @@ -48,16 +50,17 @@ Nur smarte Abkürzungen bei Routine – der Rest bleibt in Ihren bewährten Tool ### Ich kann Ihnen helfen, wenn Sie mit diesen typischen Problemen kämpfen -- „Wir tippen immer noch Daten aus gescannten Dokumenten oder handschriftlichen Notizen ab.“ -- „Angebote, Berichte oder Protokolle dauern ewig, weil alles von Hand angepasst wird.“ -- „Kunden fragen ständig dasselbe – wir antworten jedes Mal manuell.“ -- „Excel-Tabellen und Berechnungen werden ständig neu gemacht und gehen kaputt.“ +- „Wir tippen immer noch Daten aus gescannten Dokumenten oder handschriftlichen Notizen ab.“ +- „Angebote, Berichte oder Protokolle dauern ewig, weil alles von Hand angepasst wird.“ +- „Kunden fragen ständig dasselbe – wir antworten jedes Mal manuell.“ +- „Excel-Tabellen und Berechnungen werden ständig neu gemacht und gehen kaputt.“ - „Bis wir eine realistische Schätzung oder ein Angebot raus haben, vergeht zu viel Zeit.“ Schreiben Sie mir einfach einen kurzen Satz zu Ihrem größten Zeitfresser in diesem Bereich. -Ich antworte meist innerhalb von 1–2 Tagen: -- Ist das machbar? Ja/Nein -- Ca. wie viel Aufwand (meist 3–15 Stunden) & Preisrahmen +Ich antworte meist innerhalb von 1–2 Tagen: + +- Ist das machbar? Ja/Nein +- Ca. wie viel Aufwand (meist 3–15 Stunden) & Preisrahmen - Was Sie realistisch sparen können (Zeit, Nerven, Fehler) Passt es → baue ich es. @@ -70,4 +73,4 @@ Sondern gezielte Entlastung bei den Dingen, die jeden Tag Zeit und Nerven kosten Mehr Zeit. Weniger Frust. Besserer Output. Und das Gefühl: „Das läuft jetzt einfach.“ -Wenn bei Ihnen gerade etwas „von Hand gemacht wird“ oder „ewig dauert“ – Ich sage Ihnen, ob und wie schnell man das sinnvoll digitalisieren kann. \ No newline at end of file +Wenn bei Ihnen gerade etwas „von Hand gemacht wird“ oder „ewig dauert“ – Ich sage Ihnen, ob und wie schnell man das sinnvoll digitalisieren kann. diff --git a/apps/web/docs/CONTENT_RULES.md b/apps/web/src/payload/collections/ContextFiles/seed/CONTENT_RULES.md similarity index 99% rename from apps/web/docs/CONTENT_RULES.md rename to apps/web/src/payload/collections/ContextFiles/seed/CONTENT_RULES.md index 6b14635..397a6f1 100644 --- a/apps/web/docs/CONTENT_RULES.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/CONTENT_RULES.md @@ -8,6 +8,7 @@ - Nicht mehr als 5–6 visuelle Komponenten pro Blog-Post insgesamt ### Erlaubte visuelle Komponenten + - `Mermaid` / `DiagramFlow` / `DiagramSequence` — für Prozesse und Architektur - `ArticleMeme` — echte Meme-Bilder (memegen.link), kurze und knackige Texte - `BoldNumber` — einzelne Hero-Statistik mit Quelle @@ -18,6 +19,7 @@ - `ComparisonRow` — für Vorher/Nachher-Vergleiche ### Verboten + - `MemeCard` (text-basierte Memes) — nur echte Bild-Memes verwenden - AI-generierte Bilder im Content — nur Thumbnails erlaubt - `DiagramPie` — vermeiden, zu generisch diff --git a/apps/web/docs/ESTIMATION_GUIDE.md b/apps/web/src/payload/collections/ContextFiles/seed/ESTIMATION_GUIDE.md similarity index 96% rename from apps/web/docs/ESTIMATION_GUIDE.md rename to apps/web/src/payload/collections/ContextFiles/seed/ESTIMATION_GUIDE.md index fc300a4..6c6994e 100644 --- a/apps/web/docs/ESTIMATION_GUIDE.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/ESTIMATION_GUIDE.md @@ -5,6 +5,7 @@ This guide explains how to use the automated estimation system to generate profe ## 🛠 Basic Usage The primary entry point is the `ai-estimate` script. It orchestrates a 6-pass AI consultation: + 1. **Fact Extraction**: Identifying company data and project scope. 2. **Feature Deep-Dive**: Generating technical justifications for items. 3. **Strategic Content**: Creating the Briefing Analysis and Strategic Vision. @@ -15,19 +16,25 @@ The primary entry point is the `ai-estimate` script. It orchestrates a 6-pass AI ### Generating an Estimation from Scratch #### 1. With a Website URL (Recommended) + Providing a URL allows the system to crawl the existing site to understand the "Company DNA" and services. + ```bash npm run ai-estimate -- "Relaunch der Website mit Fokus auf B2B Leads" --url https://example.com ``` #### 2. From a Text File + If you have a long briefing in a `.txt` file: + ```bash npm run ai-estimate -- @briefing.txt --url https://example.com ``` #### 3. Text-Only (No URL) + If no URL is provided, the system relies entirely on your briefing text. + ```bash npm run ai-estimate -- "Neuentwicklung eines Portals für XYZ" ``` @@ -39,13 +46,17 @@ npm run ai-estimate -- "Neuentwicklung eines Portals für XYZ" The system can generate two types of documents: ### 1. Full Quote (Default) + Includes everything: Front Page, Briefing Analysis, Vision, Sitemap, Technical Principles, Detailed Pricing, Roadmap, and Legal Terms (AGB). + ```bash npm run ai-estimate -- "Project Briefing" ``` ### 2. Estimation Only + A condensed version excluding legal terms and deep technical principles. Focuses purely on the strategic fit and the price. + ```bash npm run ai-estimate -- "Project Briefing" --estimation ``` @@ -57,22 +68,27 @@ npm run ai-estimate -- "Project Briefing" --estimation To save costs and time, all AI responses and crawl results are cached in the `.cache` directory. ### Regenerating with Cached Data + If you run the same command again (identical briefing and URL), the system will use the cached results and won't call the AI APIs again. This is useful if you want to tweak the PDF layout without spending tokens. ### Forcing a Refresh + To ignore the cache and get a fresh AI consultation: + ```bash npm run ai-estimate -- "Project Briefing" --clear-cache ``` ### Manual Tweaking (JSON State) -Every run saves a detailed state to `out/estimations/json/[Company]_[Timestamp].json`. + +Every run saves a detailed state to `out/estimations/json/[Company]_[Timestamp].json`. If you want to manually edit the AI's results (e.g., fix a typo in the sitemap or description), you can edit this JSON file and then regenerate the PDF from it: ```bash npm run ai-estimate -- --json out/estimations/json/Your_Project.json ``` -*(Add `--estimation` if you want the condensed version).* + +_(Add `--estimation` if you want the condensed version)._ --- diff --git a/apps/web/docs/KEYWORDS.md b/apps/web/src/payload/collections/ContextFiles/seed/KEYWORDS.md similarity index 99% rename from apps/web/docs/KEYWORDS.md rename to apps/web/src/payload/collections/ContextFiles/seed/KEYWORDS.md index bae8929..8d14521 100644 --- a/apps/web/docs/KEYWORDS.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/KEYWORDS.md @@ -3,6 +3,7 @@ Diese 50 Keywords sind strategisch ausgewählt, um entscheidungsfreudige B2B-Kunden (Geschäftsführer, CMOs, CTOs) anzuziehen, die nach Premium-Lösungen, Performance-Architekturen und messbarem ROI suchen. Sie meiden den "Billig-Sektor" (wie "Wordpress Website günstig") und fokussieren sich auf High-End Tech und Business-Impact. ## Kategorie 1: Enterprise Performance & Core Web Vitals (Pain Point: Sichtbarkeit & Speed) + 1. "Core Web Vitals Optimierung Agentur" 2. "PageSpeed Insights 100 erreichen B2B" 3. "Website Ladezeit verbessern Conversion Rate" @@ -15,6 +16,7 @@ Diese 50 Keywords sind strategisch ausgewählt, um entscheidungsfreudige B2B-Kun 10. "Enterprise SEO Performance Tech Stack" ## Kategorie 2: Modern Tech Stack & Headless (Pain Point: Skalierbarkeit & Legacy-Code) + 11. "Next.js Agentur Deutschland" 12. "Headless CMS Migration B2B" 13. "Vercel Hosting Enterprise Architektur" @@ -27,6 +29,7 @@ Diese 50 Keywords sind strategisch ausgewählt, um entscheidungsfreudige B2B-Kun 20. "Microservices Web Architektur" ## Kategorie 3: B2B Conversion & Digital ROI (Pain Point: Umsatz & Leads) + 21. "B2B Website Relaunch Strategie" 22. "Digital Architect Consulting B2B" 23. "Conversion Rate Optimierung Tech Stack" @@ -39,6 +42,7 @@ Diese 50 Keywords sind strategisch ausgewählt, um entscheidungsfreudige B2B-Kun 30. "UX/UI Architektur für hohe Conversion" ## Kategorie 4: Infrastruktur, Sicherheit & Skalierbarkeit (Pain Point: Ausfälle & Security) + 31. "Cloudflare Enterprise Setup Agentur" 32. "DDoS Schutz Web Architektur B2B" 33. "Serverless Architecture Vorteile" @@ -51,6 +55,7 @@ Diese 50 Keywords sind strategisch ausgewählt, um entscheidungsfreudige B2B-Kun 40. "CI/CD Pipeline Webentwicklung B2B" ## Kategorie 5: Spezifische Lösungen & "Digital Architect" Keywords (Nischen-Autorität) + 41. "Digital Architect Agentur Deutschland" 42. "Mittelstand Digitalisierung Web-Infrastruktur" 43. "Industrie 4.0 B2B Website" diff --git a/apps/web/docs/LANDING_PAGE.md b/apps/web/src/payload/collections/ContextFiles/seed/LANDING_PAGE.md similarity index 66% rename from apps/web/docs/LANDING_PAGE.md rename to apps/web/src/payload/collections/ContextFiles/seed/LANDING_PAGE.md index a1a7f5c..efd6521 100644 --- a/apps/web/docs/LANDING_PAGE.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/LANDING_PAGE.md @@ -23,12 +23,12 @@ Ich sorge dafür, dass es funktioniert. Für wen das ist Für Unternehmen, die: - • regelmäßig Änderungen an Website oder Systemen brauchen - • keine Lust auf Agenturen haben - • kein CMS anfassen wollen - • keine Tickets schreiben möchten - • keinen Entwickler einstellen wollen - • und wollen, dass Dinge einfach erledigt werden +• regelmäßig Änderungen an Website oder Systemen brauchen +• keine Lust auf Agenturen haben +• kein CMS anfassen wollen +• keine Tickets schreiben möchten +• keinen Entwickler einstellen wollen +• und wollen, dass Dinge einfach erledigt werden Wenn bei Ihnen öfter der Satz fällt: @@ -151,42 +151,42 @@ Ohne Theater. Was ich konkret umsetze Websites - • neue Websites (klarer Standard, kein Chaos) - • bestehende Websites übernehmen - • Seiten ändern oder ergänzen - • Performance & SEO - • Hosting & Betrieb (inklusive) +• neue Websites (klarer Standard, kein Chaos) +• bestehende Websites übernehmen +• Seiten ändern oder ergänzen +• Performance & SEO +• Hosting & Betrieb (inklusive) ⸻ Funktionen & Systeme - • Produktbereiche - • Blogs, News, Jobs - • Formulare (auch mehrstufig) - • Downloads - • Suche & Filter - • PDF-Generatoren - • API-Ausgaben & Daten-Sync - • Sonderlogik +• Produktbereiche +• Blogs, News, Jobs +• Formulare (auch mehrstufig) +• Downloads +• Suche & Filter +• PDF-Generatoren +• API-Ausgaben & Daten-Sync +• Sonderlogik ⸻ Interne Tools - • kleine Inhouse-Tools - • Excel ersetzen - • Importe & Exporte - • Automatisierung - • Dinge, die nerven → weg +• kleine Inhouse-Tools +• Excel ersetzen +• Importe & Exporte +• Automatisierung +• Dinge, die nerven → weg ⸻ Was ich bewusst nicht mache - • keine CMS-Schulungen - • keine Agentur-Workshops - • keine Ticketsysteme - • keine Stundenabrechnung für Websites - • kein Overhead - • keine Prozessshows +• keine CMS-Schulungen +• keine Agentur-Workshops +• keine Ticketsysteme +• keine Stundenabrechnung für Websites +• kein Overhead +• keine Prozessshows Das ist kein Mangel. Das ist der Vorteil. @@ -199,34 +199,34 @@ Ich arbeite mit festen Leistungen und Fixpreisen. Keine Abos. Keine Überraschungen. Grundlage - • Website-Basis → 6.000 € - • Hosting & Betrieb → 120 € / Monat (inkl. 20 GB Medien) +• Website-Basis → 6.000 € +• Hosting & Betrieb → 120 € / Monat (inkl. 20 GB Medien) Entwicklung - • Seite → 800 € - • Feature (System) → 2.000 € - • Funktion (Logik) → 1.000 € +• Seite → 800 € +• Feature (System) → 2.000 € +• Funktion (Logik) → 1.000 € Inhalte - • Neuer Datensatz → 400 € - • Datensatz anpassen → 200 € +• Neuer Datensatz → 400 € +• Datensatz anpassen → 200 € Optional - • CMS-Einrichtung → 1.500 € - • CMS-Anbindung pro Feature → 800 € - • Speichererweiterung → +10 € / 10 GB +• CMS-Einrichtung → 1.500 € +• CMS-Anbindung pro Feature → 800 € +• Speichererweiterung → +10 € / 10 GB Sie wissen vorher, was es kostet. Immer. ⸻ Warum Kunden bleiben - • Dinge passieren schnell - • Aufgaben verschwinden wirklich - • kein Erklären - • kein Nachfassen - • kein Projektstress - • kein Agentur-Zirkus +• Dinge passieren schnell +• Aufgaben verschwinden wirklich +• kein Erklären +• kein Nachfassen +• kein Projektstress +• kein Agentur-Zirkus Kurz: Ruhe. @@ -235,4 +235,4 @@ Kurz: Ruhe. Interesse? Schreiben Sie mir einfach, was Sie brauchen. -Ich sage Ihnen ehrlich, ob ich es mache – und was es kostet. \ No newline at end of file +Ich sage Ihnen ehrlich, ob ich es mache – und was es kostet. diff --git a/apps/web/docs/MAINTENANCE.md b/apps/web/src/payload/collections/ContextFiles/seed/MAINTENANCE.md similarity index 100% rename from apps/web/docs/MAINTENANCE.md rename to apps/web/src/payload/collections/ContextFiles/seed/MAINTENANCE.md diff --git a/apps/web/docs/PRICING.md b/apps/web/src/payload/collections/ContextFiles/seed/PRICING.md similarity index 61% rename from apps/web/docs/PRICING.md rename to apps/web/src/payload/collections/ContextFiles/seed/PRICING.md index d7a7ca0..1415e07 100644 --- a/apps/web/docs/PRICING.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/PRICING.md @@ -7,13 +7,13 @@ Basis 4.000 € einmalig Die Grundlage für jede Website: - • Projekt-Setup & Infrastruktur - • Hosting-Bereitstellung - • Grundstruktur & Design-Vorlage - • technisches SEO-Basics - • Analytics (mit automatischem Mail-Report) - • Testing, Staging, Production Umgebung - • Livegang +• Projekt-Setup & Infrastruktur +• Hosting-Bereitstellung +• Grundstruktur & Design-Vorlage +• technisches SEO-Basics +• Analytics (mit automatischem Mail-Report) +• Testing, Staging, Production Umgebung +• Livegang Enthält keine Seiten, Inhalte oder Funktionen. @@ -37,12 +37,12 @@ Feature (System) Ein in sich geschlossenes System mit Datenstruktur, Darstellung und Pflegefähigkeit. Typische Beispiele: - • Produktbereich - • Blog - • News - • Jobs - • Referenzen - • Events +• Produktbereich +• Blog +• News +• Jobs +• Referenzen +• Events Ein Feature erzeugt ein Datenmodell, Übersichten & Detailseiten. @@ -53,15 +53,15 @@ Funktion (Logik) 800 € / Funktion Funktionen liefern Logik und Interaktion, z. B.: - • Kontaktformular - • Mailversand - • Suche - • Filter - • Mehrsprachigkeit (System) - • PDF-Export von Daten - • API-Anbindungen (z. B. Produkt-Sync) - • Redirect-Logik - • Automatisierte Aufgaben +• Kontaktformular +• Mailversand +• Suche +• Filter +• Mehrsprachigkeit (System) +• PDF-Export von Daten +• API-Anbindungen (z. B. Produkt-Sync) +• Redirect-Logik +• Automatisierte Aufgaben Jede Funktion ist ein klar umrissener Logikbaustein. @@ -76,10 +76,10 @@ Visuelle Inszenierung 1.500 € / Abschnitt Erweiterte Gestaltung: - • Hero-Story - • visuelle Abläufe - • Scroll-Effekte - • speziell inszenierte Sektionen +• Hero-Story +• visuelle Abläufe +• Scroll-Effekte +• speziell inszenierte Sektionen ⸻ @@ -88,9 +88,9 @@ Komplexe Interaktion 1.500 € / Interaktion Dargestellte, interaktive UI-Erlebnisse: - • Konfiguratoren - • Live-Previews - • mehrstufige Auswahlprozesse +• Konfiguratoren +• Live-Previews +• mehrstufige Auswahlprozesse (Nutzt deine bestehenden Bausteine, gehört aber zur Entwicklung.) @@ -103,11 +103,11 @@ Neuer Datensatz 200 € / Stück Beispiele: - • Produkt - • Blogpost - • News - • Case - • Job +• Produkt +• Blogpost +• News +• Case +• Job Datensätze enthalten Inhalte mit Text, Medien, Metadaten. @@ -116,9 +116,9 @@ Datensätze enthalten Inhalte mit Text, Medien, Metadaten. Datensatz anpassen 200 € / Stück - • Textupdates - • Bildwechsel - • Feldänderungen (ohne Schemaänderung) +• Textupdates +• Bildwechsel +• Feldänderungen (ohne Schemaänderung) ⸻ @@ -129,11 +129,11 @@ Hosting & Betrieb 12 Monate = 3.000 € Sichert: - • Webhosting & Verfügbarkeit - • Sicherheitsupdates - • Backups & Monitoring - • Analytics inkl. Reports - • Medien-Speicher (Standard bis 20 GB) +• Webhosting & Verfügbarkeit +• Sicherheitsupdates +• Backups & Monitoring +• Analytics inkl. Reports +• Medien-Speicher (Standard bis 20 GB) ⸻ @@ -156,12 +156,12 @@ CMS-Einrichtung 1.500 € einmalig Einrichtung eines Headless CMS: - • Struktur - • Rollen - • Rechte - • API-Anbindung - • Deployment - • kurze Einführung +• Struktur +• Rollen +• Rechte +• API-Anbindung +• Deployment +• kurze Einführung ⸻ @@ -181,11 +181,11 @@ Entwicklung nach Zeit 120 € / Stunde Für: - • interne Tools - • Prozesslogik - • Workflows - • Automatisierungen - • alles, was Zustände und Abläufe beinhaltet +• interne Tools +• Prozesslogik +• Workflows +• Automatisierungen +• alles, was Zustände und Abläufe beinhaltet (Kein Fixpreis, weil scope offen ist.) @@ -198,13 +198,13 @@ API-Schnittstelle / Daten-Sync 800 € / Zielsystem Synchronisation zu externem System (Push): - • Produkt-Sync - • CRM / ERP / Stripe / sonstige API +• Produkt-Sync +• CRM / ERP / Stripe / sonstige API Nicht enthalten: - • Betrieb fremder Systeme - • Echtzeit-Pull-Mechanismen - • Zustandsabhängige Syncs +• Betrieb fremder Systeme +• Echtzeit-Pull-Mechanismen +• Zustandsabhängige Syncs ⸻ @@ -221,14 +221,14 @@ Apps = Stunden (Prozesse & Systeme außerhalb der Website) ⸻ Leistungsausschlüsse (kurz und klar) - • Kein Betrieb von Mail-Servern - • Keine Logistik, kein Shop-Checkout - • Kein Drittanbieter-Betrieb - • Keine permanente Überwachung fremder Systeme +• Kein Betrieb von Mail-Servern +• Keine Logistik, kein Shop-Checkout +• Kein Drittanbieter-Betrieb +• Keine permanente Überwachung fremder Systeme ⸻ Satz für Kundenkommunikation Ich baue digitale Systeme mit klaren Preisen und Ergebnissen – -keine Stunden, keine Überraschungen. \ No newline at end of file +keine Stunden, keine Überraschungen. diff --git a/apps/web/src/payload/collections/ContextFiles/seed/PRINCIPLES.md b/apps/web/src/payload/collections/ContextFiles/seed/PRINCIPLES.md new file mode 100644 index 0000000..61a11c5 --- /dev/null +++ b/apps/web/src/payload/collections/ContextFiles/seed/PRINCIPLES.md @@ -0,0 +1,43 @@ +Prinzipien + +Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren. + +⸻ + +1. Volle Preis-Transparenz + Alle Kosten sind offen und nachvollziehbar. + Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. + Jeder Kunde sieht genau, wofür er bezahlt. + +⸻ + +2. Quellcode & Projektzugang + Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur. + Damit kann jeder andere Entwickler problemlos weiterarbeiten. + Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar. + +⸻ + +3. Best Practices & saubere Technik + Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein. + Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben – langfristig. + +⸻ + +4. Verantwortung & Fairness + Ich übernehme die technische Verantwortung für die Website. + Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse – nur saubere Umsetzung und stabile Systeme. + Wenn etwas nicht sinnvoll ist, sage ich es ehrlich. + +⸻ + +5. Langfristiger Wert + Eine Website ist ein Investment. + Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind. + Das schützt Ihre Investition und vermeidet teure Neuaufbauten. + +⸻ + +6. Zusammenarbeit ohne Tricks + Keine künstlichen Deadlines, kein unnötiger Overhead. + Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert. diff --git a/apps/web/docs/STANDARDS.md b/apps/web/src/payload/collections/ContextFiles/seed/STANDARDS.md similarity index 100% rename from apps/web/docs/STANDARDS.md rename to apps/web/src/payload/collections/ContextFiles/seed/STANDARDS.md diff --git a/apps/web/docs/STYLEGUIDE.md b/apps/web/src/payload/collections/ContextFiles/seed/STYLEGUIDE.md similarity index 77% rename from apps/web/docs/STYLEGUIDE.md rename to apps/web/src/payload/collections/ContextFiles/seed/STYLEGUIDE.md index c1c7bc8..7d5d3d6 100644 --- a/apps/web/docs/STYLEGUIDE.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/STYLEGUIDE.md @@ -14,33 +14,33 @@ The design should feel "Websites ohne Overhead." Every element must serve a purp The project uses a monochrome base with curated highlighter accents. -- **Primary Base**: - - **Backgrounds**: Pure White (`#ffffff`) for clarity. - - **Surfaces**: Slate-50 for subtle depth, White with 90% opacity + 10px blur for glassmorphism. -- **Grays (Slate)**: - - **Text**: Slate-800 for body, Slate-900 for headings. - - **UI Borders**: Slate-100 or Slate-200. - - **Muted text**: Slate-400 or Slate-500. +- **Primary Base**: + - **Backgrounds**: Pure White (`#ffffff`) for clarity. + - **Surfaces**: Slate-50 for subtle depth, White with 90% opacity + 10px blur for glassmorphism. +- **Grays (Slate)**: + - **Text**: Slate-800 for body, Slate-900 for headings. + - **UI Borders**: Slate-100 or Slate-200. + - **Muted text**: Slate-400 or Slate-500. - **Highlighter Accents**: Used exclusively for tags, markers, and selective emphasis. - - **Yellow**: Warm, high-visibility (`rgba(255, 235, 59, 0.95)`). - - **Pink**: Vibrant, energetic (`rgba(255, 167, 209, 0.95)`). - - **Green**: Success, technical health (`rgba(129, 199, 132, 0.95)`). - - **Blue**: Neutral, structural (`rgba(226, 232, 240, 0.95)`). + - **Yellow**: Warm, high-visibility (`rgba(255, 235, 59, 0.95)`). + - **Pink**: Vibrant, energetic (`rgba(255, 167, 209, 0.95)`). + - **Green**: Success, technical health (`rgba(129, 199, 132, 0.95)`). + - **Blue**: Neutral, structural (`rgba(226, 232, 240, 0.95)`). ## 3. Typography A high-contrast mix of fonts that balances modern tech with editorial readability. - **Headings (Sans-serif)**: Use **Inter**. - - Tracking: `-0.025em` to `-0.05em` (tracking-tighter). - - Weight: Bold (`700`). - - Color: Slate-900. + - Tracking: `-0.025em` to `-0.05em` (tracking-tighter). + - Weight: Bold (`700`). + - Color: Slate-900. - **Body (Serif)**: Use **Newsreader** or Georgia. - - Style: Defaults to serif for long-form content to provide a "notebook" feel. - - Line-height: Relaxed (`1.6` to `1.75`). + - Style: Defaults to serif for long-form content to provide a "notebook" feel. + - Line-height: Relaxed (`1.6` to `1.75`). - **Technical (Monospace)**: Use **JetBrains Mono**. - - Usage: Small labels, tags, code snippets, and "Mono-Labels" (e.g., section numbers). - - Feature: Uppercase with wide tracking (`0.3em` to `0.4em`). + - Usage: Small labels, tags, code snippets, and "Mono-Labels" (e.g., section numbers). + - Feature: Uppercase with wide tracking (`0.3em` to `0.4em`). ## 4. Layout & Rhythm @@ -54,15 +54,18 @@ Standardized containers ensure consistency across different screen sizes. ## 5. UI Elements & Interactions ### 5.1 Buttons + - **Shape**: Always pill-shaped (rounded-full). - **Style**: Thin borders (`1px`) with bold, uppercase mono-spaced text. - **Hover**: Should feel "expensive." Smooth translate-up (`-0.5rem`) and deep, soft shadows. ### 5.2 Cards & Containers + - **Glassmorphism**: Use for search boxes and floating elements (`backdrop-filter: blur(10px)`). - **Cards**: Minimalist. Use `Slate-50` or thin `Slate-100` borders. Avoid heavy shadows unless on hover. ### 5.3 Highlighters & Tags + - **Marker Effect**: Use a hand-drawn marker underline (diagonal skew, slightly erratic rotation) for key titles. - **Tags**: Small, bold, uppercase. They should use `tagPopIn` animations when appearing. diff --git a/apps/web/docs/TECH.md b/apps/web/src/payload/collections/ContextFiles/seed/TECH.md similarity index 65% rename from apps/web/docs/TECH.md rename to apps/web/src/payload/collections/ContextFiles/seed/TECH.md index 645a5dc..c9f5c89 100644 --- a/apps/web/docs/TECH.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/TECH.md @@ -13,10 +13,10 @@ Geschwindigkeit & Performance Meine Websites sind so aufgebaut, dass Inhalte extrem schnell ausgeliefert werden – unabhängig davon, ob ein Besucher am Desktop oder mobil unterwegs ist. Das bedeutet für Sie: - • kurze Ladezeiten - • bessere Nutzererfahrung - • messbar bessere Werte bei Google PageSpeed & Core Web Vitals - • geringere Absprungraten +• kurze Ladezeiten +• bessere Nutzererfahrung +• messbar bessere Werte bei Google PageSpeed & Core Web Vitals +• geringere Absprungraten Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert. @@ -26,10 +26,10 @@ Responsives Design (ohne Kompromisse) Jede Website ist von Grund auf responsiv. Layout, Inhalte und Funktionen passen sich automatisch an: - • Smartphones - • Tablets - • Laptops - • große Bildschirme +• Smartphones +• Tablets +• Laptops +• große Bildschirme Dabei wird nicht einfach skaliert, sondern gezielt für unterschiedliche Bildschirmgrößen optimiert. Das Ergebnis ist eine saubere Darstellung und gute Bedienbarkeit auf allen Geräten. @@ -41,9 +41,9 @@ Stabilität & Betriebssicherheit Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen. Für Sie heißt das: - • Fehler werden bemerkt, auch wenn niemand sie meldet - • ich werde aktiv informiert, statt erst zu reagieren, wenn etwas kaputt ist - • Probleme können frühzeitig behoben werden +• Fehler werden bemerkt, auch wenn niemand sie meldet +• ich werde aktiv informiert, statt erst zu reagieren, wenn etwas kaputt ist +• Probleme können frühzeitig behoben werden Das reduziert Ausfälle und vermeidet unangenehme Überraschungen. @@ -54,10 +54,10 @@ Datenschutz & DSGVO Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen. Ihre Vorteile: - • keine Weitergabe von Nutzerdaten an Dritte - • keine versteckten Tracker - • keine Abhängigkeit von US-Anbietern - • datenschutzfreundliche Statistik ohne Cookies +• keine Weitergabe von Nutzerdaten an Dritte +• keine versteckten Tracker +• keine Abhängigkeit von US-Anbietern +• datenschutzfreundliche Statistik ohne Cookies Die Website bleibt technisch schlank und rechtlich kontrollierbar. @@ -66,9 +66,9 @@ Die Website bleibt technisch schlank und rechtlich kontrollierbar. Unabhängigkeit & Kostenkontrolle Da ich keine proprietären Systeme oder Lizenzmodelle einsetze: - • entstehen keine laufenden Tool-Gebühren - • gibt es keine plötzlichen Preiserhöhungen - • bleibt die Website langfristig planbar betreibbar +• entstehen keine laufenden Tool-Gebühren +• gibt es keine plötzlichen Preiserhöhungen +• bleibt die Website langfristig planbar betreibbar Sie zahlen für die Leistung – nicht für Lizenzen oder Marken. @@ -77,9 +77,9 @@ Sie zahlen für die Leistung – nicht für Lizenzen oder Marken. Wartbarkeit & Erweiterbarkeit Die technische Struktur ist so aufgebaut, dass: - • Inhalte erweitert werden können - • Funktionen sauber ergänzt werden können - • Anpassungen nicht das ganze System gefährden +• Inhalte erweitert werden können +• Funktionen sauber ergänzt werden können +• Anpassungen nicht das ganze System gefährden Das schützt Ihre Investition und verhindert teure Neuaufbauten nach kurzer Zeit. @@ -88,11 +88,11 @@ Das schützt Ihre Investition und verhindert teure Neuaufbauten nach kurzer Zeit Kurz gesagt Ich baue Websites, die: - • schnell sind - • auf allen Geräten sauber funktionieren - • datenschutzkonform betrieben werden - • technisch überwacht sind - • langfristig wartbar bleiben +• schnell sind +• auf allen Geräten sauber funktionieren +• datenschutzkonform betrieben werden +• technisch überwacht sind +• langfristig wartbar bleiben Die Technik steht nicht im Vordergrund – -aber sie sorgt dafür, dass Ihre Website zuverlässig ihren Zweck erfüllt. \ No newline at end of file +aber sie sorgt dafür, dass Ihre Website zuverlässig ihren Zweck erfüllt. diff --git a/apps/web/docs/TONE.md b/apps/web/src/payload/collections/ContextFiles/seed/TONE.md similarity index 99% rename from apps/web/docs/TONE.md rename to apps/web/src/payload/collections/ContextFiles/seed/TONE.md index 1442dcc..e460cdc 100644 --- a/apps/web/docs/TONE.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/TONE.md @@ -50,4 +50,4 @@ Entscheidungen und Empfehlungen orientieren sich am langfristigen Nutzen des Kun - Keine Arroganz, kein Belehren — Wissen teilen statt damit angeben. Wir machen unsere Dinge auch nicht größer als Sie sind. Wir sind bescheiden. - Spricht Probleme direkt an, ohne dramatisch zu werden - Nutzt "ich" statt "wir" oder Passivkonstruktionen -- Vermeidet englische Buzzwords wo deutsche Begriffe existieren: "Kunden verlieren" statt "Churn Rate", "Ladezeit" statt "Time to Interactive" \ No newline at end of file +- Vermeidet englische Buzzwords wo deutsche Begriffe existieren: "Kunden verlieren" statt "Churn Rate", "Ladezeit" statt "Time to Interactive" diff --git a/apps/web/docs/WEBSITES.md b/apps/web/src/payload/collections/ContextFiles/seed/WEBSITES.md similarity index 84% rename from apps/web/docs/WEBSITES.md rename to apps/web/src/payload/collections/ContextFiles/seed/WEBSITES.md index 4f19220..7aab5b9 100644 --- a/apps/web/docs/WEBSITES.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/WEBSITES.md @@ -16,10 +16,10 @@ Eine Website ist kein Flyer. Sie ist ein System, das jeden Tag arbeitet. Deshalb baue ich sie auch so: - • stabil - • schnell - • vorhersehbar - • ohne Überraschungen +• stabil +• schnell +• vorhersehbar +• ohne Überraschungen Sie müssen nichts warten. Sie müssen nichts lernen. @@ -34,11 +34,11 @@ Viele Websites sind langsam, weil sie zusammengeklickt sind. Meine sind schnell, weil sie gebaut sind. Das bedeutet für Sie: - • Seiten laden sofort - • Google mag sie - • Besucher bleiben - • weniger Absprünge - • bessere Sichtbarkeit +• Seiten laden sofort +• Google mag sie +• Besucher bleiben +• weniger Absprünge +• bessere Sichtbarkeit 90+ Pagespeed ist bei mir kein Ziel. Es ist der Normalzustand. @@ -52,9 +52,9 @@ Keine Plugin-Sammlungen. Keine Systeme, die sich selbst zerstören. Ihre Website besteht aus: - • sauberem Code - • klarer Struktur - • festen Bausteinen +• sauberem Code +• klarer Struktur +• festen Bausteinen Das heißt: @@ -111,12 +111,12 @@ dass Dinge einfach passieren. ⸻ Das Ergebnis für Sie - • schnelle Website - • keine Pflegepflicht - • keine Überraschungen - • keine Abhängigkeit - • keine Agentur - • kein Stress +• schnelle Website +• keine Pflegepflicht +• keine Überraschungen +• keine Abhängigkeit +• keine Agentur +• kein Stress Oder anders gesagt: diff --git a/apps/web/docs/WORDING.md b/apps/web/src/payload/collections/ContextFiles/seed/WORDING.md similarity index 93% rename from apps/web/docs/WORDING.md rename to apps/web/src/payload/collections/ContextFiles/seed/WORDING.md index 2c1dafa..dd4710d 100644 --- a/apps/web/docs/WORDING.md +++ b/apps/web/src/payload/collections/ContextFiles/seed/WORDING.md @@ -11,11 +11,11 @@ Ein Gedanke pro Satz. Keine Schachtelsätze. 3. Keine Weichmacher Keine Wörter wie: - • eventuell - • möglicherweise - • grundsätzlich - • in der Regel - • normalerweise +• eventuell +• möglicherweise +• grundsätzlich +• in der Regel +• normalerweise Wenn etwas gilt, wird es gesagt. Wenn nicht, wird es ausgeschlossen. diff --git a/apps/web/src/payload/collections/Inquiries.ts b/apps/web/src/payload/collections/Inquiries.ts new file mode 100644 index 0000000..6ae46e3 --- /dev/null +++ b/apps/web/src/payload/collections/Inquiries.ts @@ -0,0 +1,56 @@ +import type { CollectionConfig } from "payload"; + +export const Inquiries: CollectionConfig = { + slug: "inquiries", + labels: { + singular: "Inquiry", + plural: "Inquiries", + }, + admin: { + useAsTitle: "name", + defaultColumns: ["name", "email", "companyName", "createdAt"], + description: "Contact form leads and inquiries.", + }, + access: { + read: ({ req: { user } }) => Boolean(user), // Admin only + create: () => true, // Everyone can submit + update: ({ req: { user } }) => Boolean(user), + delete: ({ req: { user } }) => Boolean(user), + }, + fields: [ + { + name: "name", + type: "text", + required: true, + }, + { + name: "email", + type: "text", // Using text for email format, or 'email' type if strictly enforced + required: true, + }, + { + name: "companyName", + type: "text", + }, + { + name: "projectType", + type: "text", + }, + { + name: "message", + type: "textarea", + }, + { + name: "isFreeText", + type: "checkbox", + defaultValue: false, + }, + { + name: "config", + type: "json", + admin: { + description: "The JSON data from the configurator.", + }, + }, + ], +}; diff --git a/apps/web/src/payload/collections/Media.ts b/apps/web/src/payload/collections/Media.ts index 25a9db6..90f5111 100644 --- a/apps/web/src/payload/collections/Media.ts +++ b/apps/web/src/payload/collections/Media.ts @@ -9,6 +9,7 @@ export const Media: CollectionConfig = { slug: "media", admin: { useAsTitle: "alt", + defaultColumns: ["filename", "alt", "updatedAt"], }, access: { read: () => true, // Publicly readable diff --git a/apps/web/src/payload/collections/Posts.ts b/apps/web/src/payload/collections/Posts.ts index 06d3b52..a02a699 100644 --- a/apps/web/src/payload/collections/Posts.ts +++ b/apps/web/src/payload/collections/Posts.ts @@ -1,14 +1,30 @@ import type { CollectionConfig } from "payload"; +import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical"; +import { payloadBlocks } from "../blocks/allBlocks"; export const Posts: CollectionConfig = { slug: "posts", admin: { useAsTitle: "title", + defaultColumns: ["featuredImage", "title", "date", "updatedAt", "_status"], + }, + versions: { + drafts: true, }, access: { read: () => true, // Publicly readable API }, fields: [ + { + name: "aiOptimizer", + type: "ui", + admin: { + position: "sidebar", + components: { + Field: "@/src/payload/components/OptimizeButton#OptimizeButton", + }, + }, + }, { name: "title", type: "text", @@ -21,6 +37,11 @@ export const Posts: CollectionConfig = { unique: true, admin: { position: "sidebar", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton", + ], + }, }, hooks: { beforeValidate: [ @@ -46,29 +67,55 @@ export const Posts: CollectionConfig = { name: "date", type: "date", required: true, + admin: { + position: "sidebar", + description: + "Set a future date and save as 'Published' to schedule this post. It will not appear on the frontend until this date is reached.", + }, }, { name: "tags", type: "array", required: true, + admin: { + position: "sidebar", + }, fields: [ { name: "tag", type: "text", + admin: { + description: "Kategorisiere diesen Post mit einem eindeutigen Tag", + components: { Field: "@/src/payload/components/TagSelector" }, + }, }, ], }, { - name: "thumbnail", - type: "text", // Keeping as text for now to match current MDX strings like "/blog/green-it.png" + name: "featuredImage", + type: "upload", + relationTo: "media", + admin: { + description: "The main hero image for the blog post.", + position: "sidebar", + components: { + afterInput: [ + "@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton", + ], + }, + }, }, { name: "content", - type: "code", - admin: { - language: "markdown", - }, - required: true, + type: "richText", + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + BlocksFeature({ + blocks: payloadBlocks, + }), + ], + }), }, ], }; diff --git a/apps/web/src/payload/collections/Redirects.ts b/apps/web/src/payload/collections/Redirects.ts new file mode 100644 index 0000000..7378da2 --- /dev/null +++ b/apps/web/src/payload/collections/Redirects.ts @@ -0,0 +1,33 @@ +import type { CollectionConfig } from "payload"; + +export const Redirects: CollectionConfig = { + slug: "redirects", + admin: { + useAsTitle: "from", + defaultColumns: ["from", "to", "createdAt"], + }, + access: { + read: () => true, + }, + fields: [ + { + name: "from", + type: "text", + required: true, + unique: true, + admin: { + description: + "The old URL slug that should be redirected (e.g. 'old-post-name')", + }, + }, + { + name: "to", + type: "text", + required: true, + admin: { + description: + "The new URL slug to redirect to (e.g. 'new-awesome-post')", + }, + }, + ], +}; diff --git a/apps/web/src/payload/components/ColorPicker/index.tsx b/apps/web/src/payload/components/ColorPicker/index.tsx new file mode 100644 index 0000000..cdc0f9e --- /dev/null +++ b/apps/web/src/payload/components/ColorPicker/index.tsx @@ -0,0 +1,144 @@ +"use client"; + +import React from "react"; +import { useField } from "@payloadcms/ui"; + +const PREDEFINED_COLORS = [ + { label: "Slate (Default)", value: "slate", color: "#64748b" }, + { label: "Primary Blue", value: "blue", color: "#2563eb" }, + { label: "Success Green", value: "green", color: "#10b981" }, + { label: "Danger Red", value: "red", color: "#ef4444" }, + { label: "Mintel Neon", value: "neon", color: "#d9f99d" }, + { label: "Dark Navy", value: "navy", color: "#0f172a" }, + { label: "Brand Slate", value: "brand-slate", color: "#334155" }, + { label: "Emerald", value: "emerald", color: "#059669" }, + { label: "Purple", value: "purple", color: "#8b5cf6" }, +]; + +export default function ColorPickerField({ path }: { path: string }) { + const { value, setValue } = useField({ path }); + + const handleColorClick = (colorValue: string) => { + setValue(colorValue); + }; + + return ( +
+ +
+ {PREDEFINED_COLORS.map((swatch) => { + const isSelected = value === swatch.value; + return ( +
+ + {/* Custom Hex Fallback mapping */} +
+ + Hex / String Value: + + setValue(e.target.value)} + placeholder="e.g. #ff0000 or slate-500" + style={{ + padding: "8px 12px", + border: "1px solid var(--theme-elevation-200)", + borderRadius: "6px", + background: "#ffffff", + color: "var(--theme-text)", + fontSize: "0.875rem", + width: "100%", + maxWidth: "200px", + boxShadow: "inset 0 1px 2px rgba(0,0,0,0.05)", + outline: "none", + }} + onFocus={(e) => (e.target.style.borderColor = "var(--theme-primary)")} + onBlur={(e) => + (e.target.style.borderColor = "var(--theme-elevation-200)") + } + /> +
+
+ ); +} diff --git a/apps/web/src/payload/components/FieldGenerators/AiFieldButton.tsx b/apps/web/src/payload/components/FieldGenerators/AiFieldButton.tsx new file mode 100644 index 0000000..292c341 --- /dev/null +++ b/apps/web/src/payload/components/FieldGenerators/AiFieldButton.tsx @@ -0,0 +1,136 @@ +"use client"; + +import React, { useState } from "react"; +import { useField, useDocumentInfo, useForm } from "@payloadcms/ui"; +import { generateSingleFieldAction } from "../../actions/generateField"; + +export function AiFieldButton({ path, field }: { path: string; field: any }) { + const [isGenerating, setIsGenerating] = useState(false); + const [instructions, setInstructions] = useState(""); + const [showInstructions, setShowInstructions] = useState(false); + + // Payload hooks + const { value, setValue } = useField({ path }); + const { title } = useDocumentInfo(); + const { fields } = useForm(); + + const extractText = (lexicalRoot: any): string => { + if (!lexicalRoot) return ""; + let text = ""; + const iterate = (node: any) => { + if (node.text) text += node.text + " "; + if (node.children) node.children.forEach(iterate); + }; + iterate(lexicalRoot); + return text; + }; + + const handleGenerate = async (e: React.MouseEvent) => { + e.preventDefault(); + + const lexicalValue = fields?.content?.value as any; + const legacyValue = fields?.legacyMdx?.value as string; + let draftContent = legacyValue || ""; + if (!draftContent && lexicalValue?.root) { + draftContent = extractText(lexicalValue.root); + } + + setIsGenerating(true); + try { + // Field name is passed as a label usually, fallback to path + const fieldName = typeof field?.label === "string" ? field.label : path; + const fieldDescription = + typeof field?.admin?.description === "string" + ? field.admin.description + : ""; + + const res = await generateSingleFieldAction( + (title as string) || "", + draftContent, + fieldName, + fieldDescription, + instructions, + ); + if (res.success && res.text) { + setValue(res.text); + } else { + alert("Fehler: " + res.error); + } + } catch (e) { + alert("Fehler bei der Generierung."); + } finally { + setIsGenerating(false); + setShowInstructions(false); + } + }; + + return ( +
+
+ + +
+ {showInstructions && ( +