diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b2fd93f7..ae25f5c9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -363,11 +363,43 @@ jobs: 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: name: 🔔 Notify - needs: [prepare, deploy] + needs: [prepare, deploy, smoke_test] if: always() runs-on: docker container: diff --git a/app/[locale]/[slug]/opengraph-image.tsx b/app/[locale]/[slug]/opengraph-image.tsx index da398edb..9bacaef4 100644 --- a/app/[locale]/[slug]/opengraph-image.tsx +++ b/app/[locale]/[slug]/opengraph-image.tsx @@ -5,7 +5,12 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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); if (!pageData) { @@ -15,17 +20,14 @@ export default async function Image({ params: { locale, slug } }: { params: { lo const fonts = await getOgFonts(); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } - diff --git a/app/[locale]/api/og/product/route.tsx b/app/[locale]/api/og/product/route.tsx index 290d241f..a7027ea8 100644 --- a/app/[locale]/api/og/product/route.tsx +++ b/app/[locale]/api/og/product/route.tsx @@ -5,13 +5,15 @@ import { OGImageTemplate } from '@/components/OGImageTemplate'; import { NextRequest } from 'next/server'; import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; +import { SITE_URL } from '@/lib/schema'; + export const runtime = 'nodejs'; export async function GET( request: NextRequest, { params }: { params: Promise<{ locale: string }> }, ) { - const { searchParams, origin } = new URL(request.url); + const { searchParams } = new URL(request.url); const slug = searchParams.get('slug'); const { locale } = await params; @@ -58,7 +60,7 @@ export async function GET( const featuredImage = product.frontmatter.images?.[0] ? product.frontmatter.images[0].startsWith('http') ? product.frontmatter.images[0] - : `${origin}${product.frontmatter.images[0]}` + : `${SITE_URL}${product.frontmatter.images[0]}` : undefined; return new ImageResponse( diff --git a/app/[locale]/blog/[slug]/opengraph-image.tsx b/app/[locale]/blog/[slug]/opengraph-image.tsx index 7c3138bd..21265c28 100644 --- a/app/[locale]/blog/[slug]/opengraph-image.tsx +++ b/app/[locale]/blog/[slug]/opengraph-image.tsx @@ -7,10 +7,11 @@ import { SITE_URL } from '@/lib/schema'; export const runtime = 'nodejs'; 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); if (!post) { diff --git a/app/[locale]/blog/opengraph-image.tsx b/app/[locale]/blog/opengraph-image.tsx index 4b0a8b47..d697076b 100644 --- a/app/[locale]/blog/opengraph-image.tsx +++ b/app/[locale]/blog/opengraph-image.tsx @@ -5,21 +5,16 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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 fonts = await getOgFonts(); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } diff --git a/app/[locale]/contact/opengraph-image.tsx b/app/[locale]/contact/opengraph-image.tsx index cf664684..e770e6ba 100644 --- a/app/[locale]/contact/opengraph-image.tsx +++ b/app/[locale]/contact/opengraph-image.tsx @@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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 fonts = await getOgFonts(); @@ -13,16 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale: const description = t('meta.description') || t('subtitle'); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } diff --git a/app/[locale]/opengraph-image.tsx b/app/[locale]/opengraph-image.tsx index f1fdab6e..2d074099 100644 --- a/app/[locale]/opengraph-image.tsx +++ b/app/[locale]/opengraph-image.tsx @@ -5,22 +5,20 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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 fonts = await getOgFonts(); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } - diff --git a/app/[locale]/products/opengraph-image.tsx b/app/[locale]/products/opengraph-image.tsx index eb15740f..9a94386f 100644 --- a/app/[locale]/products/opengraph-image.tsx +++ b/app/[locale]/products/opengraph-image.tsx @@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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 fonts = await getOgFonts(); @@ -13,17 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale: const description = t('meta.description') || t('subtitle'); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } - diff --git a/app/[locale]/team/opengraph-image.tsx b/app/[locale]/team/opengraph-image.tsx index 8c4707f9..e5faef5f 100644 --- a/app/[locale]/team/opengraph-image.tsx +++ b/app/[locale]/team/opengraph-image.tsx @@ -5,7 +5,8 @@ import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper'; 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 fonts = await getOgFonts(); @@ -13,17 +14,10 @@ export default async function Image({ params: { locale } }: { params: { locale: const description = t('meta.description') || t('hero.title'); return new ImageResponse( - ( - - ), + , { ...OG_IMAGE_SIZE, fonts, - } + }, ); } - diff --git a/lib/og-helper.tsx b/lib/og-helper.tsx index 088bce81..02bb7e23 100644 --- a/lib/og-helper.tsx +++ b/lib/og-helper.tsx @@ -6,37 +6,37 @@ import { join } from 'path'; * Since we are using runtime = 'nodejs', we can read them from the filesystem. */ export async function getOgFonts() { - const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf'); - const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf'); + const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff2'); + const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff2'); - try { - const boldFont = readFileSync(boldFontPath); - const regularFont = readFileSync(regularFontPath); + try { + const boldFont = readFileSync(boldFontPath); + const regularFont = readFileSync(regularFontPath); - return [ - { - name: 'Inter', - data: boldFont, - weight: 700 as const, - style: 'normal' as const, - }, - { - name: 'Inter', - data: regularFont, - weight: 400 as const, - style: 'normal' as const, - }, - ]; - } catch (error) { - console.error('Failed to load OG fonts from filesystem, falling back to system fonts:', error); - return []; - } + return [ + { + name: 'Inter', + data: boldFont, + weight: 700 as const, + style: 'normal' as const, + }, + { + name: 'Inter', + data: regularFont, + weight: 400 as const, + style: 'normal' as const, + }, + ]; + } catch (error) { + console.error('Failed to load OG fonts from filesystem, falling back to system fonts:', error); + return []; + } } /** * Common configuration for OG images */ export const OG_IMAGE_SIZE = { - width: 1200, - height: 630, + width: 1200, + height: 630, }; diff --git a/package.json b/package.json index 1a8936d4..1e3d8f4e 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", "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: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", diff --git a/public/fonts/Inter-Bold.ttf b/public/fonts/Inter-Bold.ttf deleted file mode 100644 index fc27522d..00000000 --- a/public/fonts/Inter-Bold.ttf +++ /dev/null @@ -1,1447 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Page not found · GitHub · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- Skip to content - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - -
- - - - - - - - - -
-
- - - -
-
- -
-
- 404 “This is not the web page you are looking for” - - - - - - - - - - - - -
-
- -
-
- -
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - diff --git a/public/fonts/Inter-Bold.woff2 b/public/fonts/Inter-Bold.woff2 new file mode 100644 index 00000000..b9e3cb3b Binary files /dev/null and b/public/fonts/Inter-Bold.woff2 differ diff --git a/public/fonts/Inter-Regular.ttf b/public/fonts/Inter-Regular.ttf deleted file mode 100644 index bb370eb9..00000000 --- a/public/fonts/Inter-Regular.ttf +++ /dev/null @@ -1,1447 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Page not found · GitHub · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- Skip to content - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - -
- - - - - - - - - -
-
- - - -
-
- -
-
- 404 “This is not the web page you are looking for” - - - - - - - - - - - - -
-
- -
-
- -
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - diff --git a/public/fonts/Inter-Regular.woff2 b/public/fonts/Inter-Regular.woff2 new file mode 100644 index 00000000..2bcd222e Binary files /dev/null and b/public/fonts/Inter-Regular.woff2 differ diff --git a/scripts/check-og-images.ts b/scripts/check-og-images.ts new file mode 100644 index 00000000..9daf3af2 --- /dev/null +++ b/scripts/check-og-images.ts @@ -0,0 +1,72 @@ +import fetch from 'node-fetch'; +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 { + 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')) { + throw new Error(`Content-Type: ${contentType}`); + } + + 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) { + const message = error instanceof Error ? error.message : String(error); + console.error(` ❌ FAILED: ${message}`); + 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();