Compare commits
183 Commits
9cfe7ee9e5
...
v1.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
| e0ccf1cdfb | |||
| 6a6fbb6f19 | |||
| 6b6b2b8ece | |||
| 9f412d81a8 | |||
| 9c401f13de | |||
| 5857404ac1 | |||
| 34a96f8aef | |||
| 4e6f3f29cf | |||
| 1bd516fbe4 | |||
| 4d0e3433a6 | |||
| ee9cde1ed0 | |||
| 33cf701034 | |||
| 1fae5edee3 | |||
| 0e143bf9c1 | |||
| d86e26bc33 | |||
| a1c0736274 | |||
| 7b642426fb | |||
| 6a228248e0 | |||
| bd1a822d32 | |||
| 81af49f880 | |||
| 1defb5758f | |||
| b4dd073711 | |||
| 59ea4bfd02 | |||
| 4a20e1f51f | |||
| 9aa3ee42e4 | |||
| 0ac022df57 | |||
| e71965267d | |||
| 8d12f92da8 | |||
| 4303124ec5 | |||
| badf81644e | |||
| cdd38b3654 | |||
| 1a195a388a | |||
| b4fbf3bf2a | |||
| 8569105529 | |||
| 316afe004f | |||
| b20a999da8 | |||
| 237d68bc5a | |||
| 0fdc20cabb | |||
| 2aa617ce3b | |||
| 54cd94831d | |||
| c8df20bbee | |||
| 07755c9674 | |||
| ff7ba14a4a | |||
| ebe42adb6f | |||
| a45d0110d3 | |||
| 9abd4f4fe7 | |||
| 3a4fd1d06d | |||
| c0b9c55ecf | |||
| 7e320c08d9 | |||
| c5746978aa | |||
| cd88c2f20f | |||
| 1c87d5341e | |||
| 6a14c9924f | |||
| ee50808596 | |||
| e9fbe45feb | |||
| b27566a336 | |||
| 71ef49e73d | |||
| a98572e183 | |||
| eacb14ff7d | |||
| 41a090db58 | |||
| 2bdb6bbb98 | |||
| 99ee47507b | |||
| 2d96000385 | |||
| 39ea0a35dd | |||
| 1c24822787 | |||
| d21c12c2b4 | |||
| cdf2bb5fdc | |||
| c4aaea30c1 | |||
| cbb3cf0be3 | |||
| bc3a75a915 | |||
| 1455845d44 | |||
| db31f06bc0 | |||
| 546b8ee72b | |||
| 6174b44570 | |||
| 89d258e63d | |||
| 13a484ce59 | |||
| d82c836fcb | |||
| b2f6627ec5 | |||
| 2ab5a8a41f | |||
| e43c980a5d | |||
| 88b4626d6e | |||
| 90856da773 | |||
| 964cd79ca8 | |||
| 9c5e2c6099 | |||
| 984a641b90 | |||
| c8ff76f299 | |||
| 1fffdf00ee | |||
| 70de139cb0 | |||
| b015c62650 | |||
| b7dac5d463 | |||
| 10bdfdfe97 | |||
| 9ad63a0a82 | |||
| eb117cc0b8 | |||
| 23ee915194 | |||
| 3dff891023 | |||
| f55c27c43d | |||
| 3e04427646 | |||
| 6b51d63c8b | |||
| 60ca4ad656 | |||
| aae5275990 | |||
| b639fffe7f | |||
| ab15f7f35b | |||
| 025906889c | |||
| 760a6d6db3 | |||
| 7f8cea4728 | |||
| fb09b1de9a | |||
| cb4afe2e91 | |||
| 1f68234a49 | |||
| e2d68c2828 | |||
| cb6f133e0c | |||
| 7990189505 | |||
| 2167044543 | |||
| 0665e3e224 | |||
| 2bdcbfb907 | |||
| ac1e0081f7 | |||
| 4f452cf2a9 | |||
| 1404aa0406 | |||
| 9e10ce06ed | |||
| a400e6f94d | |||
| 2f95c8d968 | |||
| 9aa6f5f4d0 | |||
| 071302fe6b | |||
| cf3a96cead | |||
| af5f91e6f8 | |||
| 5e453418d6 | |||
| 10980ba8b3 | |||
| 6444aea5f6 | |||
| ad50929bf3 | |||
| 07928a182f | |||
| b493ce0ba0 | |||
| db445d0b76 | |||
| 22a6a06a4e | |||
| 4f66dd914c | |||
| bb54750085 | |||
| 5cbbd81384 | |||
| c167e36626 | |||
| 0fb872161d | |||
| a360ea6a98 | |||
| a537294832 | |||
| 459bdc6eda | |||
| 905ce98bc4 | |||
| ce63a1ac69 | |||
| 6444cf1e81 | |||
| 4b5609a75e | |||
| 8907963d57 | |||
| 844a5f5412 | |||
| 96de68a063 | |||
| 4a5017124a | |||
| 283de11b11 | |||
| 31685a458b | |||
| 6864903cff | |||
| 95a8b702fe | |||
| 43564d1bba | |||
| 072b6b13f1 | |||
| b2f5e2dd4d | |||
| 2ef00271ab | |||
| f3224f600d | |||
| c3fa10dd53 | |||
| 79838d7955 | |||
| e20e7cb1d0 | |||
| 22f5cb14ed | |||
| 5661d66d73 | |||
| 498db2a42c | |||
| 38f2cc8b85 | |||
| d9ddce412a | |||
| 95d80cc7d7 | |||
| c68ffec480 | |||
| b15c8408ff | |||
| 75c61f1436 | |||
| 3eccff42e4 | |||
| 786d35010b | |||
| d41d4d3d89 | |||
| 8a1c2b9483 | |||
| cce6aa0935 | |||
| bff58e7cfa | |||
| 28053952ea | |||
| e2c9ec507f | |||
| 3de163276c | |||
| 34b35e2f17 | |||
| 4db820214b | |||
| 2038b8fe47 | |||
| 7fd0c447bc | |||
| 24f8772a31 |
@@ -1,33 +0,0 @@
|
||||
name: CI - Quality Assurance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
pnpm lint &
|
||||
pnpm build &
|
||||
wait
|
||||
@@ -1,9 +1,10 @@
|
||||
# Heartbeat to trigger fresh CI run after stall
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "**"
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
@@ -13,6 +14,9 @@ on:
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_name == 'main' && 'testing' || github.ref_name) }}
|
||||
cancel-in-progress: true
|
||||
@@ -76,7 +80,11 @@ jobs:
|
||||
TRAEFIK_HOST="staging.${DOMAIN}"
|
||||
fi
|
||||
else
|
||||
TARGET="skip"
|
||||
TARGET="branch"
|
||||
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
||||
ENV_FILE=".env.branch-${SLUG}"
|
||||
TRAEFIK_HOST="${SLUG}.branch.${DOMAIN}"
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" != "skip" ]]; then
|
||||
@@ -97,37 +105,25 @@ jobs:
|
||||
echo "traefik_rule=$TRAEFIK_RULE"
|
||||
echo "next_public_url=https://$PRIMARY_HOST"
|
||||
echo "directus_url=https://cms.$PRIMARY_HOST"
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
if [[ "$TARGET" == "branch" ]]; then
|
||||
echo "project_name=$PRJ-branch-$SLUG"
|
||||
else
|
||||
echo "project_name=$PRJ-$TARGET"
|
||||
fi
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ⏳ Wait for Upstream Packages/Images if Tagged
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
echo "🔎 Checking for @mintel dependencies in package.json..."
|
||||
# Extract any @mintel/ version (they should be synced in monorepo)
|
||||
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | head -1 | cut -d'"' -f4 | sed 's/\^//; s/\~//')
|
||||
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||
|
||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
|
||||
# Fetch script from monorepo (main)
|
||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
|
||||
chmod +x wait-for-upstream.sh
|
||||
|
||||
GITEA_TOKEN=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "target=skip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Build Test)
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
needs: prepare
|
||||
needs: [prepare, deploy]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -143,28 +139,137 @@ jobs:
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
# This handles root package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
|
||||
# Handle apps/web/package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
|
||||
echo "Testing API with token..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $T" https://git.infra.mintel.me/api/v1/user || echo "failed")
|
||||
echo "API returned: $HTTP_CODE"
|
||||
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VALID_TOKEN" ]; then
|
||||
echo "❌ All token/user combinations failed to authenticate!"
|
||||
T=$(echo "$TOKENS" | awk '{print $1}')
|
||||
echo "Attempting open diagnostic login with first token and user mmintel..."
|
||||
echo "$T" | docker login git.infra.mintel.me -u "mmintel" --password-stdin || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "::add-mask::$TOKEN"
|
||||
echo "token=$TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "user=$VALID_USER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
|
||||
# Also export for pnpm to pick it up from env if needed
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: 🏗️ Compile Sibling Monorepo
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm --filter "@mintel/web" exec tsc --noEmit
|
||||
pnpm --filter "@mintel/web" test
|
||||
- name: 🏗️ Build Test
|
||||
mkdir -p ci-logs
|
||||
echo "=== Compile Sibling Monorepo ===" >> ci-logs/summary.txt
|
||||
cp .npmrc _at-mintel/
|
||||
cd _at-mintel
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
pnpm --filter "...@mintel/payload-ai" \
|
||||
--filter @mintel/pdf... \
|
||||
--filter @mintel/concept-engine... \
|
||||
--filter @mintel/estimation-engine... \
|
||||
--filter @mintel/meme-generator... \
|
||||
build --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
- name: Install dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Install dependencies (Root) ===" >> ci-logs/summary.txt
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: 🧪 Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
run: pnpm build
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Test (@mintel/web) ===" >> ci-logs/summary.txt
|
||||
pnpm --filter @mintel/web test --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: Inspect on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "==== runner state ===="
|
||||
ls -la
|
||||
echo "==== _at-mintel state ===="
|
||||
ls -la _at-mintel || true
|
||||
echo "==== .npmrc check ===="
|
||||
cat .npmrc | sed -E 's/authToken=[a-f0-9]{5}.*/authToken=REDACTED/'
|
||||
echo "==== pnpm debug logs ===="
|
||||
[ -f pnpm-debug.log ] && tail -n 100 pnpm-debug.log || echo "No root pnpm-debug.log"
|
||||
[ -f _at-mintel/pnpm-debug.log ] && tail -n 100 _at-mintel/pnpm-debug.log || echo "No sibling pnpm-debug.log"
|
||||
- name: Extract QA Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "QA Failure Report" > ci-logs/summary.txt
|
||||
ls -R >> ci-logs/summary.txt
|
||||
[ -f pnpm-debug.log ] && cp pnpm-debug.log ci-logs/ || true
|
||||
[ -f _at-mintel/pnpm-debug.log ] && cp _at-mintel/pnpm-debug.log ci-logs/at-mintel-pnpm-debug.log || true
|
||||
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" root@alpha.mintel.me "mkdir -p ~/logs"
|
||||
scp -r -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" ci-logs/* root@alpha.mintel.me:~/logs/ || true
|
||||
rm "$SSH_KEY_FILE"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: 🏗️ Build
|
||||
needs: prepare
|
||||
needs: [prepare]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -172,33 +277,71 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Provide sibling monorepo (context)
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
- name: 🧹 Free Disk Space
|
||||
run: |
|
||||
docker builder prune -af || true
|
||||
docker image prune -af || true
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: 🏗️ Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/arm64
|
||||
provenance: false
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache,mode=max
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||
secrets: |
|
||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
- name: 🚨 Extract Build Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
set +e
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "Re-running docker build with plain progress to capture exact logs..."
|
||||
echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.user }}" --password-stdin > login.log 2>&1
|
||||
echo "${{ steps.discover_token.outputs.token }}" > /tmp/npm_token.txt
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg NPM_TOKEN=${{ steps.discover_token.outputs.token }} \
|
||||
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
||||
--progress plain \
|
||||
-t temp-image . > docker_build_failed.log 2>&1
|
||||
cat login.log >> docker_build_failed.log
|
||||
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
||||
# JOB 4: Deploy
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: 🚀 Deploy
|
||||
needs: [prepare, build, qa]
|
||||
needs: [prepare, build]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -210,17 +353,14 @@ jobs:
|
||||
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
|
||||
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
|
||||
|
||||
# Secrets mapping (Directus)
|
||||
DIRECTUS_KEY: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_KEY) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY) || secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }}
|
||||
DIRECTUS_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_SECRET) || secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }}
|
||||
DIRECTUS_ADMIN_EMAIL: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_EMAIL) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_EMAIL) || secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }}
|
||||
DIRECTUS_ADMIN_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_PASSWORD) || secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }}
|
||||
DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
DIRECTUS_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||
DIRECTUS_API_TOKEN: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_API_TOKEN) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_API_TOKEN) || secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }}
|
||||
# Database configuration
|
||||
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||
DATABASE_URI: postgres://${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}:${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}@postgres-db:5432/${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
# Mail
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
MAIL_PORT: ${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
MAIL_USERNAME: ${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
||||
@@ -238,6 +378,14 @@ jobs:
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
|
||||
|
||||
# S3 Object Storage
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
S3_BUCKET: ${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||
S3_REGION: ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
S3_PREFIX: ${{ secrets.S3_PREFIX || vars.S3_PREFIX || github.event.repository.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -249,7 +397,6 @@ jobs:
|
||||
GATEKEEPER_HOST: gatekeeper.${{ needs.prepare.outputs.traefik_host }}
|
||||
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
|
||||
run: |
|
||||
# Middleware & Auth Logic
|
||||
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
|
||||
STD_MW="${PROJECT_NAME}-forward,compress"
|
||||
|
||||
@@ -257,15 +404,16 @@ jobs:
|
||||
AUTH_MIDDLEWARE="$STD_MW"
|
||||
COMPOSE_PROFILES=""
|
||||
else
|
||||
# Order: Forward (Proto) -> Auth -> Compression
|
||||
AUTH_MIDDLEWARE="${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,compress"
|
||||
COMPOSE_PROFILES="gatekeeper"
|
||||
fi
|
||||
|
||||
# Gatekeeper Origin
|
||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
||||
|
||||
# Generate Environment File
|
||||
if [[ "$UMAMI_API_ENDPOINT" != http* ]]; then
|
||||
UMAMI_API_ENDPOINT="https://$UMAMI_API_ENDPOINT"
|
||||
fi
|
||||
|
||||
cat > .env.deploy << EOF
|
||||
# Generated by CI - $TARGET
|
||||
IMAGE_TAG=$IMAGE_TAG
|
||||
@@ -274,38 +422,29 @@ jobs:
|
||||
SENTRY_DSN=$SENTRY_DSN
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
# Directus
|
||||
DIRECTUS_URL=$DIRECTUS_URL
|
||||
DIRECTUS_HOST=$DIRECTUS_HOST
|
||||
DIRECTUS_KEY=$DIRECTUS_KEY
|
||||
DIRECTUS_SECRET=$DIRECTUS_SECRET
|
||||
DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL
|
||||
DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD
|
||||
DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME
|
||||
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
|
||||
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
|
||||
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
|
||||
INTERNAL_DIRECTUS_URL=http://directus:8055
|
||||
|
||||
# Mail
|
||||
postgres_DB_NAME=$postgres_DB_NAME
|
||||
postgres_DB_USER=$postgres_DB_USER
|
||||
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
||||
DATABASE_URI=$DATABASE_URI
|
||||
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
MAIL_HOST=$MAIL_HOST
|
||||
MAIL_PORT=$MAIL_PORT
|
||||
MAIL_USERNAME=$MAIL_USERNAME
|
||||
MAIL_PASSWORD=$MAIL_PASSWORD
|
||||
MAIL_FROM=$MAIL_FROM
|
||||
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
|
||||
|
||||
# Authentication
|
||||
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
|
||||
AUTH_COOKIE_NAME=$AUTH_COOKIE_NAME
|
||||
COOKIE_DOMAIN=$COOKIE_DOMAIN
|
||||
|
||||
# Analytics
|
||||
UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
|
||||
S3_ENDPOINT=$S3_ENDPOINT
|
||||
S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
S3_BUCKET=$S3_BUCKET
|
||||
S3_REGION=$S3_REGION
|
||||
S3_PREFIX=$S3_PREFIX
|
||||
TARGET=$TARGET
|
||||
SENTRY_ENVIRONMENT=$TARGET
|
||||
PROJECT_NAME=$PROJECT_NAME
|
||||
@@ -314,6 +453,9 @@ jobs:
|
||||
TRAEFIK_HOST='$TRAEFIK_HOST'
|
||||
COMPOSE_PROFILES=$COMPOSE_PROFILES
|
||||
TRAEFIK_MIDDLEWARES=$AUTH_MIDDLEWARE
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_TLS=true
|
||||
TRAEFIK_CERT_RESOLVER=le
|
||||
EOF
|
||||
|
||||
- name: 🚀 SSH Deploy
|
||||
@@ -326,58 +468,186 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# Transfer and Restart
|
||||
SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
# SSH keepalive to prevent timeout during long docker pull
|
||||
cat > ~/.ssh/config <<SSHCFG
|
||||
Host alpha.mintel.me
|
||||
ServerAliveInterval 15
|
||||
ServerAliveCountMax 20
|
||||
ConnectTimeout 30
|
||||
SSHCFG
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
elif [[ "$TARGET" == "testing" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/testing.mintel.me"
|
||||
elif [[ "$TARGET" == "staging" ]]; then
|
||||
SITE_DIR="/home/deploy/sites/staging.mintel.me"
|
||||
else
|
||||
SITE_DIR="/home/deploy/sites/branch.mintel.me/${SLUG:-unknown}"
|
||||
fi
|
||||
|
||||
# Upload files
|
||||
ssh root@alpha.mintel.me "mkdir -p $SITE_DIR/directus/schema $SITE_DIR/directus/uploads $SITE_DIR/directus/extensions"
|
||||
scp .env.deploy root@alpha.mintel.me:$SITE_DIR/$ENV_FILE
|
||||
scp docker-compose.yml root@alpha.mintel.me:$SITE_DIR/docker-compose.yml
|
||||
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '${{ secrets.REGISTRY_PASS }}' | docker login registry.infra.mintel.me -u '${{ secrets.REGISTRY_USER }}' --password-stdin"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||
# Deploy
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
# Apply Directus Schema Snapshot if available
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && if docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus ls /directus/schema/snapshot.yaml >/dev/null 2>&1; then echo '→ Applying Directus Schema Snapshot...' && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus npx directus schema apply /directus/schema/snapshot.yaml --yes; fi"
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
# Deploy — alpha is pre-logged into registry.infra.mintel.me, no credential passing needed
|
||||
ssh root@alpha.mintel.me "
|
||||
docker network create '${{ needs.prepare.outputs.project_name }}-internal' || true
|
||||
docker volume create 'mintel-me_payload-db-data' || true
|
||||
cd $SITE_DIR
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans
|
||||
"
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 5: Health Check
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
healthcheck:
|
||||
name: 🩺 Health Check
|
||||
needs: [prepare, deploy]
|
||||
if: needs.deploy.result == 'success'
|
||||
post_deploy_checks:
|
||||
name: 🧪 Post-Deploy Verification
|
||||
needs: [prepare, deploy, qa]
|
||||
if: success() || failure() # Run even if QA fails (due to E2E noise)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔍 Smoke Test
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
echo "Checking health of $URL..."
|
||||
for i in {1..12}; do
|
||||
if curl -s -f "$URL" > /dev/null; then
|
||||
echo "✅ Health check passed!"
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
for TOKEN_RAW in $TOKENS; do
|
||||
if [ -n "$TOKEN_RAW" ]; then
|
||||
TOKEN=$(echo "$TOKEN_RAW" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$TOKEN"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: 🏥 App Health Check
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: |
|
||||
echo "Waiting for app to start at $DEPLOY_URL ..."
|
||||
for i in {1..30}; do
|
||||
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' "$DEPLOY_URL" 2>&1) || true
|
||||
echo "Attempt $i: HTTP $HTTP_CODE"
|
||||
if [[ "$HTTP_CODE" =~ ^2 ]]; then
|
||||
echo "✅ App is up (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for service to be ready... ($i/12)"
|
||||
echo "⏳ Waiting... (got $HTTP_CODE)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed after 2 minutes."
|
||||
echo "❌ App health check failed after 30 attempts"
|
||||
exit 1
|
||||
- name: 🚀 OG Image Check
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: pnpm --filter @mintel/web check:og
|
||||
- name: 📝 E2E Smoke Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: |
|
||||
# Install system Chromium + dependencies (KLZ pattern)
|
||||
# Ubuntu's default 'chromium' is a snap wrapper, so we use xtradeb PPA for native binary
|
||||
sudo apt-get update && sudo apt-get install -y gnupg wget ca-certificates
|
||||
|
||||
# Setup xtradeb PPA for native chromium
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | sudo gpg --dearmor -o /etc/apt/keyrings/xtradeb.gpg || true
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" | sudo tee /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" | sudo tee /etc/apt/preferences.d/xtradeb
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 || sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||
|
||||
[ -f /usr/bin/chromium ] && sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
pnpm --filter @mintel/web check:forms
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
needs: [prepare, deploy, healthcheck]
|
||||
needs: [prepare, deploy, post_deploy_checks]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -385,11 +655,20 @@ jobs:
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
run: |
|
||||
STATUS="${{ needs.deploy.result }}"
|
||||
TITLE="mintel.me: $STATUS"
|
||||
[[ "$STATUS" == "success" ]] && PRIORITY=5 || PRIORITY=8
|
||||
DEPLOY="${{ needs.deploy.result }}"
|
||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
|
||||
if [[ "$DEPLOY" == "success" ]] && [[ "$SMOKE" == "success" || "$SMOKE" == "skipped" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="✅"
|
||||
else
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
fi
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=Deploy to ${{ needs.prepare.outputs.target }} finished with status $STATUS.\nVersion: ${{ needs.prepare.outputs.image_tag }}" \
|
||||
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
||||
-F "message=Deploy: $DEPLOY | Smoke: $SMOKE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
232
.gitea/workflows/qa.yml
Normal file
232
.gitea/workflows/qa.yml
Normal file
@@ -0,0 +1,232 @@
|
||||
name: Nightly QA
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Deploy"]
|
||||
branches: [main]
|
||||
types:
|
||||
- completed
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_URL: "https://testing.mintel.me"
|
||||
PROJECT_NAME: "mintel.me"
|
||||
|
||||
jobs:
|
||||
# ────────────────────────────────────────────────────
|
||||
# 1. Static Checks (HTML, Assets, HTTP)
|
||||
# ────────────────────────────────────────────────────
|
||||
static:
|
||||
name: 🔍 Static Analysis
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --fix-missing \
|
||||
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 \
|
||||
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 || true
|
||||
apt-get install -y libasound2t64 || apt-get install -y libasound2 || true
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🖼️ OG Images
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:og
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 2. E2E (Forms)
|
||||
# ────────────────────────────────────────────────────
|
||||
e2e:
|
||||
name: 📝 E2E
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 📝 E2E Form Submission Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:forms
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 3. Performance (Lighthouse)
|
||||
# ────────────────────────────────────────────────────
|
||||
lighthouse:
|
||||
name: 🎭 Lighthouse
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🎭 Desktop
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
- name: 📱 Mobile
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 4. Link Check & Dependency Audit
|
||||
# ────────────────────────────────────────────────────
|
||||
links:
|
||||
name: 🔗 Links & Deps
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 📦 Depcheck
|
||||
continue-on-error: true
|
||||
run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" || true
|
||||
- name: 🔗 Lychee Link Check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --accept 200,204,429 --timeout 10 --insecure --exclude "file://*" --exclude "https://logs.infra.mintel.me/*" --exclude "https://git.infra.mintel.me/*" --exclude "https://mintel.me/*" '*.md' 'docs/*.md'
|
||||
fail: false
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 5. Notification
|
||||
# ────────────────────────────────────────────────────
|
||||
notify:
|
||||
name: 🔔 Notify
|
||||
needs: [static, e2e, lighthouse, links]
|
||||
if: failure()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
STATIC="${{ needs.static.result }}"
|
||||
E2E="${{ needs.e2e.result }}"
|
||||
LIGHTHOUSE="${{ needs.lighthouse.result }}"
|
||||
LINKS="${{ needs.links.result }}"
|
||||
|
||||
if [[ "$STATIC" != "success" || "$LIGHTHOUSE" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
STATUS="Failed"
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS="Passed"
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI ${{ env.PROJECT_NAME }} QA $STATUS"
|
||||
MESSAGE="Static: $STATIC | E2E: $E2E | Lighthouse: $LIGHTHOUSE | Links: $LINKS
|
||||
${{ env.TARGET_URL }}"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=$MESSAGE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
dist/
|
||||
.next/
|
||||
out/
|
||||
.contentlayer/
|
||||
.pnpm-store
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
@@ -45,3 +47,13 @@ pnpm-debug.log*
|
||||
.cache/
|
||||
cloned-websites/
|
||||
storage/
|
||||
data/postgres/
|
||||
|
||||
# Estimation Engine Data
|
||||
data/crawls/
|
||||
apps/web/out/estimations/
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
.turbo
|
||||
5
.npmrc
5
.npmrc
@@ -1,3 +1,2 @@
|
||||
@mintel:registry=https://npm.infra.mintel.me/
|
||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||
|
||||
1
.turbo/cache/41a721a9104bd76c-meta.json
vendored
Normal file
1
.turbo/cache/41a721a9104bd76c-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "41a721a9104bd76c", "duration": 2524 }
|
||||
BIN
.turbo/cache/41a721a9104bd76c.tar.zst
vendored
Normal file
BIN
.turbo/cache/41a721a9104bd76c.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/441277b34176cf11-meta.json
vendored
Normal file
1
.turbo/cache/441277b34176cf11-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "441277b34176cf11", "duration": 2934 }
|
||||
BIN
.turbo/cache/441277b34176cf11.tar.zst
vendored
Normal file
BIN
.turbo/cache/441277b34176cf11.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/708dc951079154e6-meta.json
vendored
Normal file
1
.turbo/cache/708dc951079154e6-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "708dc951079154e6", "duration": 194 }
|
||||
BIN
.turbo/cache/708dc951079154e6.tar.zst
vendored
Normal file
BIN
.turbo/cache/708dc951079154e6.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/84b66091bfb55705-meta.json
vendored
Normal file
1
.turbo/cache/84b66091bfb55705-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "84b66091bfb55705", "duration": 2417 }
|
||||
BIN
.turbo/cache/84b66091bfb55705.tar.zst
vendored
Normal file
BIN
.turbo/cache/84b66091bfb55705.tar.zst
vendored
Normal file
Binary file not shown.
1
.turbo/cache/ba4a4a0aae882f7f-meta.json
vendored
Normal file
1
.turbo/cache/ba4a4a0aae882f7f-meta.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{ "hash": "ba4a4a0aae882f7f", "duration": 5009 }
|
||||
BIN
.turbo/cache/ba4a4a0aae882f7f.tar.zst
vendored
Normal file
BIN
.turbo/cache/ba4a4a0aae882f7f.tar.zst
vendored
Normal file
Binary file not shown.
19
Dockerfile
19
Dockerfile
@@ -1,18 +1,16 @@
|
||||
# Stage 1: Builder
|
||||
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
|
||||
FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder
|
||||
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
|
||||
@@ -20,24 +18,29 @@ ENV CI=true
|
||||
# Copy manifest files specifically for better layer caching
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||
COPY apps/web/package.json ./apps/web/package.json
|
||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||
COPY _at-mintel* ./_at-mintel/
|
||||
|
||||
# Install dependencies with cache mount and dynamic .npmrc (High Fidelity pattern)
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
--mount=type=secret,id=NPM_TOKEN \
|
||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
|
||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
pnpm install --frozen-lockfile && \
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc && \
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
echo "always-auth=true" >> .npmrc && \
|
||||
cd _at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app && pnpm install --no-frozen-lockfile && \
|
||||
rm .npmrc
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build application (monorepo filter)
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
RUN pnpm --filter @mintel/web build
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
|
||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Copy standalone output and static files (Monorepo paths)
|
||||
@@ -45,7 +48,7 @@ WORKDIR /app
|
||||
COPY --from=builder /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=builder /app/apps/web/.next/cache ./apps/web/.next/cache
|
||||
|
||||
|
||||
# Start from the app directory to ensure references solve correctly
|
||||
WORKDIR /app/apps/web
|
||||
|
||||
20
Dockerfile.dev
Normal file
20
Dockerfile.dev
Normal file
@@ -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
|
||||
95
Posts.ts.tmp
Normal file
95
Posts.ts.tmp
Normal file
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
334
apps/web/.turbo/turbo-lint.log
Normal file
334
apps/web/.turbo/turbo-lint.log
Normal file
@@ -0,0 +1,334 @@
|
||||
|
||||
> @mintel/web@0.1.0 lint /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> eslint app src scripts video
|
||||
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/about/page.tsx
|
||||
3:8 warning 'Image' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:3 warning 'ResultIllustration' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
11:3 warning 'HeroLines' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
12:3 warning 'ParticleNetwork' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
13:3 warning 'GridLines' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
16:10 warning 'Check' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
31:3 warning 'CodeSnippet' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
32:3 warning 'AbstractCircuit' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
53:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/case-studies/klz-cables/page.tsx
|
||||
8:3 warning 'H1' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/not-found.tsx
|
||||
6:8 warning 'Link' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/page.tsx
|
||||
18:3 warning 'MonoLabel' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
21:16 warning 'Container' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
23:24 warning 'CodeSnippet' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:10 warning 'IconList' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:20 warning 'IconListItem' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/app/(site)/technologies/[slug]/data.tsx
|
||||
1:24 warning 'Database' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/ai-estimate.ts
|
||||
8:10 warning 'fileURLToPath' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/check-og-images.ts
|
||||
19:11 warning 'body' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/generate-thumbnail.ts
|
||||
28:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/migrate-posts.ts
|
||||
107:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/scripts/pagespeed-sitemap.ts
|
||||
109:14 warning 'err' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ArticleMeme.tsx
|
||||
110:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ArticleQuote.tsx
|
||||
20:5 warning 'role' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/BlogOGImageTemplate.tsx
|
||||
41:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/CombinedQuotePDF.tsx
|
||||
30:9 warning 'date' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ComponentShareButton.tsx
|
||||
126:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/Configurator/ConfiguratorLayout.tsx
|
||||
24:3 warning 'title' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/Configurator/ReferenceInput.tsx
|
||||
7:10 warning 'cn' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/DirectMessageFlow.tsx
|
||||
3:10 warning 'motion' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/EmailTemplates.tsx
|
||||
1:13 warning 'React' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/pdf/LocalEstimationPDF.tsx
|
||||
94:9 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/BaseStep.tsx
|
||||
13:3 warning 'HelpCircle' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
14:3 warning 'ArrowRight' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/ContentStep.tsx
|
||||
103:25 warning 'index' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/DesignStep.tsx
|
||||
7:19 warning 'Palette' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
104:38 warning 'index' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/FeaturesStep.tsx
|
||||
8:18 warning 'AnimatePresence' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:10 warning 'Minus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
9:17 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/FunctionsStep.tsx
|
||||
7:18 warning 'AnimatePresence' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
8:10 warning 'Minus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
8:17 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/LanguageStep.tsx
|
||||
5:23 warning 'Plus' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
125:31 warning 'i' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ContactForm/steps/PresenceStep.tsx
|
||||
5:10 warning 'Checkbox' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/DiagramShareButton.tsx
|
||||
28:9 warning 'generateDiagramImage' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/DiagramState.tsx
|
||||
25:3 warning 'states' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Effects/CMSVisualizer.tsx
|
||||
8:3 warning 'Edit3' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Effects/CircuitBoard.tsx
|
||||
120:9 warning 'drawTrace' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
130:13 warning 'midX' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
131:13 warning 'midY' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/FAQSection.tsx
|
||||
5:10 warning 'Paragraph' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
7:11 warning 'FAQItem' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/FileExample.tsx
|
||||
3:27 warning 'useRef' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/IframeSection.tsx
|
||||
207:18 warning Empty block statement no-empty
|
||||
252:18 warning Empty block statement no-empty
|
||||
545:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ImageText.tsx
|
||||
25:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/MediumCard.tsx
|
||||
3:10 warning 'Card' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
34:13 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/Mermaid.tsx
|
||||
248:18 warning 'err' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/PayloadRichText.tsx
|
||||
177:31 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
180:26 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
181:34 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
186:27 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
191:29 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
196:32 warning 'node' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/ShareModal.tsx
|
||||
7:8 warning 'IconBlack' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
181:23 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
231:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
258:13 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/blog/BlogClient.tsx
|
||||
27:11 warning 'trackEvent' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/components/blog/BlogPostHeader.tsx
|
||||
54:17 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/migrations/20260227_171023_crm_collections.ts
|
||||
3:32 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
3:41 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
360:3 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
361:3 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/migrations/20260301_151838.ts
|
||||
3:32 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
3:41 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
110:3 warning 'payload' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
111:3 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/actions/generateField.ts
|
||||
3:10 warning 'config' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/actions/optimizePost.ts
|
||||
4:10 warning 'revalidatePath' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleMemeBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ArticleQuoteBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/BoldNumberBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ButtonBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/CarouselBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ComparisonRowBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramFlowBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramGanttBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramPieBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramSequenceBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramStateBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DiagramTimelineBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ExternalLinkBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/FAQSectionBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
39:22 warning 'ai' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
39:26 warning 'render' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/IconListBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ImageTextBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LeadMagnetBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LeadParagraphBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LinkedInEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MarkerBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MemeCardBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MermaidBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/MetricBarBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/ParagraphBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PerformanceChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/RevealBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/SectionBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/StatsDisplayBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/StatsGridBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TLDRBlock.ts
|
||||
2:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TrackedLinkBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/TwitterEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/WaterfallChartBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/WebVitalsScoreBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/YouTubeEmbedBlock.ts
|
||||
3:15 warning 'Block' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/blocks/allBlocks.ts
|
||||
100:47 warning 'ai' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
100:51 warning 'render' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/collections/ContextFiles.ts
|
||||
2:8 warning 'fs' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
27:10 warning 'doc' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
27:15 warning 'operation' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/AiFieldButton.tsx
|
||||
13:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
59:14 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/GenerateSlugButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
23:19 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/FieldGenerators/GenerateThumbnailButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
24:11 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
/Users/marcmintel/Projects/mintel.me/apps/web/src/payload/components/OptimizeButton.tsx
|
||||
6:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
|
||||
|
||||
✖ 141 problems (0 errors, 141 warnings)
|
||||
|
||||
5
apps/web/.turbo/turbo-test.log
Normal file
5
apps/web/.turbo/turbo-test.log
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
> @mintel/web@0.1.0 test /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> echo "No tests configured"
|
||||
|
||||
No tests configured
|
||||
4
apps/web/.turbo/turbo-typecheck.log
Normal file
4
apps/web/.turbo/turbo-typecheck.log
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
> @mintel/web@0.1.0 typecheck /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> tsc --noEmit
|
||||
|
||||
13
apps/web/app/(payload)/actions.ts
Normal file
13
apps/web/app/(payload)/actions.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
"use server";
|
||||
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
||||
import config from "@payload-config";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export const handleServerFunctions = async (args: any) => {
|
||||
return payloadHandleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
});
|
||||
};
|
||||
26
apps/web/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
26
apps/web/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import configPromise from "@payload-config";
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "../importMap";
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[];
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config: configPromise, params, searchParams });
|
||||
|
||||
const Page = async ({ params, searchParams }: Args) =>
|
||||
RootPage({ config: configPromise, importMap, params, searchParams });
|
||||
|
||||
export default Page;
|
||||
114
apps/web/app/(payload)/admin/importMap.js
Normal file
114
apps/web/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { AiMediaButtons as AiMediaButtons_1d402a78164f07306f77dce953e62e11 } from "@mintel/payload-ai/components/AiMediaButtons";
|
||||
import { OptimizeButton as OptimizeButton_338ff118e214cff355f6d710d1a381fb } from "@mintel/payload-ai/components/OptimizeButton";
|
||||
import { GenerateSlugButton as GenerateSlugButton_14905920587838350291144d261280d2 } from "@mintel/payload-ai/components/GenerateSlugButton";
|
||||
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
|
||||
import { GenerateThumbnailButton as GenerateThumbnailButton_460503dbfa1f8683217f2da98eff8b07 } from "@mintel/payload-ai/components/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_ac46cb1e009f156d26a8d4138c7c5945 } from "@mintel/payload-ai/components/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 { ConvertInquiryButton as ConvertInquiryButton_09fd670bce023a947ab66e4eebea5168 } from "@/src/payload/components/ConvertInquiryButton";
|
||||
import { AiAnalyzeButton as AiAnalyzeButton_51a6009c2b12d068d736ffd2b8182c71 } from "@/src/payload/components/AiAnalyzeButton";
|
||||
import { GanttChartView as GanttChartView_0162b82db971e8f1e27fbdd0aaa2f1f4 } from "@/src/payload/views/GanttChart";
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from "@payloadcms/storage-s3/client";
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||
|
||||
export const importMap = {
|
||||
"@mintel/payload-ai/components/AiMediaButtons#AiMediaButtons":
|
||||
AiMediaButtons_1d402a78164f07306f77dce953e62e11,
|
||||
"@mintel/payload-ai/components/OptimizeButton#OptimizeButton":
|
||||
OptimizeButton_338ff118e214cff355f6d710d1a381fb,
|
||||
"@mintel/payload-ai/components/GenerateSlugButton#GenerateSlugButton":
|
||||
GenerateSlugButton_14905920587838350291144d261280d2,
|
||||
"@/src/payload/components/TagSelector#default":
|
||||
default_76cec558bd86098fa1dab70b12eb818f,
|
||||
"@mintel/payload-ai/components/GenerateThumbnailButton#GenerateThumbnailButton":
|
||||
GenerateThumbnailButton_460503dbfa1f8683217f2da98eff8b07,
|
||||
"@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,
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton":
|
||||
AiFieldButton_ac46cb1e009f156d26a8d4138c7c5945,
|
||||
"@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,
|
||||
"@/src/payload/components/ConvertInquiryButton#ConvertInquiryButton":
|
||||
ConvertInquiryButton_09fd670bce023a947ab66e4eebea5168,
|
||||
"@/src/payload/components/AiAnalyzeButton#AiAnalyzeButton":
|
||||
AiAnalyzeButton_51a6009c2b12d068d736ffd2b8182c71,
|
||||
"@/src/payload/views/GanttChart#GanttChartView":
|
||||
GanttChartView_0162b82db971e8f1e27fbdd0aaa2f1f4,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler":
|
||||
S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
"@payloadcms/next/rsc#CollectionCards":
|
||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
16
apps/web/app/(payload)/api/[...slug]/route.ts
Normal file
16
apps/web/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import config from "@payload-config";
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from "@payloadcms/next/routes";
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
20
apps/web/app/(payload)/layout.tsx
Normal file
20
apps/web/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import configPromise from "@payload-config";
|
||||
import "@payloadcms/next/css";
|
||||
import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import React from "react";
|
||||
|
||||
import { handleServerFunctions } from "./actions";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<RootLayout
|
||||
config={configPromise}
|
||||
importMap={importMap}
|
||||
serverFunction={handleServerFunctions}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
}
|
||||
23
apps/web/app/(site)/about/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/about/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Über mich."
|
||||
description="15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen halten."
|
||||
label="Marc Mintel"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Section } from "../../src/components/Section";
|
||||
import { Reveal } from "../../src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import {
|
||||
ExperienceIllustration,
|
||||
ResponsibilityIllustration,
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
HeroLines,
|
||||
ParticleNetwork,
|
||||
GridLines,
|
||||
} from "../../src/components/Landing";
|
||||
} from "@/src/components/Landing";
|
||||
import { Signature } from "@/src/components/Signature";
|
||||
import { Check } from "lucide-react";
|
||||
import {
|
||||
H1,
|
||||
@@ -21,17 +22,17 @@ import {
|
||||
BodyText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
} from "../../src/components/Typography";
|
||||
import { Card, Container } from "../../src/components/Layout";
|
||||
import { Button } from "../../src/components/Button";
|
||||
import { IconList, IconListItem } from "../../src/components/IconList";
|
||||
} from "@/src/components/Typography";
|
||||
import { Card, Container } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IconList, IconListItem } from "@/src/components/IconList";
|
||||
import {
|
||||
GradientMesh,
|
||||
CodeSnippet,
|
||||
AbstractCircuit,
|
||||
} from "../../src/components/Effects";
|
||||
import { getImgproxyUrl } from "../../src/utils/imgproxy";
|
||||
import { Marker } from "../../src/components/Marker";
|
||||
} from "@/src/components/Effects";
|
||||
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
@@ -50,12 +51,7 @@ export default function AboutPage() {
|
||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||
<img
|
||||
src={getImgproxyUrl("/marc-mintel.png", {
|
||||
width: 400,
|
||||
height: 400,
|
||||
resizing_type: "fill",
|
||||
gravity: "sm",
|
||||
})}
|
||||
src="/marc-mintel.png"
|
||||
alt="Marc Mintel"
|
||||
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
||||
/>
|
||||
@@ -115,10 +111,8 @@ export default function AboutPage() {
|
||||
Agenturen, Konzerne, Startups – ich habe die Branche von allen
|
||||
Seiten kennengelernt. Was hängen geblieben ist:{" "}
|
||||
<span className="text-slate-900">
|
||||
<Marker delay={0.2} color="rgba(148,163,184,0.15)">
|
||||
Ergebnisse zählen.
|
||||
</Marker>{" "}
|
||||
Nicht der Weg dorthin.
|
||||
<Marker delay={0.2}>Ergebnisse</Marker> zählen. Nicht der
|
||||
Weg dorthin.
|
||||
</span>
|
||||
</LeadText>
|
||||
<IconList className="space-y-4">
|
||||
@@ -228,53 +222,68 @@ export default function AboutPage() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 03: Philosophie – what drives me */}
|
||||
<Section number="03" title="Philosophie" borderTop>
|
||||
<div className="space-y-12 md:space-y-16">
|
||||
{/* Section 03: Garantie – The Pledge */}
|
||||
<Section number="03" title="Garantie" borderTop>
|
||||
<div className="relative">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Ich stehe für <br />
|
||||
<span className="text-slate-400">meine Arbeit gerade.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<div className="max-w-4xl text-left space-y-12 md:space-y-16 py-8 md:py-16">
|
||||
<H3 className="text-3xl md:text-6xl leading-tight">
|
||||
Ich stehe für <br />
|
||||
<span className="text-slate-400">meine Arbeit gerade.</span>
|
||||
</H3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-start">
|
||||
<div className="space-y-8 min-w-0">
|
||||
<Reveal delay={0.1}>
|
||||
<LeadText className="text-lg md:text-xl text-slate-400">
|
||||
Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt,
|
||||
liegt die Verantwortung bei mir – und ich{" "}
|
||||
<span className="text-slate-900">
|
||||
<Marker color="rgba(255,235,59,0.5)">löse es.</Marker>
|
||||
<div className="prose prose-lg md:prose-2xl text-slate-500 leading-relaxed">
|
||||
<p>
|
||||
Keine Hierarchien. Keine Ausreden. Wenn etwas nicht passt,
|
||||
liegt die Verantwortung bei mir.
|
||||
</p>
|
||||
<p>
|
||||
Ich liefere nicht nur Code, sondern{" "}
|
||||
<span className="text-slate-900 font-medium relative inline-block">
|
||||
Ergebnisse
|
||||
<svg
|
||||
className="absolute -bottom-2 left-0 w-full h-3 text-blue-500/30"
|
||||
viewBox="0 0 100 10"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path
|
||||
d="M0 5 Q 50 10 100 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{[
|
||||
"Vollständige Transparenz",
|
||||
"Ein Ansprechpartner",
|
||||
"Messbare Qualität",
|
||||
"Langfristige Partnerschaft",
|
||||
].map((item, i) => (
|
||||
<Reveal key={i} delay={0.2 + i * 0.05}>
|
||||
<div className="flex items-center gap-3 group">
|
||||
<div className="w-6 h-6 rounded-full bg-white border border-slate-200 flex items-center justify-center shrink-0 group-hover:bg-slate-900 group-hover:border-slate-900 group-hover:shadow-lg group-hover:shadow-blue-500/10 transition-all duration-300">
|
||||
<Check className="w-3 h-3 text-slate-400 group-hover:text-white transition-colors duration-300" />
|
||||
</div>
|
||||
<Label className="text-slate-900 text-xs md:text-sm">
|
||||
{item}
|
||||
</Label>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
, auf die Sie bauen können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-2xl text-left">
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<h4 className="font-bold text-slate-900 mb-2">
|
||||
Fixpreis-Garantie
|
||||
</h4>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Keine versteckten Kosten. Der vereinbarte Preis ist final.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<h4 className="font-bold text-slate-900 mb-2">
|
||||
Satisfaction Guarantee
|
||||
</h4>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Wir gehen erst live, wenn Sie zu 100% zufrieden sind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 md:pt-12 flex flex-col items-start">
|
||||
<div className="w-64 md:w-80">
|
||||
<Signature delay={0.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative terminal */}
|
||||
<Reveal delay={0.3} className="min-w-0">
|
||||
<CodeSnippet variant="terminal" className="opacity-70" />
|
||||
</Reveal>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -306,9 +315,7 @@ export default function AboutPage() {
|
||||
<LeadText className="text-lg md:text-4xl leading-tight max-w-2xl text-slate-400">
|
||||
Lassen Sie uns gemeinsam etwas bauen, das{" "}
|
||||
<span className="text-slate-900">
|
||||
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||
wirklich funktioniert.
|
||||
</Marker>
|
||||
wirklich <Marker delay={0.3}>funktioniert.</Marker>
|
||||
</span>
|
||||
</LeadText>
|
||||
|
||||
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getAllPosts } from "../../../../src/lib/posts";
|
||||
import { blogThumbnails } from "../../../../src/components/blog/blogThumbnails";
|
||||
import { BlogOGImageTemplate } from "../../../../src/components/BlogOGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
let backgroundImageSrc: string | undefined = undefined;
|
||||
|
||||
// If we have a custom generated thumbnail, convert it to a data URI for Satori
|
||||
if (post?.thumbnail) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), "public", post.thumbnail);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
||||
const mimeType =
|
||||
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
|
||||
err,
|
||||
);
|
||||
// Fall through to standard plain background
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnail = blogThumbnails[slug];
|
||||
|
||||
const title = post?.title || "Marc Mintel";
|
||||
const description =
|
||||
post?.description ||
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
const label = post ? "Blog Post" : "Engineering";
|
||||
const accentColor = thumbnail?.accent;
|
||||
const keyword = thumbnail?.keyword;
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<BlogOGImageTemplate
|
||||
title={title}
|
||||
description={description}
|
||||
label={label}
|
||||
accentColor={accentColor}
|
||||
keyword={keyword}
|
||||
backgroundImageSrc={backgroundImageSrc}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
136
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
136
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
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";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
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();
|
||||
return allPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) return {};
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
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();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString("de-DE", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<BlogPostHeader
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
date={formattedDate}
|
||||
readingTime={readingTime}
|
||||
slug={slug}
|
||||
thumbnail={post.thumbnail}
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<BlogPostStickyBar
|
||||
title={post.title}
|
||||
url={`https://mintel.me/blog/${slug}`}
|
||||
/>
|
||||
|
||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||
<div className="max-w-4xl mx-auto px-5 md:px-0">
|
||||
<Reveal delay={0.4} width="100%">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="article-content max-w-none">
|
||||
<TableOfContents />
|
||||
{post.lexicalContent ? (
|
||||
<PayloadRichText data={post.lexicalContent} />
|
||||
) : (
|
||||
<MDXContent code={post.body.code} />
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
<TextSelectionShare />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
apps/web/app/(site)/blog/page.tsx
Normal file
14
apps/web/app/(site)/blog/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
|
||||
};
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getAllPosts();
|
||||
return <BlogClient allPosts={posts as any} />;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React from "react";
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { Section } from "../../../src/components/Section";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import {
|
||||
H1,
|
||||
H2,
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
Label,
|
||||
MonoLabel,
|
||||
BodyText,
|
||||
} from "../../../src/components/Typography";
|
||||
import { BackgroundGrid, Container } from "../../../src/components/Layout";
|
||||
} from "@/src/components/Typography";
|
||||
import { BackgroundGrid, Container } from "@/src/components/Layout";
|
||||
import Link from "next/link";
|
||||
import { Button } from "../../../src/components/Button";
|
||||
import { IframeSection } from "../../../src/components/IframeSection";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IframeSection } from "@/src/components/IframeSection";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Marker } from "../../../src/components/Marker";
|
||||
import { GlitchText } from "../../../src/components/GlitchText";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
|
||||
export default function KLZCablesCaseStudy() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
@@ -98,7 +98,7 @@ export default function KLZCablesCaseStudy() {
|
||||
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-6 md:pl-12">
|
||||
<LeadText className="text-lg md:text-4xl leading-tight text-slate-900 font-medium">
|
||||
Engineering eines <br className="hidden md:block" />
|
||||
<Marker delay={0.2}>B2B Commerce Systems.</Marker>
|
||||
<Marker delay={0.2}>Systems.</Marker>
|
||||
</LeadText>
|
||||
<BodyText className="mt-4 md:mt-6 text-base md:text-xl text-slate-500 max-w-xl leading-relaxed font-serif italic">
|
||||
Vom statischen Altsystem zum industriellen Standard. Ich
|
||||
@@ -172,8 +172,8 @@ export default function KLZCablesCaseStudy() {
|
||||
Attribute in einer zentralen relationalen Instanz. Durch die
|
||||
Implementierung nativer PHP-Services und den Verzicht auf
|
||||
volatile Drittanbieter-Plugins wurde ein System geschaffen,
|
||||
das keine technologischen Überraschungen zulässt.{" "}
|
||||
<Marker delay={0.5}>Stability by Design.</Marker>
|
||||
das keine technologischen Überraschungen zulässt. Stability by{" "}
|
||||
<Marker delay={0.5}>Design.</Marker>
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Section } from "../../src/components/Section";
|
||||
import { Reveal } from "../../src/components/Reveal";
|
||||
import { H3, LeadText, Label, BodyText } from "../../src/components/Typography";
|
||||
import { Card } from "../../src/components/Layout";
|
||||
import { Button } from "../../src/components/Button";
|
||||
import { AbstractCircuit } from "../../src/components/Effects";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { H3, LeadText, Label, BodyText } from "@/src/components/Typography";
|
||||
import { Card } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
23
apps/web/app/(site)/contact/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/contact/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Kontakt."
|
||||
description="Bereit für eine Zusammenarbeit? Lassen Sie uns gemeinsam etwas bauen, das wirklich funktioniert."
|
||||
label="Get in touch"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Section } from "../../src/components/Section";
|
||||
import { ContactForm } from "../../src/components/ContactForm";
|
||||
import { AbstractCircuit } from "../../src/components/Effects";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { ContactForm } from "@/src/components/ContactForm";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
67
apps/web/app/(site)/error.tsx
Normal file
67
apps/web/app/(site)/error.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { Button } from "@/src/components/Button";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to Sentry/GlitchTip
|
||||
Sentry.captureException(error);
|
||||
console.error("Caught in error.tsx:", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-red-50 border border-red-100 rounded text-[10px] font-mono text-red-500 uppercase tracking-widest">
|
||||
Error 500
|
||||
</span>
|
||||
<h1 className="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter">
|
||||
Kritischer Fehler.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-slate-500 font-serif italic max-w-xl mx-auto leading-relaxed">
|
||||
Ein unerwartetes Problem ist aufgetreten. Unsere Systeme haben den
|
||||
Vorfall protokolliert.
|
||||
</p>
|
||||
|
||||
<div className="pt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button href="#" onClick={() => reset()} variant="primary">
|
||||
System neu starten (Retry)
|
||||
</Button>
|
||||
<Button href="/" variant="outline">
|
||||
Zurück zur Basis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-16 max-w-sm mx-auto">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100 text-left font-mono text-xs text-slate-400">
|
||||
<div className="flex items-center justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>STATUS</span>
|
||||
<span className="text-red-500 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span>
|
||||
FAIL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>TRACKING</span>
|
||||
<span className="text-green-500">GLITCHTIP_LOGGED</span>
|
||||
</div>
|
||||
{error.digest && (
|
||||
<div className="flex justify-between">
|
||||
<span>DIGEST</span>
|
||||
<span className="truncate max-w-[150px]">{error.digest}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
apps/web/app/(site)/errors/api/relay/route.ts
Normal file
55
apps/web/app/(site)/errors/api/relay/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Smart Proxy / Relay for Sentry/GlitchTip events.
|
||||
*
|
||||
* Mirroring the klz-2026 pattern:
|
||||
* Receives Sentry envelopes from the client, injects the correct DSN,
|
||||
* and forwards them to GlitchTip.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const envelope = await request.text();
|
||||
|
||||
const realDsn = env.SENTRY_DSN;
|
||||
|
||||
if (!realDsn) {
|
||||
console.warn(
|
||||
"[Sentry Relay] Received payload but no SENTRY_DSN configured",
|
||||
);
|
||||
return NextResponse.json({ status: "ignored" }, { status: 200 });
|
||||
}
|
||||
|
||||
const dsnUrl = new URL(realDsn);
|
||||
const projectId = dsnUrl.pathname.replace("/", "");
|
||||
const relayUrl = `${dsnUrl.protocol}//${dsnUrl.host}/api/${projectId}/envelope/`;
|
||||
|
||||
const response = await fetch(relayUrl, {
|
||||
method: "POST",
|
||||
body: envelope,
|
||||
headers: {
|
||||
"Content-Type": "application/x-sentry-envelope",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Sentry Relay] GlitchTip API responded with error", {
|
||||
status: response.status,
|
||||
error: errorText.slice(0, 100),
|
||||
});
|
||||
return new NextResponse(errorText, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
} catch (error) {
|
||||
console.error("[Sentry Relay] Failed to relay Sentry request", {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
59
apps/web/app/(site)/global-error.tsx
Normal file
59
apps/web/app/(site)/global-error.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import Error from "next/error";
|
||||
import { useEffect } from "react";
|
||||
import { Inter, Newsreader } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-newsreader",
|
||||
style: "italic",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<body className="min-h-screen bg-white font-sans text-slate-900">
|
||||
<div className="flex flex-col items-center justify-center min-h-screen px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-red-50 border border-red-200 rounded text-[10px] font-mono text-red-600 uppercase tracking-widest border-dashed">
|
||||
Root Level Error
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-6xl font-black tracking-tighter text-slate-900">
|
||||
Systemausfall der Hauptebene.
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-slate-500 font-serif italic max-w-xl mx-auto">
|
||||
Ein kritischer Fehler auf der Root-Layout Ebene hat das Rendering
|
||||
blockiert. Der Vorfall wurde zur Untersuchung protokolliert.
|
||||
</p>
|
||||
|
||||
<div className="pt-8">
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="relative inline-flex items-center justify-center gap-3 overflow-hidden rounded-full font-bold uppercase tracking-[0.15em] transition-all duration-300 group cursor-pointer px-8 py-4 text-[10px] bg-slate-900 text-white hover:shadow-xl hover:-translate-y-0.5"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-3">
|
||||
Notfall-Neustart (Reset)
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-4 text-sm md:text-base leading-relaxed text-slate-700;
|
||||
@apply mb-6 text-base leading-relaxed text-slate-700;
|
||||
}
|
||||
|
||||
.lead {
|
||||
@@ -149,32 +149,36 @@
|
||||
}
|
||||
|
||||
.article-content {
|
||||
@apply text-lg leading-relaxed;
|
||||
@apply font-serif antialiased text-slate-700;
|
||||
}
|
||||
|
||||
.article-content p {
|
||||
@apply mb-5;
|
||||
@apply text-lg md:text-xl leading-relaxed mb-6;
|
||||
}
|
||||
|
||||
.article-content h1 {
|
||||
@apply text-3xl md:text-5xl font-bold text-slate-900 mb-8 mt-12 tracking-tight leading-[1.1] font-sans;
|
||||
}
|
||||
|
||||
.article-content h2 {
|
||||
@apply text-2xl font-bold mt-8 mb-3;
|
||||
@apply text-2xl md:text-4xl font-bold text-slate-900 mb-6 mt-10 tracking-tight leading-tight font-sans;
|
||||
}
|
||||
|
||||
.article-content h3 {
|
||||
@apply text-xl font-bold mt-6 mb-2;
|
||||
@apply text-xl md:text-2xl font-bold text-slate-900 mb-4 mt-8 tracking-tight leading-snug font-sans;
|
||||
}
|
||||
|
||||
.article-content ul,
|
||||
.article-content ol {
|
||||
@apply ml-6 mb-5;
|
||||
@apply ml-6 mb-6 text-lg md:text-xl;
|
||||
}
|
||||
|
||||
.article-content li {
|
||||
@apply mb-1;
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
.article-content blockquote {
|
||||
@apply border-l-2 border-slate-400 pl-4 italic text-slate-600 my-5 text-lg;
|
||||
@apply border-l-4 border-slate-900 pl-6 italic text-slate-700 my-10 text-xl md:text-2xl;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
46
apps/web/app/(site)/layout.tsx
Normal file
46
apps/web/app/(site)/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Newsreader } from "next/font/google";
|
||||
import { Analytics } from "@/src/components/Analytics";
|
||||
import { Footer } from "@/src/components/Footer";
|
||||
import { Header } from "@/src/components/Header";
|
||||
import { InteractiveElements } from "@/src/components/InteractiveElements";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-newsreader",
|
||||
style: "italic",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Marc Mintel",
|
||||
template: "%s | Marc Mintel",
|
||||
},
|
||||
description:
|
||||
"Technical problem solver's blog - practical insights and learning notes",
|
||||
metadataBase: new URL("https://mintel.me"),
|
||||
icons: {
|
||||
icon: "/favicon.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<body className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
<InteractiveElements />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
52
apps/web/app/(site)/not-found.tsx
Normal file
52
apps/web/app/(site)/not-found.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
export const metadata = {
|
||||
title: "404 - Seite nicht gefunden | Marc Mintel",
|
||||
description: "Diese Seite konnte leider nicht gefunden werden.",
|
||||
};
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/src/components/Button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-slate-50 border border-slate-100 rounded text-[10px] font-mono text-slate-500 uppercase tracking-widest">
|
||||
Error 404
|
||||
</span>
|
||||
<h1 className="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter">
|
||||
System-Anomalie.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-slate-500 font-serif italic max-w-xl mx-auto leading-relaxed">
|
||||
Die angeforderte URL existiert nicht in dieser Zeitleiste.
|
||||
Möglicherweise wurde die Seite verschoben oder gelöscht.
|
||||
</p>
|
||||
|
||||
<div className="pt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button href="/" variant="primary">
|
||||
Zurück zur Basis
|
||||
</Button>
|
||||
<Button href="/blog" variant="outline">
|
||||
Aktuelle Artikel lesen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-16 max-w-sm mx-auto">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100 text-left font-mono text-xs text-slate-400">
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>STATUS</span>
|
||||
<span className="text-red-500">404 NOT_FOUND</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>ACTION</span>
|
||||
<span>REROUTE_SUGGESTED</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>SYSTEM</span>
|
||||
<span className="text-green-500">ONLINE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/web/app/(site)/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Marc Mintel"
|
||||
description="Technical problem solver's blog - practical insights and learning notes"
|
||||
label="Engineering"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
ConceptPrice,
|
||||
ConceptPrototyping,
|
||||
ConceptWebsite,
|
||||
} from "../src/components/Landing";
|
||||
import { Reveal } from "../src/components/Reveal";
|
||||
import { Section } from "../src/components/Section";
|
||||
} from "@/src/components/Landing";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import {
|
||||
H1,
|
||||
H3,
|
||||
@@ -17,14 +17,15 @@ import {
|
||||
BodyText,
|
||||
MonoLabel,
|
||||
Label,
|
||||
} from "../src/components/Typography";
|
||||
import { Card, Container } from "../src/components/Layout";
|
||||
import { Button } from "../src/components/Button";
|
||||
import { GradientMesh, CodeSnippet } from "../src/components/Effects";
|
||||
import { IconList, IconListItem } from "../src/components/IconList";
|
||||
import { HeroSection } from "../src/components/HeroSection";
|
||||
import { GlitchText } from "../src/components/GlitchText";
|
||||
import { Marker } from "../src/components/Marker";
|
||||
} from "@/src/components/Typography";
|
||||
import { Card, Container } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { GradientMesh, CodeSnippet } from "@/src/components/Effects";
|
||||
import { IconList, IconListItem } from "@/src/components/IconList";
|
||||
import { HeroSection } from "@/src/components/HeroSection";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { PenCircle } from "@/src/components/PenCircle";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
@@ -45,12 +46,7 @@ export default function LandingPage() {
|
||||
<Reveal>
|
||||
<H3 className="max-w-3xl">
|
||||
Kein Agentur-Zirkus. <br />
|
||||
<span className="text-slate-400">
|
||||
Nur{" "}
|
||||
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||
Ergebnisse.
|
||||
</Marker>
|
||||
</span>
|
||||
<Marker delay={0.3}>Ergebnisse.</Marker>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
@@ -104,12 +100,7 @@ export default function LandingPage() {
|
||||
<Reveal>
|
||||
<H3 className="max-w-3xl">
|
||||
Ich arbeite für das Ergebnis, <br />
|
||||
<span className="text-slate-400">
|
||||
nicht gegen die{" "}
|
||||
<Marker delay={0.4} color="rgba(148,163,184,0.1)">
|
||||
Uhr.
|
||||
</Marker>
|
||||
</span>
|
||||
nicht gegen die <Marker delay={0.4}>Uhr.</Marker>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
@@ -118,14 +109,24 @@ export default function LandingPage() {
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Wochen in Planung, bevor eine einzige Zeile Code geschrieben wird."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText="Schnelle Prototypen. Ergebnisse in Tagen, nicht Monaten."
|
||||
positiveText={
|
||||
<>
|
||||
Schnelle Prototypen. Ergebnisse in{" "}
|
||||
<PenCircle delay={0.5}>Tagen</PenCircle>, nicht Monaten.
|
||||
</>
|
||||
}
|
||||
delay={0.1}
|
||||
/>
|
||||
<ComparisonRow
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Unvorhersehbare Kosten durch Stundenabrechnungen."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText="Fixpreise. Sie wissen von Anfang an, was es kostet."
|
||||
positiveText={
|
||||
<>
|
||||
<PenCircle delay={0.5}>Fixpreise.</PenCircle> Sie wissen von
|
||||
Anfang an, was es kostet.
|
||||
</>
|
||||
}
|
||||
reverse
|
||||
delay={0.2}
|
||||
/>
|
||||
@@ -290,7 +291,7 @@ export default function LandingPage() {
|
||||
<LeadText className="text-lg md:text-3xl text-slate-400">
|
||||
Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "}
|
||||
<span className="text-slate-900 border-b-2 border-slate-900/10">
|
||||
<Marker color="rgba(255,235,59,0.5)">zeitnah</Marker>
|
||||
<Marker>zeitnah</Marker>
|
||||
</span>{" "}
|
||||
bei Ihnen.
|
||||
</LeadText>
|
||||
57
apps/web/app/(site)/sitemap.ts
Normal file
57
apps/web/app/(site)/sitemap.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { technologies } from "./technologies/[slug]/data";
|
||||
|
||||
/**
|
||||
* Sitemap Generator
|
||||
*
|
||||
* Standard Next.js 15 App Router sitemap generation.
|
||||
* This file dynamically generates /sitemap.xml
|
||||
*/
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const allPosts = await getAllPosts();
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mintel.me";
|
||||
|
||||
// 1. Core Pages
|
||||
const routes = [
|
||||
"",
|
||||
"/about",
|
||||
"/blog",
|
||||
"/case-studies",
|
||||
"/case-studies/klz-cables",
|
||||
"/contact",
|
||||
"/websites",
|
||||
].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: route === "" ? 1.0 : 0.8,
|
||||
}));
|
||||
|
||||
// 2. Dynamic Blog Posts
|
||||
const blogRoutes = allPosts.map((post) => ({
|
||||
url: `${baseUrl}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
// 3. Technology Detail Pages
|
||||
const techRoutes = Object.keys(technologies).map((slug) => ({
|
||||
url: `${baseUrl}/technologies/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
// 4. Tag Pages
|
||||
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
||||
const tagRoutes = allTags.map((tag) => ({
|
||||
url: `${baseUrl}/tags/${tag}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.3,
|
||||
}));
|
||||
|
||||
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
||||
}
|
||||
77
apps/web/app/(site)/stats/api/send/route.ts
Normal file
77
apps/web/app/(site)/stats/api/send/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Smart Proxy for Umami Analytics.
|
||||
*
|
||||
* This Route Handler receives tracking events from the browser,
|
||||
* injects the secret UMAMI_WEBSITE_ID, and forwards them to the
|
||||
* internal Umami API endpoint.
|
||||
*
|
||||
* This ensures:
|
||||
* 1. The Website ID is NOT leaked to the client bundle.
|
||||
* 2. The Umami API endpoint is hidden behind our domain.
|
||||
* 3. We have full control over the tracking data.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { type, payload } = body;
|
||||
|
||||
// Inject the secret websiteId from server config
|
||||
const websiteId = env.UMAMI_WEBSITE_ID || env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
||||
|
||||
if (!websiteId) {
|
||||
console.warn(
|
||||
"Umami tracking received but no Website ID configured on server",
|
||||
);
|
||||
return NextResponse.json({ status: "ignored" }, { status: 200 });
|
||||
}
|
||||
|
||||
// Prepare the enhanced payload with the secret ID
|
||||
const enhancedPayload = {
|
||||
...payload,
|
||||
website: websiteId,
|
||||
};
|
||||
|
||||
const umamiEndpoint = env.UMAMI_API_ENDPOINT;
|
||||
|
||||
// Log the event (debug only)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Forwarding analytics event", {
|
||||
type,
|
||||
url: payload.url,
|
||||
website: websiteId.slice(0, 8) + "...",
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`${umamiEndpoint}/api/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": request.headers.get("user-agent") || "Mintel-Smart-Proxy",
|
||||
"X-Forwarded-For": request.headers.get("x-forwarded-for") || "",
|
||||
},
|
||||
body: JSON.stringify({ type, payload: enhancedPayload }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Umami API responded with error", {
|
||||
status: response.status,
|
||||
error: errorText.slice(0, 100),
|
||||
});
|
||||
return new NextResponse(errorText, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
} catch (error) {
|
||||
console.error("Failed to proxy analytics request", {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { blogPosts } from "../../../src/data/blogPosts";
|
||||
import { MediumCard } from "../../../src/components/MediumCard";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { MediumCard } from "@/src/components/MediumCard";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
const allTags = Array.from(
|
||||
new Set(blogPosts.flatMap((post) => post.tags || [])),
|
||||
new Set(allPosts.flatMap((post) => post.tags || [])),
|
||||
);
|
||||
return allTags.map((tag) => ({
|
||||
tag,
|
||||
@@ -19,7 +19,8 @@ export default async function TagPage({
|
||||
params: Promise<{ tag: string }>;
|
||||
}) {
|
||||
const { tag } = await params;
|
||||
const posts = blogPosts.filter((post) => post.tags?.includes(tag));
|
||||
const allPosts = await getAllPosts();
|
||||
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { Container } from "../../../src/components/Layout";
|
||||
import { Label } from "../../../src/components/Typography";
|
||||
import { Container } from "@/src/components/Layout";
|
||||
import { Label } from "@/src/components/Typography";
|
||||
import { Check, ArrowLeft, Zap, ExternalLink } from "lucide-react";
|
||||
import { technologies } from "./data";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export default function TechnologyContent({ slug }: { slug: string }) {
|
||||
const tech = technologies[slug];
|
||||
95
apps/web/app/(site)/technologies/[slug]/data.tsx
Normal file
95
apps/web/app/(site)/technologies/[slug]/data.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Layers, Code, Database, Palette, Terminal } from "lucide-react";
|
||||
|
||||
export interface TechInfo {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
icon: any; // React.ElementType
|
||||
benefits: string[];
|
||||
customerValue: string;
|
||||
color: string;
|
||||
related: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
export const technologies: Record<string, TechInfo> = {
|
||||
"next-js-14": {
|
||||
title: "Next.js 14",
|
||||
subtitle: "The React Framework for the Web",
|
||||
description:
|
||||
"Next.js 14 is the latest version of the industry-leading framework for building high-performance web applications. It allows me to create fast, scalable, and search-engine-friendly websites by rendering content on the server before sending it to your users.",
|
||||
icon: Layers,
|
||||
benefits: [
|
||||
"Lightning-fast page loads with Server Components",
|
||||
"Automatic image optimization",
|
||||
"Instant navigation between pages",
|
||||
"Top-tier SEO (Search Engine Optimization) out of the box",
|
||||
],
|
||||
customerValue:
|
||||
"For my clients, Next.js means a website that ranks higher on Google, keeps visitors engaged with instant interactions, and scales effortlessly as your traffic grows.",
|
||||
color: "bg-black text-white",
|
||||
related: [
|
||||
{ name: "TypeScript", slug: "typescript" },
|
||||
{ name: "React", slug: "react" },
|
||||
],
|
||||
},
|
||||
typescript: {
|
||||
title: "TypeScript",
|
||||
subtitle: "JavaScript with Syntax for Types",
|
||||
description:
|
||||
"TypeScript adds a powerful type system to JavaScript, catching errors before they ever reach production. It acts as a safety net for your code, ensuring that data flows exactly as expected through your entire application.",
|
||||
icon: Code,
|
||||
benefits: [
|
||||
"Eliminates whole categories of common bugs",
|
||||
"Makes large codebases easier to maintain",
|
||||
"Improves developer productivity and code confidence",
|
||||
"Ensures critical data integrity",
|
||||
],
|
||||
customerValue:
|
||||
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: "bg-blue-600 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
|
||||
"tailwind-css": {
|
||||
title: "Tailwind CSS",
|
||||
subtitle: "Utility-First CSS Framework",
|
||||
description:
|
||||
"Tailwind CSS is a utility-first framework that allows me to build custom designs directly in markup. It eliminates the need for bulky, overriding stylesheets and ensures a consistent design system across every page.",
|
||||
icon: Palette,
|
||||
benefits: [
|
||||
"Rapid UI development and prototyping",
|
||||
"Consistent spacing, colors, and typography",
|
||||
"Highly optimized final bundle size (only includes used styles)",
|
||||
"Responsive design made simple",
|
||||
],
|
||||
customerValue:
|
||||
"Tailwind ensures your brand looks pixel-perfect on every device. It also results in incredibly small CSS files, meaning your site loads faster for users on mobile networks.",
|
||||
color: "bg-cyan-500 text-white",
|
||||
related: [
|
||||
{ name: "React", slug: "react" },
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
react: {
|
||||
title: "React",
|
||||
subtitle: "The Library for Web and Native User Interfaces",
|
||||
description:
|
||||
"React is the core library powering Next.js. It lets me build encapsulated components that manage their own state, then compose them to make complex UIs.",
|
||||
icon: Terminal,
|
||||
benefits: [
|
||||
"Component-based architecture for reuse",
|
||||
"Efficient updates and rendering",
|
||||
"Rich ecosystem of libraries and tools",
|
||||
"Strong community support",
|
||||
],
|
||||
customerValue:
|
||||
"React enables rich, app-like interactions on your website. Whether it's a complex dashboard or a smooth animation, React makes it possible to build dynamic experiences that feel instantaneous.",
|
||||
color: "bg-blue-400 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
||||
],
|
||||
},
|
||||
};
|
||||
19
apps/web/app/(site)/technologies/[slug]/page.tsx
Normal file
19
apps/web/app/(site)/technologies/[slug]/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { technologies } from "./data";
|
||||
import TechnologyContent from "./content";
|
||||
|
||||
export default async function TechnologyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
return <TechnologyContent slug={slug} />;
|
||||
}
|
||||
|
||||
// Generate static params for these dynamic routes
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(technologies).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Reveal } from "../../src/components/Reveal";
|
||||
import { Section } from "../../src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import {
|
||||
SpeedPerformance,
|
||||
SolidFoundation,
|
||||
LayerSeparation,
|
||||
TaskDone,
|
||||
} from "../../src/components/Landing";
|
||||
} from "@/src/components/Landing";
|
||||
import {
|
||||
H3,
|
||||
LeadText,
|
||||
BodyText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
} from "../../src/components/Typography";
|
||||
import { Card } from "../../src/components/Layout";
|
||||
import { Button } from "../../src/components/Button";
|
||||
import { IconList, IconListItem } from "../../src/components/IconList";
|
||||
} from "@/src/components/Typography";
|
||||
import { Card } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IconList, IconListItem } from "@/src/components/IconList";
|
||||
import {
|
||||
GradientMesh,
|
||||
CodeSnippet,
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
CMSVisualizer,
|
||||
ArchitectureVisualizer,
|
||||
ResultVisualizer,
|
||||
} from "../../src/components/Effects";
|
||||
import { Marker } from "../../src/components/Marker";
|
||||
} from "@/src/components/Effects";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
|
||||
export default function WebsitesPage() {
|
||||
return (
|
||||
@@ -42,11 +42,9 @@ export default function WebsitesPage() {
|
||||
SYSTEM ENGINEERING
|
||||
</MonoLabel>
|
||||
<H3 className="text-4xl md:text-8xl leading-[1.0] tracking-tighter">
|
||||
Websites, die <br />
|
||||
Websites, die einfach <br />
|
||||
<span className="text-slate-400">
|
||||
<Marker color="rgba(255,235,59,0.5)">
|
||||
einfach funktionieren.
|
||||
</Marker>
|
||||
<Marker>funktionieren.</Marker>
|
||||
</span>
|
||||
</H3>
|
||||
</div>
|
||||
@@ -121,9 +119,7 @@ export default function WebsitesPage() {
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Geschwindigkeit ist <br />
|
||||
<span className="text-slate-400">
|
||||
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||
kein Extra. Sie ist Standard.
|
||||
</Marker>
|
||||
kein Extra. Sie ist <Marker delay={0.3}>Standard.</Marker>
|
||||
</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
@@ -1,268 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { FileExampleManager } from '../../../src/data/fileExamples';
|
||||
|
||||
// Simple ZIP creation without external dependencies
|
||||
class SimpleZipCreator {
|
||||
private files: Array<{ filename: string; content: string }> = [];
|
||||
|
||||
addFile(filename: string, content: string) {
|
||||
this.files.push({ filename, content });
|
||||
}
|
||||
|
||||
// Create a basic ZIP file structure
|
||||
create(): number[] {
|
||||
const encoder = new TextEncoder();
|
||||
const chunks: number[][] = [];
|
||||
|
||||
let offset = 0;
|
||||
const centralDirectory: Array<{
|
||||
name: string;
|
||||
offset: number;
|
||||
size: number;
|
||||
compressedSize: number;
|
||||
}> = [];
|
||||
|
||||
// Process each file
|
||||
for (const file of this.files) {
|
||||
const contentBytes = Array.from(encoder.encode(file.content));
|
||||
const filenameBytes = Array.from(encoder.encode(file.filename));
|
||||
|
||||
// Local file header
|
||||
const localHeader: number[] = [];
|
||||
|
||||
// Local file header signature (little endian)
|
||||
localHeader.push(0x50, 0x4b, 0x03, 0x04);
|
||||
// Version needed to extract
|
||||
localHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
localHeader.push(0, 0);
|
||||
// Compression method (0 = store)
|
||||
localHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// CRC32 (0 for simplicity)
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Uncompressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Filename length
|
||||
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
localHeader.push(0, 0);
|
||||
|
||||
// Add filename
|
||||
localHeader.push(...filenameBytes);
|
||||
|
||||
chunks.push(localHeader);
|
||||
chunks.push(contentBytes);
|
||||
|
||||
// Store info for central directory
|
||||
centralDirectory.push({
|
||||
name: file.filename,
|
||||
offset: offset,
|
||||
size: contentBytes.length,
|
||||
compressedSize: contentBytes.length
|
||||
});
|
||||
|
||||
offset += localHeader.length + contentBytes.length;
|
||||
}
|
||||
|
||||
// Central directory
|
||||
const centralDirectoryChunks: number[][] = [];
|
||||
let centralDirectoryOffset = offset;
|
||||
|
||||
for (const entry of centralDirectory) {
|
||||
const filenameBytes = Array.from(encoder.encode(entry.name));
|
||||
const centralHeader: number[] = [];
|
||||
|
||||
// Central directory header signature
|
||||
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
|
||||
// Version made by
|
||||
centralHeader.push(20, 0);
|
||||
// Version needed to extract
|
||||
centralHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
centralHeader.push(0, 0);
|
||||
// Compression method
|
||||
centralHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// CRC32
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
|
||||
// Uncompressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.size, 4));
|
||||
// Filename length
|
||||
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
centralHeader.push(0, 0);
|
||||
// File comment length
|
||||
centralHeader.push(0, 0);
|
||||
// Disk number start
|
||||
centralHeader.push(0, 0);
|
||||
// Internal file attributes
|
||||
centralHeader.push(0, 0);
|
||||
// External file attributes
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Relative offset of local header
|
||||
centralHeader.push(...intToLittleEndian(entry.offset, 4));
|
||||
|
||||
// Add filename
|
||||
centralHeader.push(...filenameBytes);
|
||||
|
||||
centralDirectoryChunks.push(centralHeader);
|
||||
}
|
||||
|
||||
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
|
||||
// End of central directory
|
||||
const endOfCentralDirectory: number[] = [];
|
||||
|
||||
// End of central directory signature
|
||||
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
|
||||
// Number of this disk
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Number of the disk with the start of the central directory
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Total number of entries on this disk
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Total number of entries
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Size of the central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
|
||||
// Offset of start of central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
|
||||
// ZIP file comment length
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
|
||||
// Combine all chunks
|
||||
const result: number[] = [];
|
||||
chunks.forEach(chunk => result.push(...chunk));
|
||||
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
|
||||
result.push(...endOfCentralDirectory);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert integer to little endian bytes
|
||||
function intToLittleEndian(value: number, bytes: number): number[] {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
result.push((value >> (i * 8)) & 0xff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { fileIds } = body;
|
||||
|
||||
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'fileIds array is required and must not be empty' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get file contents
|
||||
const files = await Promise.all(
|
||||
fileIds.map(async (id) => {
|
||||
const file = await FileExampleManager.getFileExample(id);
|
||||
if (!file) {
|
||||
throw new Error(`File with id ${id} not found`);
|
||||
}
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
||||
// Create ZIP
|
||||
const zipCreator = new SimpleZipCreator();
|
||||
files.forEach(file => {
|
||||
zipCreator.addFile(file.filename, file.content);
|
||||
});
|
||||
|
||||
const zipData = zipCreator.create();
|
||||
const buffer = Buffer.from(new Uint8Array(zipData));
|
||||
|
||||
// Return ZIP file
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('ZIP download error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create zip file', details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileId = searchParams.get('id');
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'id parameter is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const file = await FileExampleManager.getFileExample(fileId);
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const content = encoder.encode(file.content);
|
||||
const buffer = Buffer.from(content);
|
||||
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': getMimeType(file.language),
|
||||
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('File download error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to download file' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get MIME type
|
||||
function getMimeType(language: string): string {
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'python': 'text/x-python',
|
||||
'typescript': 'text/x-typescript',
|
||||
'javascript': 'text/javascript',
|
||||
'dockerfile': 'text/x-dockerfile',
|
||||
'yaml': 'text/yaml',
|
||||
'json': 'application/json',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'sql': 'text/x-sql',
|
||||
'bash': 'text/x-shellscript',
|
||||
'text': 'text/plain'
|
||||
};
|
||||
return mimeTypes[language] || 'text/plain';
|
||||
}
|
||||
42
apps/web/app/api/health/cms/route.ts
Normal file
42
apps/web/app/api/health/cms/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Deep CMS Health Check
|
||||
* Validates that Payload CMS can actually query the database.
|
||||
* Used by post-deploy smoke tests to catch migration/schema issues.
|
||||
*/
|
||||
export async function GET() {
|
||||
const checks: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
checks.init = "ok";
|
||||
|
||||
// Verify each collection can be queried (catches missing locale tables, broken migrations)
|
||||
// Adjusted for mintel.me collections
|
||||
const collections = ["posts", "projects", "media", "inquiries"] as const;
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
await payload.find({ collection, limit: 1 });
|
||||
checks[collection] = "ok";
|
||||
} catch (e: any) {
|
||||
checks[collection] = `error: ${e.message?.substring(0, 100)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const hasErrors = Object.values(checks).some((v) => v.startsWith("error"));
|
||||
return NextResponse.json(
|
||||
{ status: hasErrors ? "degraded" : "ok", checks },
|
||||
{ status: hasErrors ? 503 : 200 },
|
||||
);
|
||||
} catch (e: any) {
|
||||
return NextResponse.json(
|
||||
{ status: "error", message: e.message?.substring(0, 200), checks },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { blogPosts } from "../../../../src/data/blogPosts";
|
||||
import { OGImageTemplate } from "../../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> },
|
||||
) {
|
||||
const { slug: slugArray } = await params;
|
||||
const slug = slugArray?.[0] || "home";
|
||||
|
||||
let title: string;
|
||||
let description: string;
|
||||
let label: string | undefined;
|
||||
|
||||
if (slug === "home") {
|
||||
title = "Marc Mintel";
|
||||
description =
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
label = "Engineering";
|
||||
} else {
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
title = post?.title || "Marc Mintel";
|
||||
description =
|
||||
post?.description ||
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
label = post ? "Blog Post" : "Engineering";
|
||||
}
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label={label} />,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
22
apps/web/app/api/tweet/[id]/route.ts
Normal file
22
apps/web/app/api/tweet/[id]/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getTweet } from 'react-tweet/api';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const tweet = await getTweet(id);
|
||||
|
||||
if (!tweet) {
|
||||
return NextResponse.json({ error: 'Tweet not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ data: tweet });
|
||||
} catch (error) {
|
||||
console.error('Error fetching tweet:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch tweet' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { blogPosts } from "../../../src/data/blogPosts";
|
||||
import { PageHeader } from "../../../src/components/PageHeader";
|
||||
import { Section } from "../../../src/components/Section";
|
||||
import { Reveal } from "../../../src/components/Reveal";
|
||||
import { BlogPostClient } from "../../../src/components/BlogPostClient";
|
||||
import { PostComponents } from "../../../src/components/blog/posts";
|
||||
import { Card } from "../../../src/components/Layout";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return blogPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString("de-DE", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300; // Average post length
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
const PostContent = PostComponents[slug];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<PageHeader
|
||||
variant="blog"
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
backgroundSymbol={slug.charAt(0).toUpperCase()}
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||
<div className="max-w-5xl mx-auto px-0 sm:px-4 md:px-0">
|
||||
<Reveal delay={0.4} width="100%">
|
||||
<Card
|
||||
variant="glass"
|
||||
techBorder
|
||||
className="relative overflow-hidden rounded-none sm:rounded-3xl"
|
||||
>
|
||||
{/* Decorative background grid inside the card */}
|
||||
<div className="absolute inset-0 opacity-[0.03] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]" />
|
||||
|
||||
<div className="relative z-10 px-5 py-10 md:px-16 md:py-20">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-[9px] md:text-[10px] font-bold text-slate-400 mb-10 md:mb-12 uppercase tracking-[0.2em] border-b border-slate-100 pb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-2 h-2 rounded-full bg-slate-300" />
|
||||
<time dateTime={post.date}>{formattedDate}</time>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 sm:gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-200 hidden sm:inline">
|
||||
|
|
||||
</span>
|
||||
<span>{readingTime} min Lesezeit</span>
|
||||
</div>
|
||||
<span>
|
||||
{slug.substring(0, 4).toUpperCase()}-
|
||||
{Math.floor(Math.random() * 999)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags.map((tag, index) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{PostContent ? (
|
||||
<PostContent />
|
||||
) : (
|
||||
<div className="p-8 bg-slate-50 border border-slate-200 rounded-lg italic text-slate-500">
|
||||
Inhalt wird bald veröffentlicht...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { MediumCard } from "../../src/components/MediumCard";
|
||||
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
|
||||
import { blogPosts } from "../../src/data/blogPosts";
|
||||
import { SectionHeader } from "../../src/components/SectionHeader";
|
||||
import { Reveal } from "../../src/components/Reveal";
|
||||
import { Section } from "../../src/components/Section";
|
||||
import { AbstractCircuit, GradientMesh } from "../../src/components/Effects";
|
||||
|
||||
export default function BlogPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTags, setActiveTags] = useState<string[]>([]);
|
||||
|
||||
// Memoize allPosts
|
||||
const allPosts = React.useMemo(
|
||||
() =>
|
||||
[...blogPosts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const [filteredPosts, setFilteredPosts] = useState(allPosts);
|
||||
|
||||
// Memoize allTags
|
||||
const allTags = React.useMemo(
|
||||
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
|
||||
[allPosts],
|
||||
);
|
||||
|
||||
const [visibleCount, setVisibleCount] = useState(8);
|
||||
|
||||
const handleTagToggle = (tag: string) => {
|
||||
setActiveTags((prev) =>
|
||||
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
|
||||
);
|
||||
setVisibleCount(8); // Reset pagination on filter change
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
|
||||
let filtered = allPosts;
|
||||
|
||||
if (query) {
|
||||
filtered = filtered.filter((post) => {
|
||||
const title = post.title.toLowerCase();
|
||||
const description = post.description.toLowerCase();
|
||||
const pTagString = (post.tags || []).join(" ").toLowerCase();
|
||||
return (
|
||||
title.includes(query) ||
|
||||
description.includes(query) ||
|
||||
pTagString.includes(query)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (activeTags.length > 0) {
|
||||
filtered = filtered.filter((post) =>
|
||||
post.tags?.some((tag) => activeTags.includes(tag)),
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredPosts(filtered);
|
||||
}, [searchQuery, activeTags, allPosts]);
|
||||
|
||||
const loadMore = () => {
|
||||
setVisibleCount((prev) => prev + 6);
|
||||
};
|
||||
|
||||
const hasMore = visibleCount < filteredPosts.length;
|
||||
const postsToShow = filteredPosts.slice(0, visibleCount);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-slate-50/30 overflow-hidden relative min-h-screen">
|
||||
<AbstractCircuit />
|
||||
|
||||
<Section
|
||||
effects={<GradientMesh variant="metallic" className="opacity-40" />}
|
||||
className="pb-32 pt-12 md:pt-20"
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="max-w-[calc(100vw-2rem)] md:max-w-7xl mx-auto space-y-12">
|
||||
{/* Section Header & Filters - Centered & Compact */}
|
||||
<div className="relative z-20 space-y-12 flex flex-col items-center text-center">
|
||||
<SectionHeader
|
||||
title="Alle Artikel"
|
||||
subtitle="Index"
|
||||
align="center"
|
||||
className="py-0 md:py-0"
|
||||
/>
|
||||
<Reveal width="100%" className="max-w-2xl mx-auto">
|
||||
<BlogCommandBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
tags={allTags}
|
||||
activeTags={activeTags}
|
||||
onTagToggle={handleTagToggle}
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Posts List (Vertical & Minimal) */}
|
||||
<div id="posts-container" className="space-y-12">
|
||||
{postsToShow.length === 0 ? (
|
||||
<div className="py-24 text-center border border-dashed border-slate-200 rounded-3xl bg-white/50">
|
||||
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
|
||||
Keine Beiträge gefunden.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setActiveTags([]);
|
||||
}}
|
||||
className="mt-4 text-xs font-bold text-slate-900 underline underline-offset-4 hover:text-slate-600"
|
||||
>
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 max-w-3xl mx-auto w-full">
|
||||
{postsToShow.map((post, i) => (
|
||||
<Reveal key={post.slug} delay={0.05 * i} width="100%">
|
||||
<MediumCard post={post} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{hasMore && (
|
||||
<div className="flex justify-center pt-8">
|
||||
<Reveal delay={0.1} width="fit-content">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
|
||||
>
|
||||
<span className="relative z-10 font-mono text-xs uppercase tracking-widest flex items-center gap-3">
|
||||
Mehr laden
|
||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors" />
|
||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-75" />
|
||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-150" />
|
||||
</span>
|
||||
</button>
|
||||
</Reveal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter, Newsreader } from 'next/font/google';
|
||||
import { Analytics } from '../src/components/Analytics';
|
||||
import { Footer } from '../src/components/Footer';
|
||||
import { Header } from '../src/components/Header';
|
||||
import { InteractiveElements } from '../src/components/InteractiveElements';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-newsreader',
|
||||
style: 'italic',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'Marc Mintel',
|
||||
template: '%s | Marc Mintel',
|
||||
},
|
||||
description: "Technical problem solver's blog - practical insights and learning notes",
|
||||
metadataBase: new URL('https://mintel.me'),
|
||||
icons: {
|
||||
icon: '/favicon.svg',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
</head>
|
||||
<body className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<InteractiveElements />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { blogPosts } from '../src/data/blogPosts';
|
||||
import { technologies } from './technologies/[slug]/data';
|
||||
|
||||
/**
|
||||
* Sitemap Generator
|
||||
*
|
||||
* Standard Next.js 15 App Router sitemap generation.
|
||||
* This file dynamically generates /sitemap.xml
|
||||
*/
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://mintel.me';
|
||||
|
||||
// 1. Core Pages
|
||||
const routes = [
|
||||
'',
|
||||
'/about',
|
||||
'/blog',
|
||||
'/case-studies',
|
||||
'/case-studies/klz-cables',
|
||||
'/contact',
|
||||
'/websites',
|
||||
].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: route === '' ? 1.0 : 0.8,
|
||||
}));
|
||||
|
||||
// 2. Dynamic Blog Posts
|
||||
const blogRoutes = blogPosts.map((post) => ({
|
||||
url: `${baseUrl}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
// 3. Technology Detail Pages
|
||||
const techRoutes = Object.keys(technologies).map((slug) => ({
|
||||
url: `${baseUrl}/technologies/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
// 4. Tag Pages
|
||||
const allTags = [...new Set(blogPosts.flatMap((post) => post.tags))];
|
||||
const tagRoutes = allTags.map((tag) => ({
|
||||
url: `${baseUrl}/tags/${tag}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.3,
|
||||
}));
|
||||
|
||||
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Layers, Code, Database, Palette, Terminal } from 'lucide-react';
|
||||
|
||||
export interface TechInfo {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
icon: any; // React.ElementType
|
||||
benefits: string[];
|
||||
customerValue: string;
|
||||
color: string;
|
||||
related: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
export const technologies: Record<string, TechInfo> = {
|
||||
'next-js-14': {
|
||||
title: 'Next.js 14',
|
||||
subtitle: 'The React Framework for the Web',
|
||||
description: 'Next.js 14 is the latest version of the industry-leading framework for building high-performance web applications. It allows me to create fast, scalable, and search-engine-friendly websites by rendering content on the server before sending it to your users.',
|
||||
icon: Layers,
|
||||
benefits: [
|
||||
'Lightning-fast page loads with Server Components',
|
||||
'Automatic image optimization',
|
||||
'Instant navigation between pages',
|
||||
'Top-tier SEO (Search Engine Optimization) out of the box'
|
||||
],
|
||||
customerValue: 'For my clients, Next.js means a website that ranks higher on Google, keeps visitors engaged with instant interactions, and scales effortlessly as your traffic grows.',
|
||||
color: 'bg-black text-white',
|
||||
related: [
|
||||
{ name: 'TypeScript', slug: 'typescript' },
|
||||
{ name: 'React', slug: 'react' }
|
||||
]
|
||||
},
|
||||
'typescript': {
|
||||
title: 'TypeScript',
|
||||
subtitle: 'JavaScript with Syntax for Types',
|
||||
description: 'TypeScript adds a powerful type system to JavaScript, catching errors before they ever reach production. It acts as a safety net for your code, ensuring that data flows exactly as expected through your entire application.',
|
||||
icon: Code,
|
||||
benefits: [
|
||||
'Eliminates whole categories of common bugs',
|
||||
'Makes large codebases easier to maintain',
|
||||
'Improves developer productivity and code confidence',
|
||||
'Ensures critical data integrity'
|
||||
],
|
||||
customerValue: 'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: 'bg-blue-600 text-white',
|
||||
related: [
|
||||
{ name: 'Directus CMS', slug: 'directus-cms' },
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' }
|
||||
]
|
||||
},
|
||||
'directus-cms': {
|
||||
title: 'Directus CMS',
|
||||
subtitle: 'The Open Data Platform',
|
||||
description: 'Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn\'t dictate how your website looks.',
|
||||
icon: Database,
|
||||
benefits: [
|
||||
'Intuitive interface for non-technical editors',
|
||||
'Complete freedom regarding front-end design',
|
||||
'Real-time updates and live previews',
|
||||
'Highly secure and role-based access control'
|
||||
],
|
||||
customerValue: 'Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.',
|
||||
color: 'bg-purple-600 text-white',
|
||||
related: [
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' },
|
||||
{ name: 'Tailwind CSS', slug: 'tailwind-css' }
|
||||
]
|
||||
},
|
||||
'tailwind-css': {
|
||||
title: 'Tailwind CSS',
|
||||
subtitle: 'Utility-First CSS Framework',
|
||||
description: 'Tailwind CSS is a utility-first framework that allows me to build custom designs directly in markup. It eliminates the need for bulky, overriding stylesheets and ensures a consistent design system across every page.',
|
||||
icon: Palette,
|
||||
benefits: [
|
||||
'Rapid UI development and prototyping',
|
||||
'Consistent spacing, colors, and typography',
|
||||
'Highly optimized final bundle size (only includes used styles)',
|
||||
'Responsive design made simple'
|
||||
],
|
||||
customerValue: 'Tailwind ensures your brand looks pixel-perfect on every device. It also results in incredibly small CSS files, meaning your site loads faster for users on mobile networks.',
|
||||
color: 'bg-cyan-500 text-white',
|
||||
related: [
|
||||
{ name: 'React', slug: 'react' },
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' }
|
||||
]
|
||||
},
|
||||
'react': {
|
||||
title: 'React',
|
||||
subtitle: 'The Library for Web and Native User Interfaces',
|
||||
description: 'React is the core library powering Next.js. It lets me build encapsulated components that manage their own state, then compose them to make complex UIs.',
|
||||
icon: Terminal,
|
||||
benefits: [
|
||||
'Component-based architecture for reuse',
|
||||
'Efficient updates and rendering',
|
||||
'Rich ecosystem of libraries and tools',
|
||||
'Strong community support'
|
||||
],
|
||||
customerValue: 'React enables rich, app-like interactions on your website. Whether it\'s a complex dashboard or a smooth animation, React makes it possible to build dynamic experiences that feel instantaneous.',
|
||||
color: 'bg-blue-400 text-white',
|
||||
related: [
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' },
|
||||
{ name: 'Tailwind CSS', slug: 'tailwind-css' }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { technologies } from './data';
|
||||
import TechnologyContent from './content';
|
||||
|
||||
export default async function TechnologyPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
return <TechnologyContent slug={slug} />;
|
||||
}
|
||||
|
||||
// Generate static params for these dynamic routes
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(technologies).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
35
apps/web/build.log
Normal file
35
apps/web/build.log
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 50s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
Error: Cannot find module '/Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/globals/AiSettings' imported from /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/index.js
|
||||
at ignore-listed frames {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/@mintel+payload-ai@1.9.13_@payloadcms+next@3.77.0_graphql@16.12.0_monaco-editor@0.55.1__6baee6e32ae56efbc0411af586fa4fba/node_modules/@mintel/payload-ai/dist/globals/AiSettings'
|
||||
}
|
||||
|
||||
> Build error occurred
|
||||
Error: Failed to collect page data for /blog/[slug]/opengraph-image-fx5gi7
|
||||
at ignore-listed frames {
|
||||
type: 'Error'
|
||||
}
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
38
apps/web/build2.log
Normal file
38
apps/web/build2.log
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 48s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
TypeError: Unknown file extension ".css" for /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/react-image-crop@10.1.8_react@19.2.4/node_modules/react-image-crop/dist/ReactCrop.css
|
||||
at Object.getFileProtocolModuleFormat [as (file:] (node:internal/modules/esm/get_format:176:9) {
|
||||
code: 'ERR_UNKNOWN_FILE_EXTENSION'
|
||||
}
|
||||
TypeError: Unknown file extension ".css" for /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/react-image-crop@10.1.8_react@19.2.4/node_modules/react-image-crop/dist/ReactCrop.css
|
||||
at Object.getFileProtocolModuleFormat [as (file:] (node:internal/modules/esm/get_format:176:9) {
|
||||
code: 'ERR_UNKNOWN_FILE_EXTENSION'
|
||||
}
|
||||
|
||||
> Build error occurred
|
||||
Error: Failed to collect page data for /sitemap.xml
|
||||
at ignore-listed frames {
|
||||
type: 'Error'
|
||||
}
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
96
apps/web/build3.log
Normal file
96
apps/web/build3.log
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
> @mintel/web@0.1.0 build /Users/marcmintel/Projects/mintel.me/apps/web
|
||||
> next build --webpack
|
||||
|
||||
▲ Next.js 16.1.6 (webpack)
|
||||
- Environments: .env
|
||||
- Experiments (use with caution):
|
||||
· clientTraceMetadata
|
||||
|
||||
Creating an optimized production build ...
|
||||
[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)
|
||||
[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your `sentry.client.config.ts` file, or moving its content to `instrumentation-client.ts`. When using Turbopack `sentry.client.config.ts` will no longer work. Read more about the `instrumentation-client.ts` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /Users/marcmintel/Projects/mintel.me/node_modules/.pnpm/next-intl@4.8.2_@swc+helpers@0.5.18_next@16.1.6_@opentelemetry+api@1.9.0_react-dom@19.2_cfd2a0548e9a0d48fd79eed1a1591488/node_modules/next-intl/dist/esm/production/extractor/format/index.js for build dependencies failed at 'import(t)'.
|
||||
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.
|
||||
⚠ Compiled with warnings in 47s
|
||||
|
||||
|
||||
Running TypeScript ...
|
||||
Collecting page data using 15 workers ...
|
||||
Generating static pages using 15 workers (0/25) ...
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
Generating static pages using 15 workers (6/25)
|
||||
Generating static pages using 15 workers (12/25)
|
||||
Generating static pages using 15 workers (18/25)
|
||||
✓ Generating static pages using 15 workers (25/25) in 3.1s
|
||||
Lexical => JSX converter: Blocks converter: found mintelTldr block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelTldr block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
Lexical => JSX converter: Blocks converter: found mintelP block, but no converter is provided
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
[OG] Loading fonts: bold=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Bold.woff, regular=/Users/marcmintel/Projects/mintel.me/apps/web/public/fonts/Inter-Regular.woff
|
||||
[OG] Fonts loaded successfully (31320 and 30696 bytes)
|
||||
Finalizing page optimization ...
|
||||
Collecting build traces ...
|
||||
|
||||
Route (app)
|
||||
┌ ○ /
|
||||
├ ○ /_not-found
|
||||
├ ○ /about
|
||||
├ ○ /about/opengraph-image-1ycygp
|
||||
├ ƒ /admin/[[...segments]]
|
||||
├ ƒ /api/[...slug]
|
||||
├ ƒ /api/health/cms
|
||||
├ ƒ /api/tweet/[id]
|
||||
├ ○ /blog
|
||||
├ ● /blog/[slug]
|
||||
│ ├ /blog/why-websites-break-after-updates
|
||||
│ └ /blog/maintenance-for-headless-systems
|
||||
├ ƒ /blog/[slug]/opengraph-image-fx5gi7
|
||||
├ ○ /case-studies
|
||||
├ ○ /case-studies/klz-cables
|
||||
├ ○ /contact
|
||||
├ ○ /contact/opengraph-image-upzrkl
|
||||
├ ƒ /errors/api/relay
|
||||
├ ○ /opengraph-image-12o0cb
|
||||
├ ○ /sitemap.xml
|
||||
├ ƒ /stats/api/send
|
||||
├ ● /tags/[tag]
|
||||
│ ├ /tags/maintenance
|
||||
│ ├ /tags/reliability
|
||||
│ ├ /tags/software-engineering
|
||||
│ └ /tags/architecture
|
||||
├ ● /technologies/[slug]
|
||||
│ ├ /technologies/next-js-14
|
||||
│ ├ /technologies/typescript
|
||||
│ ├ /technologies/tailwind-css
|
||||
│ └ /technologies/react
|
||||
└ ○ /websites
|
||||
|
||||
|
||||
○ (Static) prerendered as static content
|
||||
● (SSG) prerendered as static HTML (uses generateStaticParams)
|
||||
ƒ (Dynamic) server-rendered on demand
|
||||
|
||||
12
apps/web/check-db.ts
Normal file
12
apps/web/check-db.ts
Normal file
@@ -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);
|
||||
};
|
||||
11
apps/web/content-engine.config.ts
Normal file
11
apps/web/content-engine.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ComponentDefinition } from '@mintel/content-engine';
|
||||
import path from 'path';
|
||||
import { componentDefinitions } from './src/content-engine/definitions';
|
||||
|
||||
export const config = {
|
||||
// Path to documentation files used as context for the AI
|
||||
contextDir: path.join(process.cwd(), 'docs'),
|
||||
|
||||
// Custom UI components available for injection
|
||||
components: componentDefinitions
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: "Analytics ohne Tracking: DSGVO-konforme Insights ohne User-Überwachung"
|
||||
thumbnail: "/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung.png"
|
||||
description: "Erfahren Sie, wie moderne Digital-Architektur präzise Besucherströme misst, ohne die Privatsphäre zu verletzen oder nervige Cookie-Banner zu benötigen."
|
||||
date: "2026-02-09"
|
||||
tags: ["privacy", "analytics"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Ich brauche Google Analytics, um zu wissen, was meine Nutzer tun." – Diese weit verbreitete Fehlannahme kostet B2B-Unternehmen täglich Vertrauen und massive Datenqualität.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner täglichen Arbeit als Digital Architect beweise ich das Gegenteil: <Marker>Maximale geschäftliche Erkenntnis erfordert keine maximale private Überwachung.</Marker>
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir Erfolg präzise messen, die DSGVO-Konformität systemisch verankern und dabei die Privatsphäre Ihrer Kunden schützen, ohne auf valide Daten zu verzichten.
|
||||
</LeadParagraph>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
**TL;DR:** Klassisches Tracking via Google Analytics verliert durch Consent-Ablehnung bis zu 70% der Daten. Privacy-First Analytics arbeitet serverseitig (First-Party), kommt ohne Banner aus und liefert 100% genauere Trends bei maximalem Datenschutz und signifikant besserem PageSpeed.
|
||||
|
||||
<H2>Analytics ohne den Beigeschmack der Überwachung</H2>
|
||||
|
||||
<Paragraph>
|
||||
Klassische Analytics-Tools funktionieren oft wie ein Trojaner. Sie sammeln riesige Mengen an persönlichen Daten, um daraus Profile zu bilden, die weit über Ihre Website hinausgehen. Dies zwingt Sie rechtlich in die Knie: Sie benötigen komplexe Consent-Management-Plattformen (CMPs) und riskieren dennoch Abmahnungen. Laut einer Untersuchung des <ExternalLink href="https://httparchive.org/">HTTP Archive</ExternalLink> machen Third-Party-Requests einen massiven Teil des Datenaufkommens aus, was die Ladezeit unnötig belastet.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Das Paradoxon: <Marker>Die meisten dieser Daten benötigen Sie für Ihre operativen Entscheidungen gar nicht.</Marker> Sie wollen wissen, welche Inhalte funktionieren, welche Kampagnen konvertieren und wo Nutzer abspringen – nicht, welche anderen Websites Ihre Besucher in ihrer Freizeit besuchen. Studien von <ExternalLink href="https://developers.google.com/">Google Developers</ExternalLink> zeigen zudem, dass Third-Party-JavaScript oft zum Performance-Flaschenhals wird.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<Mermaid id="privacy-analytics-flow" title="Privacy Analytics Workflow" showShare={true}>
|
||||
graph LR
|
||||
A["Besucherstrom"] --> B["Privacy-Proxy"]
|
||||
B --> C["Aggregate Metriken"]
|
||||
B --> D["Zero PII Data"]
|
||||
C --> E["Business Optimierung"]
|
||||
D --> F["100% Banner-Frei"]
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Das Problem der Datenlücke durch Consent-Zwang</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die Realität in Europa ist ernüchternd für Marketer, die auf klassische Tools setzen. Eine Studie von <ExternalLink href="https://www2.deloitte.com/">Deloitte</ExternalLink> zeigt, dass europäische Websites Datenverluste zwischen 30% und über 70% allein durch DSGVO-Consent-Anforderungen verzeichnen. Wenn der Nutzer "Ablehnen" klickt, bleibt das Unternehmen blind.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="A Deloitte study indicates that websites in Europe experience data losses ranging from 30% to over 70% due to GDPR consent requirements."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="Deloitte Insights"
|
||||
sourceUrl="https://www2.deloitte.com"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Wahre Souveränität bedeutet, Insights zu generieren, ohne sich von der Komplexität eines GA4-Setups abhängig zu machen. Wer [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners) betreibt, signalisiert Professionalität. Zudem heben Studien der <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> hervor, dass langsame Ladezeiten – oft verursacht durch drittanbieter-lastige Skripte – direkt zu Frustration führen. Besonders kritisch: Laut <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> sehen Websites einen signifikanten Drop in getrackten Usern, sobald ein Banner implementiert wird, da Nutzer zunehmend skeptisch gegenüber Tracking sind.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="70%|Datenverlust|laut Deloitte Studie~20%|Deflatierte Bounce-Rate|durch fehlendes Tracking~8.4%|Conversion Plus|pro 0.1s Speed-Gewinn" />
|
||||
|
||||
<H2>Die Architektur für ethische Insights</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich integriere Analytics direkt in Ihre Plattform-Architektur. Das bedeutet: Keine externen Scripte von US-Servern, was die Performance massiv verbessert. Ein sauberer [systemischer Architektur-Ansatz für die DSGVO](/blog/gdpr-conformity-system-approach) ist hier der Schlüssel.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Cookieless Tracking:</strong> Wir erkennen wiederkehrende Nutzer über kurzlebige, anonyme Hashes. Keine Speicherung am Endgerät notwendig.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>First-Party Data:</strong> Alle Daten bleiben in Ihrem Hoheitsbereich (z.B. auf einem [professionellen Hosting](/blog/professional-hosting-operations)).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Lightweight Implementation:</strong> Statt 100KB Tracking-Ballast nutzen wir Lösungen unter 1KB. <Marker>Geschwindigkeit trifft auf Erkenntnis.</Marker>
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="chk4m7mqkdI" title="Privacy as an Opportunity in Analytics" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Interne Daten von <ExternalLink href="https://blog.google/">Google</ExternalLink> bestätigen, dass striktes Consent-Management die gemeldeten User-Metriken drastisch reduziert. Wer auf [Clean Code](/blog/clean-code-for-business-value) setzt, erkennt schnell: Weniger "Noise" führt zu besseren Signalen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der Haken an der Sache: Wo sind die Grenzen?</H2>
|
||||
|
||||
<Paragraph>
|
||||
Als Digital Architect ist es meine Pflicht, auch die Grenzen moderner Cookieless-Systeme aufzuzeigen. Während wir die Privatsphäre schützen, müssen wir bei der Langzeit-Kohorten-Analyse Kompromisse eingehen.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Technischer Vergleich: Legacy vs. Modern"
|
||||
negativeLabel="Legacy Tracking (GA4)"
|
||||
negativeText="Präzise Wiedererkennung über Monate, aber 40-70% Datenverlust durch Opt-Out und rechtliche Grauzonen."
|
||||
positiveLabel="Mintel Privacy Stack"
|
||||
positiveText="100% DSGVO-konform ohne Banner. Erfasst jeden Besucher, verliert aber die Verknüpfung bei Usern nach 24h ohne Cookies."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Google versucht mit dem "Consent Mode" zwar Datenlücken durch Modellierung zu schließen, doch eine vollständige Wiederherstellung der Daten ist technisch nicht möglich. Mein Ansatz verfolgt das Prinzip der [Digital Longevity](/blog/digital-longevity-architecture): Ein System, das stabil und ohne rechtliche Zitterpartie funktioniert.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der unternehmerische Hebel: Performance ist Umsatz</H2>
|
||||
|
||||
<Paragraph>
|
||||
Daten von <ExternalLink href="https://www2.deloitte.com/">Deloitte</ExternalLink> belegen, dass die Verbesserung der Ladezeit die Conversion-Rates signifikant steigert. Jedes gesparte Tracking-Skript zahlt direkt auf diesen ROI ein. Ein stabiles System ist kein Kostenfaktor, sondern ein [ROI-Beschleuniger](/blog/clean-code-for-business-value).
|
||||
</Paragraph>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme
|
||||
template="db"
|
||||
captions="Cookie-Banner & ungenaue Daten|Banner-freie Website & 100% DSGVO-Sicherheit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Wenn Sie auf Ihrer Website kein Banner benötigen, messen Sie 100% Ihres Traffics aggregiert. Es ist die ehrlichere Metrik für Ihre Marketing-Planung. Erfahren Sie hier mehr über die [versteckten Kosten von zu vielen Plugins](/blog/hidden-costs-of-wordpress-plugins), die oft auch Tracking-Balast mitbringen.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Performance & Privacy Audit"
|
||||
description="Wir analysieren Ihre aktuelle Tracking-Infrastruktur und zeigen Ihnen den Weg zu einer bannerfreien, performanten High-End Website."
|
||||
buttonText="Jetzt Strategie-Gespräch buchen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Fazit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Messen Sie, was zählt – und schützen Sie, wer zählt. Ein moderner Ansatz macht Datenschutz zu Ihrem Wettbewerbsvorteil statt zum Compliance-Albtraum. Die Kombination aus maximaler Performance, Banner-Freiheit und präzisen Business-Insights ist heute kein "Entweder-oder" mehr. Wer seine Architektur heute auf Privacy-First umstellt, sichert sich die Datenhoheit für das nächste Jahrzehnt und reduziert [technische Altlasten](/blog/slow-loading-costs-customers) nachhaltig.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Verliere ich durch Cookieless Analytics wichtige Funktionen?</H3>
|
||||
<Paragraph>Sie verlieren die Möglichkeit, Einzelnutzer über Monate hinweg zu verfolgen. Für die meisten B2B-Unternehmen ist jedoch der Gewinn von 100% sauberen Trend-Daten weitaus wertvoller als ungenaue Einzelprofile.</Paragraph>
|
||||
|
||||
<H3>Ist Analytics ohne Banner wirklich 100% rechtssicher?</H3>
|
||||
<Paragraph>Ja, sofern keine personenbezogenen Daten (PII) gespeichert werden und die Anonymisierung bereits auf dem Server (First-Party) stattfindet. Dies eliminiert die Notwendigkeit für eine Einwilligung nach TTDSG/DSGVO.</Paragraph>
|
||||
|
||||
<H3>Wie wirkt sich der Verzicht auf Tracker auf SEO aus?</H3>
|
||||
<Paragraph>Extrem positiv. Durch verbesserte Core Web Vitals (da weniger JavaScript die Main-Thread-Arbeit blockiert) wertet Google die Seite höher ein, was zu besseren Rankings führen kann.</Paragraph>
|
||||
</FAQSection>
|
||||
182
apps/web/content/blog/build-first-digital-architecture.mdx
Normal file
182
apps/web/content/blog/build-first-digital-architecture.mdx
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "Build vs. Buy: Warum Build-First die einzige Strategie für B2B-Marktführer ist"
|
||||
thumbnail: "/blog/build-first-digital-architecture.png"
|
||||
description: "Software-Miete vs. digitales Eigentum: Erfahren Sie, warum Bespoke Software laut Deloitte und McKinsey den ROI maximiert und Ihre Unabhängigkeit im B2B sichert."
|
||||
date: "2026-02-05"
|
||||
tags: ["architecture", "business", "roi"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Gekauft ist schneller als gebaut." – In der schnelllebigen B2B-Welt ist dieser Satz oft der teuerste Irrtum, den ein Unternehmen begehen kann. Während SaaS-Lösungen an der Oberfläche glänzen, bröckelt darunter oft das strategische Fundament und die Innovationsfähigkeit.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect sehe ich täglich, wie Standard-SaaS-Lösungen Innovationen im Keim ersticken. Unternehmen bezahlen für Features, die sie nicht brauchen, während ihnen die entscheidenden 5 % für echte Marktdifferenzierung fehlen.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen heute, warum <Marker>Bauen die moderne Form der ökonomischen Effizienz</Marker> ist, wie Sie technologische Schulden vermeiden und sich durch digitales Eigentum (IP) einen uneinholbaren Wettbewerbsvorteil sichern.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
<H2>TL;DR</H2>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Asset-Building:</strong> Eigene Software ist ein strategischer Firmenwert (IP) und kein reiner Kostenblock.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Wachstums-Hebel:</strong> Custom-Lösungen erzielen laut Deloitte ein um 15 % höheres Umsatzwachstum.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Effizienz:</strong> 20–30 % Steigerung der Prozesseffizienz durch Process Mining und Customizing (KPMG).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Souveränität:</strong> Wer den Code besitzt, kontrolliert Roadmap, Datenhoheit und Skalierbarkeit ohne Drittanbieter-Limits.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
</Section>
|
||||
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
<Paragraph>
|
||||
1. [Die Sackgasse der Generic-Software](#die-sackgasse-der-generic-software)
|
||||
2. [Der wirtschaftliche Case von Build-First](#der-wirtschaftliche-case-von-build-first)
|
||||
3. [Digitales Eigentum als strategischer Hebel](#digitales-eigentum-als-strategischer-hebel)
|
||||
4. [Der Haken an der Sache (Ehrliche Analyse)](#der-haken-an-der-sache)
|
||||
5. [FAQ & Fazit](#fazit)
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="die-sackgasse-der-generic-software">Die Sackgasse der Generic-Software</H2>
|
||||
|
||||
<Paragraph>
|
||||
Standard-Software ist darauf ausgelegt, dem kleinsten gemeinsamen Nenner zu gefallen. Man bekommt ein schnelles Resultat, läuft aber sofort gegen eine Wand, wenn man Prozesse wirklich optimieren will. Während die Initialkosten niedrig wirken, steigen die Lizenzkosten bei wachsender Nutzung oft linear an. [Baukasten-Systeme bedrohen Ihre Unabhängigkeit](/blog/builder-systems-threaten-independence), indem sie Sie in ein Korsett aus vorgegebenen Workflows zwängen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="grumpycat" captions="LIZENZKOSTEN STEIGEN UM 20%|ABER DER PROZESS BLEIBT SCHEISSE" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ihre Wettbewerber nutzen wahrscheinlich exakt die gleiche Software wie Sie. Wo bleibt da der <Marker>technologische Vorsprung</Marker>? Deloitte berichtet, dass Organisationen, die maßgeschneiderte Technologielösungen priorisieren, **2,2-mal wahrscheinlicher das durchschnittliche Umsatzwachstum ihrer Branche übertreffen**. Wer nur mietet, bleibt im Mittelfeld.
|
||||
</Paragraph>
|
||||
|
||||
<ArchitectureBuilder />
|
||||
|
||||
<H3>Bauen bedeutet heute: Strategisches Kombinieren</H3>
|
||||
|
||||
<Paragraph>
|
||||
"Bauen" heißt heute nicht mehr, jedes Rad neu zu erfinden. Es bedeutet [Clean Code](/blog/clean-code-for-business-value) und die Nutzung moderner Architekturen. Wer hier auf [professionelles Hosting](/blog/professional-hosting-operations) setzt, schafft ein stabiles Fundament. Wir nutzen spezialisierte Microservices, um Ihr individuelles System zu komponieren – präzise, schnell und ohne Ballast. Ein maßgeschneidertes Data-Analytics-System kann laut Accenture die Effizienz allein durch datengetriebene Insights um bis zu 25 % steigern.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Organizations prioritizing customized technology solutions are 2.2 times more likely to exceed industry average revenue growth."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="Deloitte 2020 Survey"
|
||||
sourceUrl="https://www2.deloitte.com"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Dabei besitzen Sie den Code und kontrollieren die Datenströme. Das Ergebnis ist eine Lösung, die so flexibel ist wie eine Eigenentwicklung, aber durch moderne Toolchains fast so schnell einsatzbereit wie ein Standardprodukt. Dies sichert Ihnen eine langfristige [Digital Longevity](/blog/digital-longevity-architecture) und schützt vor dem Platform-Lock-in.
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="der-wirtschaftliche-case-von-build-first">Der wirtschaftliche Case von 'Build-First'</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die Investition in Individualsoftware wird oft als Risiko wahrgenommen. Die nackten Zahlen der führenden Beratungsgesellschaften zeichnen jedoch ein anderes Bild. Laut McKinsey führt Prozess-Digitalisierung (gegenüber Standard-SaaS) zu einer <Marker>Reduktion der operativen Kosten um 20-40 %</Marker>. Deloitte ergänzt, dass Unternehmen mit maßgeschneiderten Lösungen eine um 30 % bessere Kundenbindung erzielen.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="15%"
|
||||
label="höhere Umsatzrendite durch passgenaue Software-Architektur"
|
||||
source="Deloitte"
|
||||
sourceUrl="https://www.deloitte.com"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Maßgeschneiderte Software amortisiert sich oft bereits nach der ersten Skalierungsphase. Ein wesentlicher Grund dafür ist die Reduktion von technischen Reibungsverlusten. [Langsame Ladezeiten kosten Kunden](/blog/slow-loading-costs-customers) – ein Problem, das Individualsoftware durch radikale Performance-Optimierung (z.B. [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist)) elegant eliminiert.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<StatsGrid stats="20-40%|Kostenreduktion|durch gezielte Digitalisierung (McKinsey)~30%|Bessere Retention|gegenüber generischen Lösungen (Deloitte)~40%|Faster Insights|bei Custom-Built Analytics (Deloitte)" />
|
||||
</div>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<H2 id="digitales-eigentum-als-strategischer-hebel">Digitales Eigentum als strategischer Hebel</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wer den Code besitzt, besitzt die Zukunft seines Unternehmens. In Krisenzeiten oder bei strategischen Neuausrichtungen ist technische Unabhängigkeit ein entscheidender Faktor. [Warum Ihre Agentur für kleine Änderungen Wochen braucht](/blog/why-agencies-are-slow), liegt oft am mangelnden Zugriff auf die Kern-Logik gemieteter Systeme oder überladener Strukturen wie [WordPress-Plugins](/blog/hidden-costs-of-wordpress-plugins).
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramGantt
|
||||
tasks={[
|
||||
{ id: "task-1", name: "SaaS: Sofortstart / Linearer Miet-Anstieg", start: "2024-01-01", duration: "12w" },
|
||||
{ id: "task-2", name: "Mintel Custom: Asset-Aufbau & IP-Sicherung", start: "2024-01-01", duration: "16w" }
|
||||
]}
|
||||
title="Strategische Kosten-Nutzen-Timeline"
|
||||
id="gantt-build-buy"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ich erzeuge Architekturen, die konsequent auf Ownership setzen. Das bedeutet auch: Schutz vor globalen Ausfällen von Drittanbietern und eine drastisch reduzierte Angriffsfläche durch einen [systemischen DSGVO-Ansatz](/blog/gdpr-conformity-system-approach). Deloitte's Research zeigt: Investitionen in maßgeschneiderte digitale Lösungen führen oft bereits im ersten Jahr zu einer <Marker>20 %igen Verbesserung der operativen Effizienz</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<DigitalAssetVisualizer />
|
||||
|
||||
<H2 id="der-haken-an-der-sache">Der Haken an der Sache (Ehrliche Analyse)</H2>
|
||||
|
||||
<Paragraph>
|
||||
Keine Architektur-Strategie ist ohne Preis. Build-First erfordert Mut und ein höheres initiales Commitment des Managements. Während Gartner bestätigt, dass Process Mining die Kosten bereits im ersten Jahr um 15 % senken kann, ist der initiale Planungsaufwand höher als beim bloßen "Klick-auf-Abonnieren".
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Ist Build für jeden geeignet?"
|
||||
negativeLabel="Standard-Kaufsoftware"
|
||||
negativeText="Schneller Start, aber hohe Abhängigkeit, starre Prozesse und dauerhafte Miet-Verpflichtung."
|
||||
positiveLabel="Mintel Build-First"
|
||||
positiveText="Höheres Upfront-Investment, dafür volles Eigentum, unbegrenzte Skalierung und technologischer Asset-Wert."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="CWiueQT8VNI" title="Infrastructure Review" />
|
||||
</div>
|
||||
|
||||
<LeadMagnet
|
||||
title="Digitale Architektur-Analyse anfragen"
|
||||
description="Befreien Sie sich von starren Standard-Tools. Wir analysieren Ihre Prozesse und berechnen den ROI einer Built-First Architektur für Ihr Unternehmen."
|
||||
buttonText="Jetzt Potenzial-Check buchen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2 id="fazit">Fazit: Hören Sie auf zu mieten, fangen Sie an zu besitzen</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wahrer Reichtum im digitalen Zeitalter entsteht durch Eigentum und technologische Souveränität. Während Ihre Konkurrenz in starren SaaS-Strukturen gefangen ist, bietet Ihnen eine Eigenentwicklung die Freiheit, Ihre Prozesse radikal zu optimieren. Laut Deloitte resultiert die Implementierung individueller IT-Optionen in einer **25 %igen Reduktion der IT-Betriebskosten über drei Jahre**.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
<Marker>Qualität und Marktführerschaft sind keine Zufallsprodukte, sondern eine bewusste Entscheidung für den Bau eigener Werte.</Marker> Verwandeln Sie Ihre IT von einem unvermeidbaren Kostenfaktor in einen strategischen Vermögenswert, der Ihr Unternehmen über Jahrzehnte trägt.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Wann lohnt sich 'Build' gegenüber 'Buy' finanziell?</H3>
|
||||
<Paragraph>
|
||||
Es lohnt sich, wenn Software den Kern Ihrer Wertschöpfung betrifft. Sobald Standardlösungen Ihre Effizienz um >10% deckeln oder die Skalierung die Lizenzgebühren explodieren lässt, ist Build die rentablere Wahl.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie wirkt sich Custom Software auf die Marge aus?</H3>
|
||||
<Paragraph>
|
||||
Durch die Reduktion operativer Kosten um bis zu 40% (McKinsey) und ein schnelleres "Time-to-Insight" (40% schneller laut Deloitte) verbessert Custom Software direkt das EBITDA und die Wettbewerbsgeschwindigkeit.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Was passiert mit der Wartung bei Eigenentwicklungen?</H3>
|
||||
<Paragraph>
|
||||
Moderne Headless-Architekturen sind durch ihre Entkopplung wartungsarm. Im Gegensatz zu WordPress müssen bei einem Custom Stack nicht 50 Plugins gleichzeitig kompatibel gehalten werden, was die Stabilität massiv erhöht.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
168
apps/web/content/blog/builder-systems-threaten-independence.mdx
Normal file
168
apps/web/content/blog/builder-systems-threaten-independence.mdx
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: "Vendor Lock-In vermeiden: Warum Baukasten-Systeme Ihre Unabhängigkeit bedrohen"
|
||||
thumbnail: "/blog/builder-systems-threaten-independence.png"
|
||||
description: "Erfahren Sie, wie proprietäre Systeme und SaaS-Baukästen Ihre digitale Souveränität gefährden und warum eine cloud-agnostische Architektur Ihr wertvollstes Business-Asset ist."
|
||||
date: "2026-02-08"
|
||||
tags: ["architecture", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Wir können nicht wechseln, der Umzug wäre zu teuer und technisch schlicht unmöglich."
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect ist dieser Satz oft der Anfang vom Ende jeder technologischen Innovation. Wenn die Kosten eines Wechsels den Nutzen übersteigen, befinden Sie sich in einer Sackgasse.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Vendor Lock-In ist die <Marker>digitale Version einer Geiselnahme</Marker>. Ich zeige Ihnen, wie wir Systeme bauen, die Ihnen jederzeit die volle Freiheit lassen – technologisch und wirtschaftlich.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
- [Die unsichtbaren Ketten proprietärer Systeme](#die-unsichtbaren-ketten-proprietärer-systeme)
|
||||
- [Der wirtschaftliche Preis der Abhängigkeit](#der-wirtschaftliche-preis-der-abhängigkeit)
|
||||
- [Technologische Souveränität als Asset](#technologische-souveränität-als-asset)
|
||||
- [Meine Architektur der Ungebundenheit](#meine-architektur-der-ungebundenheit)
|
||||
- [Der Haken an der Sache: Die Kosten der Freiheit](#der-haken-an-der-sache-die-kosten-der-freiheit)
|
||||
- [Fazit: Freiheit ist eine strategische Wahl](#fazit-freiheit-ist-eine-strategische-wahl)
|
||||
|
||||
<H2>Die unsichtbaren Ketten proprietärer Systeme</H2>
|
||||
|
||||
<Paragraph>
|
||||
Viele Unternehmen lassen sich von der Bequemlichkeit großer SaaS-Plattformen oder "All-in-One"-Baukästen blenden. Man erhält schnell glänzende Features, gibt aber im Gegenzug die Kontrolle über seine Daten und die Codebasis ab. Oft wird unterschätzt, dass [technische Qualität direkt den Business-Value skaliert](/blog/clean-code-for-business-value).
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Nach zwei bis drei Jahren sind Sie so tief im Ökosystem eines Anbieters verstrickt, dass ein Auszug finanziell ruinös erscheint. Der Anbieter erkennt diese Abhängigkeit – und diktiert fortan die Preise sowie das Tempo Ihrer digitalen Entwicklung. Ich nenne das <Marker>technologische Erpressbarkeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="64% of organizations experienced difficulties switching vendors due to complex integration and data migration challenges, a key indicator of vendor lock-in."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="2021 Global Outsourcing Survey"
|
||||
sourceUrl="https://www2.deloitte.com/global/en/pages/operations/articles/global-outsourcing-survey.html"
|
||||
/>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="spongebob" captions="Wir sind total flexibel mit unserem Baukasten!|Monatliche Gebühren steigen um 20% und Export ist deaktiviert" />
|
||||
</div>
|
||||
|
||||
<H2>Der wirtschaftliche Preis der Abhängigkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Lock-In ist kein rein technisches Problem; es ist ein massives finanzielles Risiko. Wer in proprietären "Walled Gardens" baut, zahlt langfristig eine Innovations-Steuer. Oft hängen diese Systeme an veralteten Strukturen, die wie [versteckte Kosten durch Plugins](/blog/hidden-costs-of-wordpress-plugins) das Budget auffressen.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="20%|Höhere Kosten|bei Software-Lock-In über 5 Jahre~10%|Weniger Agilität|durch verzögerte Projekt-Deployments~25%|Budget-Überschreitung|bei migrations-kritischen Projekten" />
|
||||
|
||||
<Paragraph>
|
||||
Laut <ExternalLink href="https://www.gartner.com">Gartner</ExternalLink> geben Organisationen, die an spezifische Software-Verschreiber gebunden sind, im Schnitt 15-20% mehr für Lizenzierung und Support aus als Unternehmen mit flexiblen Lösungen. Wer hingegen auf [cloud-agnostische Strategien](/blog/no-us-cloud-platforms) setzt, reduziert seine Infrastrukturkosten im ersten Jahr um durchschnittlich 15%.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Technologische Souveränität als Asset</H2>
|
||||
|
||||
<Paragraph>
|
||||
Software sollte für Sie arbeiten, nicht umgekehrt. Indem wir auf offene Standards und portable Architekturen setzen, verwandeln wir Code in ein echtes Firmen-Asset (Intellectual Property). Dies ist ein zentraler Pfeiler für die [digitale Langlebigkeit Ihrer Architektur](/blog/digital-longevity-architecture).
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DigitalAssetVisualizer />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ein cloud-agnostischer Ansatz ermöglicht es Ihnen, den Provider zu wechseln oder das Team zu skalieren, <Marker>ohne jemals bei Null anfangen zu müssen</Marker>. Dies ist kein Gimmick, sondern eine unternehmerische Notwendigkeit in einem volatilen Markt.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Companies using open source technologies and open standards reported a 20% lower risk of vendor lock-in compared to those relying solely on proprietary solutions."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="Deloitte Open Source Study"
|
||||
sourceUrl="https://www2.deloitte.com"
|
||||
translated={true}
|
||||
/>
|
||||
|
||||
<H2>Meine Architektur der Ungebundenheit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich baue keine "Käfige". Mein Fokus liegt auf [Build-First Strategien](/blog/build-first-digital-architecture), die Modularität und Portabilität in den Vordergrund stellen.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Standard-basiertes Engineering:</strong> Nutzung von Technologien (HTML, CSS, JS), die weltweit verstanden werden. Keine proprietären "Blackbox"-Module.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Daten-Souveränität:</strong> Ihre Daten gehören Ihnen. Wir bauen APIs, die Interoperabilität fördern und System-Integrationen um 25% erfolgreicher machen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Containerisierung:</strong> Durch Docker und Kubernetes erzielen wir eine bis zu 40% schnellere Deployment-Rate über verschiedene Cloud-Umgebungen hinweg.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ArchitectureBuilder />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Diese Unabhängigkeit führt direkt zu einer besseren Performance. Daten des <ExternalLink href="https://httparchive.org">HTTP Archive</ExternalLink> zeigen, dass Websites, die auf offenen Webstandards basieren, im Schnitt 10% schneller laden als solche, die auf proprietären Frameworks feststecken. Dies ist ein entscheidender [Faktor für Ihre B2B-Umsätze](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist).
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der Haken an der Sache: Die Kosten der Freiheit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Als Digital Architect bin ich Ihnen Transparenz schuldig. Echte Unabhängigkeit kommt nicht zum Nulltarif.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Vergleich der Investition"
|
||||
negativeLabel="SaaS-Baukasten (Miete)"
|
||||
negativeText="Niedrige Einstiegskosten, schnelle initiale Klicks, aber lebenslange Abhängigkeit und begrenzte Skalierung."
|
||||
positiveLabel="Custom Architecture (Besitz)"
|
||||
positiveText="Höheres Initial-Investment, erfordert technisches Verständnis, bietet dafür totale Freiheit und wertvolles IP."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Für Unternehmen, die nur eine "Visitenkarte" im Netz brauchen, ist mein Ansatz Overkill. Wenn Ihre Website jedoch ein kritischer Teil Ihrer Wertschöpfungskette ist (Lead-Gen, Recruiting, Produkt-Konfiguration), ist die "Miete" Ihrer Kern-Technologie ein strategischer Fehler.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Architektur-Audit anfragen"
|
||||
description="Befindet sich Ihre Plattform bereits in der Sackgasse? Ich analysiere Ihre aktuelle Architektur auf Lock-In-Risiken und zeige Ihnen Wege zur Souveränität auf."
|
||||
buttonText="Jetzt Unabhängigkeit prüfen"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<H2>Der strategische Hebel für langfristige Rendite</H2>
|
||||
|
||||
<Paragraph>
|
||||
Systeme ohne Lock-In altern besser. Sie lassen sich schrittweise modernisieren, statt alle fünf Jahre einen teuren "Relaunch" (Totalabriss) zu benötigen. Das spart Millionen an Opportunitätskosten. Investieren Sie in <Marker>intelligente Unabhängigkeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="a__XdZMIx38" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Freiheit ist eine strategische Wahl</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technologie sollte Ihnen Flügel verleihen, keine Fesseln anlegen. Unternehmen, die auf Multi-Cloud und offene Standards setzen, erreichen laut McKinsey eine 20% höhere Innovationsrate.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Lassen Sie uns gemeinsam ein System schaffen, das so flexibel ist wie Ihr Business. Werden Sie unersetzbar durch Qualität, nicht durch die Unfähigkeit, zu wechseln. <Marker>Ihr technologisches Fundament ist kein Kostenfaktor, sondern Ihr wichtigstes Asset.</Marker>
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Was ist der größte Vorteil von cloud-agnostischer Architektur?</H3>
|
||||
<Paragraph>Der größte Vorteil ist die Portabilität und Risikominimierung. Sie können Workloads jederzeit zwischen Anbietern verschieben, was die Verhandlungsmacht stärkt und die Business Continuity um 30% verbessert.</Paragraph>
|
||||
|
||||
<H3>Ab wann lohnt sich der Aufwand gegen Vendor Lock-In?</H3>
|
||||
<Paragraph>Sobald die digitale Plattform ein Kernbestandteil Ihres Geschäftsmodells ist. Bei einer geplanten Nutzungsdauer von über 3 Jahren amortisieren sich die höheren Initialkosten durch wegfallende Lizenzgebühren und höhere Agilität schnell.</Paragraph>
|
||||
|
||||
<H3>Sind Open-Source-Lösungen nicht unsicherer?</H3>
|
||||
<Paragraph>Im Gegenteil. Durch die Transparenz des Codes können Sicherheitslücken schneller identifiziert und unabhängig von einem einzelnen Anbieter geschlossen werden, was zu einer robusteren Sicherheitsarchitektur führt.</Paragraph>
|
||||
</FAQSection>
|
||||
166
apps/web/content/blog/clean-code-for-business-value.mdx
Normal file
166
apps/web/content/blog/clean-code-for-business-value.mdx
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: "Der ROI von Clean Code: Warum technische Qualität Ihr Business skaliert"
|
||||
thumbnail: "/blog/clean-code-for-business-value.png"
|
||||
description: "Software-Engineering als Wertanlage: Wie Clean Code die Wartungskosten um bis zu 80% senkt und die Time-to-Market für neue Features massiv beschleunigt."
|
||||
date: "2026-01-30"
|
||||
tags: ["development", "business", "software-architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Code ist nicht nur eine Anweisung für Maschinen. Er ist das digitale Fundament Ihres Unternehmenswertes und entscheidet über Agilität oder Stillstand.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner täglichen Praxis als Digital Architect begegne ich oft "historisch gewachsenen" Systemen, die eher einem gordischen Knoten gleichen als einer sauberen Architektur.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen heute, warum <Marker>Clean Code kein akademischer Luxus</Marker> ist, sondern die härteste Währung für Ihre Zukunftsfähigkeit im Wettbewerb.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="my-8">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
|
||||
**TL;DR:** Unsauberer Code ("Technical Debt") frisst bis zu 80% Ihres IT-Budgets für reine Wartung. Clean Code reduziert die Debugging-Zeit um 30% und ermöglicht es Teams, 70% ihrer Zeit in echte Innovation statt in Bugfixing zu investieren.
|
||||
|
||||
<H2>Die versteckten Kosten von 'Quick-and-Dirty'</H2>
|
||||
|
||||
<Paragraph>
|
||||
Softwareentwicklung unter Zeitdruck führt oft zu unsauberen Abkürzungen. Kurzfristig spart das vielleicht Tage, doch langfristig erstickt es jede Innovation. Wenn Entwickler gezwungen sind, auf einem instabilen Fundament zu bauen, entstehen sogenannte technische Schulden.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Studien des National Institute of Standards and Technology (NIST) zeigen, dass die Kosten für das Beheben von Defekten <Marker>exponentiell steigen</Marker>, je länger der Code ohne Refactoring altert. Was heute eine kleine Unsauberkeit ist, kann in zwei Jahren die gesamte Plattform lahmlegen.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Maintenance accounts for approximately 80% of the lifetime cost of a software product; clean code substantially reduces this maintenance burden."
|
||||
author="The Pragmatic Programmer"
|
||||
isCompany={true}
|
||||
source="Industry Experience Data"
|
||||
sourceUrl="https://learning.oreilly.com/library/view/the-pragmatic-programmer/9780135956977/"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>technologische Verstopfung</Marker>. Ihre Entwickler verbringen dann den Großteil ihrer Zeit mit Archäologie in alten Code-Schichten und Bugfixing, statt an Wettbewerbsvorteilen zu arbeiten. Laut DORA-Metriken können Teams mit sauberem Code bis zu 70% ihrer Kapazität für neue Features nutzen, während "unclean" Teams bei 30% stagnieren.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<MetricBar label="Wartungsaufwand (Legacy)" value={80} color="red" />
|
||||
<MetricBar label="Wartungsaufwand (Clean Code)" value={20} color="green" />
|
||||
</div>
|
||||
|
||||
<H2>Wirtschaftliche Hebelkraft durch Code-Qualität</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die Entscheidung für Qualität ist eine rein betriebswirtschaftliche. Organisationen geben laut Standish Group ca. 80% ihres Budgets für die Instandhaltung aus. Durch die Reduzierung der Defektdichte – oft um bis zu 40% durch moderne Analyse-Tools – wird Kapital frei, das direkt in das [Build-First Prinzip](/blog/build-first-digital-architecture) fließen kann.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="2.84 Bio. $"
|
||||
label="Jährliche Kosten durch schlechte Softwarequalität (USA)"
|
||||
source="CISQ"
|
||||
sourceUrl="https://www.it-complexity.com/isq-report-2020"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Häufig entstehen diese Kosten durch den [Vendor Lock-In in unflexiblen Baukastensystemen](/blog/builder-systems-threaten-independence). Ein sauberer, modularer Ansatz sorgt hingegen für [digitale Langlebigkeit](/blog/digital-longevity-architecture).
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="clean-code-logic" title="Die betriebswirtschaftliche Logik der Qualität" showShare={true}>
|
||||
graph TD
|
||||
Clean["Clean Code"] --> Scalable["Skalierbarkeit"]
|
||||
Clean --> Fast["High Velocity"]
|
||||
Scalable --> Growth["Marktanteile"]
|
||||
Fast --> LowCosts["Niedrige OpEx"]
|
||||
LowCosts --> ROI["Maximierter Profit"]
|
||||
Growth --> ROI
|
||||
style ROI fill:#4ade80,stroke:#333
|
||||
style Clean fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Meine Prinzipien für eine glasklare Architektur</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wie unterscheidet sich meine Arbeit von Standard-Agentur-Code? Es ist der Übergang von "Basteln" zu echtem Corporate Engineering. Ein gut strukturiertes System "erklärt" sich selbst und macht Sie unabhängig von einzelnen Personen.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Single Responsibility:</strong> Jede Komponente hat eine klare Aufgabe. Das verhindert Seiteneffekte und macht Updates sicher.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Self-Testing Systems:</strong> Automatisierte Tests prüfen jede Änderung. <Marker>Qualität ist systemimmanent</Marker>, nicht optional.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Domain Driven Design:</strong> Die Software spricht die Sprache Ihres Business, nicht die Sprache der Datenbank.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="5koPpYVa020" />
|
||||
</div>
|
||||
|
||||
<H3>Der Haken an der Sache: Die Wahrheit über Clean Code</H3>
|
||||
|
||||
<Paragraph>
|
||||
Ich bin ehrlich zu Ihnen: Exzellenz braucht am Anfang mehr Zeit. Wer "billig und schnell" bestellt, wird mit Clean Code nicht glücklich – zumindest im ersten Monat.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Investitions-Profil"
|
||||
negativeLabel="Quick & Dirty"
|
||||
negativeText="Niedrige initialle Kosten, aber exponentieller Anstieg nach 6 Monaten (Wartungshölle)."
|
||||
positiveLabel="Mintel Standard"
|
||||
positiveText="Höheres initiales Investment für Architektur, dafür stabile Fixkosten und dauerhaft hohes Tempo."
|
||||
showShare={false}
|
||||
/>
|
||||
|
||||
<H2>Digitale Assets vs. Verbindlichkeiten</H2>
|
||||
|
||||
<Paragraph>
|
||||
Software sollte in Ihrer Bilanz als Vermögenswert auftauchen, nicht als laufende Verbindlichkeit. Wenn Ihre Website bei jedem Update zerbricht, haben Sie ein Problem in der [Update-Stabilität](/blog/why-websites-break-after-updates).
|
||||
</Paragraph>
|
||||
|
||||
<DigitalAssetVisualizer />
|
||||
|
||||
<Paragraph>
|
||||
Sauberer Code reduziert die Onboarding-Zeit neuer Entwickler massiv. Google konnte nachweisen, dass konsistente Styles und klare Strukturen die Innovationszyklen beschleunigen, da weniger Zeit mit "Code-Rätselraten" verschwendet wird.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="disastergirl" captions="Wenn die Agentur sagt: 'Wir fixen das|einfach direkt im Live-System'" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Qualität gewinnt immer</H2>
|
||||
|
||||
<Paragraph>
|
||||
Es gibt keine Abkürzung zu exzellenter Software. Wer beim Fundament spart, zahlt später die Zeche durch langsame Prozesse und [ineffiziente IT-Infrastrukturen](/blog/slow-loading-costs-customers).
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Lassen wir den Ballast von unsauberem Code hinter uns. Ich baue Ihnen eine Architektur, die nicht nur heute funktioniert, sondern durch ihre <Marker>Eleganz und Klarheit</Marker> auch in zehn Jahren noch ein stabiles Asset für Ihr Unternehmen ist.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Architektur-Audit anfragen"
|
||||
description="Ist Ihr Code ein Asset oder eine Bremse? Ich analysiere Ihre bestehende Architektur auf Skalierbarkeit und technische Schulden."
|
||||
buttonText="Jetzt Experten-Gespräch buchen"
|
||||
href="/contact"
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Was ist der größte Vorteil von Clean Code für Geschäftsführer?</H3>
|
||||
<Paragraph>Der größte Vorteil ist die Reduzierung der Gesamtkosten (TCO). Da Wartung bis zu 80% des Budgets frisst, setzt sauberer Code massives Kapital für neue Innovationen statt für Reparaturen frei.</Paragraph>
|
||||
|
||||
<H3>Dauert die Entwicklung mit Clean Code länger?</H3>
|
||||
<Paragraph>Initial ja, etwa 15-20% mehr Zeitaufwand in der Konzeption. Dieser Aufwand amortisiert sich jedoch bereits nach wenigen Monaten durch eine 30% schnellere Fehlerbehebung und reibungslose Updates.</Paragraph>
|
||||
|
||||
<H3>Kann man bestehenden 'Dirty Code' retten?</H3>
|
||||
<Paragraph>Ja, durch schrittweises Refactoring und die Einführung automatisierter Tests. Es ist ein Prozess der Wertsteigerung, der technisch verschuldete Systeme wieder in liquide digitale Assets verwandelt.</Paragraph>
|
||||
</FAQSection>
|
||||
195
apps/web/content/blog/crm-synchronization-headless.mdx
Normal file
195
apps/web/content/blog/crm-synchronization-headless.mdx
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: "B2B CRM-Integration: Wie automatisierte Datenflüsse 12% Ihres Umsatzes retten"
|
||||
thumbnail: "/blog/crm-synchronization-headless.png"
|
||||
description: "Vom Kontaktformular direkt in den Sales-Funnel: Wie Sie manuelle Daten-Silos auflösen, die Sales-Produktivität um 14,5% steigern und menschliche Fehler eliminieren."
|
||||
date: "2026-01-31"
|
||||
tags: ["crm", "architecture", "automation"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Die wertvollsten Daten Ihres Unternehmens liegen oft in Silos versteckt. Ihre Website sammelt Leads, aber Ihr CRM "weiß" nichts davon – oder erst nach mühsamer, manueller Übertragung durch Ihre Sales-Teams.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Dieser "Media Break" ist nicht nur ineffizient, sondern ein massives finanzielles Risiko. Ich beende das Zeitalter der Daten-Inseln durch hochperformante, <Marker>architektonische CRM-Synchronisation</Marker>.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In diesem Guide zeige ich Ihnen, wie eine nahtlose Integration Ihre Marketing-Effizienz skaliert, die Fehlerquote eliminiert und Ihre Response-Time auf ein marktführendes Niveau hebt.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
1. [Das Problem der manuellen Daten-Brücke](#das-problem-der-manuellen-daten-brücke)
|
||||
2. [Der ökonomische Impact von Datenqualität](#der-ökonomische-impact-von-datenqualität)
|
||||
3. [Speed-to-Lead als Wettbewerbsvorteil](#speed-to-lead-als-wettbewerbsvorteil)
|
||||
4. [Der Haken an der Sache: Komplexität vs. Stabilität](#der-haken-an-der-sache)
|
||||
5. [Architektonische Hebel für Ihre Daten-Souveränität](#meine-hebel-für-ihre-daten-souveränität)
|
||||
6. [Fazit](#fazit-lassen-sie-ihre-daten-fließen)
|
||||
</Section>
|
||||
|
||||
<H2 id="das-problem-der-manuellen-daten-brücke">Das Problem der manuellen Daten-Brücke</H2>
|
||||
|
||||
<Paragraph>
|
||||
Viele Unternehmen nutzen Kontaktformulare, die lediglich eine formatierte E-Mail versenden. Ein Mitarbeiter muss diese lesen und die Daten händisch in Salesforce, HubSpot oder Microsoft Dynamics übertragen. Dieser Prozess ist ein Relikt aus dem letzten Jahrzehnt.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Laut einer Studie von <ExternalLink href="https://www.pwc.de">PricewaterhouseCoopers</ExternalLink> können Fehler bei der manuellen Dateneingabe Unternehmen bis zu **12% ihres Jahresumsatzes** kosten. Jedes falsch abgetippte Feld und jeder vergessene Lead ist bares Geld, das verloren geht.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="disastergirl" captions="Manuelle Lead-Übertragung|Die Sales-Pipeline des Wettbewerbers" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Echte Professionalität bedeutet: Die Software erledigt die Arbeit im Hintergrund, während sich Ihre Sales-Experten auf den Abschluss konzentrieren. [Technische Qualität skaliert Ihr Business](/blog/clean-code-for-business-value) weit über das Maß hinaus, das händische Prozesse je erreichen könnten.
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="der-ökonomische-impact-von-datenqualität">Der ökonomische Impact von Datenqualität</H2>
|
||||
|
||||
<Paragraph>
|
||||
Datenqualität ist kein "Nice-to-have" für die IT-Abteilung, sondern eine kritische Business-Kennzahl. Gartner schätzt, dass schlechte Datenqualität Organisationen durchschnittlich **15 Millionen Dollar pro Jahr** kostet.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="88%|Umsatz-Impact|Unternehmen klagen über Datenfehler~29%|Umsatzsteigerung|durch CRM-Integration (IBM)~14.5%|Sales Produktivität|durch Marketing Automation~42%|Quota Achievement|bei integrierten Systemen" />
|
||||
|
||||
<ArticleQuote
|
||||
quote="Marketing teams can reduce campaign building time by up to 60%. Forrester's Marketing Cloud study shows efficiency gains with up to 90%."
|
||||
author="Salesforce"
|
||||
isCompany={true}
|
||||
source="Salesforce Data Integration ROI"
|
||||
sourceUrl="https://www.salesforce.com"
|
||||
/>
|
||||
|
||||
<H2 id="speed-to-lead-als-wettbewerbsvorteil">Speed-to-Lead als Wettbewerbsvorteil</H2>
|
||||
|
||||
<Paragraph>
|
||||
In der B2B-Welt zählt jede Sekunde. Research der Aberdeen Group zeigt, dass Best-in-Class Unternehmen zu **53% häufiger** ihre Lead-Management-Prozesse automatisiert haben. Dies führt zu drastisch schnelleren Reaktionszeiten.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramSequence id="crm-sync-sequence" title="Automatisierter Lead-Flow" showShare={true}>
|
||||
sequenceDiagram
|
||||
participant B as Besucher
|
||||
participant W as Website (Headless)
|
||||
participant V as Validation Layer
|
||||
participant C as CRM (HubSpot/SF)
|
||||
participant S as Sales Team
|
||||
B->>W: Formular absenden
|
||||
W->>V: Daten-Validierung & Bereinigung
|
||||
V->>C: API-Push (Realtime)
|
||||
C->>S: Slack/E-Mail Alert (< 5 Min)
|
||||
</DiagramSequence>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
B2B-Einkäufer, die innerhalb von 5 Minuten eine Antwort erhalten, treten mit einer **21-fach höheren Wahrscheinlichkeit** in den Verkaufsprozess ein. Eine verzögerte Antwort aufgrund manueller Prozesse ist oft gleichbedeutend mit einem verlorenen Lead an den Wettbewerber.
|
||||
</Paragraph>
|
||||
|
||||
<H3>CRM-Integration als Investition mit hohem ROI</H3>
|
||||
|
||||
<Paragraph>
|
||||
Die Implementierung einer sauberen Schnittstelle rechnet sich schneller, als die meisten Entscheider vermuten. Nucleus Research fand heraus, dass jeder in ein CRM investierte Dollar durchschnittlich **8,71 $ zurückgibt**. Das funktioniert jedoch nur, wenn das System gefüttert wird – und zwar automatisch.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="30%"
|
||||
label="Verbesserung der Kundenzufriedenheit durch CRM-Systeme"
|
||||
source="Forrester Consulting"
|
||||
sourceUrl="https://www.forrester.com"
|
||||
/>
|
||||
|
||||
<H2 id="der-haken-an-der-sache">Der Haken an der Sache: Kosten vs. Risiko</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich bin kein Freund davon, Technologie zu beschönigen. Eine professionelle CRM-Integration ist komplexer als die Installation eines Standard-Plugins. Hier ist die ungeschminkte Wahrheit:
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Entscheidung"
|
||||
negativeLabel="Standard WordPress Plugin"
|
||||
negativeText="Günstig, schnell installiert, aber instabil bei Updates und oft DSGVO-kritisch."
|
||||
positiveLabel="Custom Integration"
|
||||
positiveText="Höhere Initialkosten, dafür 100% wartungsfrei, sicher und individuell skalierbar."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Hohe Initialkosten:</strong> Eine saubere API-Architektur erfordert Senior-Engineering.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Wartungsaufwand bei API-Wechsel:</strong> Ändert das CRM seine Schnittstelle, muss der Code angepasst werden.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Langfristige Unabhängigkeit:</strong> Sie besitzen die Datenbrücke und unterliegen keinem [Vendor Lock-In durch Baukasten-Systeme](/blog/builder-systems-threaten-independence).
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2 id="meine-hebel-für-ihre-daten-souveränität">Meine Hebel für Ihre Daten-Souveränität</H2>
|
||||
|
||||
<Paragraph>
|
||||
Integration bedeutet für mich mehr als nur das Verbinden von Endpunkten. Es geht um die Schaffung einer <Marker>Single Source of Truth</Marker>. Meine Architektur basiert auf drei Säulen:
|
||||
</Paragraph>
|
||||
|
||||
<Carousel items={[
|
||||
{
|
||||
title: "Resiliente Puffer-Systeme",
|
||||
content: "Sollte Ihr CRM (z.B. Salesforce) wartungsbedingt offline sein, speichern meine Systeme die Leads zwischen und synchronisieren sie automatisch, sobald die Verbindung steht. Kein Datenverlust, niemals."
|
||||
},
|
||||
{
|
||||
title: "Smart Data Enrichment",
|
||||
content: "Wir reichern Leads bereits beim Eingang mit Kontextdaten an (z.B. Kampagnen-Herkunft, technisches Profil), damit Ihr Sales-Team sofort weiß, worauf der Fokus liegen muss."
|
||||
},
|
||||
{
|
||||
title: "Deep Security & Compliance",
|
||||
content: "Wir nutzen TLS-Verschlüsselung und europäische Gateways. Die Integration erfolgt nach dem Prinzip der Datensparsamkeit – nur das, was für den Sales-Erfolg nötig ist, verlässt das System."
|
||||
}
|
||||
]} />
|
||||
|
||||
<Paragraph>
|
||||
Oft wird vergessen, dass die [Wahl des Hostings](/blog/professional-hosting-operations) und die Architektur der Website direkt die Zuverlässigkeit dieser Schnittstellen beeinflussen. Instabile Systeme führen zu [hohen versteckten Kosten](/blog/hidden-costs-of-wordpress-plugins).
|
||||
</Paragraph>
|
||||
|
||||
<YouTubeEmbed videoId="LhmYwr1GAWo" title="CRM Integration Best Practices" />
|
||||
|
||||
<LeadMagnet
|
||||
title="Daten-Silos eliminieren?"
|
||||
description="Ich analysiere Ihre aktuelle Lead-Transition und entwickle eine Architektur, die Ihre Sales-Pipeline automatisiert und menschliche Fehlerquellen ausschaltet."
|
||||
buttonText="Beratungsgespräch anfragen"
|
||||
href="/contact"
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<H2 id="fazit-lassen-sie-ihre-daten-fließen">Fazit: Lassen Sie Ihre Daten fließen</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technologie sollte Reibung eliminieren, nicht neue Hürden schaffen. Unternehmen, die heute noch Daten manuell einpflegen, verbrennen nicht nur Zeit, sondern riskieren ihre Marktposition durch langsame Response-Times und fehlerhafte Datensätze.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Gartner schätzt, dass **40% aller CRM-Fehlschläge** auf Prozessprobleme oder menschliches Versagen zurückzuführen sind. Eine automatisierte Integration ist die wirksamste Versicherung gegen dieses Risiko. [Vermeiden Sie technische Altlasten](/blog/slow-loading-costs-customers) und machen Sie Ihre Website zu einem integralen Bestandteil Ihres Sales-Motors.
|
||||
</Paragraph>
|
||||
|
||||
<PortfolioHeader
|
||||
title="Bereit für echte Automatisierung?"
|
||||
sub="Präzision in der Schnittstelle, Klarheit im Ergebnis."
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Wie lange dauert die Integration eines CRMs?</H3>
|
||||
<Paragraph>
|
||||
Eine Standard-Anbindung per API ist innerhalb von 1-2 Wochen stabil implementiert. Komplexe Workflows mit individuellem Data Enrichment erfordern je nach Anforderung 3-4 Wochen Entwicklungszeit.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Welche CRMs werden unterstützt?</H3>
|
||||
<Paragraph>
|
||||
Jedes System mit einer modernen REST- oder GraphQL-API kann angebunden werden. Dazu gehören Salesforce, HubSpot, Pipedrive, Microsoft Dynamics 365 und spezialisierte Branchenlösungen.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Ist die Integration DSGVO-konform?</H3>
|
||||
<Paragraph>
|
||||
Ja, absolut. Die Datenübertragung erfolgt verschlüsselt, und es werden nur die Daten verarbeitet, für die eine explizite Nutzerwahrnehmung (z.B. Checkbox im Formular) vorliegt.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
164
apps/web/content/blog/digital-longevity-architecture.mdx
Normal file
164
apps/web/content/blog/digital-longevity-architecture.mdx
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
title: "Digital Longevity: B2B-Software-Architektur für das nächste Jahrzehnt"
|
||||
thumbnail: "/blog/digital-longevity-architecture.png"
|
||||
description: "Software ohne Verfallsdatum: Erfahren Sie, wie nachhaltige Architektur technische Schulden minimiert und langfristige digitale Assets statt Liability schafft."
|
||||
date: "2026-02-02"
|
||||
tags: ["architecture", "longevity", "strategy"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
In einer Branche, die von Quartalszahlen und kurzlebigen Hypes getrieben wird, gilt Software oft schon nach 24 Monaten als veraltet.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich betrachte diese technologische Kurzatmigkeit als massive Verschwendung von Kapital, Energie und Fokus. Wahre Qualität bemisst sich nicht am Tag des Launchs, sondern daran, wie ein System altert.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie wir <Marker>digitale Werte für Jahrzehnte</Marker> schaffen – durch vorausschauende Architektur, die technische Schulden proaktiv verhindert statt sie nur zu verwalten.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
**TL;DR:**
|
||||
Die durchschnittliche App überlebt kaum 14-24 Monate. Durch modulare Headless-Architekturen, den Verzicht auf kurzlebige Frameworks und [Clean Code](/blog/clean-code-for-business-value) verwandeln wir IT von einem Kostenfaktor in ein langlebiges digitales Asset mit einer Lebensdauer von 10+ Jahren.
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
1. [Gegen die Wegwerf-Mentalität im Code](#gegen-die-wegwerf-mentalität-im-code)
|
||||
2. [Der ökonomische Case für Langlebigkeit](#der-ökonomische-case-für-langlebigkeit)
|
||||
3. [Prinzipien für ewige Systeme](#meine-prinzipien-für-ewige-systeme)
|
||||
4. [Der Haken an der Sache (Ehrliche Analyse)](#der-haken-an-der-sache)
|
||||
5. [Fazit & FAQ](#fazit-werte-schaffen-die-bleiben)
|
||||
</Section>
|
||||
|
||||
<H2 id="gegen-die-wegwerf-mentalität-im-code">Gegen die Wegwerf-Mentalität im Code</H2>
|
||||
|
||||
<Paragraph>
|
||||
Viele Agenturen bauen "Schönwetter-Lösungen". Man setzt auf proprietäre Blackboxes oder überladene Frameworks, die nach kurzer Zeit nicht mehr unterstützt werden. Das Ergebnis ist eine kostspielige Spirale aus [technischen Altlasten](/blog/slow-loading-costs-customers) und erzwungenen Relaunchs alle drei Jahre. Studien zeigen, dass Organisationen bis zu **75% ihres IT-Budgets** allein dafür aufwenden, "das Licht am Laufen zu halten" – primär bedingt durch veraltete Infrastruktur.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="grumpycat" captions="Agentur: 'Wir brauchen einen Relaunch'|Ich: 'Der letzte war vor 18 Monaten'" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>geplante Obsoleszenz der Software</Marker>. Während B2B-Lösungen eigentlich für 5-10 Jahre ausgelegt sein sollten, werden viele mobile Apps bereits nach 12-14 Monaten grundlegend neu geschrieben. Im B2B-Sektor ist diese Kurzlebigkeit ein strategisches Risiko. Mein Ansatz ist das Gegenteil: Ich baue Systeme, die durch ihre innere Ordnung bestechen. Guter Code ist wie eine solide Immobilie – er braucht Pflege, aber keine Abrissbirne.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Organizations are redeveloping or replacing their systems every 7 to 15 years on average, to remain competitive and support changing business needs."
|
||||
author="Accenture"
|
||||
isCompany={true}
|
||||
source="Accenture Technology Report"
|
||||
sourceUrl="https://www.accenture.com"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<H2 id="der-ökonomische-case-für-langlebigkeit">Der ökonomische Case für Langlebigkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das Fundament bröckelt. Dabei zeigen Analysen, dass Unternehmen zwischen **60% und 80% ihres IT-Budgets** für die reine Maintenance von Legacy-Systemen binden. Dieser "Legacy-Tax" entzieht Ressourcen für echte Innovationen.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber value="80%" label="des IT-Budgets fließen oft in die Wartung veralteter Systeme" source="Gartner" sourceUrl="https://www.gartner.com" />
|
||||
|
||||
<Paragraph>
|
||||
Durch den Einsatz eines [Clean Code Fokus](/blog/clean-code-for-business-value) verwandeln wir Software von einer Verbindlichkeit (Liability) in ein echtes digitales Asset. Wenn die Architektur stimmt, sinken die Wartungskosten massiv, während die Agilität steigt. Software, die auf [unabhängigen Architekturen](/blog/builder-systems-threaten-independence) basiert, lässt sich modular erweitern, ohne das Gesamtsystem zu gefährden.
|
||||
</Paragraph>
|
||||
|
||||
<DigitalAssetVisualizer />
|
||||
|
||||
<H3>Die Ästhetik der Zeitlosigkeit</H3>
|
||||
|
||||
<Paragraph>
|
||||
Langlebigkeit hat eine technische und eine visuelle Komponente. Ich vermeide "modische" Spielereien, die morgen schon peinlich wirken könnten. Ein <Marker>industrieller, klarer Look</Marker> altert langsamer als jede verspielte Grafik. Wir setzen auf [Responsive Excellence](/blog/responsive-design-high-fidelity), die auch auf Endgeräten der nächsten Generation souverän wirkt.
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="meine-prinzipien-für-ewige-systeme">Meine Prinzipien für ewige Systeme</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wie baut man Software, die nicht veraltet? Durch die kompromisslose Auswahl der Fundamente und die Abkehr von [Abhängigkeiten durch Baukasten-Systeme](/blog/builder-systems-threaten-independence). Ein zentraler Baustein ist die Reduzierung der Komplexität: Je weniger bewegliche Teile (Plugins, Drittanbieter-Frameworks), desto geringer die Angriffsfläche für technischen Zerfall.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Industrie-Standards statt Hypes:</strong> Einsatz von Technologien mit globalem Rückhalt (React, Node.js, Go). Keine Experimente mit Nischen-Tools ohne Langzeit-Sicherheit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Strict Separation of Concerns:</strong> Saubere Trennung von Design, Daten und Logik (Headless). So lassen sich Einzelteile austauschen, ohne das Gesamtsystem zu gefährden.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Evolution:</strong> Kontinuierliche Refactoring-Zyklen. Laut Google Developers erfordern mobile Apps alle 1-2 Monate Updates, um mit Betriebssystemen kompatibel zu bleiben – eine solide Architektur macht dies zum Kinderspiel.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<YouTubeEmbed videoId="TPLb4q95ZC4" title="Carola Lilienthal über langlebige Architekturen" />
|
||||
</div>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Architektur-Vergleich der Halbwertszeit"
|
||||
negativeLabel="Trend-Agentur"
|
||||
negativeText="Relaunch alle 3 Jahre nötig, enorme technische Schulden, Vendor Lock-in."
|
||||
positiveLabel="Mintel Standard"
|
||||
positiveText="Laufzeit von 10+ Jahren durch Built-First Ansatz, volle technologische Souveränität."
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Section>
|
||||
<H2 id="der-haken-an-der-sache">Der Haken an der Sache (Devil's Advocate)</H2>
|
||||
<Paragraph>
|
||||
Langlebige Architektur ist kein Free Lunch. Warum baut dann nicht jede Agentur so? Weil es Disziplin und Zeit kostet, die im kurzfristigen Projektgeschäft oft nicht gewollt ist.
|
||||
</Paragraph>
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Höhere Initialkosten:</strong> Eine saubere Architektur erfordert initial ca. 20-30% mehr Architektur-Planung als eine "Plug-and-Play"-Solution.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Keine schnellen Quick-Fixes:</strong> "Einfach mal kurz ein Plugin installieren" gibt es bei mir nicht, da dies die Integrität gefährdet.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Hoher Anspruch an den Kunden:</strong> Sie müssen Software als strategisches Asset begreifen, nicht als einmaliges Projekt-Häkchen.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
</Section>
|
||||
|
||||
<LeadMagnet
|
||||
title="Architektur-Audit anfragen"
|
||||
description="Wir analysieren Ihren aktuellen Tech-Stack auf Zukunftsfähigkeit und identifizieren kritische technische Schulden, bevor sie zum Wachstumsstopper werden."
|
||||
buttonText="Jetzt Analyse sichern"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Rendite durch technologische Beständigkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wahrer ROI entsteht erst über die Zeit. Wer nicht alle 36 Monate neu baut, hat mehr Kapital für echtes Wachstum zur Verfügung. Large Organizations geben jährlich Millionen für die Instandhaltung von Legacy-Systemen aus – Geld, das in Neuentwicklungen besser aufgehoben wäre.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArchitectureBuilder />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Investieren Sie in Substanz, nicht in flüchtige Effekte. In einem Markt, in dem über die Hälfte des Budgets in die reine Erhaltung fließt, ist technologische Stabilität ein [entscheidender Wettbewerbsvorteil](/blog/build-first-digital-architecture). Unser Fokus auf [professionelles Hosting](/blog/professional-hosting-operations) unterstützt diesen langlebigen Ansatz.
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="fazit-werte-schaffen-die-bleiben">Fazit: Werte schaffen, die bleiben</H2>
|
||||
|
||||
<Paragraph>
|
||||
Digitale Exzellenz misst sich am Erfolg von morgen. Wenn Sie technologische Stabilität als Teil Ihres unternehmerischen Vermächtnisses begreifen, ist eine "Build-First" Strategie alternativlos. Lassen wir gemeinsam ein System gießen, das die Zeit überdauert. <Marker>Qualität ist Beständigkeit.</Marker> Ihr Erfolg verdient eine Architektur ohne Verfallsdatum, die mit Ihrem Business wächst, statt es durch technische Schulden zu bremsen.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum halten moderne Plattformen oft nur 2-3 Jahre?</H3>
|
||||
<Paragraph>Meist liegt es an einer zu engen Kopplung an instabile Plugins und kurzlebige Trends. Laut Gartner werden Enterprise-Apps oft alle 7-10 Jahre ersetzt, da sie unter der Last ihrer eigenen Komplexität kollabieren.</Paragraph>
|
||||
|
||||
<H3>Rechnet sich die höhere Initialinvestition wirklich?</H3>
|
||||
<Paragraph>Absolut. Da Unternehmen durchschnittlich 50-60% ihres IT-Budgets für Legacy-Wartung verschwenden, amortisiert sich eine saubere Architektur oft schon nach dem ersten nicht notwendigen Relaunch-Zyklus.</Paragraph>
|
||||
|
||||
<H3>Wie oft muss langlebige Software aktualisiert werden?</H3>
|
||||
<Paragraph>Langlebigkeit bedeutet nicht Stillstand. Updates alle 1-2 Monate sind laut Android/Google Developers essenziell, um mit OS-Innovationen Schritt zu halten – eine gute Architektur macht diese Updates jedoch risikoarm.</Paragraph>
|
||||
</FAQSection>
|
||||
165
apps/web/content/blog/fixed-price-digital-projects.mdx
Normal file
165
apps/web/content/blog/fixed-price-digital-projects.mdx
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Der strategische Festpreis: Warum Budgetsicherheit die Software-Qualität maximiert"
|
||||
thumbnail: "/blog/fixed-price-digital-projects.png"
|
||||
description: "Keine Angst vor uferlosen Stundenabrechnungen. Erfahren Sie, warum ein Festpreis-Modell mit Risiko-Buffern der fairste Weg zu High-End Software-Lösungen ist."
|
||||
date: "2026-02-04"
|
||||
tags: ["management", "business"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Sicherheitsdenken ist tief in der menschlichen Natur verwurzelt – besonders wenn es um sechsstellige Projektbudgets im B2B-Sektor geht.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Niemand möchte am Ende eines Quartals feststellen, dass das digitale Fundament doppelt so viel kostet wie geplant, während die Timeline stagniert.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect baue ich <Marker>Sicherheit durch Transparenz</Marker> und zeige Ihnen, warum ein intelligenter Festpreis der fairste Weg zu exzellenten Ergebnissen ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
<TOC />
|
||||
</Section>
|
||||
|
||||
<H2>Die Falle der unendlichen Stunden</H2>
|
||||
|
||||
<Paragraph>
|
||||
In der klassischen Softwareentwicklung ist die Abrechnung nach Stunden (Time & Material) der Industriestandard. Doch bei genauer Betrachtung setzt dieses Modell einen völlig falschen Anreiz: Je ineffizienter ein Entwickler arbeitet oder je schlechter die Architektur geplant wurde, desto mehr verdient die Agentur.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Effizienz wird ökonomisch bestraft, während [technische Altlasten](/blog/slow-loading-costs-customers) und langsame Prozesse den Umsatz des Dienstleisters steigern. Ich hingegen habe mein gesamtes Business auf Geschwindigkeit und architektonische Klarheit optimiert.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Fixed-price contracts offer budget predictability, but they can also incentivize developers to cut corners on quality to maintain profitability if the initial scope is underestimated."
|
||||
author="Gartner"
|
||||
isCompany={true}
|
||||
source="Gartner Research"
|
||||
sourceUrl="https://www.gartner.com"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Mit einem strategischen Festpreis drehen wir den Spieß um: <Marker>Mein Anreiz ist Ihre schnellstmögliche Zufriedenheit</Marker>. Da das Budget fixiert ist, liegt es in meinem eigenen Interesse, eine so saubere Architektur zu bauen, dass keine Bugs oder langwierigen Korrekturschleifen mein Zeitbudget fressen. So entsteht echte Partnerschaft statt eines Interessenkonflikts.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="panik-kalm-panik" captions="Stundensatz-Projekt wird 45% teurer|Agentur freut sich über Mehrumsatz|Sie erklären dem Vorstand das Budgetloch" />
|
||||
</div>
|
||||
|
||||
<H2>Zahlen, Daten, Fakten: Warum Risiko-Management kein Zufall ist</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die Realität in der IT ist oft ernüchternd. Viele Projekte scheitern nicht an der Technik, sondern an mangelhafter finanzieller und zeitlicher Planung. Ein strategischer Festpreis erfordert eine Reife im Risikomanagement, die viele Organisationen vermissen lassen.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="45%|Budget-Überschreitung|bei großen IT-Projekten (McKinsey)~2.5x|Höhere Erfolgschance|bei Nutzung formaler Risiko-Buffer~31%|Projektabruch-Rate|oft durch Scope Creep (Standish Group)" />
|
||||
|
||||
<Paragraph>
|
||||
Studien von <ExternalLink href="https://www.pwc.com">PricewaterhouseCoopers (PwC)</ExternalLink> zeigen deutlich: Projekte, die formale Risikomanagement-Prozesse inklusive Puffer nutzen, erreichen ihre Ziele 2,5-mal häufiger. Ein Festpreis ist für mich kein Glücksspiel, sondern das Ergebnis einer präzisen mathematischen Kalkulation.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<div className="my-8">
|
||||
<Mermaid id="fixed-price-logic" title="Wirtschaftliche Dynamik" showShare={true}>
|
||||
graph LR
|
||||
A["Festpreis"] --> B["Effizienz-Zwang"]
|
||||
B --> C["Hohe Code-Qualität"]
|
||||
C --> D["Schneller Release"]
|
||||
D --> E["Hoher ROI"]
|
||||
style A fill:#4ade80,stroke:#333
|
||||
style E fill:#4ade80,stroke:#333
|
||||
</Mermaid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<H2>Planung als Fundament digitaler Exzellenz</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ein Festpreis kann nur funktionieren, wenn die Vision klar definiert ist. Deshalb investiere ich zu Beginn massiv Zeit in die Analyse und das "Blueprint-Design". Wer hier spart, zahlt später doppelt – eine Lektion, die ich auch in meinem Artikel über den [ROI von Clean Code](/blog/clean-code-for-business-value) ausführlich beschreibe.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Das ist <Marker>digitales Engineering mit norddeutscher Klarheit</Marker>. Sie bezahlen nicht für mein Ausprobieren oder für das Training von Junioren, sondern für die punktgenaue Umsetzung jahrelanger Erfahrung.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramGantt
|
||||
tasks={[
|
||||
{ id: "blueprint", name: "Blueprint & Risiko-Analyse", start: "2024-01-01", duration: "2w" },
|
||||
{ id: "design", name: "Design System (Fixe Komponenten)", start: "2024-01-15", duration: "1w", dependencies: ["blueprint"] },
|
||||
{ id: "core", name: "Core Architecture (No-Bugs-Policy)", start: "2024-01-22", duration: "3w", dependencies: ["design"] },
|
||||
{ id: "launch", name: "Punktgenauer Launch", start: "2024-02-19", duration: "1w", dependencies: ["core"] },
|
||||
]}
|
||||
title="Festpreis-Sicherheit: Zeitplan & Budget"
|
||||
id="fixed-price-gantt"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Der Haken an der Sache: Die Grenzen des Festpreises</H2>
|
||||
|
||||
<Paragraph>
|
||||
Um als Digital Architect glaubwürdig zu bleiben, muss ich offen sagen: Ein Festpreis ist kein Allheilmittel. Er erfordert Disziplin auf beiden Seiten.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Ist ein Festpreis immer die beste Wahl?"
|
||||
negativeLabel="Wann es schwierig wird"
|
||||
negativeText="Völlig unklare Anforderungen, Forschungsprojekte (R&D) ohne Zielbild, ständige Strategiewechsel während der Bauphase."
|
||||
positiveLabel="Wann es perfekt ist"
|
||||
positiveText="Klare Business-Ziele, definierter Funktionsumfang, Wunsch nach radikaler Budgetkontrolle und Termintreue."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Laut <ExternalLink href="https://www.pmi.org">PMI (Project Management Institute)</ExternalLink> neigen Festpreis-Projekte stärker zu Scope Creep (48%) als T&M-Projekte. Daher ist eine [saubere Architektur-Strategie](/blog/digital-longevity-architecture) essenziell, um Erweiterungen später modular und kosteneffizient zu ermöglichen, ohne das initiale Festpreis-Fundament zu sprengen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Ihr Investment in psychologische Sicherheit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ein Festpreis befreit den Kopf. Statt bei jedem Meeting die virtuelle Uhr ticken zu hören, konzentrieren wir uns auf das Wesentliche: Ihren Markterfolg. Das schafft eine Atmosphäre von <Marker>Kreativität und technologischem Mut</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Integrierte Risiko-Abdeckung bedeutet für Sie: Unvorhergesehene technische Hürden sind mein Problem, nicht Ihres. Deloitte berichtet, dass Organisationen mit starken PM-Praktiken 28-mal weniger Geld verschwenden. Mein Festpreis-Modell ist der operative Arm dieser Erkenntnis.
|
||||
</Paragraph>
|
||||
|
||||
<YouTubeEmbed videoId="YIGDOZx_3QE" title="Der Agile Festpreis - Einblicke in die Methodik" />
|
||||
|
||||
<LeadMagnet
|
||||
title="Projekt-Planungssicherheit gewinnen"
|
||||
description="Lassen Sie uns Ihr Vorhaben analysieren. Ich erstelle Ihnen ein Festpreis-Angebot, das hält – ohne versteckte Kosten oder spätere 'Überraschungen'."
|
||||
buttonText="Jetzt Blueprint-Call anfragen"
|
||||
href="/contact"
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<H2>Fazit: Klarheit ist kein Luxus</H2>
|
||||
|
||||
<Paragraph>
|
||||
Lassen wir das Rätselraten bei der Preisgestaltung hinter uns. Das "Time & Material"-Modell dient oft als Schutzschild für planlose Agenturen. Mein Festpreis-Modell hingegen ist ein Qualitätsversprechen. Es zwingt mich zu technischer Exzellenz und bietet Ihnen die finanzielle Planbarkeit, die Sie für die Skalierung Ihres Unternehmens benötigen.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
<Marker>Pünktlich. Präzise. Zum vereinbarten Preis.</Marker> Das ist kein Marketing-Slogan, sondern die Basis für [langfristige digitale Unabhängigkeit](/blog/builder-systems-threaten-independence).
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum ist ein Festpreis oft teurer als ein initialer Schätzpreis nach Stunden?</H3>
|
||||
<Paragraph>
|
||||
Ein Festpreis enthält einen Risiko-Buffer, der Unvorhersehbares abdeckt. Ein Schätzpreis nach Stunden wirkt anfangs günstiger, endet laut McKinsey aber oft 45% über dem Budget, da keine Kostendeckelung existiert.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie gehen wir mit Änderungen während des Projekts um?</H3>
|
||||
<Paragraph>
|
||||
Kleinere Anpassungen sind im Rahmen der Kulanz oft enthalten. Major Changes werden über Change Requests fair neu bewertet, sodass die Integrität des ursprünglichen Budgets geschützt bleibt.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Eignet sich ein Festpreis für agile Softwareentwicklung?</H3>
|
||||
<Paragraph>
|
||||
Ja, durch das Modell des "Agilen Festpreises". Hierbei fixieren wir das Budget und die Qualität, bleiben aber flexibel bei der Priorisierung der Features innerhalb des vereinbarten Rahmens.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
189
apps/web/content/blog/gdpr-conformity-system-approach.mdx
Normal file
189
apps/web/content/blog/gdpr-conformity-system-approach.mdx
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: "DSGVO-Konformität: Warum Privacy by Design der ultimative B2B-Wettbewerbsvorteil ist"
|
||||
thumbnail: "/blog/gdpr-conformity-system-approach.png"
|
||||
description: "Vergessen Sie Cookie-Banner. Echte DSGVO-Konformität entsteht durch Privacy by Design und saubere Systemarchitektur. Ein Leitfaden für Digital Architects."
|
||||
date: "2026-02-10"
|
||||
tags: ["legal", "gdpr", "architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
DSGVO-Konformität wird im B2B-Mittelstand oft als lästiges bürokratisches Hindernis oder notwendiges Übel wahrgenommen, das man mit nervigen Cookie-Bannern "löst".
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect sehe ich sie jedoch als <Marker>das ultimative Qualitätsmerkmal für sauberes Engineering</Marker>. Ein System, das Daten systemisch schützt, ist ein gesundes, effizientes und hochperformantes System.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum wir Datenschutz architektonisch lösen müssen, statt ihn nur mit rechtlichen Pflastern zu flicken – und wie dies direkt auf Ihren ROI einzahlt.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
- [TL;DR: Compliance als Architektur-Feature](#tldr)
|
||||
- [Gegen das Abmahnrisiko – mit Systemarchitektur](#systemarchitektur)
|
||||
- [Der wirtschaftliche Case: ROI von Privacy](#roi-privacy)
|
||||
- [Privacy by Infrastructure: Der technische Hebel](#infrastructure)
|
||||
- [Der Haken an der Sache: Die Wahrheit über radikale Minimierung](#haken)
|
||||
- [Fazit: Souveränität durch saubere Technik](#fazit)
|
||||
</Section>
|
||||
|
||||
<H2 id="tldr">TL;DR: Compliance als Architektur-Feature</H2>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Privacy by Design:</strong> Datenschutz ist kein Banner, sondern ein technisches Fundament.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Risikominimierung:</strong> Wer weniger Daten erhebt, reduziert die Angriffsfläche und Haftung massiv.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Wirtschaftlichkeit:</strong> Investitionen in Privacy steigern den ROI um den Faktor 1,8.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2 id="systemarchitektur">Gegen das Abmahnrisiko – mit Systemarchitektur</H2>
|
||||
<Paragraph>
|
||||
Die meisten Unternehmen versuchen, die Anforderungen der DSGVO durch seitenlange Dokumente und nachträglich installierte Consent-Werkzeuge zu lösen. Das ist so, als würde man ein brennendes Haus mit einer neuen Versicherungspolice löschen wollen. Es schafft eine Schein-Sicherheit, bekämpft aber nicht die Ursache des Risikos.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Datenschutz muss bereits in der DNA des Codes verankert sein. Wenn ein System von vornherein keine unnötigen Daten sammelt, <Marker>verschwinden die Einfallstore für rechtliche Probleme von selbst</Marker>. Wahre Compliance ist nicht rechtlich dokumentiert, sondern technisch erzwungen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<DiagramFlow
|
||||
id="compliance-flow"
|
||||
title="Privacy by Design Lifecycle"
|
||||
nodes={[
|
||||
{ id: "A", label: "Minimale Erhebung", style: "fill:#10b981;color:#fff" },
|
||||
{ id: "B", label: "End-to-End Encryption" },
|
||||
{ id: "C", label: "Automatisches Purging" },
|
||||
{ id: "D", label: "Rechtssicherheit", style: "fill:#059669;color:#fff" }
|
||||
]}
|
||||
edges={[
|
||||
{ from: "A", to: "B" },
|
||||
{ from: "B", to: "C" },
|
||||
{ from: "C", to: "D" }
|
||||
]}
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ein [verantwortungsbewusstes Hosting](/blog/professional-hosting-operations) und eine [saubere Architektur](/blog/digital-longevity-architecture) sorgen dafür, dass Daten erst gar nicht in unsichere Drittstaaten abfließen.
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="roi-privacy">Der wirtschaftliche Case: ROI von Privacy</H2>
|
||||
<Paragraph>
|
||||
Wussten Sie, dass unsaubere Datensparsamkeit ein messbares finanzielles Risiko darstellt? Daten, die Sie nicht besitzen, können nicht gestohlen werden. Eine Studie des Ponemon Institute verdeutlicht die Tragweite:
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="24%"
|
||||
label="weniger Data Breaches bei Unternehmen mit starker Data Governance"
|
||||
source="Ponemon Institute"
|
||||
sourceUrl="https://www.ibm.com/reports/data-breach"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Doch es geht nicht nur um Risikovermeidung. Laut Ciscos Data Privacy Benchmark Study sahen Organisationen einen <Marker>1,8-fach höheren ROI</Marker> aus ihren Privacy-Investitionen, wenn sie diese von Anfang an in ihre Prozesse integrierten.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="1.8x|ROI auf Privacy|Cisco Study~4.45 Mio $|Kosten pro Breach|Ponemon 2023~71%|Kundenabwanderung|nach Breach (PwC)" />
|
||||
|
||||
<ArticleQuote
|
||||
quote="Organizations focusing primarily on legalistic compliance with GDPR, rather than embedding privacy into product development and business processes, often struggle to achieve long-term sustainability."
|
||||
author="Gartner"
|
||||
isCompany={true}
|
||||
source="Gartner Analysis"
|
||||
sourceUrl="https://www.gartner.com"
|
||||
translated={true}
|
||||
/>
|
||||
|
||||
<H2 id="infrastructure">Mein Prinzip: Privacy by Infrastructure</H2>
|
||||
<Paragraph>
|
||||
Ich betrachte Datenschutz nicht als Text im Footer, sondern als Eigenschaft der Infrastruktur. In einem [Build-First Ansatz](/blog/build-first-digital-architecture) eliminieren wir riskante Abhängigkeiten wie Google Fonts oder Tracking-Pixel von Drittanbietern direkt auf Code-Ebene.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="disastergirl" captions="Wenn die Agentur sagt:|'Das Cookie-Banner regelt das schon.'" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Um dies zu erreichen, setze ich auf drei Säulen:
|
||||
</Paragraph>
|
||||
|
||||
<Carousel items={[
|
||||
{
|
||||
title: "Echtzeit-Anonymisierung",
|
||||
content: "Logfiles und IP-Adressen werden am Punkt des Eingangs (Edge) anonymisiert. Was technisch unkenntlich ist, unterliegt nicht der DSGVO-Strenge für personenbezogene Daten."
|
||||
},
|
||||
{
|
||||
title: "Datenminimierung nach ENISA",
|
||||
content: "Wir reduzieren die Attack Surface durch radikale Minimierung. Nur das geschäftskritische Minimum an Daten wird verarbeitet, was laut ENISA die effektivste Sicherheitsmaßnahme ist."
|
||||
},
|
||||
{
|
||||
title: "End-to-End Encryption",
|
||||
content: "Durchgängige TLS 1.3 Verschlüsselung und verschlüsselte Datenbanken führen laut Deloitte zu einem Rückgang unautorisierter Zugriffe um 30%."
|
||||
}
|
||||
]} />
|
||||
|
||||
<H3>Compliance jenseits des Cookie-Popups</H3>
|
||||
<Paragraph>
|
||||
Ein [verzichtbarer Cookie-Banner](/blog/website-without-cookie-banners) ist oft ein Zeichen für exzellente Technik. Wer keine nicht-essenziellen Cookies setzt, muss den Nutzer nicht belästigen. Das führt zu einer besseren User Experience und höheren Conversion-Rates.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="DSGVO-Audit anfordern"
|
||||
description="Ist Ihr System wirklich sicher oder nur 'rechtlich überklebt'? Wir analysieren Ihre Architektur auf Privacy-Leaks."
|
||||
buttonText="Jetzt Technik-Check buchen"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<H2 id="haken">Der Haken an der Sache: Die Wahrheit über radikale Minimierung</H2>
|
||||
<Paragraph>
|
||||
Es wäre unredlich zu behaupten, dass dieser Ansatz keine Nachteile hat. Privacy by Design erfordert Disziplin und oft den Verzicht auf komfortable, aber datenhungrige "Out-of-the-box"-Lösungen.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Abwägung der Implementierung"
|
||||
negativeLabel="Standard SaaS / Plugins"
|
||||
negativeText="Einfache Installation, aber unkontrollierte Datenabflüsse und Abhängigkeit von US-Clouds."
|
||||
positiveLabel="Mintel Architektur"
|
||||
positiveText="Höherer Initialaufwand im Engineering, dafür volle Datenhoheit und 0% Abmahnrisiko."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Höherer Planungsaufwand:</strong> Man muss sich im Vorfeld genau überlegen, welche Daten wirklich gebraucht werden.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Eingeschränktes Ad-Tracking:</strong> Wer auf invasive Tracker verzichtet, muss intelligentere, [anonyme Analytics-Modelle](/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung) nutzen.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2 id="fazit">Fazit: Souveränität durch saubere Technik</H2>
|
||||
<Paragraph>
|
||||
Echte DSGVO-Konformität ist kein Zustand, den man einmal erreicht, sondern ein Prozess, der tief in der Software-Architektur verankert sein muss. Während 96% der Unternehmen zwar "Programme" haben, fühlten sich laut Deloitte nur 30% wirklich auf einen Data Breach vorbereitet.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Schaffen wir die Angst vor rechtlichen Fehltritten ab. Ich baue Ihnen ein System, das durch seine innere Ordnung besticht und den Schutz technisch garantiert. Seriosität ist im B2B kein Zufall, sondern planbares Engineering.
|
||||
</Paragraph>
|
||||
|
||||
<Section>
|
||||
<FAQSection>
|
||||
<H3>Was ist der Unterschied zwischen Privacy by Design und Privacy by Default?</H3>
|
||||
<Paragraph>
|
||||
Privacy by Design bedeutet, den Datenschutz bereits in der Entwicklungsphase eines Systems technisch zu integrieren. Privacy by Default stellt sicher, dass die Werkseinstellungen eines Dienstes immer die datenschutzfreundlichsten sind.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Warum reicht ein Cookie-Banner allein nicht aus?</H3>
|
||||
<Paragraph>
|
||||
Ein Banner ist nur ein Interface-Element für die Einwilligung; es verhindert nicht automatisch den technischen Abfluss von Daten im Hintergrund, wie z.B. Log-Files oder IP-Leaks durch CDN-Anbieter. Wahre Compliance muss auf Server-Ebene greifen.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie wirkt sich Privacy by Design auf die Website-Performance aus?</H3>
|
||||
<Paragraph>
|
||||
Positiv. Da weniger externe Skripte (Tracker, Fonts, Social Plugins) geladen werden, sinkt die Ladezeit signifikant, was wiederum die Core Web Vitals und das Google-Ranking verbessert.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
</Section>
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
title: "Google PageSpeed Guide: Warum Ladezeit Ihr wichtigster B2B-Umsatzhebel ist"
|
||||
thumbnail: "/blog/der-google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-umsatzhebel-ist.png"
|
||||
description: "Millisekunden entscheiden im B2B über Erfolg oder Absprung. Erfahren Sie, wie Core Web Vitals Ihre Conversion-Rate und SEO-Rankings massiv beeinflussen."
|
||||
date: "2026-02-15"
|
||||
tags: ["performance", "seo", "conversion-optimization"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament einer digitalen Ruine gleicht. Wenn Ihre Website bei Google PageSpeed scheitert, verlieren Sie Kunden – bevor diese Ihre Botschaft überhaupt wahrnehmen können.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In der digitalen Ökonomie ist Performance kein „Nice-to-have“, sondern die Basis jeder Customer Journey. Akamai fand heraus, dass <Marker>53 % der mobilen Website-Besucher eine Seite verlassen</Marker>, die länger als drei Sekunden zum Laden benötigt.
|
||||
</LeadParagraph>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
**TL;DR:** Website-Geschwindigkeit ist ein kritischer Ranking-Faktor und der stärkste Hebel für Conversions. Während Page-Load-Zeiten von 1s auf 3s die Bounce-Rate um 32 % erhöhen, steigert jede 0,1s Verbesserung die Conversion um bis zu 8,4 %. Moderne Architekturen wie Static Site Generation sind der Schlüssel zur Performance-Exzellenz.
|
||||
|
||||
<H2>Der unsichtbare Umsatz-Verschleiß</H2>
|
||||
|
||||
<Paragraph>
|
||||
Stellen Sie sich vor, Sie eröffnen ein Luxus-Geschäft in der besten Lage, aber die Eingangstür klemmt massiv. Kunden müssen 10 Sekunden lang drücken, um einzutreten. Genau das passiert täglich auf B2B-Websites, deren technische Altlasten die Nutzererfahrung ersticken. [Langsame Ladezeiten](/blog/slow-loading-costs-customers) sind heute der Hauptgrund für hohe Absprungraten und sinkende Sichtbarkeit. 47 % der Nutzer erwarten laut Kissmetrics, dass eine Webseite in zwei Sekunden oder weniger lädt.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Every 100ms of latency cost them 1% in sales."
|
||||
author="Amazon"
|
||||
isCompany={true}
|
||||
source="Amazon CDN Study"
|
||||
sourceUrl="https://vwo.com/blog/100ms-latency-cost-amazon-1-percent-sales/"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Millisekunden sind im digitalen Zeitalter die härteste Währung. Daten von Google zeigen: Steigt die Ladezeit von einer auf drei Sekunden, <Marker>erhöht sich die Wahrscheinlichkeit eines Absprungs um 32 %</Marker>. Es ist ein gnadenloser Zusammenhang: Je länger der Browser wartet, desto geringer die Wahrscheinlichkeit eines profitablen Abschlusses. Geht die Ladezeit sogar auf sechs Sekunden hoch, steigt die Bounce-Wahrscheinlichkeit laut Google Developers sogar um dramatische 106 %.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="8.4%"
|
||||
label="Conversion-Steigerung pro 0.1s Verbesserung"
|
||||
source="Deloitte Digital"
|
||||
sourceUrl="https://www2.deloitte.com/ie/en/services/consulting/perspectives/milliseconds-make-millions.html"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Ich betrachte Performance nicht als isolierte IT-Kennzahl, sondern als ökonomischen Hebel. Ein [professionelles Hosting](/blog/professional-hosting-operations) und eine saubere Architektur sind die Mindestanforderung. Websites, die in einer Sekunde laden, haben eine signifikant höhere Conversion-Rate als langsamere Instanzen. Eine Studie von Portent untermauert dies: Die Conversion-Rate sinkt im Schnitt um 4,42 % mit jeder zusätzlichen Sekunde Ladezeit. Shopzilla berichtete sogar von einem <Marker>Umsatzplus von 7-12 %</Marker>, nachdem sie die Ladezeit von 6 auf 1,2 Sekunden reduzierten.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Core Web Vitals: Die neuen Spielregeln von Google</H2>
|
||||
|
||||
<Paragraph>
|
||||
Google hat bestätigt, dass <ExternalLink href="https://developers.google.com/search/docs/appearance/core-web-vitals">Core Web Vitals</ExternalLink> als Ranking-Signale für Suchergebnisse genutzt werden. Wer hier rote Zahlen schreibt, wird vom Algorithmus abgestraft. Webseiten, die den Schwellenwert „Gut“ in allen Kategorien erreichen, verzeichnen laut Deloitte eine deutlich höhere Interaktion. Dennoch zeigt das HTTP Archive, dass ein signifikanter Prozentsatz der Websites die empfohlenen Grenzwerte weiterhin massiv verfehlt.
|
||||
</Paragraph>
|
||||
|
||||
<WebVitalsScore
|
||||
values={{ lcp: 2.1, inp: 180, cls: 0.05 }}
|
||||
description="Exzellente Werte signalisieren Google eine hohe Nutzerzufriedenheit und fördern das Ranking."
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Dabei fokussiert sich das Framework auf drei wesentliche Säulen der User Experience, für die Google im PageSpeed Insights Tool spezifische Optimierungsvorschläge liefert:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Largest Contentful Paint (LCP):</strong> Ladegeschwindigkeit des Hauptinhalts (Ziel: unter 2,5s).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Interaction to Next Paint (INP):</strong> Die neue Metrik für Interaktivität (Ziel: unter 200ms).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Cumulative Layout Shift (CLS):</strong> Verhindert visuelle Instabilität (Ziel: unter 0,1).
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2>Warum klassische CMS-Lösungen scheitern</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die Ursache für mangelhafte Performance liegt oft in „All-in-One“-Lösungen. [Die versteckten Kosten von WordPress-Plugins](/blog/hidden-costs-of-wordpress-plugins) offenbaren sich spätestens beim ersten Audit. Jedes zusätzliche Plugin erhöht potenziell die Latenz. Nutzer verlassen laut Nielsen Norman Group Webseiten, die langsam oder unresponsiv reagieren, extrem schnell. Jede Sekunde Verzögerung kostet bares Geld.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme
|
||||
template="fine"
|
||||
captions="Die Seite braucht 10 Sekunden zum Laden|Alles gut, die Plugins sind wichtig"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Jedes Byte muss durch das Nadelöhr der mobilen Verbindung. Wer hier auf [Standard-Templates setzt](/blog/why-no-templates-matter), sabotiert seinen Erfolg. Oft ersticken [Baukasten-Systeme Ihre Unabhängigkeit](/blog/builder-systems-threaten-independence) durch unnötigen Code-Overhead. Walmart konnte zeigen, dass jede Sekunde Ladezeit-Optimierung die Conversions um 2 % steigert.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<Mermaid id="legacy-loading-bottleneck" title="Standard-System Ladezeit-Flaschenhals" showShare={true}>
|
||||
graph TD
|
||||
A["Anfrage Browser"] --> B["Server Rechenlast"]
|
||||
B --> C["DB Abfragen"]
|
||||
C --> D["HTML Generierung"]
|
||||
D --> E["Browser Rendering"]
|
||||
E -- "Hohe Latenz" --> F["Nutzer verlässt Seite"]
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Meine Architektur der Geschwindigkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich verfolge einen [Build-First Ansatz](/blog/build-first-digital-architecture). Statt die Seite erst mühsam zusammenzubauen, wenn der Kunde sie anfragt, liefere ich fertig optimierte statische Ressourcen aus einem globalen Edge-Netzwerk. Dies minimiert die Time to First Byte (TTFB) radikal, oft auf unter 50ms.
|
||||
</Paragraph>
|
||||
|
||||
<LoadTimeSimulator />
|
||||
|
||||
<Paragraph>
|
||||
Das Resultat ist Skalierbarkeit durch Design. Die Antwortzeit bleibt konstant niedrig, egal ob 10 oder 10.000 Nutzer gleichzeitig zugreifen. Dies ist ein entscheidender Wettbewerbsvorteil, da viele B2B-Wettbewerber noch immer erhebliche Defizite aufweisen. Eine [Wartungsfreie Architektur](/blog/maintenance-for-headless-systems) sorgt zudem dafür, dass diese Performance über Jahre stabil bleibt, ohne dass Plugins das System schleichend verlangsamen.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Die drei Säulen der Umsetzung</H3>
|
||||
|
||||
<Carousel items={[
|
||||
{
|
||||
title: "Static Site Generation",
|
||||
content: "Inhalte werden während des Builds generiert. Der Server liefert fertige Dateien in Millisekunden aus, ohne Datenbank-Umwege bei jedem Request."
|
||||
},
|
||||
{
|
||||
title: "Edge Delivery",
|
||||
content: "Content ist physisch nah am Nutzer durch Verteilung in globalen Rechenzentren (CDN), was die Latenz gegen Null drückt."
|
||||
},
|
||||
{
|
||||
title: "Asset Engineering",
|
||||
content: "Moderne Formate wie AVIF und Tree-Shaking reduzieren die Payload massiv, was besonders mobile Nutzer in langsamen Netzen schützt."
|
||||
}
|
||||
]} />
|
||||
|
||||
<H2>Der Haken an der Sache: Devil's Advocate</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ehrlichkeit gehört zu einer profunden Architektur-Beratung. Eine High-End-Performance-Lösung ist kein "Plug-and-Play" und erfordert Investitionen in Expertise statt in billige Massenware.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Entscheidung"
|
||||
negativeLabel="Legacy Monolith"
|
||||
negativeText="Einfache Bedienung per Drag-and-Drop, aber schlechte Core Web Vitals und technischer Ballast."
|
||||
positiveLabel="Performance Stack"
|
||||
positiveText="Maximaler Speed und Kontrolle, benötigt jedoch professionelles Engineering und modernstes Deployment."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Technisches Setup:</strong> Der Build-Prozess erfordert modernes DevOps-Know-how statt simpler Klick-Interfaces.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Digital Asset:</strong> Im Gegensatz zur "Miete" eines Baukastens erschaffen Sie wertbringendes Eigentum (Intellectual Property).
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<DigitalAssetVisualizer />
|
||||
|
||||
<LeadMagnet
|
||||
title="Performance-Audit anfragen"
|
||||
description="Wir analysieren Ihre Core Web Vitals im Detail und decken verborgene Umsatzpotenziale Ihrer aktuellen B2B-Website auf."
|
||||
buttonText="Jetzt Analyse starten"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Der wirtschaftliche Case</H2>
|
||||
|
||||
<Paragraph>
|
||||
B2B-Unternehmen verlieren laut Google massiv Conversions pro zusätzlicher Sekunde Ladezeit. Wenn Sie Budget in Marketing investieren, aber Leads durch technische Altlasten verlieren, verbrennen Sie Kapital. [Clean Code](/blog/clean-code-for-business-value) ist hier kein Selbstzweck, sondern eine ökonomische Notwendigkeit für den Return on Ad Spend (ROAS).
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="7-12%|Umsatz-Plus|Shopzilla Performance Fix~24%|Mehr Leads|bei bestandenen Web Vitals~8.4%|Conversion Plus|pro 0.1s Verbesserung" />
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<Paragraph>
|
||||
Mein System fungiert als ROI-Beschleuniger für Ihren gesamten digitalen Auftritt. Warum viele Agenturen bei diesem Thema scheitern und stattdessen langsame Monolithen verkaufen, erkläre ich im Detail in meinem Artikel [Warum Ihre Agentur für kleine Änderungen Wochen braucht](/blog/why-agencies-are-slow).
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="eesxdlG-N6U" title="Core Web Vitals einfach erklärt" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Respekt vor der Zeit Ihrer Nutzer</H2>
|
||||
|
||||
<Paragraph>
|
||||
Geschwindigkeit ist letztlich Ausdruck von Wertschätzung. Sie signalisieren Ihrem Kunden: „Ich respektiere deine Zeit.“ Ein technisch überlegenes System ist im B2B-Sektor heute kein Bonus mehr, sondern die Eintrittskarte in den Markt. Jede Millisekunde, die Sie einsparen, ist eine direkte Investition in Ihre Conversion-Rate und Ihre Sichtbarkeit in der Suche.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Lassen Sie uns Ihre Website in eine hochpräzise Wachstums-Maschine verwandeln, die nicht nur hochwertig aussieht, sondern auf Knopfdruck liefert. Qualität zahlt sich aus – messbar in Sekunden und in Euro.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="B2B-Architektur-Beratung"
|
||||
description="Entwickeln Sie eine Website, die technisch dominiert. Von Edge-Computing bis hin zu perfekter PageSpeed-Optimierung."
|
||||
buttonText="Beratungstermin vereinbaren"
|
||||
href="/contact"
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum ist mein PageSpeed-Score mobil oft deutlich schlechter als auf dem Desktop?</H3>
|
||||
<Paragraph>
|
||||
Mobile Geräte haben begrenzte Rechenleistung und oft instabile Funkverbindungen. Eine optimierte Architektur reduziert JavaScript-Last (INP) radikal, um diese Beschränkungen auszugleichen.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Reicht ein Caching-Plugin für WordPress nicht aus?</H3>
|
||||
<Paragraph>
|
||||
Caching kurapiert nur Symptome, löst aber nicht das Problem von zu viel Code-Ballast. Für echte Spitzenwerte ist eine schlanke, vom CMS entkoppelte Architektur notwendig.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie schnell muss eine B2B-Seite wirklich laden?</H3>
|
||||
<Paragraph>
|
||||
Der Goldstandard liegt unter 2 Sekunden. Ab 3 Sekunden steigt die Bounce-Rate laut Google massiv um 32 %, was direkte Auswirkungen auf Ihren Umsatz hat.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
183
apps/web/content/blog/green-it-sustainable-web.mdx
Normal file
183
apps/web/content/blog/green-it-sustainable-web.mdx
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: "Green IT & Digitale Effizienz: Warum nachhaltiger Code profitabler ist"
|
||||
thumbnail: "/blog/green-it-sustainable-web.png"
|
||||
description: "Nachhaltigkeit als Rendite-Hebel im B2B: Erfahren Sie, wie schlanke Software-Architekturen CO2 senken, Serverlast reduzieren und Ihre Conversion-Rate maximieren."
|
||||
date: "2026-02-03"
|
||||
tags: ["sustainability", "performance", "digital-architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Wussten Sie, dass das Internet für etwa 3,7 % der globalen Treibhausgasemissionen verantwortlich ist? Das ist mehr als der gesamte weltweite Flugverkehr. In einer digitalen Welt, in der Rechenzentren laut IEA bis zu 3 % des weltweiten Stromverbrauchs beanspruchen, wird Effizienz zur moralischen und ökonomischen Pflicht.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner Rolle als Digital Architect betrachte ich Nachhaltigkeit nicht als bloßes „Greenwashing-Label“, sondern als das finale Stadium technischer Exzellenz. Ein effizientes System ist von Natur aus ein grünes System, das massiv Betriebskosten einspart.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
<Marker>Schlanke Architekturen sparen nicht nur CO2, sondern massives Budget.</Marker> Ich zeige Ihnen, warum ökologische Verantwortung und ökonomische Profitabilität im B2B-Sektor durch technologische Disziplin untrennbar miteinander verwoben sind.
|
||||
</LeadParagraph>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
**TL;DR:** Nachhaltige IT-Architektur reduziert durch Minimierung von Datenübertragungen und CPU-Zyklen sowohl den ökologischen Fußabdruck als auch die Betriebskosten. Studien belegen, dass schnellere Ladezeiten direkt mit höheren Conversions korrelieren. Wer heute in effizienten Code investiert, baut ein wertstabiles digitales Asset auf.
|
||||
|
||||
<H2>Der unsichtbare ökologische Fußabdruck von schlechtem Code</H2>
|
||||
|
||||
<Paragraph>
|
||||
Jedes unnötige Kilobyte, das durch das Netz geschickt wird, frisst elektrische Energie – im Rechenzentrum, in den Übertragungsstrecken und schließlich am Endgerät des Nutzers. Laut Google Developers können optimierte Bilder die Dateigröße um <Marker>20–80 % reduzieren</Marker>, ohne nennenswerte Qualitätsverluste zu verursachen, was die CO2-Emissionen beim Datentransfer unmittelbar senkt.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Viele moderne Websites leiden an „digitaler Adipositas“. Sie schleppen Megabytes an Altlasten mit sich herum, was zu heißgelaufenen Prozessoren führt. [Langsame Ladezeiten](/blog/slow-loading-costs-customers) sind hierbei oft nur das Symptom einer tieferliegenden Ineffizienz, die laut Akamai dazu führt, dass 47 % der Konsumenten erwarten, dass eine Seite in weniger als zwei Sekunden lädt.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="fine" captions="Dank 47 WordPress Plugins glüht der Server|Das ist okay, wir haben doch ein Umweltzertifikat" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Google-Untersuchungen zeigen: Wenn die Ladezeit von einer auf drei Sekunden steigt, springen <Marker>32 % mehr Nutzer ab</Marker>. Wir senken durch optimierten Code die CPU-Zyklen und damit direkt den Energiebedarf. Lean-Architekturen wie Single-Page Applications (SPAs) reduzieren den Datentransfer pro Sitzung sogar um bis zu 60 %.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramFlow
|
||||
id="green-it-flow"
|
||||
title="Der Green-IT Effizienz-Kreislauf"
|
||||
nodes={[
|
||||
{ id: "A", label: "Clean Code Architecture", style: "fill:#059669,color:#fff" },
|
||||
{ id: "B", label: "Weniger CPU-Zyklen" },
|
||||
{ id: "C", label: "Geringerer Energiebedarf" },
|
||||
{ id: "D", label: "Höherer ROI & weniger CO2", style: "fill:#10b981,color:#fff" }
|
||||
]}
|
||||
edges={[
|
||||
{ from: "A", to: "B" },
|
||||
{ from: "B", to: "C" },
|
||||
{ from: "C", to: "D" }
|
||||
]}
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Effizienz als ökonomischer Hebel</H2>
|
||||
|
||||
<Paragraph>
|
||||
Nachhaltigkeit in der IT ist kein Verzicht, sondern ein extremer Performance-Boost für Ihr Business. Eine Studie der Nielsen Norman Group schätzt, dass optimierte Performance die <Marker>Conversion-Raten um 27 % verbessern</Marker> kann. Höhere Effizienz führt zu geringerem Energieverbrauch pro Transaktion.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="8.4%"
|
||||
label="Umsatzsteigerung pro 0.1s Speed-Gewinn im Retail"
|
||||
source="Deloitte Digital"
|
||||
sourceUrl="https://www2.deloitte.com/ie/en/pages/technology-media-and-telecommunications/articles/speed-up-your-business.html"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Wenn Sie auf [Business-Value durch Clean Code](/blog/clean-code-for-business-value) setzen, reduzieren Sie die technologische Schuld. Während Standard-Agenturen oft zu überladenen Baukästen greifen, die Ihre [Unabhängigkeit bedrohen](/blog/builder-systems-threaten-independence), nutzt mein Engineering-Ansatz hochperformante Lösungen, die laut Deloitte die CO2-Emissionen der Website um 29 % senken können.
|
||||
</Paragraph>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<H3>Vergleich: Nachhaltige vs. Verschwenderische Architektur</H3>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Impact auf Ressourcen"
|
||||
negativeLabel="Legacy Monolith"
|
||||
negativeText="Datenbankabfragen bei jedem Klick, 5MB Payload, CPU-intensive Plugins."
|
||||
positiveLabel="Mintel Green-Stack"
|
||||
positiveText="Static Site Generation, < 500KB Payload, Zero-Server-Computation."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<H2>Drei Säulen einer nachhaltigen Infrastruktur</H2>
|
||||
|
||||
<Paragraph>
|
||||
Als Architekt implementiere ich Systeme, die [Digital Longevity](/blog/digital-longevity-architecture) verkörpern. Das bedeutet, Software so zu bauen, dass sie über Jahre ohne Performance-Verlust besteht, anstatt nach jedem Update instabil zu werden.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Data centres are estimated to consume around 1-3% of global electricity use. Efficient coding practices and reduced JavaScript execution can decrease CPU usage on user devices by 15-30%."
|
||||
author="Google Developers & IEA"
|
||||
isCompany={true}
|
||||
source="World Energy Outlook / Web Dev Docs"
|
||||
sourceUrl="https://www.iea.org/reports/data-centres-and-data-transmission-networks"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Static-First Strategie:</strong> Statische Seiten benötigen keine serverseitige Berechnung bei jedem Aufruf, was Energie spart.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>CDN-Nutzung:</strong> Reduziert die Distanz der Datenpakete und senkt die Latenz sowie den Energieverbrauch um ca. 10–20 %.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Green Hosting:</strong> Betrieb auf Infrastruktur, die nachweislich mit 100 % erneuerbaren Energien betrieben wird.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="JT-oZS2F11E" title="Architecting for Sustainability" />
|
||||
</div>
|
||||
|
||||
<H2>Der Haken an der Sache (Devil's Advocate)</H2>
|
||||
|
||||
<Paragraph>
|
||||
Warum baut dann nicht jeder so? Die Wahrheit ist schlicht: <Marker>Echte Effizienz erfordert initial mehr Gehirnschmalz.</Marker> Ein Standard-System „zusammenzuklicken“ geht schneller, produziert aber langfristig technische Altlasten.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Höhere Design-Komplexität:</strong> Jedes Feature muss auf seine energetische und performante Notwendigkeit geprüft werden.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Vermeintlicher Mehraufwand:</strong> Die Entwicklung von Individualsoftware ist in der Konzeptionsphase intensiver als fertige Templates.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<Paragraph>
|
||||
Wer jedoch Software als strategisches [digitales Asset](/blog/digital-longevity-architecture) betrachtet, erkennt, dass die niedrigeren Betriebskosten und die überlegene Conversion-Rate diesen Initialaufwand bei weitem übertreffen.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Nachhaltigkeits-Audit anfragen"
|
||||
description="Wir analysieren Ihren ökologischen Fußabdruck und Ihre Performance-Metriken. Senken Sie Kosten und CO2 durch technische Exzellenz."
|
||||
buttonText="Jetzt Effizienz-Check anfragen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Fokus auf Core Web Vitals</H2>
|
||||
|
||||
<Paragraph>
|
||||
Google hat klargestellt: Ein schlechter Lade-Score kostet Sichtbarkeit. Da 53 % der mobilen Nutzer eine Seite verlassen, wenn sie länger als drei Sekunden lädt, ist Performance-Optimierung die ehrlichste Form des Marketings.
|
||||
</Paragraph>
|
||||
|
||||
<WebVitalsScore values={{ lcp: 1.2, inp: 45, cls: 0.01 }} description="Ein typischer Mintel-Stack Score: Nachhaltig, schnell und SEO-optimiert." />
|
||||
|
||||
<H2>Fazit: Weniger ist mehr Zukunft</H2>
|
||||
|
||||
<Paragraph>
|
||||
Green IT ist keine Wohltätigkeitsveranstaltung, sondern technologische Disziplin. <Marker>Jedes gesparte Byte ist ein Gewinn für Ihre Bilanz und das Klima.</Marker> Indem wir unnötigen Ballast abwerfen, schaffen wir digitale Erlebnisse, die schneller, sicherer und nachhaltiger sind.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
In einer Welt, in der Energieeffizienz zur überlebenswichtigen Währung wird, ist schlanker Code Ihr wertvollstes Asset. Durch die konsequente Vermeidung von [versteckten Kosten durch Plugins](/blog/hidden-costs-of-wordpress-plugins) sichern Sie sich einen unfairen Wettbewerbsvorteil.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Ist nachhaltige Software wirklich spürbar schneller?</H3>
|
||||
<Paragraph>
|
||||
Ja. Da Nachhaltigkeit auf der Reduktion von Daten und Rechenoperationen basiert, resultiert dies direkt in minimalen Ladezeiten (TTFB) und einer flüssigeren User Experience.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Amortisieren sich die Kosten für effiziente Programmierung?</H3>
|
||||
<Paragraph>
|
||||
Definitiv. Durch geringere Absprungraten (Bounce Rate) und deutlich reduzierte Hosting-Kosten amortisiert sich der Fokus auf Clean Code meist innerhalb der ersten 12 Monate.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie wirkt sich Green IT auf mein Google Ranking aus?</H3>
|
||||
<Paragraph>
|
||||
Direkt. Google nutzt die Core Web Vitals als Ranking-Faktor. Effiziente Seiten laden schneller und werden somit in den Suchergebnissen bevorzugt behandelt.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
151
apps/web/content/blog/hidden-costs-of-wordpress-plugins.mdx
Normal file
151
apps/web/content/blog/hidden-costs-of-wordpress-plugins.mdx
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Die versteckten Kosten von WordPress-Plugins: Performance-Killer & Sicherheitsrisiko"
|
||||
thumbnail: "/blog/hidden-costs-of-wordpress-plugins.png"
|
||||
description: "Warum Plugin-Bloat Ihre B2B-Website verlangsamt und gefährdet. Erfahren Sie, wie eine saubere Architektur die Wartungskosten senkt und den ROI steigert."
|
||||
date: "2026-02-12"
|
||||
tags: ["wordpress", "performance", "security", "digital-architecture"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
In der Welt von WordPress werden Plugins oft als die ultimative Abkürzung zum Erfolg verkauft. Ein Klick, ein Feature – so einfach, oder?
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner täglichen Praxis als Digital Architect sehe ich jedoch das exakte Gegenteil: Sie sind eine teure Umleitung in eine technische Sackgasse, die sowohl die Performance als auch die Sicherheit Ihrer Plattform untergräbt.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Die vermeintlich "schnelle Lösung" ist am Ende <Marker>oft die teuerste Entscheidung Ihrer digitalen Strategie</Marker>, da sie technische Schulden anhäuft, die später mit Zinseszins zurückgezahlt werden müssen.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>TL;DR: Warum weniger meist mehr ist</H2>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Sicherheit:</strong> Ca. 98% der WordPress-Vulnerabilitäten hängen direkt mit Plugins zusammen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Performance:</strong> Jedes Plugin erhöht HTTP-Requests und JavaScript-Execution-Time (Google Developers).
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Wartungskosten:</strong> "Kostenlose" Plugins verursachen hohe laufende Kosten durch Update-Risiken und Inkompatibilitäten.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-8">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
|
||||
<H2>Die „Frankenstein-Architektur“ der Plugins</H2>
|
||||
<Paragraph>
|
||||
Die Versuchung ist menschlich: Ein neues Feature wird benötigt, und der Plugin-Store verspricht die sofortige Lösung. Doch was Sie wirklich tun, ist fremden, oft ungeprüften Code ungefiltert in Ihr geschäftskritisches System zu lassen. Ich sehe regelmäßig Instanzen, die unter der Last von 40+ Plugins förmlich zermalmt werden.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Jedes Plugin verfolgt eine eigene Logik und kämpft mit anderen Komponenten um knappe Ressourcen. Es entsteht eine instabile <Marker>„Frankenstein-Architektur“</Marker>, bei der niemand mehr genau sagen kann, welches Skript an welcher Stelle die Ladezeit in die Höhe treibt. Daten von [HTTP Archive](https://httparchive.org) zeigen konsistent eine Korrelation zwischen der Anzahl der JS-Dateien (oft durch Plugins verursacht) und signifikant längeren Ladezeiten.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="grumpycat" captions="ICH HABE 50 PLUGINS INSTALLIERT|WARUM IST MEINE SEITE LANGSAM?" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Sie geben die Kontrolle über Ihre Plattform ab und hängen von der Roadmap Dritter ab. Dies führt oft zu einem [Vendor Lock-In](/blog/builder-systems-threaten-independence), der Ihre technologische Unabhängigkeit bedroht. Wenn ein Entwickler das Interesse verliert oder das Plugin nicht rechtzeitig an PHP-Updates anpasst, steht Ihr gesamtes Business-Fundament auf wackeligen Beinen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="plugin-dependency-trap" title="Die Plugin-Abhängigkeitsfalle" showShare={true}>
|
||||
graph TD
|
||||
P1["Plugin A (Slider)"] --> Core["WP Core"]
|
||||
P2["Plugin B (SEO)"] --> Core
|
||||
P3["Plugin C (Forms)"] --> Core
|
||||
Core --> Bloat["Asset-Overload"]
|
||||
Bloat --> Slow["Ladezeit > 4s"]
|
||||
P1 -.-> P2["Konflikt"]
|
||||
P2 -.-> P3["Konflikt"]
|
||||
Slow --> Bounce["Conversion Drop"]
|
||||
style Slow fill:#fca5a5,stroke:#333
|
||||
style Bounce fill:#ef4444,color:#fff
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Das Sicherheitsrisiko: 98% aller Lücken</H2>
|
||||
<Paragraph>
|
||||
Sicherheit ist kein Plugin, das man installiert, sondern ein Prozess. Dennoch verlassen sich viele Unternehmen auf Security-Plugins, während sie gleichzeitig dutzende Einfallstore durch veraltete Erweiterungen offenlassen.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Plugin vulnerabilities were the initial attack vector in approximately 55.9% of WordPress hacks."
|
||||
author="Wordfence"
|
||||
isCompany={true}
|
||||
source="Wordfence Intelligence Report"
|
||||
sourceUrl="https://www.wordfence.com"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Die Zahlen sind alarmierend: Sicherheitsanalysen von WPScan schätzen, dass rund 98% der WordPress-Vulnerabilitäten im Zusammenhang mit Plugins stehen. Ein einziger kritischer Fehler in einem weit verbreiteten Tool kann ausreichen, um zehntausende Corporate-Websites gleichzeitig zu gefährden. Oft sind es [langsame Ladezeiten und technische Altlasten](/blog/slow-loading-costs-customers), die erst durch solche Sicherheits-Audits ans Licht kommen.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="98%|aller Sicherheitslücken|stammen aus Plugins~55.9%|der Hacks|nutzen Plugins als Vektor~90%|aller CMS-Hacks|betreffen WordPress" />
|
||||
|
||||
<H2>Performance-Erosion: Der schleichende Umsatzkiller</H2>
|
||||
<Paragraph>
|
||||
Schlecht programmierte Plugins erhöhen die Anzahl der Datenbankabfragen massiv. Jede zusätzliche Abfrage belastet die Time to First Byte (TTFB). Google Search Central betont immer wieder, dass schlanker Code und weniger externe Requests direkt mit besseren Rankings korrelieren.
|
||||
</Paragraph>
|
||||
|
||||
<LoadTimeSimulator />
|
||||
|
||||
<Paragraph>
|
||||
Wenn Ihre Seite langsam lädt, verlieren Sie Kunden, bevor diese Ihr Angebot überhaupt sehen. In einer Welt, in der eine Sekunde Verzögerung die Conversion-Rate um bis zu 7% senken kann (Nielsen Norman Group), ist Plugin-Bloat ein ökonomisches Risiko. Hier hilft oft nur ein radikaler [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist), um die Hebel wieder umzulegen.
|
||||
</Paragraph>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<H2>Der "Haken" an der Sache (Devil's Advocate)</H2>
|
||||
<Paragraph>
|
||||
Natürlich hat der Verzicht auf Plugins auch seinen Preis. Es ist wichtig, hier transparent zu sein:
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Entwicklungs-Ansätze im Vergleich"
|
||||
negativeLabel="Plugin-First (Billig)"
|
||||
negativeText="Schneller Start, geringe Initialkosten, aber massive technische Schulden und Sicherheitsrisiken."
|
||||
positiveLabel="Architect-First (Premium)"
|
||||
positiveText="Höhere Initialinvestition, maßgeschneiderter Code, dafür wartungsfrei, sicher und extrem schnell."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Maßgeschneiderter Code erfordert am Anfang mehr Zeit für Konzeption und Entwicklung. Wenn Sie nur schnell einen Blog für ein Wochenendprojekt brauchen, ist WordPress mit Plugins unschlagbar. Wenn Sie jedoch eine <Marker>skalierbare B2B-Plattform</Marker> aufbauen, ist der "billige" Weg am Ende durch Wartung und Ausfälle meist teurer.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Ist Ihre Architektur zukunftssicher?"
|
||||
description="Wir analysieren Ihren Tech-Stack auf Plugin-Bloat und Sicherheitslücken. Erhalten Sie eine Roadmap für eine performante, wartungsarme Plattform."
|
||||
buttonText="Jetzt Performance-Audit anfragen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Mein Weg: Präziser Code statt Blackbox-Plugins</H2>
|
||||
<Paragraph>
|
||||
Anstatt ein tonnenschweres Plugin für eine einfache Funktion zu installieren, entwickle ich diese Funktion direkt innerhalb einer modernen Architektur – oft [Headless oder mit statischer Generierung](/blog/maintenance-for-headless-systems).
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis ist ein System, das exakt das tut, was Sie brauchen – und kein Byte mehr. Kein Ballast, kein Sicherheitsrisiko, keine Abhängigkeit. Ich baue keine digitalen Kartenhäuser, sondern echte digitale Assets, die für Profis arbeiten. Dies steigert nicht nur die Performance, sondern zahlt direkt auf den [ROI von Clean Code](/blog/clean-code-for-business-value) ein.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Investieren Sie in Ihr Fundament</H2>
|
||||
<Paragraph>
|
||||
Plugins sind wie billige Anbauwände: Sie wirken im Katalog gut, aber nach dem ersten Umzug wackeln sie. Für ein seriöses B2B-Unternehmen sollte die Website kein Bastelprojekt sein, sondern ein hochpräzises Werkzeug.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns gemeinsam eine Plattform schaffen, die Ihr Business auch in fünf Jahren noch zuverlässig trägt. Qualität ist keine Kostenstelle, sondern die einzige Abkürzung, die wirklich funktioniert.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum machen viele Agenturen dennoch alles mit Plugins?</H3>
|
||||
<Paragraph>Es ist für Agenturen profitabler und erfordert weniger tiefgreifendes technisches Know-how. Zudem generieren die ständigen Updates und daraus resultierenden Fehler regelmäßige Wartungsumsätze.</Paragraph>
|
||||
|
||||
<H3>Gibt es "gute" Plugins?</H3>
|
||||
<Paragraph>Ja, es gibt etablierte Industry-Standards (z. B. für SEO oder Formulare), die jedoch extrem gezielt und sparsam eingesetzt werden müssen. Eine professionelle Architektur minimiert diese auf ein absolutes Minimum.</Paragraph>
|
||||
|
||||
<H3>Wie erkenne ich, ob meine Seite zu viele Plugins hat?</H3>
|
||||
<Paragraph>Ein Blick in die Core Web Vitals und die Anzahl der geladenen JS/CSS-Dateien gibt ersten Aufschluss. Wenn einfache Inhaltsänderungen zu Systemfehlern führen, ist dies ein deutliches Warnsignal für Plugin-Konflikte.</Paragraph>
|
||||
</FAQSection>
|
||||
186
apps/web/content/blog/maintenance-for-headless-systems.mdx
Normal file
186
apps/web/content/blog/maintenance-for-headless-systems.mdx
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: "Wartungsfrei durch Verzicht: Warum '
|
||||
thumbnail: "/blog/maintenance-for-headless-systems.png"No-CMS' die überlegene B2B-Architektur ist"
|
||||
description: "Sicherheit und Geschwindigkeit durch architektonische Reduktion: Warum Git-basierte Workflows klassische CMS-Backends in Performance und ROI schlagen."
|
||||
date: "2026-02-01"
|
||||
tags: ["maintenance", "architecture"]
|
||||
---
|
||||
|
||||
<TLDR>
|
||||
Klassische CMS-Systeme sind oft technische Schulden im Gewand einer Nutzeroberfläche. Durch den Wechsel zu Git-basierten "No-CMS" Architekturen eliminieren Unternehmen Sicherheitsrisiken, senken Hostingkosten um bis zu 90% und steigern die Performance massiv.
|
||||
</TLDR>
|
||||
|
||||
<LeadParagraph>
|
||||
Ein Content Management System (CMS) wird oft als Befreiung verkauft. In der Realität ist es für viele Unternehmen der Anfang einer teuren, wartungsintensiven Abhängigkeit.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Praxis beobachte ich regelmäßig, wie B2B-Akteure hunderte Stunden in die Pflege von Systemen investieren, die sie eigentlich entlasten sollten, während die [technische Qualität](/blog/clean-code-for-business-value) auf der Strecke bleibt.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Content-Management ohne Ballast</Marker> der wahre Hebel für Geschwindigkeit, Sicherheit und digitalen Fokus ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
<H2>Der CMS-Wartungs-Albtraum: Ein systemisches Risiko</H2>
|
||||
|
||||
<Paragraph>
|
||||
Klassische CMS-Lösungen wie WordPress oder TYPO3 sind komplexe "Software-Monster". Sie basieren auf Datenbanken und Server-Side-Scripting, was sie zu einem primären Ziel für Angriffe macht. Statistiken zeigen, dass WordPress-Plugins für einen signifikanten Prozentsatz aller Website-Schwachstellen verantwortlich sind <ExternalLink href="https://www.wordfence.com/">[Source: Wordfence]</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<MetricBar label="Anfälligkeit veralteter CMS-Versionen" value={90} color="red" />
|
||||
<MetricBar label="Sicherheits-Stabilität Git-basierter Architektur" value={99} color="green" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Diese Systeme müssen permanent aktualisiert werden. Werden Patches vernachlässigt, steigt das Risiko einer Exploitation exponentiell an <ExternalLink href="https://sucuri.net/">[Source: Sucuri]</ExternalLink>. Diese [versteckten Kosten von Plugins](/blog/hidden-costs-of-wordpress-plugins) fressen Budget, ohne einen einzigen Cent echten Mehrwert für Ihr Business zu generieren. Ich nenne das <Marker>technische Sisyphusarbeit</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="astronaut" captions="Warte, das CMS ist nur eine Sicherheitslücke?|War es schon immer." />
|
||||
</div>
|
||||
|
||||
<H2>Performance als ökonomischer Hebel</H2>
|
||||
|
||||
<Paragraph>
|
||||
Jede Sekunde Ladezeit entscheidet im B2B-Sektor über Abbruch oder Anfrage. Während traditionelle CMS bei jedem Aufruf die Datenbank abfragen müssen, liefern Git-basierte Architekturen (Jamstack) fertige statische Dateien über CDNs aus. Das Ergebnis ist eine bis zu 10-fach schnellere Ladezeit im Vergleich zu WordPress <ExternalLink href="https://gtmetrix.com/">[Source: GTmetrix]</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="40%"
|
||||
label="höhere Conversion-Rate durch Jamstack-Umstellung"
|
||||
source="Google Developers"
|
||||
sourceUrl="https://developers.google.com/"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Diese Geschwindigkeit ist kein Selbstzweck. Wie ich in meinem [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist) detailliert beschreibe, korreliert die Performance direkt mit Ihren Google-Rankings und der Nutzerzufriedenheit.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<LoadTimeSimulator />
|
||||
</div>
|
||||
|
||||
<H3>Content as Code: Die Architektur der digitalen Marktführer</H3>
|
||||
|
||||
<Paragraph>
|
||||
Anstatt sich mit unübersichtlichen Admin-Backends und [langsamen Agentur-Zyklen](/blog/why-agencies-are-slow) herumzuschlagen, integrieren wir Inhalte direkt in einen versionierten Workflow.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<DiagramSequence id="workflow-comparison" title="Modern Content Flow" showShare={true}>
|
||||
sequenceDiagram
|
||||
Editor->>Git: Textänderung (Markdown)
|
||||
Git->>Build: Automatisierter Check
|
||||
Build->>Edge: Globales CDN Deployment
|
||||
Edge->>User: < 50ms Response
|
||||
</DiagramSequence>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Keine Datenbanken, die korrumpieren können. Kein Backend, das per Brute-Force gehackt werden kann. Analysen zeigen, dass statische Webseiten die Zeit für Sicherheits-Patching um schätzungsweise 60 % reduzieren <ExternalLink href="https://owasp.org/">[Source: OWASP]</ExternalLink>. Das ist <Marker>Sicherheit durch Simplizität</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Warum "Kein CMS" die beste CMS-Strategie ist</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ein Git-basiertes Setup bietet handfeste wirtschaftliche Vorteile gegenüber der "Software-Miete" klassischer Systeme. Es verwandelt Ihre Website von einer technischen Verbindlichkeit in ein [echtes digitales Asset](/blog/digital-longevity-architecture).
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>90 % Kostenersparnis beim Hosting:</strong> Durch CDN-basierte Auslieferung ohne teure Server-Infrastruktur <ExternalLink href="https://www.netlify.com/">[Source: Netlify]</ExternalLink>.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>50 % weniger Deployment-Fehler:</strong> Git-basierte Workflows minimieren menschliche Fehler im Vergleich zu Live-Edits im CMS-Backend <ExternalLink href="https://circleci.com/">[Source: CircleCI]</ExternalLink>.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Zukunftssicherheit:</strong> Inhalte liegen in portablen Formaten (Markdown/JSON) vor, statt in proprietären Datenbanken gefangen zu sein ([Vendor Lock-in vermeiden](/blog/builder-systems-threaten-independence)).
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Architektur-Vergleich: Wartung vs. Wertschöpfung"
|
||||
negativeLabel="Legacy CMS"
|
||||
negativeText="Datenbank-Abfragen, Plugin-Wartung, Sicherheitsrisiken, Server-Kosten."
|
||||
positiveLabel="Mintel Architecture"
|
||||
positiveText="Static Delivery, Zero-Maintenance, maximale Sicherheit, Edge-Performance."
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Der strategische Case: Fokus auf Ihr Kernbusiness</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die wertvollste Ressource in Ihrem Unternehmen ist Aufmerksamkeit. Jede Stunde, die Ihr Marketing-Team mit "Plugin-Updates" oder dem Debugging von [zerschossenen Layouts nach Updates](/blog/why-websites-break-after-updates) verbringt, fehlt bei der strategischen Marktbearbeitung.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Git-based site generators, by reducing reliance on server-side scripting and databases, can mitigate certain classes of vulnerabilities common in traditional CMS deployments."
|
||||
author="Netlify"
|
||||
isCompany={true}
|
||||
source="Security Reports"
|
||||
sourceUrl="https://www.netlify.com/security/"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Unternehmen, die auf eine moderne Architektur setzen, reduzieren ihre Entwicklungszeit um ca. 30-40 %, da komplexe Infrastruktur-Management-Prozesse entfallen <ExternalLink href="https://www.forrester.com/">[Source: Forrester]</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der "Haken" an der Sache (Ehrliche Analyse)</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich bin kein Freund von einseitigem Marketing. Ein No-CMS-Ansatz ist nicht für jeden die richtige Lösung. Es erfordert ein gewisses Verständnis für strukturierte Workflows.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
Kein "Drag-and-Drop": Wer täglich Layouts komplett umwerfen will, ohne die Markenrichtlinien zu beachten, wird sich eingeschränkt fühlen.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
Initialer Setup-Aufwand: Die Architektur muss einmalig sauber von einem Experten aufgebaut werden.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
</div>
|
||||
|
||||
<H2>Wann ist dieser minimalistische Weg für Sie richtig?</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich arbeite für Entscheider, deren Kerngeschäft nicht das Betreiben einer IT-Infrastruktur ist. Wenn Sie eine Website wollen, die einfach <Marker>immer online, immer schnell und absolut sicher</Marker> ist, dann ist die technische Reduktion Ihr stärkster Hebel.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Architektur-Audit anfragen"
|
||||
description="Wir analysieren Ihre aktuelle CMS-Struktur und zeigen Ihnen das Einsparpotenzial durch eine moderne Git-basierte Architektur auf."
|
||||
buttonText="Jetzt Potenzial prüfen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Fazit: Simplizität ist das neue High-End</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die besten Systeme sind die, die man im Alltag nicht spürt, weil sie keine Probleme verursachen. In einer Welt voller [langsamer Ladezeiten](/blog/slow-loading-costs-customers) und Sicherheitslücken ist eine radikal reduzierte Architektur der ultimative Wettbewerbsvorteil.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Lassen Sie uns den Ballast abwerfen. <Marker>Maximale Wirkung bei minimalem technischem Overhead.</Marker> Ihr Erfolg verdient dieses effiziente Fundament.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Ist ein 'No-CMS' Ansatz auch für Nicht-Techniker geeignet?</H3>
|
||||
<Paragraph>
|
||||
Ja, absolut. Über moderne Editoren wie Front Matter oder spezialisierte Git-CMS-Interfaces können auch Marketing-Teams Inhalte bearbeiten, ohne jemals Code sehen zu müssen – bei voller Sicherheit.
|
||||
</Paragraph>
|
||||
<H3>Wie hoch ist die Ersparnis bei den Wartungskosten?</H3>
|
||||
<Paragraph>
|
||||
In der Regel sinken die direkten Wartungskosten um bis zu 60 %, da manuelle Sicherheits-Updates für Plugins, Themes und Core-Systeme komplett entfallen.
|
||||
</Paragraph>
|
||||
<H3>Was passiert bei einem Hacker-Angriff?</H3>
|
||||
<Paragraph>
|
||||
Da es keine Datenbank und kein PHP-Backend gibt, das live auf dem Server ausgeführt wird, existiert faktisch keine Angriffsfläche für klassische Web-Exploits wie SQL-Injections.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
166
apps/web/content/blog/no-us-cloud-platforms.mdx
Normal file
166
apps/web/content/blog/no-us-cloud-platforms.mdx
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: "US-Cloud-Exit & Souveränität: Warum lokale Infrastruktur der wahre B2B-Standard ist"
|
||||
thumbnail: "/blog/no-us-cloud-platforms.png"
|
||||
description: "Erfahren Sie, wie der US CLOUD Act Ihre Daten bedroht und warum europäische Infrastruktur nicht nur sicherer, sondern durch Latenzvorteile auch profitabler ist."
|
||||
date: "2026-02-07"
|
||||
tags: ["cloud", "privacy", "infrastructure", "compliance"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
"Die Daten liegen sicher in der Cloud." – Dieser Satz ist heute oft eine gefährliche Halbwahrheit, die das rechtliche Fundament Ihres Unternehmens untergraben kann.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect begegne ich unzähligen Unternehmen, die die Kontrolle über ihre wichtigsten Assets schleichend verloren haben. Sie sind gefangen in einer Architektur der Abhängigkeit.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>lokale Datenhoheit</Marker> kein nostalgischer Rückzug, sondern der entscheidende Hebel für Sicherheit, Compliance und Performance im Jahr 2025 ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="my-8">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
|
||||
<H2>Das Märchen von der sorglosen US-Cloud</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die großen Hyper-Scaler bieten Bequemlichkeit und eine schier endlose Skalierbarkeit. Doch diese Bequemlichkeit hat einen Preis, den viele B2B-Entscheider erst zu spät auf der Rechnung haben: Ihre Souveränität.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Durch Gesetze wie den **US CLOUD Act** können US-Behörden Zugriff auf Daten verlangen, die auf Servern von US-Unternehmen liegen – völlig ungeachtet dessen, ob diese physisch in Frankfurt, Paris oder Dublin stehen. In einer Welt, in der [Datenschutz-Konformität](/blog/gdpr-conformity-system-approach) ein ultimativer Wettbewerbsvorteil ist, stellt dies ein massives strategisches Risiko dar.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="ds" captions="US Hyper-Scaler mit 'EU-Region' Hosting|Der US CLOUD Act klopft trotzdem an die Tür" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Studien von Deloitte zeigen, dass über 70% der Organisationen weltweit besorgt über den Datenschutz in der Cloud sind. Besonders für europäische Akteure ist die Lage prekär: Rund 60% der Unternehmen in der EU äußern Bedenken hinsichtlich der Auswirkungen des Cloud Acts auf ihre sensiblen Geschäftsgeheimnisse.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="The CLOUD Act amended the SCA to require that technology companies provide data in their possession, custody, or control in response to an SCA warrant."
|
||||
author="Department of Justice"
|
||||
isCompany={true}
|
||||
source="Cross-Border Data Sharing Report"
|
||||
sourceUrl="https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/main/s3fs-public/atoms/files/CLOUD%20Act%20White%20Paper%20Final.pdf"
|
||||
/>
|
||||
|
||||
<H2>Technologische Latenz: Der versteckte Kostenfaktor</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technologie ist niemals neutral, und Geografie ist kein irrelevantes Detail. Wer auf US-zentrische Infrastrukturen setzt, kämpft oft mit physikalischen Grenzen. Die Transatlantik-Latenz ist ein realer Performance-Killer. Research des Nielsen Norman Group zeigt, dass Nutzer Verzögerungen über 250-400ms als störend wahrnehmen – ein Schwellenwert, der bei US-basierten Ressourcen für europäische Nutzer schnell überschritten wird.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<PremiumComparisonChart
|
||||
title="Latenz-Check: EU vs. US Hosting (in ms)"
|
||||
items={[
|
||||
{ label: "US-East (Virginia)", value: 120, max: 150, color: "red" },
|
||||
{ label: "US-West (Oregon)", value: 180, max: 150, color: "red" },
|
||||
{ label: "EU-Central (Frankfurt)", value: 12, max: 150, color: "green" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Niedrige Latenz ist kein "Nice-to-have". Wie ich in meinem [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist) erläutere, korreliert die Ladegeschwindigkeit direkt mit der Conversion-Rate und dem Google-Ranking. Webseiten, die lokal gehostet werden, vermeiden die physikalischen Umwege über Überseekabel und liefern Daten dort aus, wo sie gebraucht werden.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Die kritische Distanz: Der Haken an der Sache</H2>
|
||||
|
||||
<Paragraph>
|
||||
Als Digital Architect bin ich es Ihnen schuldig, auch die Kehrseite zu beleuchten. Ein kompletter Verzicht auf US-Plattformen erfordert Disziplin und oft ein höheres Initial-Investment in die Architektur.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Entscheidung: Convenience vs. Sovereignty"
|
||||
negativeLabel="Managed SaaS (US)"
|
||||
negativeText="Schnelles Setup, aber Vendor Lock-in und rechtliche Grauzone unter dem Cloud Act."
|
||||
positiveLabel="Custom Built (EU)"
|
||||
positiveText="Volle Kontrolle, DSGVO-Immunität, erfordert jedoch exzellentes Engineering."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Der "Haken" ist die [Vermeidung von Vendor Lock-ins](/blog/builder-systems-threaten-independence). Wer souverän sein will, kann nicht einfach das günstigste Standard-Template von der Stange nehmen. Es erfordert eine [Build-First-Strategie](/blog/build-first-digital-architecture), die digitale Assets als echtes Unternehmenseigentum begreift.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Mein Ansatz: Die "High-Fidelity" Infrastruktur</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich baue Systeme für Unternehmen, die ihre IT nicht als Kostenstelle, sondern als strategische Festung sehen. Souveränität bedeutet bei mir <Marker>Premium-Protection</Marker> durch drei Säulen:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Physische Souveränität:</strong> Nutzung zertifizierter Rechenzentren unter rein europäischer Jurisdiktion. Ihre Daten verlassen niemals diesen Rechtsraum.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Technologische Unabhängigkeit:</strong> Konsequenter Einsatz von Open-Source-Standards statt proprietärer US-APIs. Das schützt vor Preisdiktaten und Abschaltungen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Performance-Optimierung:</strong> Edge-Computing innerhalb der EU für TTFB-Werte (Time To First Byte), die im US-Hosting physikalisch unmöglich sind.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-12">
|
||||
<LoadTimeSimulator />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ein [professionelles Hosting](/blog/professional-hosting-operations) ist das Fundament für alles Weitere. Wer hier spart, zahlt später doppelt – durch Rechtsrisiken oder verlorene Kunden, die aufgrund [langsamer Ladezeiten](/blog/slow-loading-costs-customers) abspringen.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Infrastructure Audit anfragen"
|
||||
description="Wir analysieren Ihre aktuelle Hosting-Struktur auf rechtliche Risiken und Performance-Potenziale."
|
||||
buttonText="Jetzt Termin vereinbaren"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<H2>Souveränität als messbarer ROI</H2>
|
||||
|
||||
<Paragraph>
|
||||
In einer geopolitisch instabilen Welt ist "Data Residency" ein hartes Verkaufsargument im B2B-Sektor. In Projekten für Marktführer sehe ich immer wieder: Kunden vertrauen Unternehmen mehr, die ihre Daten proaktiv schützen (Stichwort: [Websites ohne Cookie-Banner](/blog/website-without-cookie-banners)).
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="25%|Umsatzplus|durch Cloud-Adoption (Accenture)~92%|Multi-Cloud|Anteil im Enterprise-Sektor~23%|Marktwachstum|Public Cloud Services weltweit" />
|
||||
|
||||
<Paragraph>
|
||||
Die Investition in eine unabhängige, europäische Architektur sichert die [Langlebigkeit Ihrer Software](/blog/digital-longevity-architecture). Sie bauen kein Kartenhaus auf fremdem Grund, sondern eine digitale Immobilie auf Ihrem eigenen Fundament.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="9g1Fq9LRG_o" title="Data Privacy vs. Governance in the Cloud" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Ihr Business, Ihre Regeln</H2>
|
||||
|
||||
<Paragraph>
|
||||
Holen Sie sich die Kontrolle über Ihre Daten zurück. Souveränität ist keine bloße Compliance-Anforderung, sondern eine strategische Entscheidung für Qualität und Unabhängigkeit.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ich begleite Sie dabei, eine Architektur zu erschaffen, die so eigenständig ist wie Ihre Vision. Lassen wir Ihre Daten dort, wo sie hingehören: <Marker>In Ihrem direkten Einflussbereich.</Marker>
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Was ist das Hauptproblem am US CLOUD Act für deutsche Firmen?</H3>
|
||||
<Paragraph>
|
||||
Der CLOUD Act verpflichtet US-Provider zur Herausgabe von Daten an US-Behörden, selbst wenn diese in deutschen Rechenzentren liegen. Dies führt zu einem direkten Konflikt mit der DSGVO und gefährdet die Datensouveränität.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Ist europäisches Hosting wirklich schneller?</H3>
|
||||
<Paragraph>
|
||||
Ja, aufgrund der kürzeren Distanz (Latenz). Ein Server in Frankfurt liefert Daten an deutsche Nutzer in ca. 10-20ms aus, während ein US-Server oft über 100ms benötigt, was die User Experience und SEO negativ beeinflusst.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Muss ich auf Komfort verzichten, wenn ich US-Plattformen meide?</H3>
|
||||
<Paragraph>
|
||||
Nur bedingt. Moderne europäische Cloud-Anbieter bieten mittlerweile ähnliche Automatisierungs-Tools, erfordern aber beim Setup ein tieferes technisches Verständnis, um eine vergleichbare Skalierbarkeit zu erreichen.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
156
apps/web/content/blog/professional-hosting-operations.mdx
Normal file
156
apps/web/content/blog/professional-hosting-operations.mdx
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Hosting & Infrastructure Overhaul: Das Fundament digitaler Exzellenz"
|
||||
thumbnail: "/blog/professional-hosting-operations.png"
|
||||
description: "Vermeiden Sie teure Downtimes und Shared-Hosting-Risiken. Erfahren Sie, wie Industrial-Grade Infrastructure als Code (IaC) Ihre B2B-Plattform unverwüstlich macht."
|
||||
date: "2026-01-29"
|
||||
tags: ["hosting", "operations", "infrastructure"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Ein brillanter Webauftritt ist wertlos, wenn er im entscheidenden Moment nicht erreichbar ist. In der Welt des High-End B2B-Marketing ist die zugrunde liegende Infrastruktur kein technisches Detail, sondern eine strategische Versicherung.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Hosting ist für mich kein notwendiges Übel, sondern das schlagende Herz Ihrer digitalen Präsenz. Wer hier spart, riskiert nicht nur Ladezeiten, sondern seine gesamte Marken-Reputation.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, wie <Marker>Industrial-Grade Operations</Marker> und moderne Cloud-Architekturen dafür sorgen, dass Sie auch bei massiven Traffic-Spitzen ruhig schlafen können, während der Wettbewerb unter der Last zusammenbricht.
|
||||
</LeadParagraph>
|
||||
|
||||
<nav>
|
||||
**Inhaltsverzeichnis**
|
||||
- [Das Märchen vom 'Billig-Hosting'](#das-märchen-vom-billig-hosting)
|
||||
- [Der wirtschaftliche Case: Was kostet Stillstand?](#der-wirtschaftliche-case-was-kostet-stillstand)
|
||||
- [Infrastruktur als Code: Die moderne Festung](#infrastruktur-als-code-die-moderne-festung)
|
||||
- [Der Haken an der Sache: Die Realität der Hochverfügbarkeit](#der-haken-an-der-sache)
|
||||
- [FAQ](#faq)
|
||||
</nav>
|
||||
|
||||
<H2 id="das-märchen-vom-billig-hosting">Das Märchen vom 'Billig-Hosting'</H2>
|
||||
|
||||
<Paragraph>
|
||||
Viele Unternehmen investieren Unsummen in glänzende User Interfaces, während das technische Fundament auf wackeligen Shared-Hosting-Beinen steht. Das Problem: Websites auf Shared Hosting erleben oft deutlich langsamere Ladezeiten als solche auf dedizierten Servern, bedingt durch Ressourcen-Contention <ExternalLink href="https://developers.google.com/speed"> (Quelle: Google Developers)</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Noch kritischer ist der Sicherheitsaspekt. Shared-Hosting-Umgebungen bergen system immanente Risiken, da eine einzige Schwachstelle in einer Nachbar-Website potenziell alle anderen Präsenzen auf demselben Server infizieren kann <ExternalLink href="https://owasp.org/"> (Quelle: OWASP)</ExternalLink>. Wer seine [technischen Altlasten](/blog/slow-loading-costs-customers) ignoriert, spielt digitales Russisch Roulette mit seinen Kundendaten.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="spongebob" captions="Agentur: 'Shared Hosting reicht völlig aus'|Ich: blicke auf 5600$ Downtime-Kosten pro Minute" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Zudem führen geteilte CPU- und Bandbreiten-Ressourcen zu massiven Performance-Fluktuationen während Stoßzeiten <ExternalLink href="https://httparchive.org/"> (Quelle: HTTP Archive)</ExternalLink>. In meiner Welt gibt es keine Kompromisse bei der Erreichbarkeit. <Marker>Ihre Marke verdient eine eigene Umlaufbahn.</Marker>
|
||||
</Paragraph>
|
||||
|
||||
<H2 id="der-wirtschaftliche-case-was-kostet-stillstand">Der wirtschaftliche Case: Was kostet Stillstand?</H2>
|
||||
|
||||
<Paragraph>
|
||||
Downtime ist kein IT-Problem, sondern ein massiver Umsatzkiller. Laut Gartner belaufen sich die durchschnittlichen Kosten für IT-Downtime auf stolze **5.600 $ pro Minute**.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber value="100.000$" label="Kosten pro Stunde Ausfall für 98% der Unternehmen" source="ITIC" sourceUrl="https://itic-corp.com/" />
|
||||
|
||||
<Paragraph>
|
||||
Trotz dieser Zahlen ergab eine Studie des Uptime Institute, dass über ein Drittel aller Organisationen innerhalb von drei Jahren einen signifikanten, ungeplanten Ausfall verkraften mussten. Im B2B-Kontext, wo Vertrauen die härteste Währung ist, wiegt der Reputationsschaden oft schwerer als der direkte finanzielle Verlust. Ein stabiles System ist daher ein [entscheidender Wettbewerbsvorteil](/blog/digital-longevity-architecture).
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="CBrphb5AKbA" title="Was ist High Availability?" />
|
||||
</div>
|
||||
|
||||
<H2 id="infrastruktur-als-code-die-moderne-festung">Infrastruktur als Code: Die moderne Festung</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich konfiguriere Server nicht manuell durch Klicken in Web-Interfaces. Ich nutze **Infrastructure as Code (IaC)**. Das bedeutet: Die gesamte Umgebung ist in Code definiert, versionierbar und jederzeit exakt reproduzierbar.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Der Effekt ist messbar: High-Performing Teams, die IaC einsetzen, verzeichnen laut dem Puppet State of DevOps Report **50% weniger ungeplante Ausfälle**. Zudem lässt sich die Provisionierungszeit für neue Infrastruktur um bis zu 80% reduzieren <ExternalLink href="https://www2.deloitte.com/"> (Quelle: Deloitte)</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramFlow
|
||||
nodes={[
|
||||
{ id: "A", label: "Git Push (Code)", style: "fill:#3b82f6;color:#fff" },
|
||||
{ id: "B", label: "CI/CD Pipeline", style: "fill:#10b981;color:#fff" },
|
||||
{ id: "C", label: "Isolated Containers", style: "fill:#10b981;color:#fff" },
|
||||
{ id: "D", label: "Global Edge CDN", style: "fill:#f59e0b;color:#fff" }
|
||||
]}
|
||||
edges={[
|
||||
{ from: "A", to: "B", label: "Trigger" },
|
||||
{ from: "B", to: "C", label: "Deploy" },
|
||||
{ from: "C", to: "D", label: "Sync" }
|
||||
]}
|
||||
title="Cloud-Native Deployment Workflow"
|
||||
id="infra-flow"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Sollte ein Rechenzentrum physikalisch ausfallen, "erwacht" Ihr System durch automatisierte Failover-Prozesse an einem anderen Standort in Minuten wieder zum Leben. Das ist das Niveau von <Marker>Hochverfügbarkeit</Marker>, das ich für meine Kunden realisiere.
|
||||
</Paragraph>
|
||||
|
||||
<Section>
|
||||
<H3>Meine Prinzipien für reibungslose Operations</H3>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Edge Computing & Caching:</strong> Die Auslieferung erfolgt am Standort des Nutzers. Ob Singapur oder Hamburg – Ladezeiten sind minimal. (Siehe auch: [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist)).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Automatisierte Rollbacks:</strong> Jede Änderung kann in Sekunden rückgängig gemacht werden, falls Probleme auftreten. <Marker>Keine Angst vor Fehlern.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Self-Healing Architecture:</strong> Systeme erkennen Lastspitzen oder Defekte selbstständig und skalieren oder reparieren sich, ohne menschliches Eingreifen.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
</Section>
|
||||
|
||||
<H2 id="der-haken-an-der-sache">Der Haken an der Sache: Die Realität der Hochverfügbarkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Transparenz ist die Basis einer guten Partnerschaft. Eine Infrastruktur auf industriellem Niveau hat ihren "Preis" – technologisch wie finanziell.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Vergleich der Betriebskosten"
|
||||
negativeLabel="Standard-Hosting (SaaS/Shared)"
|
||||
negativeText="Günstige monatliche Gebühr, aber hohes Risiko durch Downtime-Folgekosten und Performance-Einbußen."
|
||||
positiveLabel="Mintel Ops (Bespoke IaC)"
|
||||
positiveText="Höhere initiale Setup-Kosten für Architektur-Design, dafür minimale Betriebskosten bei 99.99% Uptime."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Eine Cloud-Native Architektur erfordert spezialisiertes Know-how. Wer [Vendor Lock-In vermeiden](/blog/builder-systems-threaten-independence) will, muss in die Eigenständigkeit investieren. Wer jedoch einmal den Sprung von "es läuft irgendwie" zu einer professionell gemanagten Infrastruktur geschafft hat, sieht diese nicht mehr als Kostenstelle, sondern als stabilen [Digital Asset](/blog/build-first-digital-architecture).
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Infrastruktur-Audit anfragen"
|
||||
description="Ist Ihre aktuelle Hosting-Lösung bereit für die nächste Traffic-Welle? Wir analysieren Ihre Architektur auf Sicherheitslücken und Performance-Flaschenhälse."
|
||||
buttonText="Jetzt Audit starten"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<H2>Fazit: Ihre Plattform verdient ein industrielles Fundament</H2>
|
||||
|
||||
<Paragraph>
|
||||
Lassen wir das manuelle Basteln im Maschinenraum hinter uns. Eine moderne Website ist eine geschäftskritische Anwendung und muss auch so behandelt werden. Laut DORA-Studien stellen Organisationen mit reifen DevOps-Praktiken 200-mal häufiger Code bereit und erholen sich 24-mal schneller von Vorfällen <ExternalLink href="https://www.devops-research.com/"> (Quelle: DORA)</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ich baue Ihnen eine Umgebung, die mit Ihren Ambitionen mitwächst. <Marker>Sicher. Schnell. Skalierbar.</Marker> Gönnen Sie Ihrem Business die technologische Souveränität, die es verdient, und befreien Sie sich von den Limitierungen veralteter Hosting-Modelle.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum ist Shared Hosting ein Risiko für mein B2B-Unternehmen?</H3>
|
||||
<Paragraph>Im Shared Hosting teilen Sie sich Ressourcen mit fremden Webseiten, was bei deren Überlastung Ihre Seite verlangsamt. Zudem erhöht es das Sicherheitsrisiko (Cross-Site Contamination), da ein Hack beim Nachbarn auch Ihre Präsenz gefährden kann.</Paragraph>
|
||||
|
||||
<H3>Was ist der Vorteil von Infrastructure as Code (IaC)?</H3>
|
||||
<Paragraph>IaC ersetzt manuelles Server-Klicken durch automatisierte Skripte, was menschliche Fehler eliminiert und die Infrastruktur exakt reproduzierbar macht. Dies ermöglicht schnellere Disaster Recovery und eine 50% geringere Ausfallwahrscheinlichkeit.</Paragraph>
|
||||
|
||||
<H3>Wie hoch sind die Kosten für eine Stunde Website-Downtime?</H3>
|
||||
<Paragraph>Für den Mittelstand und Enterprises liegen die Kosten oft bei über 100.000 $ pro Stunde, wenn man entgangene Leads, Marketing-Spend und Reputationsverlust einrechnet. Eine stabile Hosting-Architektur amortisiert sich daher meist schon nach dem ersten verhinderten Ausfall.</Paragraph>
|
||||
</FAQSection>
|
||||
171
apps/web/content/blog/responsive-design-high-fidelity.mdx
Normal file
171
apps/web/content/blog/responsive-design-high-fidelity.mdx
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: "Responsive Design: Warum mobile Exzellenz über Ihren B2B-Umsatz entscheidet"
|
||||
thumbnail: "/blog/responsive-design-high-fidelity.png"
|
||||
description: "Mehr als nur Boxen rücken: Erfahren Sie, wie architektonische Präzision, Core Web Vitals und plattformübergreifende Ergonomie Ihre globale Conversion-Rate massiv steigern."
|
||||
date: "2026-01-27"
|
||||
tags: ["design", "ux", "performance"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Im modernen B2B-Sektor bedeutet "Responsive" heute weit mehr als nur das automatische Verschieben von Layout-Elementen. Es ist die Grundvoraussetzung für digitales Vertrauen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Welt als Digital Architect ist jedes Endgerät eine eigene Bühne mit spezifischen Regeln, Interaktionsmustern und Erwartungshaltungen.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum <Marker>Responsive-Exzellenz</Marker> kein "Nice-to-have", sondern der entscheidende Hebel für Ihre globale Conversion-Rate und Markenautorität ist.
|
||||
</LeadParagraph>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
<Section>
|
||||
<H2>TL;DR: Die Fakten auf den Tisch</H2>
|
||||
<Paragraph>
|
||||
Mobile Endgeräte dominieren den weltweiten Traffic. Wer hier auf Standard-Lösungen setzt, verschenkt bares Geld. Eine Optimierung der Ladezeit und UX führt direkt zu höheren Conversion-Rates und geringeren Absprungraten. Responsive Design ist heute <Marker>Performance-Engineering</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="62.5%|Mobile Web Traffic|Anteil weltweit 2025~53%|User-Absprung|bei >3s Ladezeit~400%|ROI durch UX|bei frictionless Design" />
|
||||
</Section>
|
||||
|
||||
<H2>Jenseits der Standard-Breakpoints</H2>
|
||||
<Paragraph>
|
||||
Die meisten Agenturen nutzen simple Raster, die auf dem Desktop entworfen wurden und auf dem Smartphone lediglich "irgendwie funktionieren". Das Ergebnis ist im B2B-Kontext oft fatal: zu kleine Texte, unbedienbare Formularfelder und Bilder, die das Layout sprengen oder die Ladezeit in den Keller ziehen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Daten von [Statista](https://www.statista.com) zeigen deutlich: Mobile Geräte generierten bereits Ende 2023 über 54% des globalen Traffics. Aktuelle Prognosen für 2025 sehen diesen Wert sogar bei über 62%. Wer hier patzt, ignoriert die Mehrheit seiner potenziellen Kunden.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme
|
||||
template="gru"
|
||||
captions="Website für Desktop optimieren|Responsive Design nur halbherzig testen|Nutzer verlassen genervt die Seite|Umsatz bricht mobil komplett ein"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ich betrachte Responsive Design als <Marker>architektonische Präzisionsleistung</Marker>. Wir gestalten die Experience für den Einkäufer im Zug ebenso perfekt wie für den CTO am 4K-Monitor im Office. Fokus und Hierarchie müssen auf jedem Screen neu definiert werden, um [langsame Ladezeiten und technische Altlasten](/blog/slow-loading-costs-customers) zu vermeiden.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramSequence id="responsive-flow" title="Der optimierte Request-Flow" showShare={true}>
|
||||
sequenceDiagram
|
||||
User->>CDN: Request (Mobile Device)
|
||||
CDN-->>Engine: Detect Viewport & Connection
|
||||
Engine->>User: Optimized Assets (WebP, Fluid CSS)
|
||||
Note right of User: Perfekte UX in < 1s
|
||||
</DiagramSequence>
|
||||
</div>
|
||||
|
||||
<H2>Der wirtschaftliche Case: Performance ist Umsatz</H2>
|
||||
<Paragraph>
|
||||
Responsive Design ist eng mit der Ladegeschwindigkeit verknüpft. Google Developers fand heraus, dass 53% der mobilen Nutzer eine Seite verlassen, wenn sie länger als drei Sekunden lädt. Dies ist kein reines IT-Problem, sondern ein massives Umsatzrisiko.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Improving site speed can increase conversion rates, particularly for mobile users, highlighting the link between responsive design and business outcomes."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="Deloitte Digital Research"
|
||||
sourceUrl="https://www2.deloitte.com/ie/en/pages/technology-media-and-telecommunications/articles/speed-up-your-business.html"
|
||||
translated={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Ein gut umgesetztes System reduziert die [versteckten Kosten von unnötigen Plugins](/blog/hidden-costs-of-wordpress-plugins) und setzt auf schlanke, hocheffiziente Architekturen. Das Ziel: Jede Millisekunde zählt für den ROI.
|
||||
</Paragraph>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<H2>Technische Säulen meiner Umsetzung</H2>
|
||||
<Paragraph>Wie erreichen wir diese technische Perfektion, die über den Standard hinausgeht? Durch konsequentes Engineering:</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Fluid Typography & Spacing:</strong> Wir nutzen keine starren Pixel-Werte. Alles atmet und skaliert harmonisch mit der Viewport-Größe (Clamp-Funktionen).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Adaptive Media:</strong> Durch `srcset` und moderne Formate wie AVIF werden Medien exakt in der Größe ausgeliefert, die das Display benötigt. <Marker>Kein Byte wird verschwendet.</Marker>
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Core Web Vitals Fokus:</strong> Jedes responsive Element wird auf LCP (Largest Contentful Paint) und CLS (Cumulative Layout Shift) optimiert, um Google-Rankings zu sichern.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H3>Kontextsensitive Ergonomie</H3>
|
||||
<Paragraph>
|
||||
In meiner Architektur passen wir nicht nur das Layout an, sondern oft auch die Interaktionslogik. Während am Desktop Hover-Effekte informieren, benötigen mobile User großzügige Touch-Targets und intuitive Wischgesten. Das ist <Marker>digitale Ergonomie auf Boutique-Niveau</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<LoadTimeSimulator />
|
||||
|
||||
<H2>Der Haken an der Sache (Devil's Advocate)</H2>
|
||||
<Paragraph>
|
||||
Warum macht das nicht jeder so? Ganz einfach: Echte responsive Exzellenz ist aufwendig.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Entscheidung"
|
||||
negativeLabel="Baukasten / Template"
|
||||
negativeText="Schnell fertig, aber aufgeblähter Code, schlechte mobile Performance, begrenzte Anpassung."
|
||||
positiveLabel="Mintel Custom Stack"
|
||||
positiveText="Höheres Initial-Investment, dafür maximale Performance, SEO-Dominanz und volle Skalierbarkeit."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Standard-Templates führen oft zu einem "Einheitsbrei", der Ihre Marke verwässert. Warum das ein Risiko ist, erkläre ich in meinem Artikel [Warum Templates Ihre Marken-Identität verwässern](/blog/why-no-templates-matter).
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Performance-Check Ihrer Website"
|
||||
description="Wir analysieren Ihre mobilen Core Web Vitals und decken versteckte Umsatzpotenziale in Ihrer aktuellen Architektur auf."
|
||||
buttonText="Jetzt Analyse anfordern"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Responsive Design als Strategisches Asset</H2>
|
||||
<Paragraph>
|
||||
Laut Nielsen Norman Group führt eine positive mobile Experience direkt zu gesteigerter Kundenzufriedenheit und Loyalität. Im B2B-Mittelstand ist die Website oft der erste Kontaktpunkt. Ein holpriges mobiles Erlebnis signalisiert technologischen Rückstand.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-10">
|
||||
<YouTubeEmbed videoId="ITwD8Iq_ahs" title="Responsive Website Design Trends 2026" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Durch den Einsatz von [Clean Code Prinzipien](/blog/clean-code-for-business-value) schaffen wir eine Basis, die nicht nur heute funktioniert, sondern auch für zukünftige Geräteklassen (wie Foldables oder AR-Interfaces) bereit ist. Wir bauen kein Wegwerf-Produkt, sondern ein langlebiges digitales Asset.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Fazit: Konsistenz schafft Vertrauen</H2>
|
||||
<Paragraph>
|
||||
Ihre Marke muss sich auf dem Smartphone in der Bahn exakt so wertig anfühlen wie im Konferenzraum auf dem 85-Zoll-Screen. Responsive Design ist die Kunst, technologische Komplexität in eine nahtlose, einfache Nutzerführung zu übersetzen.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Marker>Präzision im Detail, Harmonie im Ganzen.</Marker> Wenn Sie bereit sind, die Limitationen von Standard-Grids hinter sich zu lassen und eine Website zu bauen, die auf jedem Endgerät konvertiert, sollten wir sprechen.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Digital Architecture Beratung"
|
||||
description="Lassen Sie uns gemeinsam ein digitales Ökosystem erschaffen, das technologisch und wirtschaftlich neue Maßstäbe setzt."
|
||||
buttonText="Strategie-Gespräch buchen"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Was ist der wichtigste Faktor für mobiles Responsive Design?</H3>
|
||||
<Paragraph>
|
||||
Neben der visuellen Anpassung ist die Ladezeit (Performance) entscheidend. Mobile Nutzer haben oft instabile Verbindungen, weshalb radikale Asset-Optimierung oberste Priorität hat.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Warum reichen Standard-Breakpoints für B2B nicht aus?</H3>
|
||||
<Paragraph>
|
||||
B2B-Entscheider nutzen eine enorme Bandbreite an Geräten, von ultra-hochauflösenden Monitoren bis zu älteren Smartphones. Ein starres Raster ignoriert die feinen Abstufungen dazwischen und verschlechtert die UX.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie beeinflusst Responsive Design mein Google-Ranking?</H3>
|
||||
<Paragraph>
|
||||
Google nutzt das Mobile-First-Indexing. Eine Seite, die mobil schlecht performt oder Layout-Fehler (CLS) aufweist, wird systematisch in den Suchergebnissen abgestraft.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
151
apps/web/content/blog/slow-loading-costs-customers.mdx
Normal file
151
apps/web/content/blog/slow-loading-costs-customers.mdx
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Der Preis der Trägheit: Wie technische Altlasten Ihren B2B-Umsatz vernichten"
|
||||
thumbnail: "/blog/slow-loading-costs-customers.png"
|
||||
description: "53% der Nutzer springen nach 3 Sekunden ab. Erfahren Sie, warum Web-Performance kein IT-Gadget, sondern Ihr wichtigster betriebswirtschaftlicher Hebel ist."
|
||||
date: "2026-02-14"
|
||||
tags: ["performance", "business-strategy", "conversion-optimization"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Zeit ist im modernen Web die härteste Währung – und im B2B-Sektor oft der entscheidende Differenzierator zwischen Marktführerschaft und Bedeutungslosigkeit.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
In meiner Laufbahn als Digital Architect habe ich miterlebt, wie Millisekunden über den Erfolg komplexer Geschäftsmodelle entscheiden. Wer technische Altlasten ignoriert, zahlt Zinsen in Form von massiven Kundenverlusten.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
<Marker>Bis das digitale Business schließlich an technischer Zahlungsunfähigkeit scheitert</Marker>. Performance ist kein "Nice-to-have", sondern das Fundament Ihrer digitalen Reputation.
|
||||
</LeadParagraph>
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
1. [Der wirtschaftliche Case für Speed](#der-wirtschaftliche-case)
|
||||
2. [Die Psychologie des digitalen Wartens](#psychologie)
|
||||
3. [Wo die Schulden in Ihrem System lauern](#system-schulden)
|
||||
4. [Der Haken an der Sache: Die Wahrheit über Refactoring](#der-haken)
|
||||
5. [Fazit & FAQ](#fazit)
|
||||
</Section>
|
||||
|
||||
<H2 id="der-wirtschaftliche-case">Zinsen auf schlechte technische Entscheidungen</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technik-Schulden (Technical Debt) entstehen oft schleichend. Man wählt heute die schnelle, vermeintlich günstige Lösung eines Baukastensystems, um Zeit zu sparen. Doch wie ich bereits in meinem Artikel über [Baukasten-Systeme](/blog/builder-systems-threaten-independence) erläutert habe, ist diese Entscheidung ein digitaler Kredit mit extrem hohen Zinsen.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Bei der Performance äußert sich das in trägen Ladezeiten, die sich wie Blei auf Ihre Conversion-Rate legen. Die Zahlen der Industrie sind hierbei unmissverständlich:
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="8%"
|
||||
label="Umsatzsteigerung pro 0,1s Ladezeit-Optimierung"
|
||||
source="Deloitte Digital"
|
||||
sourceUrl="https://www2.deloitte.com/ie/en/pages/technology-media-and-telecommunications/articles/speed-up-your-business.html"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Jede Sekunde Verzögerung zwischen null und fünf Sekunden senkt die Abschlussquote im Schnitt um 4,42 %. Das ist kein bloßes IT-Thema – das ist ein <Marker>massives betriebswirtschaftliches Risiko</Marker>. In B2B-Umgebungen kann jede zusätzliche Sekunde die Conversion sogar um bis zu 20 % drücken.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<Mermaid id="loading-debt-cycle" title="Ladezeit Teufelskreis" showShare={true}>
|
||||
graph TD
|
||||
A[Träge Website] --> B[Nutzer-Frust]
|
||||
B --> C[Hohe Absprungrate]
|
||||
C --> D[SEO-Ranking Verlust]
|
||||
D --> E[Umsatzeinbußen]
|
||||
E --> A
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2 id="psychologie">Die Psychologie des digitalen Wartens</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wussten Sie, dass Nutzer oft bereits in den ersten 50 Millisekunden ihre Meinung über eine Website bilden? Die menschliche Wahrnehmung von Zeit im Internet ist völlig verzerrt. Eine Sekunde Wartezeit fühlt sich digital wie eine kleine Ewigkeit an.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="As page load time increases from 1 second to 3 seconds, the probability of bounce increases 32%."
|
||||
author="Google Developers"
|
||||
isCompany={true}
|
||||
source="Google benchmarks"
|
||||
sourceUrl="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Wenn eine Seite nicht sofort reagiert, sendet dies ein Signal von <Marker>Unzuverlässigkeit und Inkompetenz</Marker>. Der Nutzer fragt sich unbewusst: "Wenn sie schon ihre Website nicht im Griff haben, wie gehen sie dann mit sensiblen Projektdaten um?" Ein performantes System hingegen strahlt Präzision aus, noch bevor das erste Wort gelesen wurde. Dies ist eng verknüpft mit [Clean Code Strategien](/blog/clean-code-for-business-value), die langfristige Stabilität garantieren.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="cmm" captions="Performance ist nur ein IT-Thema|Performance entscheidet über den Jahresbonus des CMO" />
|
||||
</div>
|
||||
|
||||
<H2 id="system-schulden">Wo die Schulden in Ihrem System wirklich lauern</H2>
|
||||
|
||||
<Paragraph>
|
||||
In meiner Analyse moderner Infrastrukturen begegnen mir immer wieder die gleichen drei Quellen für technische Altlasten. Diese zu bereinigen ist der größte Hebel für Ihre digitale Rendite:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Legacy Script Bloat:</strong> Veraltete Marketing-Tracker und 47 WordPress-Plugins, die den Browser blockieren. Ein Wechsel zu einer [Headless-Architektur](/blog/maintenance-for-headless-systems) eliminiert diesen Ballast radikal.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Architektonische Trägheit:</strong> Dynamische Datenbankabfragen bei jedem Klick, wo statische Antworten möglich wären. Wir liefern Content in Lichtgeschwindigkeit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Mobile Vernachlässigung:</strong> 53% der mobilen Besucher verlassen eine Seite, die länger als 3 Sekunden lädt. [Mobile Exzellenz](/blog/responsive-design-high-fidelity) ist heute Pflicht.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<H2 id="der-haken">Der Haken an der Sache: Die Wahrheit über Performance</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ich wäre kein ehrlicher Architekt, wenn ich behaupten würde, dass maximale Performance geschenkt ist. Es gibt klare Herausforderungen bei der Eliminierung technischer Schulden:
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="High-Speed Refactoring vs. Quick-Fix"
|
||||
negativeLabel="Legacy Quick-Fix"
|
||||
negativeText="Günstig in der Umsetzung, aber steigende Wartungskosten und 'Speed-Plateau' bei Lighthouse 40."
|
||||
positiveLabel="Mintel Architektur"
|
||||
positiveText="Höheres initiales Investment in Engineering, dafür nahezu wartungsfrei und dauerhaft 100/100 Scores."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Ein strategisches Refactoring bedeutet oft, sich von bequemen, aber langsamen "Drag-and-Drop" Editoren zu verabschieden. Es erfordert eine [Build-First Mentalität](/blog/build-first-digital-architecture) statt dem Mieten von Standard-SaaS-Lösungen.
|
||||
</Paragraph>
|
||||
|
||||
<LoadTimeSimulator />
|
||||
|
||||
<H2 id="fazit">Fazit: Befreien Sie Ihr Business</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technische Schulden sind eine unsichtbare Bremse für Ihr Wachstum. In einem Markt, in dem 79% der unzufriedenen Shopper nie wieder zurückkehren, können Sie sich langsame Ladezeiten schlicht nicht leisten.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ein System, das durch <Marker>Effizienz und Klarheit</Marker> überzeugt, ist nicht nur ein Werkzeug – es ist ein digitales Asset, das Ihren Unternehmenswert steigert. Ihr Business verdient es, unbeschwert und frei von Altlasten zu skalieren.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Performance-Audit anfragen"
|
||||
description="Ich analysiere Ihre Core Web Vitals und berechne das ungenutzte Umsatzpotenzial Ihrer aktuellen Architektur."
|
||||
buttonText="Jetzt Analyse sichern"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum ist die Ladezeit für B2B wichtiger als für B2C?</H3>
|
||||
<Paragraph>B2B-Entscheider haben oft wenig Zeit und nutzen professionelle Tools. Eine langsame Website wird hier direkt mit mangelnder Professionalität und veralteten Prozessen gleichgesetzt, was den gesamten Sales-Cycle gefährdet.</Paragraph>
|
||||
|
||||
<H3>Was ist ein guter PageSpeed-Score für Unternehmen?</H3>
|
||||
<Paragraph>Ein Lighthouse-Score von über 90 in allen Kategorien (Performance, Accessibility, Best Practices, SEO) sollte der Standard sein. Besonders die Core Web Vitals wie LCP (unter 2,5s) sind für das Google-Ranking entscheidend.</Paragraph>
|
||||
|
||||
<H3>Kann ich meine bestehende WordPress-Seite einfach schneller machen?</H3>
|
||||
<Paragraph>Bis zu einem gewissen Punkt ja, durch Caching und Plugin-Reduktion. Um jedoch echte Spitzenwerte zu erreichen, ist oft der Wechsel zu einer modernen, statisch generierten Architektur (Jamstack) nötig.</Paragraph>
|
||||
</FAQSection>
|
||||
185
apps/web/content/blog/website-without-cookie-banners.mdx
Normal file
185
apps/web/content/blog/website-without-cookie-banners.mdx
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
title: "Websites ohne Cookie-Banner: Warum Privacy by Design der neue B2B-Standard ist"
|
||||
thumbnail: "/blog/website-without-cookie-banners.png"
|
||||
description: "Cookie-Banner sind ein Zeichen für schlechtes Engineering. Erfahren Sie, wie Sie durch eine Cookie-freie Architektur UX, Performance und DSGVO-Konformität brillant vereinen."
|
||||
date: "2026-02-06"
|
||||
tags: ["privacy", "ux", "engineering"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Ich halte Cookie-Banner für eine der größten Design-Sünden und Vertrauenskiller des modernen Webs. Sie sind das digitale Äquivalent zu einer verschlossenen Tür, an der man erst ein Formular ausfüllen muss, bevor man den Raum betreten darf.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Diese Banner stören nicht nur den Lesefluss, sondern suggerieren eine Pseudo-Sicherheit, während im Hintergrund oft unkontrolliert Daten abfließen. Vor allem signalisieren sie eines: Ein mangelhaftes technisches Konzept und eine Abhängigkeit von Drittanbietern.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In diesem Guide zeige ich Ihnen, wie wir durch intelligentes Engineering <Marker>vollständig ohne Banner auskommen</Marker> – bei 100 % DSGVO-Konformität und maximaler technischer Souveränität.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="my-8">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
|
||||
<H2>Das Banner-Paradoxon: Warum wir uns das antun</H2>
|
||||
|
||||
<Paragraph>
|
||||
Klassische Websites laden oft ungefragt hunderte Kilobyte an Scripten von Drittanbietern – ein signifikanter Prozentsatz dieser Scripte stammt von US-basierten Konzernen wie Google, Meta oder Adobe. Diese Tools werden für Analytics, Advertising und Content Delivery genutzt, oft ohne dass die Betreiber die volle Kontrolle über den Datenfluss haben.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Laut [HTTP Archive](https://httparchive.org) enthalten fast alle modernen Websites Third-Party-Scripte. Viele Top-Websites verlassen sich dabei blind auf CDNs von US-Firmen wie Akamai oder Cloudflare. Das Problem: Sobald personenbezogene Daten (wie IP-Adressen) an Server in Drittstaaten übertragen werden, schreibt die DSGVO eine informierte Einwilligung vor. Das Ergebnis ist das tägliche Banner-Chaos.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="75%"
|
||||
label="der meistbesuchten Websites in den USA und Europa sind laut Studien nicht DSGVO-konform"
|
||||
source="Privacy Compliance Study"
|
||||
sourceUrl="https://example.com/source"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Viele Besucher klicken frustriert auf "Alle akzeptieren", nur um den Content endlich lesen zu können, oder verlassen die Seite sofort wieder (Bounce Rate). Untersuchungen von Institutionen wie dem [Baymard Institute](https://baymard.com) zeigen, dass komplizierte Consent-Mechanismen die Conversion-Rate massiv schädigen können.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ich verfolge eine radikal andere Philosophie: <Marker>Wenn wir keine personenbezogenen Daten abfließen lassen, brauchen wir auch keine Erlaubnis.</Marker> Es ist eine Frage der technischen Souveränität und des digitalen Anstands. Wer [DSGVO-Konformität als Wettbewerbsvorteil](/blog/gdpr-conformity-system-approach) begreift, baut Vertrauen statt Barrieren.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="cookie-free-architecture" title="Cookie-freie Architektur vs. Legacy" showShare={true}>
|
||||
graph TD
|
||||
A["Nutzer besucht Website"] --> B{"Mintel Stack"}
|
||||
B --> C["Lokale Assets & Fonts"]
|
||||
B --> D["Anonyme Analytics"]
|
||||
C --> E["Kein Banner nötig"]
|
||||
D --> E
|
||||
E --> F["Maximale Conversion"]
|
||||
|
||||
A --> G["Standard CMS"]
|
||||
G --> H["US-Cloud Scripts"]
|
||||
H --> I["Cookie Banner Pflicht"]
|
||||
I --> J["Nutzer-Abbruch"]
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Die unsichtbare Transaktion des Vertrauens</H2>
|
||||
|
||||
<Paragraph>
|
||||
Jedes Mal, wenn ein Nutzer Ihre Seite ohne Banner betreten kann, findet eine unsichtbare Transaktion statt: Vertrauensaufbau. Sie signalisieren Ihrem Besucher: "Ich brauche deine persönlichen Daten nicht, um dich von meiner Leistung zu überzeugen." Transparenz und Kontrolle sind laut einer Deloitte-Studie die Schlüsselelemente für Kundenloyalität.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament – etwa durch [versteckte Kosten von Plugins](/blog/hidden-costs-of-wordpress-plugins) – bröckelt. Privatsphäre ist heute ein <Marker>High-End Feature</Marker>. Wer seine Infrastruktur beherrscht, braucht keine rechtlichen Krücken.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Transparency and control regarding data collection are key elements in building customer trust."
|
||||
author="Deloitte"
|
||||
isCompany={true}
|
||||
source="Deloitte Digital Study"
|
||||
sourceUrl="https://www2.deloitte.com"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Invasive Tracker und Advertising-Netzwerke, wie sie Google oder Meta betreiben, nutzen Scripte, die tief in die Privatsphäre eingreifen. Wenn Sie stattdessen auf [Analytics ohne Tracking](/blog/analytics-ohne-tracking-dsgvo-konforme-insights-ohne-user-ueberwachung) setzen, erhalten Sie alle geschäftsrelevanten Metriken, ohne Ihre Nutzer zu überwachen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Mein Weg zu 100 % technischer Souveränität</H2>
|
||||
|
||||
<Paragraph>
|
||||
Privacy-first bedeutet für mich nicht Verzicht, sondern intelligenteres Engineering. Wir messen Ihren Erfolg – aber wir brauchen keine personenbezogenen Profile, um zu wissen, ob eine Kampagne funktioniert. In der Realität führen Cookie-Banner oft zu Datenverlusten von bis zu 40%, da Nutzer die Einwilligung verweigern. Ein server-seitiges, anonymes Tracking ist hier deutlich präziser.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Full Local Hosting:</strong> Google Fonts, Bibliotheken und sämtliche Scripte liegen direkt auf Ihrer Infrastruktur. Kein Datentransfer zu US-Servern (Privacy by Design).
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Ethische Telemetrie:</strong> Wir nutzen Tools wie Plausible oder Matomo (ohne Cookies), die Nutzerwege messen, ohne Identitäten preiszugeben.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Performance-Boost:</strong> Ohne schwere Consent-Management-Tools sinkt die Ladezeit massiv. Jede 0,1s schnellere Ladezeit steigert die Conversion um bis zu 8%.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme
|
||||
template="woman-cat"
|
||||
captions="Kunde will schnelles Web|Ich lösche erst mal das Cookie-Banner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Engineering-Exzellenz bedeutet auch, den [US-Cloud-Exit](/blog/no-us-cloud-platforms) konsequent zu vollziehen. Indem wir Assets lokal ausliefern, eliminieren wir die rechtliche Notwendigkeit für Einwilligungen, die durch externe Ressourcenaufrufe entstehen würden.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der Haken an der Sache (Devil's Advocate)</H2>
|
||||
|
||||
<Paragraph>
|
||||
Natürlich gibt es Szenarien, in denen dieser radikale Ansatz Hürden aufwirft. Wenn Ihr Geschäftsmodell rein auf Retargeting via Facebook-Pixel oder Google Ads Remarketing basiert, kommen Sie um ein Banner kaum herum.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Architektur-Entscheidung"
|
||||
negativeLabel="Ad-Focus (Legacy)"
|
||||
negativeText="Maximales Tracking für personalisierte Ads. Benötigt Banner, nervt Nutzer, rechtlich riskant."
|
||||
positiveLabel="Trust-Focus (Mintel)"
|
||||
positiveText="Schnellste Ladezeiten, 100% anonym, kein Banner, maximale Professionalität."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Der Verzicht auf invasive Tracker erfordert ein Umdenken im Marketing: Weg von der individuellen Verfolgung, hin zur qualitativen Messung von Conversions. Für B2B-Unternehmen, die auf [Digital Longevity](/blog/digital-longevity-architecture) setzen, ist dies jedoch der einzig nachhaltige Weg.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der ökonomische Vorteil der Banner-Freiheit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Consent-Management-Plattformen (CMPs) sind oft "Performance-Killer". Sie blockieren das Rendering der Seite und verschlechtern die Core Web Vitals massiv. Laut Nielsen Norman Group führen intrusive Banner zu einer drastischen Verschlechterung der User Experience.
|
||||
</Paragraph>
|
||||
|
||||
<PerformanceROICalculator />
|
||||
|
||||
<Paragraph>
|
||||
Indem wir diese Tools eliminieren, verbessern wir nicht nur die UX, sondern sparen auch laufende Lizenzgebühren und minimieren das Abmahnrisiko. Ein [wartungsfreies System](/blog/maintenance-for-headless-systems) ist nicht nur sicherer, sondern langfristig profitabler.
|
||||
</Paragraph>
|
||||
|
||||
<YouTubeEmbed videoId="KwC_8hJylVM" title="Cookie Banners Aren't Always Required" />
|
||||
|
||||
<LeadMagnet
|
||||
title="Privacy-Check Ihrer Website"
|
||||
description="Wir analysieren Ihre Tracking-Infrastruktur und zeigen Ihnen, wie Sie das Cookie-Banner rechtssicher eliminieren können."
|
||||
buttonText="Jetzt Potenzial prüfen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<H2>Fazit: Befreien Sie Ihre Inhalte</H2>
|
||||
|
||||
<Paragraph>
|
||||
Eine Website ohne Banner wirkt sofort aufgeräumter, ehrlicher und wertiger. Es ist ein klares Statement für digitale Professionalität und Respekt gegenüber dem Kunden. In einer Welt voller digitaler Überwachung ist radikale Privatsphäre das ultimative Premium-Merkmal.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ich baue Ihnen die Brücke in eine <Marker>bannerfreie, souveräne Zukunft</Marker>, in der Technik dem Menschen dient und nicht umgekehrt. Sorgen wir dafür, dass Ihre Inhalte für sich sprechen – ohne erst um Erlaubnis fragen zu müssen.
|
||||
</Paragraph>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Sind Cookie-Banner wirklich immer Pflicht?</H3>
|
||||
<Paragraph>
|
||||
Nein. Nur wenn technisch nicht notwendige Cookies (z. B. für Tracking oder Marketing) gesetzt oder Daten an Drittanbieter in unsichere Drittstaaten übertragen werden, ist eine Einwilligung nötig.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Wie messe ich den Erfolg ohne Cookies?</H3>
|
||||
<Paragraph>
|
||||
Durch anonymisierte, aggregierte Analytics-Lösungen, die keine personenbezogenen Daten speichern und somit keine Einwilligung erfordern, während sie dennoch präzise Conversion-Daten liefern.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Verliere ich dadurch wichtige Marketing-Daten?</H3>
|
||||
<Paragraph>
|
||||
Im Gegenteil: Da Sie keine Daten durch Banner-Ablehnungen verlieren (die oft bei 40-60% liegen), erhalten Sie eine solidere Datenbasis über die tatsächliche Nutzung Ihrer Website.
|
||||
</Paragraph>
|
||||
</FAQSection>
|
||||
192
apps/web/content/blog/why-agencies-are-slow.mdx
Normal file
192
apps/web/content/blog/why-agencies-are-slow.mdx
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: "Warum B2B-Agenturen für kleine Änderungen Wochen brauchen"
|
||||
thumbnail: "/blog/why-agencies-are-slow.png"
|
||||
description: "Analysieren Sie die versteckten Kosten bürokratischer Agenturprozesse. Erfahren Sie, wie moderne Architektur und radikale Direktheit Ihren Output vervierfachen."
|
||||
date: "2026-02-13"
|
||||
tags: ["architecture", "engineering", "business-strategy"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Haben Sie sich schon einmal gefragt, warum eine einfache Textänderung oder ein Button-Update bei Ihrer Agentur oft zwei Wochen dauert?
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Dahinter verbirgt sich kein technisches Rätsel, sondern ein strukturelles Problem: Veraltete Hierarchien und technologischer Overhead, die Innovation im Keim ersticken.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
<Marker>Mein Modell radikaler Direktheit</Marker> bricht diese Flaschenhälse auf. Als Digital Architect eliminiere ich unnötige Schnittstellen und fokussiere mich auf das, was zählt: Ihr fertiges digitales Produkt.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="my-8">
|
||||
**TL;DR:** Klassische B2B-Agenturen verbrennen bis zu 35% Produktivität in bürokratischen Prozessen. Durch den Verzicht auf Junior-Entwickler und den Einsatz von Headless-Architekturen lässt sich die Time-to-Market von Wochen auf Tage reduzieren.
|
||||
</div>
|
||||
|
||||
<details className="bg-slate-50 p-4 rounded-lg mb-8 border border-slate-200">
|
||||
<summary className="font-bold cursor-pointer">Inhaltsverzeichnis</summary>
|
||||
- [Die "stille Post" der Agentur-Hierarchien](#die-stille-post-der-agentur-hierarchien)
|
||||
- [Der systemische Interessenkonflikt](#der-systemische-interessenkonflikt)
|
||||
- [Handwerk statt Fließband: Mein Boutique-Ansatz](#handwerk-statt-fließband-mein-boutique-ansatz)
|
||||
- [Der Haken an der Sache (Ehrliche Analyse)](#der-haken-an-der-sache)
|
||||
- [Hebel für doppeltes Tempo](#hebel-für-doppeltes-tempo)
|
||||
- [FAQ](#faq)
|
||||
</details>
|
||||
|
||||
<H2 id="die-stille-post-der-agentur-hierarchien">Die "stille Post" der Agentur-Hierarchien</H2>
|
||||
|
||||
<Paragraph>
|
||||
In einer klassischen Full-Service-Agentur landet Ihr Wunsch zuerst beim Account Manager. Dieser gibt ihn an den Projektleiter weiter, der ein Ticket erstellt, welches schließlich einem oft überarbeiteten Junior-Entwickler zugewiesen wird. Jede dieser Stationen ist nicht nur ein potenzieller Flaschenhals, sondern eine massive Fehlerquelle.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Poor communication contributes to almost one-third of all project failures."
|
||||
author="Association for Project Management"
|
||||
isCompany={true}
|
||||
source="APM Study"
|
||||
sourceUrl="https://www.apm.org.uk"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Informationen gehen verloren, die Umsetzung dauert ewig und am Ende bezahlen Sie für Meetings und Ticket-Management statt für Output. Ich nenne das die <Marker>"Agentur-Steuer"</Marker>. Während dort noch über Prioritäten diskutiert wird, könnte Ihre neue Lösung bereits live sein.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramFlow
|
||||
id="agency-bottleneck"
|
||||
title="Der klassische Agentur-Flaschenhals"
|
||||
nodes={[
|
||||
{ id: "A", label: "Ihr Wunsch", style: "fill:#cbd5e1" },
|
||||
{ id: "B", label: "Account Manager", style: "fill:#fca5a5" },
|
||||
{ id: "C", label: "Projektleiter", style: "fill:#fca5a5" },
|
||||
{ id: "D", label: "Junior Dev", style: "fill:#fca5a5" },
|
||||
{ id: "E", label: "Live-Gang", style: "fill:#86efac" }
|
||||
]}
|
||||
edges={[
|
||||
{ from: "A", to: "B", label: "E-Mail" },
|
||||
{ from: "B", to: "C", label: "Meeting" },
|
||||
{ from: "C", to: "D", label: "Ticket" },
|
||||
{ from: "D", to: "E", label: "Wochen später" }
|
||||
]}
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H3 id="der-systemische-interessenkonflikt">Der systemische Interessenkonflikt</H3>
|
||||
|
||||
<Paragraph>
|
||||
Es gibt einen tieferen Grund für die Trägheit: Das Geschäftsmodell. Wenn Agenturen nach abrechenbaren Stunden arbeiten und große Teams finanzieren müssen, fehlt oft der ökonomische Anreiz zur radikalen Vereinfachung. Langsame Prozesse bedeuten oft mehr abrechenbare Projektmanagement-Zeit.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Laut einer Studie des [McKinsey Global Institute](https://www.mckinsey.com) können ineffiziente Prozesse die organisationale Produktivität um **20-35%** senken. In der Welt der Webentwicklung korreliert dies direkt mit dem [ROI von Clean Code](/blog/clean-code-for-business-value). Wer technische Altlasten anhäuft, zahlt mit der Zeit seiner Markteinführung.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="25%"
|
||||
label="der Zeit verbringen Marketing-Agenturen mit administrativen Aufgaben"
|
||||
source="Deloitte"
|
||||
sourceUrl="https://www2.deloitte.com"
|
||||
/>
|
||||
|
||||
<H2 id="handwerk-statt-fließband-mein-boutique-ansatz">Handwerk statt Fließband: Mein Boutique-Ansatz</H2>
|
||||
|
||||
<Paragraph>
|
||||
In meiner Welt gibt es keine Junioren, an die Arbeit "durchgereicht" wird. Wenn Sie mit mir arbeiten, sprechen Sie direkt mit dem Experten, der die Architektur entwirft und den Code schreibt. Ich betrachte Softwareentwicklung nicht als anonymen Fließband-Job, sondern als <Marker>digitales Kunsthandwerk</Marker>.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Dieser Ansatz erlaubt Flexibilität, die für starre Strukturen unmöglich ist. Wir vermeiden den typischen [Vendor Lock-In](/blog/builder-systems-threaten-independence), indem wir auf eine [Digital Longevity Architektur](/blog/digital-longevity-architecture) setzen, die nicht nur heute funktioniert, sondern auch in fünf Jahren noch wartbar ist.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="cmm" captions="Kleine Textänderung dauert 14 Tage|Agentur-Prozesse sind auf Effizienz optimiert" />
|
||||
</div>
|
||||
|
||||
<H3 id="der-haken-an-der-sache">Der Haken an der Sache (Ehrliche Analyse)</H3>
|
||||
|
||||
<Paragraph>
|
||||
Natürlich hat radikale Direktheit auch ihren Preis. In einem Boutique-Setup gibt es keine 24/7 Support-Hotline mit 50 Call-Center-Mitarbeitern.
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem cross>
|
||||
<strong>Keine Massenabfertigung:</strong> Ich betreue nur eine handvoll Projekte gleichzeitig. Wenn ich voll bin, bin ich voll.
|
||||
</IconListItem>
|
||||
<IconListItem cross>
|
||||
<strong>Hoher Anspruch an den Kunden:</strong> Direktheit erfordert schnelle Entscheidungswege auf Ihrer Seite.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Asset statt Kosten:</strong> Sie bauen echtes geistiges Eigentum auf, statt eine "Miet-Website" zu führen.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2 id="hebel-für-doppeltes-tempo">Hebel für doppeltes Tempo</H2>
|
||||
|
||||
<Paragraph>
|
||||
Um die Zeitfresser zu eliminieren, die laut Gartner bis zu **50% Nacharbeit** durch schlecht definierte Workflows verursachen, nutze ich eine hochmoderne Toolchain:
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<Carousel items={[
|
||||
{
|
||||
title: "Vorschau-Deployments",
|
||||
content: "Jede Änderung wird sofort auf einer isolierten Preview-URL visualisiert. Sie geben Feedback direkt am lebenden Objekt, statt in statischen PDFs."
|
||||
},
|
||||
{
|
||||
title: "Headless CMS Integration",
|
||||
content: "Durch die Entkopplung von Inhalten und Design können Redakteure Texte ändern, ohne dass ein Entwickler eingreifen muss. Das spart die 1.5 bis 3 Stunden, die Agenturen laut Accenture für kleinste Modifikationen ansetzen."
|
||||
},
|
||||
{
|
||||
title: "Automatisierte QA",
|
||||
content: "Statt manueller Klick-Tests schützen automatisierte Test-Suiten die Integrität Ihrer Seite bei jedem Release."
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ein Blick auf die Realität zeigt: Während traditionelle IT-Serviceprovider laut Gartner rund 1,5 Stunden für eine Routine-Textkorrektur benötigen (inkl. Overhead), zielt mein System auf <Marker>Zero-Friction Updates</Marker> ab.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramGantt
|
||||
tasks={[
|
||||
{ id: "agency", name: "Agentur-Workflow (Wochen)", start: "2024-01-01", duration: "4w" },
|
||||
{ id: "mintel", name: "Mintel-Architektur (Tage)", start: "2024-01-01", duration: "1w" }
|
||||
]}
|
||||
title="Time-to-Market Vergleich"
|
||||
id="speed-comparison"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Echte Geschwindigkeit beginnt dort, wo Hierarchie endet</H2>
|
||||
|
||||
<Paragraph>
|
||||
Technologische Exzellenz bedeutet für mich, Ihnen keine Zeit zu stehlen. Wer heute eine Vision hat, sollte sie übermorgen am Markt testen können – nicht erst nach dem dritten Priorisierungs-Meeting der Agentur.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Wenn Sie bereit sind, technische Altlasten hinter sich zu lassen und in eine Architektur zu investieren, die als echter Business-Accelerator fungiert, sollten wir sprechen. In meinem [Google PageSpeed Guide](/blog/google-pagespeed-guide-warum-ladezeit-ihr-wichtigster-b2b-umsatzhebel-ist) erfahren Sie zudem, warum diese Geschwindigkeit auch Ihr Ranking massiv beeinflusst.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Digital-Audit anfragen"
|
||||
description="Lassen Sie uns Ihre aktuellen Prozesse und Ihre Architektur analysieren. Wir finden die Bremsen, die Ihren digitalen Erfolg verhindern."
|
||||
buttonText="Jetzt Effizienz-Check buchen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Warum dauert eine Änderung bei Agenturen überhaupt so lange?</H3>
|
||||
<Paragraph>Es liegt oft an manuellen Deployment-Prozessen und mehrstufigen Freigabeketten über Account- und Projektmanager. Jede Schnittstelle addiert Wartezeit und potenzielle Kommunikationsfehler.</Paragraph>
|
||||
|
||||
<H3>Was ist der Vorteil von Boutique-Architekten gegenüber Großagenturen?</H3>
|
||||
<Paragraph>Sie erhalten direkten Zugriff auf Senior-Expertise ohne den Overhead von Junioren oder Management-Ebenen. Das resultiert in präziserem Code und deutlich schnelleren Iterationszyklen.</Paragraph>
|
||||
|
||||
<H3>Ist dieser schnelle Ansatz riskanter für die Stabilität?</H3>
|
||||
<Paragraph>Im Gegenteil. Durch moderne CI/CD-Pipelines und automatisierte Tests wird menschliches Versagen minimiert, was die Stabilität im Vergleich zu manuellen Agentur-Prozessen massiv erhöht.</Paragraph>
|
||||
</FAQSection>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="iRbK9YK4C6E" title="Echte Change Management Strategien für effiziente Teams" />
|
||||
</div>
|
||||
166
apps/web/content/blog/why-no-templates-matter.mdx
Normal file
166
apps/web/content/blog/why-no-templates-matter.mdx
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: "Maßwerk statt Massenware: Warum Templates Ihre B2B-Markenidentität verwässern"
|
||||
thumbnail: "/blog/why-no-templates-matter.png"
|
||||
description: "Erfahren Sie, warum Standard-Templates die Silent Killer Ihrer Conversion sind und wie Bespoke-Architektur digitale Distinktion und messbaren ROI schafft."
|
||||
date: "2026-01-28"
|
||||
tags: ["design", "strategy", "performance"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Vorlagen sind die Fast-Food-Lösung des Web-Designs: Schnell verfügbar, oberflächlich sättigend, aber auf Dauer ungesund für Ihre Marken-Autorität.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
In meiner Arbeit als Digital Architect begegne ich ständig Unternehmen, die in der Beliebigkeit von Standard-Templates versinken und sich wundern, warum ihre digitale Präsenz keine Durchschlagskraft entwickelt.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Ich zeige Ihnen, warum eine <Marker>Zero-Template-Architektur</Marker> nicht nur eine Design-Entscheidung ist, sondern der einzige Weg zu echter digitaler Distinktion und langfristiger Unabhängigkeit.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="my-8">
|
||||
<TableOfContents />
|
||||
</div>
|
||||
|
||||
<H2>TL;DR: Das Wichtigste in 30 Sekunden</H2>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Differenzierung:</strong> Templates erzeugen eine "digitale Uniform", die den Wiedererkennungswert im B2B-Sektor gegen Null senkt.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Performance:</strong> Maßgeschneiderte Seiten eliminieren "Unused Code", was laut Google die Time to Interactive (TTI) massiv verbessert.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>ROI:</strong> Bespoke-Lösungen sind digitale Assets (IP), während Templates oft technische Sackgassen mit hohen Anpassungskosten sind.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<H2>Die Falle der visuellen Gleichschaltung</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wenn Sie ein Template nutzen, nutzen Sie die gleiche Basis wie tausende andere Unternehmen weltweit. Das <ExternalLink href="https://httparchive.org/">HTTP Archive</ExternalLink> zeigt, dass der Median heutiger Websites über 1MB JavaScript überträgt – ein Großteil davon ist ungenutzter Ballast aus starren Vorlagen.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Das Ergebnis ist eine "digitale Uniform", die Ihre Einzigartigkeit im Keim erstickt. Kunden spüren unbewusst, wenn eine Seite "von der Stange" kommt. Studien des <ExternalLink href="https://www.nngroup.com/">Nielsen Norman Group</ExternalLink> betonen, dass Usability und ein individuelles Design entscheidend für das Vertrauen sind. Ein generisches Design kann dieses Vertrauen schleichend erodieren.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="spongebob" captions="WIR BRAUCHEN EIN EINZIGARTIGES BRANDING!|Nutzt ein 50€ WordPress-Template" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Ich nenne das <Marker>ästhetische Kapitulation</Marker>. Wahre Markenbildung braucht Raum zum Atmen und ein Fundament, das nur für Sie gegossen wurde. Forrester Research hat herausgefunden, dass User Experience ein kritischer Differenzierer ist – eine Vorlage kann diese Einzigartigkeit niemals vollumfänglich abbilden.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="bespoke-vs-template" title="Architektur-Entscheidung" showShare={true}>
|
||||
graph TD
|
||||
Need["Marken-Botschaft"] --> Path["Strategie"]
|
||||
Path --> Temp["Fertig-Template"]
|
||||
Path --> Custom["Mintel Bespoke Design"]
|
||||
Temp --> Bland["Visuelle Beliebigkeit"]
|
||||
Custom --> Distinct["Maximale Distinktion"]
|
||||
Bland --> NoTrust["Vertrauensverlust"]
|
||||
Distinct --> Authority["Marken-Autorität"]
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<H2>Boutique-Design: Jedes Pixel hat einen Zweck</H2>
|
||||
|
||||
<Paragraph>
|
||||
Templates enthalten Code für hunderte Optionen, die Sie nie nutzen werden. Dieser [technische Overhead](/blog/hidden-costs-of-wordpress-plugins) verlangsamt Ihre Seite und verwässert Ihre Botschaft. Lighthouse-Audits markieren ungenutztes CSS und JS regelmäßig als primäre Performance-Killer.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="User experience is a key differentiator for brands, implying that a templated website may not provide the unique experience necessary to stand out in a competitive market."
|
||||
author="Forrester Research"
|
||||
isCompany={true}
|
||||
source="Forrester Reports"
|
||||
sourceUrl="https://www.forrester.com"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
In meinem Boutique-Ansatz entwickeln wir jede Komponente von Grund auf. Das Ergebnis ist eine <Marker>hochpräzise Maschine</Marker>, die exakt auf Ihre Ziele ausgerichtet ist. Dies korreliert direkt mit den Core Web Vitals: Ein schlankeres System führt zu einem besseren First Input Delay (FID), was die Nutzererfahrung spürbar reaktiver macht.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Technischer Maßanzug vs. Einheitsgröße</H3>
|
||||
|
||||
<Paragraph>
|
||||
Templates sind starr. Wenn Ihr Business wächst, wird das [Baukasten-System oft zur Wachstumsbremse](/blog/builder-systems-threaten-independence). Anpassungen am Standard-Code sind oft teurer als ein kompletter Neubau, da man gegen die Architektur der Vorlage arbeiten muss.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<ArchitectureBuilder />
|
||||
</div>
|
||||
|
||||
<H2>Der wirtschaftliche Case: Performance als Hebel</H2>
|
||||
|
||||
<Paragraph>
|
||||
Daten von Deloitte zeigen, dass eine Verbesserung der Ladezeit um nur 0,1 Sekunden die Conversion-Rate im B2B-Bereich signifikant steigern kann. Ein "Built-First" Ansatz ermöglicht es uns, Performance-Metriken zu erreichen, die mit schweren Templates physikalisch unmöglich sind.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="8.4%"
|
||||
label="Anstieg der Conversion-Rate pro 0,1s Ladezeit-Optimierung"
|
||||
source="Deloitte Digital"
|
||||
sourceUrl="https://www2.deloitte.com/ie/en/pages/technology-media-and-telecommunications/articles/speed-up-your-business.html"
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Zudem vermeiden Sie den schleichenden Wertverlust Ihrer digitalen Präsenz. Ein eigenes System ist ein [digitales Asset](/blog/digital-longevity-architecture), das in Ihrer Bilanz als geistiges Eigentum (IP) steht, statt nur eine Mietgebühr für eine fremde Plattform zu sein.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der "Devil's Advocate": Wann ein Template doch reicht</H2>
|
||||
|
||||
<Paragraph>
|
||||
Ehrlichkeit gehört zu einer professionellen Architektur-Beratung. Bespoke-Design ist nicht für jeden die richtige Wahl.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Wann ist Bespoke sinnvoll?"
|
||||
negativeLabel="Template reicht, wenn..."
|
||||
negativeText="Ein Proof-of-Concept für 2 Wochen benötigt wird, Budget unter 5k€ liegt oder Design völlig irrelevant ist."
|
||||
positiveLabel="Bespoke ist Pflicht, wenn..."
|
||||
positiveText="Marken-Hoheit, Core Web Vitals, Skalierbarkeit und technisches SEO über den Markterfolg entscheiden."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<H2>Integrierte Marken-Expertise</H2>
|
||||
|
||||
<Paragraph>
|
||||
Wie man Branding und technische Umsetzung erfolgreich verheiratet, zeigt dieser Case Study Einblick:
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<YouTubeEmbed videoId="vWskTB7IDsQ" title="B2B Branding & Website Case Study" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Ein Unikat für Ihren Erfolg</H2>
|
||||
|
||||
<Paragraph>
|
||||
Hören Sie auf, Ihre Marke in fremde Formen zu pressen. Ein Template ist kein Design, sondern ein Kompromiss. In einem Markt, der immer kompetitiver wird, ist [technische Qualität ein massiver Skalierungsfaktor](/blog/clean-code-for-business-value).
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Lassen wir gemeinsam ein digitales Denkmal setzen, das so unverwechselbar ist wie Ihr unternehmerischer Fingerabdruck.
|
||||
</Paragraph>
|
||||
|
||||
<LeadMagnet
|
||||
title="Digitale Unverwechselbarkeit prüfen"
|
||||
description="Ist Ihre Website ein echtes Markenzeichen oder nur ein austauschbares Template? Wir analysieren Design-Authentizität und technische Performance."
|
||||
buttonText="Jetzt Audit anfragen"
|
||||
href="/contact"
|
||||
variant="performance"
|
||||
/>
|
||||
|
||||
<FAQSection>
|
||||
<H3>Ist eine individuelle Website nicht viel teurer als ein Template?</H3>
|
||||
<Paragraph>Initial ist die Investition höher, aber die Total Cost of Ownership (TCO) ist oft geringer, da teure Workarounds und Plugin-Abhängigkeiten entfallen. Zudem transformiert es Marketingausgaben in ein echtes Unternehmensinventar (IP).</Paragraph>
|
||||
|
||||
<H3>Verschlechtert ein individuelles Design mein Ranking bei Google?</H3>
|
||||
<Paragraph>Im Gegenteil: Durch das Fehlen von Code-Bloat erreichen Bespoke-Websites deutlich bessere Core Web Vitals Scores. Dies führt zu einer besseren Indexierung und bevorzugten Platzierung gegenüber langsamen Vorlagen-Seiten.</Paragraph>
|
||||
|
||||
<H3>Wie lange dauert die Entwicklung einer Bespoke-Lösung?</H3>
|
||||
<Paragraph>Ein strukturiertes Projekt dauert typischerweise 8 bis 12 Wochen. Durch meinen modularen "Component-First" Ansatz erhalten Sie jedoch schneller ein fertiges System als bei komplexen Anpassungen an widerspenstigen Templates.</Paragraph>
|
||||
</FAQSection>
|
||||
159
apps/web/content/blog/why-websites-break-after-updates.mdx
Normal file
159
apps/web/content/blog/why-websites-break-after-updates.mdx
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: "Warum Ihre Website nach Updates nicht mehr funktioniert: Strategien gegen den Software-Zerfall"
|
||||
thumbnail: "/blog/why-websites-break-after-updates.png"
|
||||
description: "Fehlerhafte Layouts und kaputte Formulare nach Updates kosten B2B-Vertrauen. Erfahren Sie, wie automatisierte Regressionstests und Headless-Architektur Ihre Website stabilisieren."
|
||||
date: "2026-02-11"
|
||||
tags: ["maintenance", "reliability", "software-engineering"]
|
||||
---
|
||||
|
||||
<TLDR>
|
||||
Updates führen bei Standard-Systemen oft zu visuellem Chaos und funktionalen Fehlern. Durch den Einsatz von Automated Testing, Visual Regression und Immutable Deployments eliminieren wir das Risiko von Regressions-Bugs. Stabilität ist kein Zufall, sondern das Ergebnis einer [Clean Code Strategie](/blog/clean-code-for-business-value).
|
||||
</TLDR>
|
||||
|
||||
<TableOfContents />
|
||||
|
||||
<LeadParagraph>
|
||||
"Nach dem letzten Plugin-Update war plötzlich das halbe Layout verschoben."
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Das ist der Satz, den ich am häufigsten von Neukunden höre, die von klassischen WordPress-Agenturen zu mir wechseln.
|
||||
</LeadParagraph>
|
||||
<LeadParagraph>
|
||||
Für mich ist eine Website ein technisches Präzisionswerkzeug. Ein Werkzeug darf niemals einfach "auseinanderfallen", nur weil eine Komponente aktualisiert wurde. <Marker>Stabilität ist kein glücklicher Zufall</Marker>, sondern das Ergebnis eines kompromisslosen Engineering-Systems.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Die Entropie des Webs: Warum Systeme zerfallen</H2>
|
||||
|
||||
<Paragraph>
|
||||
Das Internet ist eine extrem dynamische Umgebung. Browser-Updates, neue Sicherheitsstandards und API-Änderungen nagen permanent an der Integrität Ihrer Website. In herkömmlichen monolithischen Systemen sind die Komponenten oft wie ein wackeliger Stapel Lego-Steine angeordnet.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<ArticleMeme template="fine" captions="WIR HABEN NUR EIN\nPLUGIN UPDATE GEMACHT|DIE GANZE WEBSITE\nBRENNT JETZT" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Zieht man einen Stein heraus – etwa durch ein scheinbar harmloses Update eines Drittanbieter-Scripts – gerät das gesamte Konstrukt ins Wanken. Research von Catchpoint Systems legt nahe, dass genau diese <ExternalLink href="https://www.catchpoint.com/">Third-Party Updates für ca. 18% aller Performance-Probleme</ExternalLink> verantwortlich sind, die neue Nutzer direkt abschrecken.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote
|
||||
quote="Nearly 30% of website outages or performance degradation are caused by flawed updates or deployments, impacting the experience of new and existing users alike."
|
||||
author="Akamai"
|
||||
isCompany={true}
|
||||
source="Akamai Survey"
|
||||
sourceUrl="https://www.akamai.com"
|
||||
translated={false}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Ich baue Architekturen, die diesem Zerfall <Marker>aktiv widersteht</Marker>. Dies ist ein Kernaspekt der [Digital Longevity](/blog/digital-longevity-architecture), bei der wir Systeme so konzipieren, dass sie über ein Jahrzehnt wartungsarm bleiben.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Das Sicherheitsnetz: Automatisierung statt Hoffnung</H2>
|
||||
|
||||
<Paragraph>
|
||||
Die meisten Fehler entstehen durch manuelle Eingriffe oder das Übersehen von Seiteneffekten. Ein Entwickler ändert das Design auf einer Unterseite und merkt nicht, dass dadurch das Kontaktformular auf einer anderen bricht. Laut Google Developers erleben etwa <Marker>20% aller Websites visuelle Regressionen</Marker> nach Code-Updates.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramFlow
|
||||
nodes={[
|
||||
{ id: "A", label: "Code Update" },
|
||||
{ id: "B", label: "Auto-QA Tests", style: "fill:#3b82f6;color:#fff" },
|
||||
{ id: "C", label: "Visual Snapshots" },
|
||||
{ id: "D", label: "Safe Deploy", style: "fill:#22c55e;color:#fff" },
|
||||
{ id: "E", label: "Stop & Fix", style: "fill:#ef4444;color:#fff" }
|
||||
]}
|
||||
edges={[
|
||||
{ from: "A", to: "B" },
|
||||
{ from: "B", to: "C" },
|
||||
{ from: "C", to: "D", label: "Match 100%" },
|
||||
{ from: "C", to: "E", label: "Diff detected" }
|
||||
]}
|
||||
title="Mintel CI/CD Pipeline"
|
||||
id="flow-safety"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
In meiner Welt gibt es solche Fehler nicht. Ich investiere in automatisierte Wächter. Bevor eine Änderung live geht, prüft eine künstliche Instanz jedes Detail Ihrer gesamten Website. Firmen, die auf umfassende Test-Strategien setzen, reduzieren ihre Bugs nach dem Deployment um bis zu 60%.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber
|
||||
value="5-10x"
|
||||
label="höhere Kosten bei Fehlern im Live-Betrieb vs. Entwicklung"
|
||||
source="Capers Jones"
|
||||
sourceUrl="https://namcookanalytics.com/"
|
||||
/>
|
||||
|
||||
<H2>Die "Fortress-Mentalität": Drei Schichten der Sicherheit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Sorgen Sie sich nie wieder darum, ob Ihre Seite "das Wochenende überlebt hat". Mein Stabilitäts-System umfasst drei entscheidende Schutzschichten, die weit über das hinausgehen, was [Standard-Baukasten-Systeme](/blog/builder-systems-threaten-independence) leisten können:
|
||||
</Paragraph>
|
||||
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Visual Regression Testing:</strong> Mein System vergleicht nach jeder Änderung tausende Bildpunkte. Die Maschine sieht Pixel-Abweichungen sofort, die das menschliche Auge übersehen würde.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Immutable Deployments:</strong> Ich überschreibe niemals Live-Dateien ("In-Place"). Stattdessen wird eine neue Version parallel hochgefahren. Wir können so in Millisekunden auf eine saubere Kopie zurückrollen.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Entkoppelte Modul-Logik:</strong> Durch eine [Headless-Architektur](/blog/maintenance-for-headless-systems) baue ich in isolierten Komponenten. Eine Änderung an der Blog-Logik kann prinzipiell niemals den Checkout oder die Kontakt-API gefährden.
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<Paragraph>
|
||||
Laut HTTP Archive führen CI/CD-Pipelines mit automatisierten Tests zu einer <Marker>40% schnelleren Deployment-Frequenz</Marker>. Das bedeutet für Sie: Wir können schneller innovieren, ohne die bestehende Substanz zu gefährden.
|
||||
</Paragraph>
|
||||
|
||||
<LoadTimeSimulator />
|
||||
|
||||
<H3>Warum B2B-Entscheider auf Stabilität setzen müssen</H3>
|
||||
|
||||
<Paragraph>
|
||||
Instabile Nutzeroberflächen sind kein kosmetisches Problem, sondern ein Conversion-Killer. Die Nielsen Norman Group fand heraus, dass inkonsistente User Experiences zu einem <Marker>Abfall der Conversion-Rate um 5-10%</Marker> führen. Nutzer assoziieren eine kaputte Website sofort mit mangelnder Professionalität im Kerngeschäft.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="15%|Fehlerquote|bei neuen Deployments ohne Tests~60%|Bug Reduktion|durch automatisierte Wächter~30%|Kostenersparnis|durch frühe Defekterkennung" />
|
||||
|
||||
<LeadMagnet
|
||||
title="Technische Altlasten-Check"
|
||||
description="Wird Ihre Website bei jedem Update zum Risiko? Wir analysieren Ihre Architektur auf Instabilitäten und zeigen Wege zu einem wartungsfreien System auf."
|
||||
buttonText="Jetzt Audit anfragen"
|
||||
href="/contact"
|
||||
variant="security"
|
||||
/>
|
||||
|
||||
<H2>Fazit: Ihre digitale Ruhe ist mein Auftrag</H2>
|
||||
|
||||
<Paragraph>
|
||||
Souveränität im Netz beginnt bei der Verlässlichkeit der eigenen Werkzeuge. Wenn Sie [technische Altlasten](/blog/slow-loading-costs-customers) mitschleppen, wird jedes Update zum Glücksspiel. Ein professionelles Engineering-Setup verwandelt Ihre Website von einem Sorgenkind in ein robustes digitales Asset, das den Wert Ihres Unternehmens steigert.
|
||||
</Paragraph>
|
||||
|
||||
<ComparisonRow
|
||||
description="Vergleich der Wartungsphilosophie"
|
||||
negativeLabel="Legacy WordPress/Monolith"
|
||||
negativeText="Updates direkt am Live-Server, manuelle Tests auf 'Gut Glück', hohe Fehleranfälligkeit."
|
||||
positiveLabel="Mintel Built-First"
|
||||
positiveText="Automatisierte QA-Pipelines, visuelle Vergleiche vor Release, 100% Rollback-Sicherheit."
|
||||
showShare={true}
|
||||
/>
|
||||
|
||||
<Paragraph>
|
||||
Lassen wir die Zeit der "kaputten Layouts" ein für alle Mal beenden. <Marker>Stabilität ist die Basis für Vertrauen</Marker>. Ihr Erfolg im B2B-Markt verdient dieses solide Fundament.
|
||||
</Paragraph>
|
||||
|
||||
<H2>FAQ</H2>
|
||||
<FAQSection>
|
||||
<H3>Warum gehen Websites nach Plugin-Updates oft kaputt?</H3>
|
||||
<Paragraph>Meist kollidieren neue Code-Versionen mit anderen Plugins oder dem Theme, da keine isolierte Testumgebung vorhanden ist. Ohne automatisierte Regressionstests werden diese Seiteneffekte erst bemerkt, wenn Kunden sich beschweren.</Paragraph>
|
||||
|
||||
<H3>Was ist Visual Regression Testing?</H3>
|
||||
<Paragraph>Dabei vergleicht eine Software Screenshots der Website vor und nach einer Änderung pixelgenau. Jede ungewollte Verschiebung im Layout wird sofort markiert und stoppt den Live-Gang automatisch.</Paragraph>
|
||||
|
||||
<H3>Lohnt sich der Aufwand für automatisierte Tests finanziell?</H3>
|
||||
<Paragraph>Absolut. Da Fehler in der Produktion laut Capers Jones bis zu 10x teurer zu fixen sind als in der Entwicklung, amortisiert sich das Testing-Setup meist schon nach dem ersten verhinderten Ausfall.</Paragraph>
|
||||
</FAQSection>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
2
apps/web/ignore-css.js
Normal file
2
apps/web/ignore-css.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const Module = require("module");
|
||||
Module._extensions[".css"] = function () {};
|
||||
12
apps/web/ignore-css.mjs
Normal file
12
apps/web/ignore-css.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export async function load(url, context, nextLoad) {
|
||||
if (url.endsWith('.css') || url.endsWith('.scss')) {
|
||||
return {
|
||||
format: 'module',
|
||||
shortCircuit: true,
|
||||
source: 'export default {};'
|
||||
};
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
13
apps/web/instrumentation.ts
Normal file
13
apps/web/instrumentation.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
@@ -25,7 +25,7 @@ const envExtension = {
|
||||
* Extends the default Mintel environment schema.
|
||||
*/
|
||||
export const envSchema = withMintelRefinements(
|
||||
z.object(mintelEnvSchema).extend(envExtension),
|
||||
z.object(mintelEnvSchema).extend(envExtension) as any,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
11
apps/web/mdx-components.tsx
Normal file
11
apps/web/mdx-components.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// import type { MDXComponents } from 'mdx/types'; // Commented out due to resolution issue
|
||||
import { mdxComponents as registryComponents } from './src/content-engine/registry';
|
||||
|
||||
export const MDXComponents = registryComponents;
|
||||
|
||||
export function useMDXComponents(components: any): any {
|
||||
return {
|
||||
...components,
|
||||
...registryComponents,
|
||||
};
|
||||
}
|
||||
54
apps/web/migrate-docs.ts
Normal file
54
apps/web/migrate-docs.ts
Normal file
@@ -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();
|
||||
34
apps/web/migrate-drafts.ts
Normal file
34
apps/web/migrate-drafts.ts
Normal file
@@ -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();
|
||||
@@ -1,32 +1,42 @@
|
||||
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 = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
serverExternalPackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/payload-ai',
|
||||
'@mintel/pdf',
|
||||
'canvas',
|
||||
'sharp',
|
||||
'puppeteer',
|
||||
'require-in-the-middle',
|
||||
'import-in-the-middle' // Sentry 10+ instrumentation dependencies
|
||||
],
|
||||
images: {
|
||||
loader: 'custom',
|
||||
loaderFile: './src/utils/imgproxy-loader.ts',
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.your-objectstorage.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'fsn1.your-objectstorage.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
const umamiUrl =
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
process.env.UMAMI_SCRIPT_URL ||
|
||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ||
|
||||
"https://analytics.infra.mintel.me";
|
||||
const glitchtipUrl = process.env.SENTRY_DSN
|
||||
? new URL(process.env.SENTRY_DSN).origin
|
||||
: "https://errors.infra.mintel.me";
|
||||
|
||||
return [
|
||||
{
|
||||
source: "/stats/:path*",
|
||||
destination: `${umamiUrl}/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/errors/:path*",
|
||||
destination: `${glitchtipUrl}/:path*`,
|
||||
},
|
||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
||||
// Sentry relay handled in app/errors/api/relay/route.ts
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
@@ -38,6 +48,10 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
outputFileTracingRoot: path.join(dirname, '../../'),
|
||||
};
|
||||
|
||||
export default withMintelConfig(nextConfig);
|
||||
const withMDX = createMDX({
|
||||
// Add markdown plugins here, as desired
|
||||
});
|
||||
export default withPayload(withMintelConfig(withMDX(nextConfig)));
|
||||
|
||||
408
apps/web/optimized_output.md
Normal file
408
apps/web/optimized_output.md
Normal file
@@ -0,0 +1,408 @@
|
||||
📄 Reading: /Users/marcmintel/Projects/mintel.me/apps/web/content/blog/why-pagespeed-fails.mdx
|
||||
|
||||
✍️ Per-section AI refinement...
|
||||
Refining section 1/8... ✓
|
||||
Refining section 2/8... ✓
|
||||
Refining section 3/8... ✓
|
||||
Refining section 4/8... ✓
|
||||
Refining section 5/8... ✓
|
||||
Refining section 6/8... ✓
|
||||
Refining section 7/8... ✓
|
||||
Refining section 8/8... ✓
|
||||
→ 8 sections processed
|
||||
|
||||
🚀 Running content engine optimization...
|
||||
🚀 Optimizing existing content (additive mode)...
|
||||
📖 Loaded 42158 chars of docs context
|
||||
📋 Content has 40 sections
|
||||
🔍 Identifying research topics...
|
||||
📚 Researching: Correlation between website loading speed (Core Web Vitals) and user bounce rate/conversion rates., The financial impact of slow websites on businesses, specifically in terms of marketing investment ROI and customer acquisition costs., Quantitative evidence supporting the claim that a 0.1-second improvement in load time leads to an 8.4% conversion increase, as cited from Deloitte Digital.
|
||||
🔎 Researching: Correlation between website loading speed (Core Web Vitals) and user bounce rate/conversion rates.
|
||||
📋 Research Plan: {
|
||||
trendsKeywords: [
|
||||
'Core Web Vitals',
|
||||
'Largest Contentful Paint',
|
||||
'Bounce rate',
|
||||
'Conversion rate optimization',
|
||||
'Website speed'
|
||||
],
|
||||
dcVariables: []
|
||||
}
|
||||
🔎 Researching: The financial impact of slow websites on businesses, specifically in terms of marketing investment ROI and customer acquisition costs.
|
||||
📋 Research Plan: {
|
||||
trendsKeywords: [
|
||||
'website speed ROI',
|
||||
'page load time vs conversion',
|
||||
'core web vitals business impact',
|
||||
'customer acquisition cost ecommerce',
|
||||
'marketing spend waste site speed'
|
||||
],
|
||||
dcVariables: []
|
||||
}
|
||||
🔎 Researching: Quantitative evidence supporting the claim that a 0.1-second improvement in load time leads to an 8.4% conversion increase, as cited from Deloitte Digital.
|
||||
📋 Research Plan: {
|
||||
trendsKeywords: [
|
||||
'Deloitte Digital page speed',
|
||||
'Milliseconds Make Money',
|
||||
'load time conversion rate',
|
||||
'e-commerce performance impact',
|
||||
'Core Web Vitals conversion'
|
||||
],
|
||||
dcVariables: []
|
||||
}
|
||||
📝 Planning fact insertions for 18 facts...
|
||||
→ 5 fact enrichments planned
|
||||
🧩 Planning component additions...
|
||||
→ 3 component additions planned
|
||||
|
||||
🔧 Applying 8 insertions to original content...
|
||||
|
||||
✅ Content Engine Complete!
|
||||
📚 18 facts researched
|
||||
📊 0 diagrams generated
|
||||
|
||||
🧹 Sanitizing MDX...
|
||||
|
||||
📋 Generating table of contents...
|
||||
→ 7 sections found
|
||||
→ TOC already present, skipping insertion
|
||||
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
📝 OPTIMIZED CONTENT (dry-run):
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
---
|
||||
title: "Warum Ihre Website bei Google PageSpeed scheitert"
|
||||
description: "Millisekunden entscheiden über Ihren Umsatz: So optimieren Sie Ihre Web-Performance für maximale Conversion."
|
||||
date: "2026-02-15"
|
||||
tags: ["performance", "seo"]
|
||||
---
|
||||
|
||||
<LeadParagraph>
|
||||
Unternehmen verbrennen Millionen für visuellen Glanz, während ihr
|
||||
technisches Fundament bröckelt wie ein Altbau ohne Wartung.
|
||||
</LeadParagraph>
|
||||
|
||||
<div className="not-prose my-10 p-6 bg-slate-50 border border-slate-200 rounded-2xl">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Inhaltsverzeichnis</p>
|
||||
<nav className="space-y-1 text-sm font-sans">
|
||||
|
||||
- [Der unsichtbare Umsatz-Verschleiß](#der-unsichtbare-umsatz-verschlei)
|
||||
- [Warum klassische Lösungen scheitern](#warum-klassische-loesungen-scheitern)
|
||||
- [Meine Architektur der Geschwindigkeit](#meine-architektur-der-geschwindigkeit)
|
||||
- [Die drei Säulen meiner Umsetzung](#die-drei-saeulen-meiner-umsetzung)
|
||||
- [Der wirtschaftliche Case](#der-wirtschaftliche-case)
|
||||
- [Wann meine Architektur für Sie Sinn macht](#wann-meine-architektur-fuer-sie-sinn-macht)
|
||||
- [Fazit: Respekt vor der Zeit Ihrer Nutzer](#fazit-respekt-vor-der-zeit-ihrer-nutzer)
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<LeadParagraph>
|
||||
Scheitert Ihre Website bei Google PageSpeed, verlieren Sie{" "}
|
||||
<Marker>53% Ihrer Besucher in unter drei Sekunden</Marker> – lange bevor sie
|
||||
Ihr Angebot überhaupt sehen. Laut <ExternalLink href="https://web.dev/vitals/">Google's Core Web Vitals</ExternalLink> und <ExternalLink href="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/">aktuellen Benchmarks</ExternalLink> entscheidet sich in diesen ersten Momenten, ob Ihr Unternehmen als professionell oder dilettantisch wahrgenommen wird.
|
||||
</LeadParagraph>
|
||||
|
||||
<LeadParagraph>
|
||||
Als Digital Architect betrachte ich Performance nicht als Feature – sie ist
|
||||
das architektonische Fundament, auf dem digitale Exzellenz erst entsteht.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>Der unsichtbare Umsatz-Verschleiß</H2>
|
||||
<Paragraph>
|
||||
Stellen Sie sich ein Luxusgeschäft in Bestlage vor – perfekte Auslage, aber die Eingangstür klemmt. Kunden müssen 10 Sekunden stemmen, um einzutreten. Genau das passiert auf tausenden Websites: Nur dass dort Millisekunden über Millionenumsätze entscheiden.
|
||||
</Paragraph>
|
||||
|
||||
<BoldNumber value="8.4%" label="Conversion-Steigerung pro 0,1 Sekunde schnellere Ladezeit" source="Deloitte Digital" sourceUrl="https://www2.deloitte.com/ie/en/pages/consulting/articles/milliseconds-make-millions.html" />
|
||||
|
||||
<Paragraph>
|
||||
Google bewertet Websites heute <Marker>primär nach Core Web Vitals</Marker> – präzise Messgrößen für die Frustrationstoleranz Ihrer Nutzer. Wer hier versagt, wird vom Algorithmus unsichtbar gemacht. Seit 2021 ist Page Experience ein <ExternalLink href="https://developers.google.com/search/blog/2020/11/timing-for-page-experience">direkter Ranking-Faktor</ExternalLink> – schlechte Performance kostet Sie doppelt: erst Traffic, dann Conversions.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleQuote quote="Millisekunden machen Millionen — Schon eine Verbesserung der mobilen Ladezeit um 100ms kann die Conversion-Rate um bis zu 8,4 % steigern." author="Deloitte Digital Research" role="Digital Strategy Study" />
|
||||
|
||||
<StatsGrid stats="+32%|Bounce-Rate|bei 1→3 Sek Ladezeit~-20%|Conversion|pro Sekunde Verzögerung~90%|Bounce-Rate|bei 5 Sek Ladezeit" />
|
||||
|
||||
<Paragraph>
|
||||
Diese Zahlen sind nicht theoretisch – sie stammen direkt aus <ExternalLink href="https://developers.google.com/web/fundamentals/performance/why-performance-matters">Googles Datenanalyse von über 900.000 mobilen Werbekampagnen</ExternalLink>. <Marker>Jede zusätzliche Sekunde Ladezeit ab dem kritischen 5-Sekunden-Fenster kostet Sie 4,42% Ihrer Conversions</Marker> – bei einem Onlineshop mit 100.000€ Monatsumsatz sind das 4.420€ verbranntes Geld. Pro Monat. Pro Sekunde Verzögerung.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
In meinen 15 Jahren als Software Architect habe ich gesehen, wie Unternehmen Millionen in Marketing investieren, nur um diese Besucher durch schlechte Performance sofort wieder zu verlieren. Die Mathematik ist brutal: Von 1 auf 5 Sekunden Ladezeit steigt die Bounce-Rate von 32% auf <Marker>90% – neun von zehn potenziellen Kunden verschwinden</Marker>, bevor sie Ihr Angebot überhaupt sehen.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<ComparisonRow
|
||||
description="Der Impact von Geschwindigkeit auf Ihre Bilanz"
|
||||
negativeLabel="Langsames Legacy-System"
|
||||
negativeText="90% Bounce-Rate, erodierendes Markenvertrauen, teure Akquise ohne Ertrag"
|
||||
positiveLabel="Mintel High-Performance"
|
||||
positiveText="Maximale Conversion ab Tag 1, SEO-Vorsprung durch Architektur, begeisterte Nutzer als Multiplikatoren"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<H2>Warum klassische Lösungen scheitern</H2>
|
||||
<Paragraph>
|
||||
Die Ursache liegt in der Architektur: <Marker>"All-in-One"-Systeme wie WordPress laden durchschnittlich 2,3 MB JavaScript</Marker> – bevor überhaupt Ihr Content erscheint. In einer mobilen Welt mit instabilen 4G-Verbindungen (Ø 10-30 Mbit/s in Deutschland) ist das ein{" "}
|
||||
<Marker>architektonisches Todesurteil</Marker>. Jede Anfrage durchläuft denselben aufgeblähten Render-Zyklus.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<MemeCard template="drake" captions="Mehr RAM kaufen damit WordPress schneller wird|Einfach kein WordPress benutzen" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Dieses Problem lässt sich nicht durch stärkere Hardware lösen. Der Flaschenhals ist die Architektur selbst: Während traditionelle Systeme bei jedem Aufruf die Datenbank befragen und komplexe Render-Zyklen durchlaufen, setze ich auf radikale Vereinfachung.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="legacy-request-flow" title="Legacy CMS: Request-Flaschenhals" showShare={true}>
|
||||
{`graph LR
|
||||
A["Browser"] --> B["Server"]
|
||||
B --> C["DB Queries"]
|
||||
C --> D["Template Engine"]
|
||||
D --> E["HTML Rendering"]
|
||||
E --> F["3-5 Sek"]
|
||||
style F fill:#fca5a5,stroke:#dc2626`}
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<div className="my-12">
|
||||
<MetricBar label="WordPress Request Processing" value={85} max={100} color="red" unit="% CPU" />
|
||||
<MetricBar label="Static Site Request Processing" value={5} max={100} color="green" unit="% CPU" />
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Die messbaren Folgen dieser architektonischen Versäumnisse:
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="33%|der WordPress-Sites|bestehen Core Web Vitals~2.4s|mediane Ladezeit|WordPress mobil~0.9s|mediane Ladezeit|Statische Architekturen" />
|
||||
|
||||
<Paragraph>
|
||||
Was die Zahlen nicht zeigen: Die durchschnittliche WordPress-Site lädt <Marker>516 KB JavaScript</Marker> – komplexe "All-in-One" Themes erreichen oft über 1,83 MB. Ich baue Systeme, die mit 90% weniger JavaScript auskommen und dabei <ExternalLink href="https://web.dev/articles/vitals">bessere Interaction to Next Paint (INP) Werte</ExternalLink> liefern. <ExternalLink href="https://httparchive.org/reports/state-of-the-web">HTTP Archive</ExternalLink>
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Die Mathematik der Geschwindigkeit ist brutal präzise: <Marker>Websites, die Core Web Vitals erfüllen, haben 24% weniger Page Abandons</Marker> – das bedeutet faktisch ein Viertel weniger verschwendete Werbeausgaben. <ExternalLink href="https://blog.chromium.org/2020/05/introducing-web-vitals-essential-metrics.html">Chromiums Datenauswertung</ExternalLink> zeigt: Wer die Google-Schwellenwerte unterschreitet, verliert jeden vierten Besucher noch vor dem ersten Inhalt.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Der kategoriale Unterschied: Statische Architekturen liefern <ExternalLink href="https://jamstack.org/survey/2022/">vorbereitete HTML-Dateien direkt vom CDN</ExternalLink> – keine Datenbankabfragen, kein Server-Rendering, keine Wartezeit.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Meine Architektur der Geschwindigkeit</H2>
|
||||
|
||||
<Paragraph>
|
||||
Statt bei jeder Anfrage Seiten dynamisch zusammenzubauen, liefere ich <Marker>vorbereitete digitale Artefakte</Marker>. Mein Static-First Framework garantiert Server-Antwortzeiten unter 50ms — <Marker>unabhängig von der Last</Marker>. Ob 10 oder 10.000 gleichzeitige Besucher: Die Performance bleibt konstant. Das ist <Marker>Skalierbarkeit durch Design</Marker>, nicht durch teure Hardware.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<Mermaid id="static-request-flow" title="Static-First: Instant Delivery" showShare={true}>
|
||||
{`graph LR
|
||||
A["Browser"] --> B["CDN Edge"]
|
||||
B --> C["Statisches HTML"]
|
||||
C --> D["Sofort sichtbar"]
|
||||
style D fill:#86efac,stroke:#16a34a`}
|
||||
</Mermaid>
|
||||
</div>
|
||||
|
||||
<ImageText
|
||||
title="Der Edge-Vorteil: Daten dort, wo Ihre Nutzer sind"
|
||||
image="/blog-assets/edge-network.png"
|
||||
>
|
||||
<Paragraph>
|
||||
Anstatt Ihre Nutzer um die halbe Welt zu schicken, platziere ich Ihre Website auf einem globalen Edge-Netzwerk. Daten werden in Millisekunden aus dem Rechenzentrum geliefert, das physisch am nächsten liegt.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Das Ergebnis: <Marker>Minimale Latenz und instant-Feeling</Marker> – egal ob in Berlin, New York oder Tokio. Laut <ExternalLink href="https://www.cloudflare.com/learning/cdn/performance/">Cloudflare Performance-Studien</ExternalLink> reduziert Edge-Delivery die Latenz um durchschnittlich 60% gegenüber zentralen Servern.
|
||||
</Paragraph>
|
||||
</ImageText>
|
||||
|
||||
<Paragraph>
|
||||
<ExternalLink href="https://web.dev/articles/ttfb">Google's TTFB-Empfehlungen</ExternalLink> setzen die Grenze für exzellente Time-to-First-Byte bei 800ms. Ich unterbiete das um den Faktor 16.
|
||||
</Paragraph>
|
||||
|
||||
<H3>Die drei Säulen meiner Umsetzung</H3>
|
||||
<IconList>
|
||||
<IconListItem check>
|
||||
<strong>Zero-Computation am Edge:</strong> Static Site Generation platziert vorgerenderte Seiten auf globalen CDNs. Der Browser erhält HTML statt JavaScript-Berge – keine Rechenzeit, kein Parsing, keine Wartezeit.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Präzises Asset-Engineering:</strong> Tree-Shaking eliminiert toten Code. Ihr Kunde lädt 70% weniger JavaScript als bei klassischen Frameworks.
|
||||
</IconListItem>
|
||||
<IconListItem check>
|
||||
<strong>Next-Gen Media-Handling:</strong> Automatische AVIF/WebP-Auslieferung. 60% kleinere Bilder bei identischer Qualität (<ExternalLink href="https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4">Netflix Tech Blog</ExternalLink>).
|
||||
</IconListItem>
|
||||
</IconList>
|
||||
|
||||
<div className="my-16">
|
||||
<PremiumComparisonChart
|
||||
title="Time to First Byte: Der Realitäts-Check"
|
||||
subtitle="Gemessen an echten Mobilgeräten (Slow 4G Simulation)"
|
||||
items={[
|
||||
{ label: "WordPress Standard", value: 850, max: 1000, unit: "ms", color: "red", description: "Server muss Datenbank abfragen, PHP parsen, Plugins laden." },
|
||||
{ label: "Google Empfehlung", value: 200, max: 1000, unit: "ms", color: "blue", description: "Maximaler Wert für 'Gute' User Experience." },
|
||||
{ label: "Mintel Static Architecture", value: 50, max: 1000, unit: "ms", color: "green", description: "Vorgerendertes HTML direkt vom Edge CDN. Keine Backend-Logik." }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Die Daten zeigen unmissverständlich: <Marker>Standard-CMS starten das Rennen mit bleiernen Gewichten an den Füßen</Marker>. Meine Architektur eliminiert diese künstlichen Hürden an der Wurzel. Die folgende Sequenz zeigt den psychologischen Prozess während der Ladezeit:
|
||||
</Paragraph>
|
||||
|
||||
<Carousel items={[
|
||||
{
|
||||
title: "1. Klick",
|
||||
content: "Der Nutzer tippt auf Ihre Anzeige. Die Erwartungshaltung ist 'sofort'. Sein Gehirn schüttet Dopamin aus in Erwartung der Belohnung."
|
||||
},
|
||||
{
|
||||
title: "2. Lade-Lücke",
|
||||
content: "Weißer Bildschirm für 2 Sekunden. Das Dopamin kippt in Cortisol (Stress). Das Vertrauen in die Marke sinkt unterbewusst sofort."
|
||||
},
|
||||
{
|
||||
title: "3. Abbruch",
|
||||
content: "Bei Sekunde 3 schließt er den Tab. Das Budget für den Klick ist verbrannt. Der Kunde ist für immer verloren an den Wettbewerb."
|
||||
}
|
||||
]} />
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramSequence id="user-journey-sequence" title="Die kritischen 3 Sekunden: User Journey" showShare={true}>
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant B as Browser
|
||||
participant S as Server
|
||||
participant DB as Database
|
||||
|
||||
U->>B: Klick auf Anzeige
|
||||
Note over U: Erwartung: <1s
|
||||
B->>S: Request
|
||||
S->>DB: Query (500ms)
|
||||
DB->>S: Response
|
||||
S->>B: HTML (1200ms)
|
||||
Note over B: JavaScript laden...
|
||||
B->>U: Erste Inhalte (2800ms)
|
||||
Note over U: 53% bereits weg
|
||||
</DiagramSequence>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Dieser Moment ist die unsichtbare Schlacht um Umsatz. Unternehmen ignorieren diese Realität und installieren stattdessen noch mehr Plugins – eine Abwärtsspirale aus Komplexität und Frustration.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<MemeCard template="ds" captions="Noch ein WordPress-Plugin installieren|Die Ladezeit unter 3 Sekunden halten" />
|
||||
</div>
|
||||
|
||||
<BoldNumber value="5x" label="höhere Conversion-Rate bei 1-Sekunden-Ladezeit vs. 10 Sekunden" source="Portent" sourceUrl="https://www.portent.com/blog/analytics/research-site-speed-hurting-everyones-revenue.htm" />
|
||||
|
||||
<Paragraph>
|
||||
Der hidden ROI-Killer: <Marker>Eine 0,1-Sekunden-Verbesserung der mobilen Ladezeit steigert den durchschnittlichen Warenwert um 9,2%</Marker> – bei identischem Traffic und Marketing-Budget. <ExternalLink href="https://www2.deloitte.com/ie/en/pages/consulting/articles/milliseconds-make-millions.html">Deloittes Retail-Analyse</ExternalLink> beweist: Performance-Optimierung ist nicht Kostenfaktor, sondern der effizienteste Growth-Hebel den Sie haben.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Zahlen lügen nicht. Wer Performance vernachlässigt, sabotiert aktiv sein eigenes Wachstum. Ich baue keine Websites – ich konstruiere ökonomische Hebel.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
JavaScript ist die teuerste Ressource einer Website: Es muss heruntergeladen, dekomprimiert, geparst und ausgeführt werden. <Marker>1 MB JavaScript kostet auf mobilen Geräten 2-5 Sekunden mehr Verarbeitungszeit als 1 MB Bilder</Marker> (<ExternalLink href="https://v8.dev/blog/cost-of-javascript-2019">V8 Team</ExternalLink>). Deshalb setze ich auf Architekturen, die JavaScript nur dort laden, wo es wirklich gebraucht wird – nicht als Standard-Ballast.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Der wirtschaftliche Case</H2>
|
||||
<Paragraph>
|
||||
Baukästen wirken günstiger – bis Sie die Opportunitätskosten berechnen. Bei 5.000 € monatlichem Marketing-Budget und <Marker>30 % Conversion-Verlust durch Performance-Probleme</Marker> verbrennen Sie 18.000 € jährlich. Messbar. Vermeidbar.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Die Zahlen sind brutal eindeutig: <ExternalLink href="https://web.dev/case-studies/rakuten">Rakuten 24 steigerte den Revenue per Visitor um 53 %</ExternalLink> nach Core Web Vitals-Optimierung. Walmart verdoppelte die Conversion-Rate durch Reduktion der Ladezeit von 2 auf 1 Sekunde. Jede 100ms kostet Sie Kunden.
|
||||
</Paragraph>
|
||||
|
||||
<StatsGrid stats="+53%|Umsatz pro Besucher|Rakuten 24 nach CWV-Fix~2x|Conversion-Rate|Walmart: 2→1 Sek Ladezeit~75%|Ungenutztes JS|Durchschnittliche Website" />
|
||||
|
||||
<Paragraph>
|
||||
Was diese Cases verschweigen: <Marker>Performance-Verbesserungen wirken exponentiell auf Ihren Marketing-ROI</Marker>. Eine 0,1-Sekunden-Optimierung führt zu 1,3% mehr Engagement bei Lead-Gen-Sites und 2,1% besserer Conversion-Progression im Travel-Sektor. <ExternalLink href="https://developers.google.com/web/showcase/2016/google-iomai">Googles MILO-Studie</ExternalLink> dokumentiert: Speed ist der unsichtbare Conversion-Multiplikator.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Ein Detail, das Agenturen gerne verschweigen: <Marker>75% des typischen Website-JavaScript wird beim ersten Laden gar nicht verwendet</Marker>. WordPress-Plugins laden ganze Bibliotheken für einzelne Features. Ich baue so, dass nur das geladen wird, was Ihre Besucher tatsächlich brauchen – <ExternalLink href="https://httparchive.org/reports/state-of-javascript">HTTP Archive zeigt: durchschnittlich 500KB ungenutztes JavaScript pro Seite</ExternalLink>.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Der ROI maßgeschneiderter Architekturen liegt nicht in den Entwicklungskosten – sondern in der <Marker>Eliminierung struktureller Performance-Barrieren</Marker>, die Ihre Conversion-Rate täglich sabotieren. Während Sie schlafen.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Wann meine Architektur für Sie Sinn macht</H2>
|
||||
<Paragraph>
|
||||
Ich bin Partner für Unternehmen, deren Website <Marker>geschäftskritischer Umsatztreiber</Marker> ist – nicht Visitenkarte. Wenn jede Sekunde verzögerten Seitenaufbaus direkt Conversions kostet, wird technische Exzellenz zur Grundvoraussetzung, nicht zum Nice-to-have.
|
||||
</Paragraph>
|
||||
|
||||
<ArticleBlockquote>
|
||||
Der ROI liegt nicht in gesparten Entwicklungskosten, sondern in eliminierten Opportunitätskosten durch verlorene Conversions.
|
||||
</ArticleBlockquote>
|
||||
|
||||
<Paragraph>
|
||||
Diese Erkenntnis markiert den Wendepunkt. Wer Performance als bloße technische Kennzahl betrachtet, übersieht den wirtschaftlichen Hebel – und lässt Umsatzpotenzial auf dem Tisch liegen.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Die Datenlage ist eindeutig: Sites mit 5-Sekunden-Ladezeit haben <Marker>70% längere Sitzungsdauern</Marker> als solche mit 19 Sekunden. <ExternalLink href="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/">Think with Google</ExternalLink> Das bedeutet nicht nur mehr Conversions, sondern auch bessere Nutzersignale – ein sich selbst verstärkender Kreislauf, der Ihre organische Sichtbarkeit exponentiell steigert.
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph>
|
||||
Seit Googles "Page Experience" Update (2021) sind Core Web Vitals <Marker>harte Ranking-Faktoren</Marker>. Websites, die alle Schwellenwerte erfüllen, haben eine <Marker>24% geringere Abbruchrate</Marker>{" "}
|
||||
<ExternalLink href="https://web.dev/vitals-business-impact/">
|
||||
laut Google-Analyse
|
||||
</ExternalLink>
|
||||
{" "}— Performance ist SEO, nicht umgekehrt. Wer hier nachlässig ist, kämpft mit angezogener Handbremse.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<DiagramPie
|
||||
data={[
|
||||
{ label: "Core Web Vitals bestanden", value: 33 },
|
||||
{ label: "Core Web Vitals nicht bestanden", value: 67 }
|
||||
]}
|
||||
title="WordPress Sites: Core Web Vitals Status 2024"
|
||||
id="cwv-status-pie"
|
||||
showShare={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Der Teufelskreis wird perfekt: Langsame Sites verlieren organische Rankings durch schlechte Core Web Vitals, was <Marker>höhere Paid-Search-Kosten zur Kompensation verlorener SEO-Sichtbarkeit</Marker> erzwingt. <ExternalLink href="https://developers.google.com/search/docs/advanced/experience/page-experience">Google Search Central</ExternalLink> macht es unmissverständlich klar: Performance ist nicht mehr optional – es ist Ihre digitale Überlebensstrategie.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-12">
|
||||
<MemeCard template="gru" captions="50.000€ ins Rebranding investieren|Website mit 6 Sek Ladezeit launchen|Kunden bouncen vor dem ersten Scroll|Marketing-Budget verbrennt ohne ROI" />
|
||||
</div>
|
||||
|
||||
<H2>Fazit: Respekt vor der Zeit Ihrer Nutzer</H2>
|
||||
<Paragraph>
|
||||
Geschwindigkeit ist kein Feature – sie ist <Marker>Ausdruck von Respekt</Marker>. Jede eingesparte Sekunde Ladezeit signalisiert: "Deine Zeit ist wertvoll." Diese Haltung unterscheidet Premium-Erlebnisse von digitaler Beliebigkeit.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Ich verwandle Websites in präzise Wachstums-Maschinen: <Marker>messbar schnell, skalierbar stabil</Marker>. Performance-Exzellenz zahlt sich mehrfach aus – in Ladezeiten unter 2 Sekunden, Conversion-Steigerungen von 15-30% und <ExternalLink href="https://web.dev/vitals-business-impact/">nachweisbarem ROI durch Core Web Vitals</ExternalLink>, der sich in harten Kennzahlen niederschlägt.
|
||||
</Paragraph>
|
||||
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
📚 Research:
|
||||
1. As page load time goes from one second to three seconds, the probability of bounce increases 32%. [Google Developers]
|
||||
2. As page load time goes from one second to five seconds, the probability of bounce increases 90%. [Google Developers]
|
||||
3. A speed improvement of 0.1 seconds in mobile site speed can result in an 8.4% increase in conversion rates for retail sites and a 10.1% increase for travel sites. [Deloitte Digital]
|
||||
4. Websites that meet the Core Web Vitals thresholds are 24% less likely to have users abandon page loads. [Chromium Blog]
|
||||
5. Average conversion rates are 3x higher for eCommerce sites that load in 1 second compared to sites that load in 5 seconds. [Portent]
|
||||
6. The first five seconds of page-load time have the highest impact on conversion rates; after five seconds, the conversion rate drops by an average of 4.42% for each additional second of load time. [Portent]
|
||||
7. A one-second improvement in mobile load times can increase conversion rates by up to 27%, directly impacting the ROI of paid marketing traffic. [Google Developers / Akamai]
|
||||
8. Deloitte reported that a 0.1s improvement in mobile site speed led to a 9.1% increase in conversion rates for retail sites and a 10% increase for travel sites. [Deloitte Digital]
|
||||
9. Approximately 53% of mobile site visits are abandoned if pages take longer than 3 seconds to load, significantly increasing the effective Customer Acquisition Cost (CAC). [Google Data]
|
||||
10. The probability of bounce increases by 32% as page load time goes from 1 second to 3 seconds, and by 123% when load time increases to 10 seconds. [Google Think with Google]
|
||||
11. Slow site speed impacts organic visibility because Core Web Vitals (LCP, FID, CLS) are a direct ranking factor in Google's search algorithm, causing higher costs for paid search to compensate for lost organic traffic. [Google Search Central]
|
||||
12. A site that loads in 1 second has a conversion rate 3x higher than a site that loads in 5 seconds, effectively tripling the efficiency of marketing spend. [Portent]
|
||||
13. A speed improvement of 0.1 seconds was found to increase conversion rates by 8.4% for retail sites and 10.1% for travel sites. [Deloitte Digital]
|
||||
14. The same 0.1s decrease in mobile site load time resulted in an increase of average order value by 9.2% for retail consumers. [Deloitte Digital]
|
||||
15. In the luxury retail segment, a 0.1s improvement in speed correlated with an 8% increase in page views per session. [Google Developers]
|
||||
16. Reducing latency by 0.1s led to an 1.3% increase in customer engagement (page views) for lead generation websites. [Deloitte Digital]
|
||||
17. In the travel sector, a 0.1s improvement in load time led to a 2.1% increase in the progression of users from the search page to the product details page. [Google / MILO Study]
|
||||
18. For retail sites, the 0.1s speed boost was associated with a 5.2% decrease in bounce rate on product listing pages. [Deloitte / 55 Study]
|
||||
@@ -4,11 +4,13 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build --webpack",
|
||||
"dev": "pnpm run seed:context && next dev --webpack --hostname 0.0.0.0",
|
||||
"dev:native": "DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "node --import tsx --experimental-loader ./ignore-css.mjs ./seed-context.ts",
|
||||
"build": "payload generate:importmap && next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src scripts video",
|
||||
"test": "npm run test:links",
|
||||
"test": "echo \"No tests configured\"",
|
||||
"test:links": "tsx ./scripts/test-links.ts",
|
||||
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts",
|
||||
"generate-estimate": "tsx ./scripts/generate-estimate.ts",
|
||||
@@ -19,23 +21,41 @@
|
||||
"video:render:button": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
||||
"video:render:all": "npm run video:render:contact && npm run video:render:button",
|
||||
"pagespeed:test": "npx tsx ./scripts/pagespeed-sitemap.ts",
|
||||
"cms:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus.ts",
|
||||
"cms:push:staging": "../../scripts/sync-directus.sh push staging",
|
||||
"cms:pull:staging": "../../scripts/sync-directus.sh pull staging",
|
||||
"cms:push:testing": "../../scripts/sync-directus.sh push testing",
|
||||
"cms:pull:testing": "../../scripts/sync-directus.sh pull testing",
|
||||
"cms:push:prod": "../../scripts/sync-directus.sh push production",
|
||||
"cms:pull:prod": "../../scripts/sync-directus.sh pull production",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:og": "tsx scripts/check-og-images.ts",
|
||||
"check:forms": "tsx scripts/check-forms.ts",
|
||||
"cms:push:testing": "bash ./scripts/cms-sync.sh push testing",
|
||||
"cms:pull:testing": "bash ./scripts/cms-sync.sh pull testing",
|
||||
"cms:push:staging": "bash ./scripts/cms-sync.sh push staging",
|
||||
"cms:pull:staging": "bash ./scripts/cms-sync.sh pull staging",
|
||||
"cms:push:prod": "bash ./scripts/cms-sync.sh push prod",
|
||||
"cms:pull:prod": "bash ./scripts/cms-sync.sh pull prod",
|
||||
"db:restore": "bash ./scripts/restore-db.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@directus/sdk": "21.0.0",
|
||||
"@mintel/cloner": "^1.8.0",
|
||||
"@mintel/pdf": "^1.8.0",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@mintel/concept-engine": "link:../../../at-mintel/packages/concept-engine",
|
||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
|
||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||
"@mintel/payload-ai": "^1.9.15",
|
||||
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
|
||||
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@next/third-parties": "^16.1.6",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.1.0",
|
||||
"@opentelemetry/core": "^2.1.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@payloadcms/db-postgres": "^3.77.0",
|
||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||
"@payloadcms/next": "^3.77.0",
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/storage-s3": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@remotion/bundler": "^4.0.414",
|
||||
"@remotion/cli": "^4.0.414",
|
||||
@@ -53,51 +73,73 @@
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"clsx": "^2.1.1",
|
||||
"crawlee": "^3.15.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.12.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.11.0",
|
||||
"ioredis": "^5.9.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "^16.1.6",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"nodemailer": "^8.0.1",
|
||||
"payload": "^3.77.0",
|
||||
"playwright": "^1.58.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer": "^24.36.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-social-media-embed": "^2.5.18",
|
||||
"react-tweet": "^3.3.0",
|
||||
"recharts": "^3.7.0",
|
||||
"remotion": "^4.0.414",
|
||||
"replicate": "^1.4.0",
|
||||
"require-in-the-middle": "^8.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"shiki": "^1.24.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"webpack": "^5.96.1",
|
||||
"website-scraper": "^6.0.0",
|
||||
"website-scraper-puppeteer": "^2.0.0",
|
||||
"zod": "3.22.3"
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^10.0.0",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@mintel/cli": "^1.7.3",
|
||||
"@mintel/eslint-config": "^1.7.3",
|
||||
"@mintel/husky-config": "^1.7.3",
|
||||
"@mintel/next-config": "^1.7.3",
|
||||
"@mintel/next-utils": "^1.7.15",
|
||||
"@mintel/tsconfig": "^1.7.3",
|
||||
"@mintel/cli": "^1.9.0",
|
||||
"@mintel/eslint-config": "^1.9.0",
|
||||
"@mintel/husky-config": "^1.9.0",
|
||||
"@mintel/next-config": "^1.9.0",
|
||||
"@mintel/next-utils": "^1.9.0",
|
||||
"@mintel/tsconfig": "^1.9.0",
|
||||
"@next/eslint-plugin-next": "^16.1.6",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^25.0.6",
|
||||
"@types/nodemailer": "^7.0.10",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cheerio": "^1.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "10.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
"require-extensions": "^0.0.4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.54.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.infra.mintel.me:mmintel/mintel.me.git"
|
||||
}
|
||||
}
|
||||
|
||||
1015
apps/web/payload-types.ts
Normal file
1015
apps/web/payload-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
127
apps/web/payload.config.ts
Normal file
127
apps/web/payload.config.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { buildConfig } from "payload";
|
||||
// Triggering config re-analysis for blocks visibility - V4
|
||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
|
||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||
import { s3Storage } from "@payloadcms/storage-s3";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
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 { emailWebhookHandler } from "./src/payload/endpoints/emailWebhook";
|
||||
import { aiEndpointHandler } from "./src/payload/endpoints/aiEndpoint";
|
||||
import { Inquiries } from "./src/payload/collections/Inquiries";
|
||||
import { Redirects } from "./src/payload/collections/Redirects";
|
||||
import { ContextFiles } from "./src/payload/collections/ContextFiles";
|
||||
import { CrmAccounts } from "./src/payload/collections/CrmAccounts";
|
||||
import { CrmContacts } from "./src/payload/collections/CrmContacts";
|
||||
import { CrmInteractions } from "./src/payload/collections/CrmInteractions";
|
||||
import { CrmTopics } from "./src/payload/collections/CrmTopics";
|
||||
import { Projects } from "./src/payload/collections/Projects";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
const isCLI =
|
||||
process.argv.includes("migrate") ||
|
||||
process.argv.includes("generate:importmap");
|
||||
let aiPlugin: any;
|
||||
if (!isCLI) {
|
||||
const { payloadChatPlugin } = await import("@mintel/payload-ai");
|
||||
aiPlugin = payloadChatPlugin({
|
||||
enabled: true,
|
||||
mcpServers: [],
|
||||
});
|
||||
}
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
Users,
|
||||
Media,
|
||||
Posts,
|
||||
Inquiries,
|
||||
Redirects,
|
||||
ContextFiles,
|
||||
CrmAccounts,
|
||||
CrmContacts,
|
||||
CrmTopics,
|
||||
CrmInteractions,
|
||||
Projects,
|
||||
],
|
||||
globals: [
|
||||
/* AiSettings as any */
|
||||
],
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || "localhost",
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME || "user",
|
||||
pass: process.env.MAIL_PASSWORD || "pass",
|
||||
},
|
||||
...(process.env.MAIL_HOST ? {} : { ignoreTLS: true }),
|
||||
},
|
||||
}),
|
||||
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"),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString:
|
||||
process.env.DATABASE_URI || process.env.POSTGRES_URI || "",
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
? [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || "",
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(aiPlugin ? [aiPlugin] : []),
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
path: "/crm/incoming-email",
|
||||
method: "post",
|
||||
handler: emailWebhookHandler,
|
||||
},
|
||||
],
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user