Compare commits
27 Commits
v1.0.0-rc.
...
v1.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
| cd7be080d7 | |||
| 4e602da15d | |||
| e47982d394 | |||
| 877108020b | |||
| 0fff5ae52a | |||
| 459716d09c | |||
| a0d4023f89 | |||
| 9746416146 | |||
| fc9746335d | |||
| 4058abab13 | |||
| 6074747b34 | |||
| 319b2b3e0c | |||
| d7f5504149 | |||
| 0f705b474b | |||
| 67046b9301 | |||
| 0b6211cf5f | |||
| c7f2c3fdfe | |||
| f30c93ffce | |||
| 3e6bbe9a93 | |||
| c6cbb02dfa | |||
| bec1916ccc | |||
| ab17e9e758 | |||
| f257e5428f | |||
| 797411ccc3 | |||
| 94a609e438 | |||
| 409ac3fea7 | |||
| b3876666c8 |
@@ -85,10 +85,10 @@ jobs:
|
|||||||
|
|
||||||
# Standardize Traefik Rule
|
# Standardize Traefik Rule
|
||||||
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
||||||
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(`%s`)%s", $i, (i==NF?"":" || ")}')
|
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\"%s\")%s", $i, (i==NF?"":" || ")}')
|
||||||
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
||||||
else
|
else
|
||||||
TRAEFIK_RULE="Host(\`$TRAEFIK_HOST\`)"
|
TRAEFIK_RULE="Host(\"$TRAEFIK_HOST\")"
|
||||||
PRIMARY_HOST="$TRAEFIK_HOST"
|
PRIMARY_HOST="$TRAEFIK_HOST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -104,6 +104,39 @@ jobs:
|
|||||||
echo "short_sha=$SHORT_SHA"
|
echo "short_sha=$SHORT_SHA"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$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 | grep -v "next-utils" | cut -d'"' -f4 | sed 's/\^//; s/\~//' | sort -V | tail -1)
|
||||||
|
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||||
|
|
||||||
|
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||||
|
# 1. Discovery (Works without token for public repositories)
|
||||||
|
UPSTREAM_SHA=$(git ls-remote --tags https://git.infra.mintel.me/mmintel/at-mintel.git "$TAG_TO_WAIT" | grep "$TAG_TO_WAIT" | tail -n1 | awk '{print $1}')
|
||||||
|
|
||||||
|
if [[ -z "$UPSTREAM_SHA" ]]; then
|
||||||
|
echo "❌ Error: Tag $TAG_TO_WAIT not found in mmintel/at-mintel."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Tag verified: Found upstream SHA $UPSTREAM_SHA for $TAG_TO_WAIT"
|
||||||
|
|
||||||
|
# 2. Status Check (Requires GITEA_PAT for cross-repo API access)
|
||||||
|
POLL_TOKEN="${{ secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}"
|
||||||
|
|
||||||
|
if [[ -n "$POLL_TOKEN" ]]; then
|
||||||
|
echo "⏳ GITEA_PAT found. Checking upstream build status..."
|
||||||
|
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="$POLL_TOKEN" ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||||
|
else
|
||||||
|
echo "ℹ️ No GITEA_PAT secret found. Skipping build status wait (Actions API is restricted)."
|
||||||
|
echo " If this build fails, ensure that mmintel/at-mintel $TAG_TO_WAIT has finished its Docker build."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 2: QA (Lint, Typecheck, Test)
|
# JOB 2: QA (Lint, Typecheck, Test)
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -165,6 +198,8 @@ jobs:
|
|||||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||||
|
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
|
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||||
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
|
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
|
||||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache
|
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache
|
||||||
@@ -213,6 +248,10 @@ jobs:
|
|||||||
|
|
||||||
# Gatekeeper
|
# Gatekeeper
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
|
||||||
|
# Analytics
|
||||||
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -242,51 +281,59 @@ jobs:
|
|||||||
# Gatekeeper Origin
|
# Gatekeeper Origin
|
||||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
||||||
|
|
||||||
cat > .env.deploy << EOF
|
{
|
||||||
# Generated by CI - $TARGET
|
echo "# Generated by CI - $TARGET"
|
||||||
IMAGE_TAG=$IMAGE_TAG
|
echo "IMAGE_TAG=$IMAGE_TAG"
|
||||||
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
echo "NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL"
|
||||||
GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN
|
echo "GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN"
|
||||||
SENTRY_DSN=$SENTRY_DSN
|
echo "SENTRY_DSN=$SENTRY_DSN"
|
||||||
LOG_LEVEL=$LOG_LEVEL
|
echo "LOG_LEVEL=$LOG_LEVEL"
|
||||||
MAIL_HOST=$MAIL_HOST
|
echo "MAIL_HOST=$MAIL_HOST"
|
||||||
MAIL_PORT=$MAIL_PORT
|
echo "MAIL_PORT=$MAIL_PORT"
|
||||||
MAIL_USERNAME=$MAIL_USERNAME
|
echo "MAIL_USERNAME=$MAIL_USERNAME"
|
||||||
MAIL_PASSWORD=$MAIL_PASSWORD
|
echo "MAIL_PASSWORD=$MAIL_PASSWORD"
|
||||||
MAIL_FROM=$MAIL_FROM
|
echo "MAIL_FROM=$MAIL_FROM"
|
||||||
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
|
echo "MAIL_RECIPIENTS=$MAIL_RECIPIENTS"
|
||||||
|
echo ""
|
||||||
|
echo "# Directus"
|
||||||
|
echo "DIRECTUS_URL=$DIRECTUS_URL"
|
||||||
|
echo "DIRECTUS_HOST=$DIRECTUS_HOST"
|
||||||
|
echo "DIRECTUS_KEY=$DIRECTUS_KEY"
|
||||||
|
echo "DIRECTUS_SECRET=$DIRECTUS_SECRET"
|
||||||
|
echo "DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL"
|
||||||
|
echo "DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD"
|
||||||
|
echo "DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME"
|
||||||
|
echo "DIRECTUS_DB_USER=$DIRECTUS_DB_USER"
|
||||||
|
echo "DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD"
|
||||||
|
echo "DIRECTUS_DB_CLIENT=pg"
|
||||||
|
echo "DIRECTUS_DB_HOST=directus-db"
|
||||||
|
echo "DIRECTUS_DB_PORT=5432"
|
||||||
|
echo "DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN"
|
||||||
|
echo "INTERNAL_DIRECTUS_URL=http://directus:8055"
|
||||||
|
echo ""
|
||||||
|
echo "# Gatekeeper"
|
||||||
|
echo "GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD"
|
||||||
|
echo "AUTH_COOKIE_NAME=klz_gatekeeper_session"
|
||||||
|
echo "COOKIE_DOMAIN=$COOKIE_DOMAIN"
|
||||||
|
echo ""
|
||||||
|
echo "# Analytics"
|
||||||
|
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
||||||
|
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||||
|
echo ""
|
||||||
|
echo "TARGET=$TARGET"
|
||||||
|
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||||
|
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||||
|
printf 'TRAEFIK_HOST_RULE=%s\n' "$TRAEFIK_RULE"
|
||||||
|
echo "TRAEFIK_HOST=$TRAEFIK_HOST"
|
||||||
|
echo "ENV_FILE=$ENV_FILE"
|
||||||
|
echo "COMPOSE_PROFILES=$COMPOSE_PROFILES"
|
||||||
|
echo "AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE"
|
||||||
|
echo "AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED"
|
||||||
|
} > .env.deploy
|
||||||
|
|
||||||
# Directus
|
echo "--- Generated .env.deploy ---"
|
||||||
DIRECTUS_URL=$DIRECTUS_URL
|
cat .env.deploy
|
||||||
DIRECTUS_HOST=$DIRECTUS_HOST
|
echo "----------------------------"
|
||||||
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_DB_CLIENT=pg
|
|
||||||
DIRECTUS_DB_HOST=directus-db
|
|
||||||
DIRECTUS_DB_PORT=5432
|
|
||||||
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
|
|
||||||
INTERNAL_DIRECTUS_URL=http://directus:8055
|
|
||||||
|
|
||||||
# Gatekeeper
|
|
||||||
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
|
|
||||||
AUTH_COOKIE_NAME=klz_gatekeeper_session
|
|
||||||
COOKIE_DOMAIN=$COOKIE_DOMAIN
|
|
||||||
|
|
||||||
TARGET=$TARGET
|
|
||||||
SENTRY_ENVIRONMENT=$TARGET
|
|
||||||
PROJECT_NAME=$PROJECT_NAME
|
|
||||||
TRAEFIK_HOST_RULE='$TRAEFIK_RULE'
|
|
||||||
TRAEFIK_HOST=$TRAEFIK_HOST
|
|
||||||
ENV_FILE=$ENV_FILE
|
|
||||||
COMPOSE_PROFILES=$COMPOSE_PROFILES
|
|
||||||
AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE
|
|
||||||
AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: 🚀 SSH Deploy
|
- name: 🚀 SSH Deploy
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -320,11 +367,43 @@ jobs:
|
|||||||
run: docker builder prune -f --filter "until=1h"
|
run: docker builder prune -f --filter "until=1h"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 5: Notifications
|
# JOB 5: Smoke Test (OG Images)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
smoke_test:
|
||||||
|
name: 🧪 Smoke Test
|
||||||
|
needs: [prepare, deploy]
|
||||||
|
if: needs.deploy.result == 'success'
|
||||||
|
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: 🚀 Run OG Image Check
|
||||||
|
env:
|
||||||
|
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
|
run: pnpm run check:og
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# JOB 6: Notifications
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
notifications:
|
notifications:
|
||||||
name: 🔔 Notify
|
name: 🔔 Notify
|
||||||
needs: [prepare, deploy]
|
needs: [prepare, deploy, smoke_test]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ WORKDIR /app
|
|||||||
ARG NEXT_PUBLIC_BASE_URL
|
ARG NEXT_PUBLIC_BASE_URL
|
||||||
ARG NEXT_PUBLIC_TARGET
|
ARG NEXT_PUBLIC_TARGET
|
||||||
ARG DIRECTUS_URL
|
ARG DIRECTUS_URL
|
||||||
|
ARG UMAMI_WEBSITE_ID
|
||||||
|
ARG UMAMI_API_ENDPOINT
|
||||||
ARG NPM_TOKEN
|
ARG NPM_TOKEN
|
||||||
|
|
||||||
# Environment variables for Next.js build
|
# Environment variables for Next.js build
|
||||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
ENV DIRECTUS_URL=$DIRECTUS_URL
|
||||||
|
ENV UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||||
|
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||||
ENV CI=true
|
ENV CI=true
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string } }) {
|
export default async function Image({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ locale: string; slug: string }>;
|
||||||
|
}) {
|
||||||
|
const { locale, slug } = await params;
|
||||||
const pageData = await getPageBySlug(slug, locale);
|
const pageData = await getPageBySlug(slug, locale);
|
||||||
|
|
||||||
if (!pageData) {
|
if (!pageData) {
|
||||||
@@ -15,17 +20,14 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
|
|||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate
|
||||||
<OGImageTemplate
|
title={pageData.frontmatter.title}
|
||||||
title={pageData.frontmatter.title}
|
description={pageData.frontmatter.excerpt}
|
||||||
description={pageData.frontmatter.excerpt}
|
label="Information"
|
||||||
label="Information"
|
/>,
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { OGImageTemplate } from '@/components/OGImageTemplate';
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||||
|
|
||||||
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ locale: string }> },
|
{ params }: { params: Promise<{ locale: string }> },
|
||||||
) {
|
) {
|
||||||
const { searchParams, origin } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const slug = searchParams.get('slug');
|
const slug = searchParams.get('slug');
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ export async function GET(
|
|||||||
const featuredImage = product.frontmatter.images?.[0]
|
const featuredImage = product.frontmatter.images?.[0]
|
||||||
? product.frontmatter.images[0].startsWith('http')
|
? product.frontmatter.images[0].startsWith('http')
|
||||||
? product.frontmatter.images[0]
|
? product.frontmatter.images[0]
|
||||||
: `${origin}${product.frontmatter.images[0]}`
|
: `${SITE_URL}${product.frontmatter.images[0]}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { SITE_URL } from '@/lib/schema';
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({
|
export default async function Image({
|
||||||
params: { locale, slug },
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { locale: string; slug: string };
|
params: Promise<{ locale: string; slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
|
const { locale, slug } = await params;
|
||||||
const post = await getPostBySlug(slug, locale);
|
const post = await getPostBySlug(slug, locale);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
|||||||
@@ -5,21 +5,16 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
|
||||||
<OGImageTemplate
|
|
||||||
title={t('title')}
|
|
||||||
description={t('description')}
|
|
||||||
label="Blog"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
@@ -13,16 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale:
|
|||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate title={title} description={description} label="Contact" />,
|
||||||
<OGImageTemplate
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
label="Contact"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,20 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate
|
||||||
<OGImageTemplate
|
title={t('title')}
|
||||||
title={t('title')}
|
description={t('description')}
|
||||||
description={t('description')}
|
label="Reliable Energy Infrastructure"
|
||||||
label="Reliable Energy Infrastructure"
|
/>,
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
@@ -13,17 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale:
|
|||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate title={title} description={description} label="Products" />,
|
||||||
<OGImageTemplate
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
label="Products"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Team' });
|
const t = await getTranslations({ locale, namespace: 'Team' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|
||||||
@@ -13,17 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale:
|
|||||||
const description = t('meta.description') || t('hero.title');
|
const description = t('meta.description') || t('hero.title');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
<OGImageTemplate title={title} description={description} label="Our Team" />,
|
||||||
<OGImageTemplate
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
label="Our Team"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,24 +10,25 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# HTTP ⇒ HTTPS redirect
|
# HTTP ⇒ HTTPS redirect
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.rule=${TRAEFIK_HOST_RULE:-Host(\"klz-cables.com\")}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.entrypoints=web"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.entrypoints=web"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.middlewares=redirect-https"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.middlewares=redirect-https"
|
||||||
# HTTPS router (Standard)
|
# HTTPS router (Standard)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.rule=${TRAEFIK_HOST_RULE:-Host(\"klz-cables.com\")}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.entrypoints=websecure"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls.certresolver=le"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls.certresolver=le"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls=true"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.service=${PROJECT_NAME:-klz-cables}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.service=${PROJECT_NAME:-klz-cables}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
|
||||||
|
|
||||||
# HTTPS router (Unprotected - for Analytics & Errors)
|
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)} && PathPrefix(`/stats`, `/errors`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.rule=(${TRAEFIK_HOST_RULE:-Host(\"klz-cables.com\")}) && (PathPrefix(\"/health\", \"/sitemap.xml\", \"/robots.txt\", \"/manifest.webmanifest\", \"/api/og\") || PathPrefix(\"/de/opengraph-image\", \"/en/opengraph-image\", \"/de/blog/opengraph-image\", \"/en/blog/opengraph-image\", \"/de/products/opengraph-image\", \"/en/products/opengraph-image\") || PathRegexp(\"^/.*opengraph-image.*$\"))"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.entrypoints=websecure"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.tls.certresolver=le"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.tls.certresolver=le"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.tls=true"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.service=${PROJECT_NAME:-klz-cables}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.service=${PROJECT_NAME:-klz-cables}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.priority=2000"
|
||||||
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.port=3000"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.scheme=http"
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.scheme=http"
|
||||||
@@ -43,7 +44,7 @@ services:
|
|||||||
# Authentication Middleware (ForwardAuth)
|
# Authentication Middleware (ForwardAuth)
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME:-klz-cables}-gatekeeper:3000/gatekeeper/api/verify"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME:-klz-cables}-gatekeeper:3000/gatekeeper/api/verify"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||||
|
|
||||||
# Middleware Definitions
|
# Middleware Definitions
|
||||||
@@ -58,7 +59,7 @@ services:
|
|||||||
|
|
||||||
gatekeeper:
|
gatekeeper:
|
||||||
profiles: [ "gatekeeper" ]
|
profiles: [ "gatekeeper" ]
|
||||||
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.10
|
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
infra:
|
infra:
|
||||||
@@ -77,7 +78,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=(Host(`${TRAEFIK_HOST:-testing.klz-cables.com}`) && PathPrefix(`/gatekeeper`))"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=(Host(\"${TRAEFIK_HOST:-testing.klz-cables.com}\") && PathPrefix(\"/gatekeeper\"))"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.entrypoints=websecure"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls.certresolver=le"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls.certresolver=le"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls=true"
|
||||||
@@ -119,7 +120,7 @@ services:
|
|||||||
- ./directus/migrations:/directus/migrations
|
- ./directus/migrations:/directus/migrations
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.rule=Host(`${DIRECTUS_HOST}`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.rule=Host(\"${DIRECTUS_HOST}\")"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.entrypoints=websecure"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls.certresolver=le"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls.certresolver=le"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ const envExtension = {
|
|||||||
|
|
||||||
INFRA_DIRECTUS_URL: z.string().url().optional(),
|
INFRA_DIRECTUS_URL: z.string().url().optional(),
|
||||||
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
UMAMI_WEBSITE_ID: z.string().optional(),
|
||||||
|
UMAMI_API_ENDPOINT: z.string().optional(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,37 +6,41 @@ import { join } from 'path';
|
|||||||
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
||||||
*/
|
*/
|
||||||
export async function getOgFonts() {
|
export async function getOgFonts() {
|
||||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf');
|
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
|
||||||
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf');
|
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const boldFont = readFileSync(boldFontPath);
|
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||||
const regularFont = readFileSync(regularFontPath);
|
const boldFont = readFileSync(boldFontPath);
|
||||||
|
const regularFont = readFileSync(regularFontPath);
|
||||||
|
console.log(
|
||||||
|
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Inter',
|
name: 'Inter',
|
||||||
data: boldFont,
|
data: boldFont,
|
||||||
weight: 700 as const,
|
weight: 700 as const,
|
||||||
style: 'normal' as const,
|
style: 'normal' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Inter',
|
name: 'Inter',
|
||||||
data: regularFont,
|
data: regularFont,
|
||||||
weight: 400 as const,
|
weight: 400 as const,
|
||||||
style: 'normal' as const,
|
style: 'normal' as const,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load OG fonts from filesystem, falling back to system fonts:', error);
|
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common configuration for OG images
|
* Common configuration for OG images
|
||||||
*/
|
*/
|
||||||
export const OG_IMAGE_SIZE = {
|
export const OG_IMAGE_SIZE = {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ const intlMiddleware = createMiddleware({
|
|||||||
|
|
||||||
export default function middleware(request: NextRequest) {
|
export default function middleware(request: NextRequest) {
|
||||||
const { method, url, headers } = request;
|
const { method, url, headers } = request;
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// Explicit bypass for infrastructure routes to avoid locale redirects/interception
|
||||||
|
if (
|
||||||
|
pathname.startsWith('/stats') ||
|
||||||
|
pathname.startsWith('/errors') ||
|
||||||
|
pathname.startsWith('/health') ||
|
||||||
|
pathname.startsWith('/api/og') ||
|
||||||
|
pathname.includes('opengraph-image')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Build header object for logging
|
// Build header object for logging
|
||||||
const headerObj: Record<string, string> = {};
|
const headerObj: Record<string, string> = {};
|
||||||
@@ -62,5 +74,5 @@ export default function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// Match only internationalized pathnames
|
// Match only internationalized pathnames
|
||||||
matcher: ['/((?!api|_next|_vercel|stats|errors|health|.*\\..*).*)', '/', '/(de|en)/:path*'],
|
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)', '/', '/(de|en)/:path*'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -322,6 +322,15 @@ const nextConfig = {
|
|||||||
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||||
},
|
},
|
||||||
async rewrites() {
|
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';
|
||||||
|
|
||||||
const directusUrl = process.env.INTERNAL_DIRECTUS_URL || process.env.DIRECTUS_URL || 'https://cms.klz-cables.com';
|
const directusUrl = process.env.INTERNAL_DIRECTUS_URL || process.env.DIRECTUS_URL || 'https://cms.klz-cables.com';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -4,9 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "^21.0.0",
|
"@directus/sdk": "^21.0.0",
|
||||||
"@mintel/mail": "^1.7.10",
|
"@mintel/mail": "1.7.12",
|
||||||
"@mintel/next-config": "^1.7.10",
|
"@mintel/next-config": "1.7.12",
|
||||||
"@mintel/next-feedback": "^1.7.10",
|
"@mintel/next-feedback": "1.7.12",
|
||||||
"@mintel/next-utils": "^1.7.15",
|
"@mintel/next-utils": "^1.7.15",
|
||||||
"@react-email/components": "^1.0.7",
|
"@react-email/components": "^1.0.7",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
"@commitlint/cli": "^20.4.0",
|
"@commitlint/cli": "^20.4.0",
|
||||||
"@commitlint/config-conventional": "^20.4.0",
|
"@commitlint/config-conventional": "^20.4.0",
|
||||||
"@lhci/cli": "^0.15.1",
|
"@lhci/cli": "^0.15.1",
|
||||||
"@mintel/eslint-config": "^1.7.10",
|
"@mintel/eslint-config": "1.7.12",
|
||||||
"@mintel/tsconfig": "^1.7.10",
|
"@mintel/tsconfig": "1.7.12",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"test:og": "vitest run tests/og-image.test.ts",
|
"test:og": "vitest run tests/og-image.test.ts",
|
||||||
|
"check:og": "tsx scripts/check-og-images.ts",
|
||||||
"cms:branding:local": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
"cms:branding:local": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
||||||
"cms:branding:testing": "DIRECTUS_URL=https://cms.testing.klz-cables.com npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
"cms:branding:testing": "DIRECTUS_URL=https://cms.testing.klz-cables.com npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
||||||
"cms:branding:staging": "DIRECTUS_URL=https://cms.staging.klz-cables.com npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
"cms:branding:staging": "DIRECTUS_URL=https://cms.staging.klz-cables.com npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -15,14 +15,14 @@ importers:
|
|||||||
specifier: ^21.0.0
|
specifier: ^21.0.0
|
||||||
version: 21.1.0
|
version: 21.1.0
|
||||||
'@mintel/mail':
|
'@mintel/mail':
|
||||||
specifier: ^1.7.10
|
specifier: 1.7.12
|
||||||
version: 1.7.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.7.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@mintel/next-config':
|
'@mintel/next-config':
|
||||||
specifier: ^1.7.10
|
specifier: 1.7.12
|
||||||
version: 1.7.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0)
|
version: 1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0)
|
||||||
'@mintel/next-feedback':
|
'@mintel/next-feedback':
|
||||||
specifier: ^1.7.10
|
specifier: 1.7.12
|
||||||
version: 1.7.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
version: 1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
||||||
'@mintel/next-utils':
|
'@mintel/next-utils':
|
||||||
specifier: ^1.7.15
|
specifier: ^1.7.15
|
||||||
version: 1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)
|
version: 1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)
|
||||||
@@ -133,11 +133,11 @@ importers:
|
|||||||
specifier: ^0.15.1
|
specifier: ^0.15.1
|
||||||
version: 0.15.1
|
version: 0.15.1
|
||||||
'@mintel/eslint-config':
|
'@mintel/eslint-config':
|
||||||
specifier: ^1.7.10
|
specifier: 1.7.12
|
||||||
version: 1.7.10(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
version: 1.7.12(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@mintel/tsconfig':
|
'@mintel/tsconfig':
|
||||||
specifier: ^1.7.10
|
specifier: 1.7.12
|
||||||
version: 1.7.10
|
version: 1.7.12
|
||||||
'@tailwindcss/cli':
|
'@tailwindcss/cli':
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
@@ -1028,20 +1028,20 @@ packages:
|
|||||||
'@types/react': '>=16'
|
'@types/react': '>=16'
|
||||||
react: '>=16'
|
react: '>=16'
|
||||||
|
|
||||||
'@mintel/eslint-config@1.7.10':
|
'@mintel/eslint-config@1.7.12':
|
||||||
resolution: {integrity: sha512-77M3mi2BmjXf0hopQEL6jXsNC+x06McrqYMdNFbwDSLGg9fb7ebFFp+BWWn7zL8PlO2QO1DEEWMDm+eVasfTYQ==}
|
resolution: {integrity: sha512-ofX68JWCW8ztD9tt/1MDb6pSr9MJKq3js3Vny7VoT/bObjpR/iO9tJp0ekiq12Ps8VTEgDh1qwwmf2wrJJBRpQ==}
|
||||||
|
|
||||||
'@mintel/mail@1.7.10':
|
'@mintel/mail@1.7.12':
|
||||||
resolution: {integrity: sha512-ISfXEob8eVEoh8Lnsa2LlZ/42iJT3OXbZF4i8IvEXByFVEDe2OFEDUxtNMCgEz050OXTb0EdIGbR7AOiB7Q7kg==}
|
resolution: {integrity: sha512-2MqGSDhXQ6jaswUs/s74pz2LwmOhtOTWltGgwb8JF2JdfQt8FE5H4XZsvSV12Q1Fs1n5+V09OZTWU1TyFmh6lw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.0.0
|
react: ^19.0.0
|
||||||
react-dom: ^19.0.0
|
react-dom: ^19.0.0
|
||||||
|
|
||||||
'@mintel/next-config@1.7.10':
|
'@mintel/next-config@1.7.12':
|
||||||
resolution: {integrity: sha512-ip+7CTjiU5YwoezBkgfPRZLwOcVB4VQs2xFZYmwNDGPQgu1q/1dDD+TiU7PVVRL9PsxHgghzpBZstSEDoubNEA==}
|
resolution: {integrity: sha512-GBFIgF2vRzPl03B2RwaEyBpwjXgDRHUtaal4QQ2n7TW4G1lNIKLTBLdsJtmxMn8VlUbzRVMaFNm7m6joSNiJ0w==}
|
||||||
|
|
||||||
'@mintel/next-feedback@1.7.10':
|
'@mintel/next-feedback@1.7.12':
|
||||||
resolution: {integrity: sha512-WgENMSWp9AcWZ4lIN8p7xSp8UiABbSyY5Hch9XUg+ZPBxeClWGjl6typkqjhz7YsMAgj0DYKlLtri9cYy71suQ==}
|
resolution: {integrity: sha512-nlaeV+IRmqwzaAZFTmj8+RvyMsvh+SNs0BopWgbDdAt2x1yoz4fC6cpn+v7KjfnVW0YWPZhMeGD/uEgMhVrXRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.0.0
|
react: ^19.0.0
|
||||||
react-dom: ^19.0.0
|
react-dom: ^19.0.0
|
||||||
@@ -1049,8 +1049,8 @@ packages:
|
|||||||
'@mintel/next-utils@1.7.15':
|
'@mintel/next-utils@1.7.15':
|
||||||
resolution: {integrity: sha512-CqSe3eHamq9zLs+AJxGOPypTLchw/oZ3JcLkor007PcUDMTv/Lspfv5oCaXK2s0FeIOJaa2QwSGPDI1h5/3ZVw==}
|
resolution: {integrity: sha512-CqSe3eHamq9zLs+AJxGOPypTLchw/oZ3JcLkor007PcUDMTv/Lspfv5oCaXK2s0FeIOJaa2QwSGPDI1h5/3ZVw==}
|
||||||
|
|
||||||
'@mintel/tsconfig@1.7.10':
|
'@mintel/tsconfig@1.7.12':
|
||||||
resolution: {integrity: sha512-3PtaPA0UYitv21ND3/BGAn9WCqfrOjllbr4/Cb+XCTJFQYp2sacBfoSTzE/4RV9TnEIBpAZlgINT1WrvIvN6pQ==}
|
resolution: {integrity: sha512-WGs/p2E1xQGkzNasLCZKoplKIhxC17NZRhBYH5O43lp98aHOZMC3BKgNeLYUfoEFGiIN1hx2FUJ69DosQc0xmw==}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||||
@@ -7473,7 +7473,7 @@ snapshots:
|
|||||||
'@types/react': 19.2.13
|
'@types/react': 19.2.13
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
'@mintel/eslint-config@1.7.10(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@mintel/eslint-config@1.7.12(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/eslintrc': 3.3.3
|
'@eslint/eslintrc': 3.3.3
|
||||||
'@eslint/js': 9.39.2
|
'@eslint/js': 9.39.2
|
||||||
@@ -7490,13 +7490,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@mintel/mail@1.7.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@mintel/mail@1.7.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
'@mintel/next-config@1.7.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0)':
|
'@mintel/next-config@1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry/nextjs': 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0)
|
'@sentry/nextjs': 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0)
|
||||||
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
||||||
@@ -7519,7 +7519,7 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- webpack
|
- webpack
|
||||||
|
|
||||||
'@mintel/next-feedback@1.7.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)':
|
'@mintel/next-feedback@1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/sdk': 21.1.0
|
'@directus/sdk': 21.1.0
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@@ -7557,7 +7557,7 @@ snapshots:
|
|||||||
- sass
|
- sass
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@mintel/tsconfig@1.7.10': {}
|
'@mintel/tsconfig@1.7.12': {}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
public/fonts/Inter-Bold.woff
Normal file
BIN
public/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
public/fonts/Inter-Regular.woff
Normal file
BIN
public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
74
scripts/check-og-images.ts
Normal file
74
scripts/check-og-images.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { SITE_URL } from '../lib/schema.js';
|
||||||
|
|
||||||
|
const BASE_URL = process.env.TEST_URL || SITE_URL;
|
||||||
|
|
||||||
|
console.log(`\n🚀 Starting OG Image Verification for ${BASE_URL}\n`);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
'/de/opengraph-image',
|
||||||
|
'/en/opengraph-image',
|
||||||
|
'/de/blog/opengraph-image',
|
||||||
|
'/de/api/og/product?slug=nay2y',
|
||||||
|
'/en/api/og/product?slug=medium-voltage-cables',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function verifyImage(path: string): Promise<boolean> {
|
||||||
|
const url = `${BASE_URL}${path}`;
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
console.log(`Checking ${url}...`);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (!contentType?.includes('image/png')) {
|
||||||
|
const body = await response.text();
|
||||||
|
console.log(` Headers: ${JSON.stringify(Object.fromEntries(response.headers))}`);
|
||||||
|
throw new Error(
|
||||||
|
`Content-Type: ${contentType}. Body preview: ${body.substring(0, 500).replace(/\n/g, ' ')}...`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
// PNG Signature: 89 50 4E 47 0D 0A 1A 0A
|
||||||
|
if (bytes[0] !== 0x89 || bytes[1] !== 0x50 || bytes[2] !== 0x4e || bytes[3] !== 0x47) {
|
||||||
|
throw new Error('Invalid PNG signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.length < 10000) {
|
||||||
|
throw new Error(`Image too small (${bytes.length} bytes), likely blank`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✅ OK (${bytes.length} bytes, ${duration}ms)`);
|
||||||
|
return true;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error(` ❌ FAILED:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
let allOk = true;
|
||||||
|
for (const route of routes) {
|
||||||
|
const ok = await verifyImage(route);
|
||||||
|
if (!ok) allOk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allOk) {
|
||||||
|
console.log('\n✨ All OG images verified successfully!\n');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.error('\n⚠️ Some OG images failed verification.\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
Reference in New Issue
Block a user