Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a7cf6a77 | |||
| cd7be080d7 | |||
| 4e602da15d | |||
| e47982d394 | |||
| 877108020b | |||
| 0fff5ae52a | |||
| 459716d09c | |||
| a0d4023f89 | |||
| 9746416146 | |||
| fc9746335d | |||
| 4058abab13 | |||
| 6074747b34 | |||
| 319b2b3e0c | |||
| d7f5504149 |
@@ -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
|
||||||
|
|
||||||
@@ -199,7 +199,6 @@ jobs:
|
|||||||
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_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
NEXT_PUBLIC_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' }}
|
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 }}
|
||||||
@@ -252,7 +251,6 @@ jobs:
|
|||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
NEXT_PUBLIC_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' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -283,56 +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
|
|
||||||
|
|
||||||
# Analytics
|
|
||||||
UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
|
||||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
|
||||||
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
|
||||||
|
|
||||||
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
|
||||||
@@ -366,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:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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_WEBSITE_ID
|
||||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
|
||||||
ARG UMAMI_API_ENDPOINT
|
ARG UMAMI_API_ENDPOINT
|
||||||
ARG NPM_TOKEN
|
ARG NPM_TOKEN
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ 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_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
|
||||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
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,26 +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}.priority=1000"
|
|
||||||
- "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.priority=2000"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-public.service=${PROJECT_NAME:-klz-cables}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.service=${PROJECT_NAME:-klz-cables}"
|
- "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}-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.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"
|
||||||
@@ -79,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"
|
||||||
@@ -121,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"
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ function createConfig() {
|
|||||||
|
|
||||||
analytics: {
|
analytics: {
|
||||||
umami: {
|
umami: {
|
||||||
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || env.UMAMI_WEBSITE_ID,
|
websiteId: env.UMAMI_WEBSITE_ID,
|
||||||
apiEndpoint: env.UMAMI_API_ENDPOINT,
|
apiEndpoint: env.UMAMI_API_ENDPOINT,
|
||||||
enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || env.UMAMI_WEBSITE_ID),
|
enabled: Boolean(env.UMAMI_WEBSITE_ID),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
15
lib/env.ts
15
lib/env.ts
@@ -25,8 +25,21 @@ const envExtension = {
|
|||||||
|
|
||||||
// Analytics
|
// Analytics
|
||||||
UMAMI_WEBSITE_ID: z.string().optional(),
|
UMAMI_WEBSITE_ID: z.string().optional(),
|
||||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
|
|
||||||
UMAMI_API_ENDPOINT: z.string().optional(),
|
UMAMI_API_ENDPOINT: z.string().optional(),
|
||||||
|
|
||||||
|
// Mail Configuration
|
||||||
|
MAIL_HOST: z.string().optional(),
|
||||||
|
MAIL_PORT: z.coerce.number().optional(),
|
||||||
|
MAIL_USERNAME: z.string().optional(),
|
||||||
|
MAIL_PASSWORD: z.string().optional(),
|
||||||
|
MAIL_FROM: z.string().optional(),
|
||||||
|
MAIL_RECIPIENTS: z.string().optional(),
|
||||||
|
|
||||||
|
// Directus Authentication
|
||||||
|
DIRECTUS_URL: z.string().url().optional(),
|
||||||
|
DIRECTUS_ADMIN_EMAIL: z.string().email().optional(),
|
||||||
|
DIRECTUS_ADMIN_PASSWORD: z.string().optional(),
|
||||||
|
DIRECTUS_API_TOKEN: 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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,15 +68,12 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
private async sendPayload(type: 'event', data: Record<string, any>) {
|
private async sendPayload(type: 'event', data: Record<string, any>) {
|
||||||
if (!this.options.enabled) return;
|
if (!this.options.enabled) return;
|
||||||
|
|
||||||
|
// On the client, we don't need the websiteId (it's injected by the server-side proxy handler).
|
||||||
|
// On the server, we need it because we're calling the Umami API directly.
|
||||||
const isClient = typeof window !== 'undefined';
|
const isClient = typeof window !== 'undefined';
|
||||||
const websiteId =
|
|
||||||
this.websiteId ||
|
|
||||||
(isClient ? (process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID as string) : undefined);
|
|
||||||
|
|
||||||
if (!websiteId) {
|
if (!isClient && !this.websiteId) {
|
||||||
this.logger.warn(
|
this.logger.warn('Umami tracking called on server but no Website ID configured');
|
||||||
`Umami tracking called on ${isClient ? 'client' : 'server'} but no Website ID configured`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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*'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -338,14 +338,6 @@ const nextConfig = {
|
|||||||
source: '/cms/:path*',
|
source: '/cms/:path*',
|
||||||
destination: `${directusUrl}/:path*`,
|
destination: `${directusUrl}/:path*`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/stats/:path*',
|
|
||||||
destination: `${umamiUrl}/:path*`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/errors/:path*',
|
|
||||||
destination: `${glitchtipUrl}/:path*`,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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();
|
||||||
@@ -9,7 +9,7 @@ if [ -z "$ENV" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//')
|
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//' | sed 's/-nextjs$//')
|
||||||
|
|
||||||
case $ENV in
|
case $ENV in
|
||||||
local)
|
local)
|
||||||
@@ -25,7 +25,10 @@ case $ENV in
|
|||||||
case $ENV in
|
case $ENV in
|
||||||
testing) PROJECT_NAME="${PRJ_ID}-testing" ;;
|
testing) PROJECT_NAME="${PRJ_ID}-testing" ;;
|
||||||
staging) PROJECT_NAME="${PRJ_ID}-staging" ;;
|
staging) PROJECT_NAME="${PRJ_ID}-staging" ;;
|
||||||
production) PROJECT_NAME="${PRJ_ID}-prod" ;;
|
production)
|
||||||
|
PROJECT_NAME="${PRJ_ID}-prod"
|
||||||
|
OLD_PROJECT_NAME="${PRJ_ID}com" # Fallback for legacy naming
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "📤 Uploading snapshot to $ENV..."
|
echo "📤 Uploading snapshot to $ENV..."
|
||||||
@@ -34,8 +37,16 @@ case $ENV in
|
|||||||
echo "🔍 Detecting remote container..."
|
echo "🔍 Detecting remote container..."
|
||||||
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus")
|
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus")
|
||||||
|
|
||||||
|
if [ -z "$REMOTE_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
||||||
|
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
||||||
|
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus")
|
||||||
|
if [ -n "$REMOTE_CONTAINER" ]; then
|
||||||
|
PROJECT_NAME=$OLD_PROJECT_NAME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$REMOTE_CONTAINER" ]; then
|
if [ -z "$REMOTE_CONTAINER" ]; then
|
||||||
echo "❌ Remote container for $ENV not found."
|
echo "❌ Remote container for $ENV not found (checked $PROJECT_NAME)."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user