diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index a7fc2d38..af7c33ed 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -427,19 +427,23 @@ jobs: 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 + id: deps run: pnpm install --frozen-lockfile # ── Critical Smoke Tests (MUST pass) ────────────────────────────────── - name: 🚀 OG Image Check + if: always() && steps.deps.outcome == 'success' env: TEST_URL: ${{ needs.prepare.outputs.next_public_url }} run: pnpm run check:og - name: 🌐 Full Sitemap HTTP Validation + if: always() && steps.deps.outcome == 'success' env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} run: pnpm run check:http - name: 🌐 Locale & Language Switcher Validation + if: always() && steps.deps.outcome == 'success' env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} @@ -447,24 +451,28 @@ jobs: # ── Quality Gates (informational, don't block pipeline) ─────────────── - name: 🌐 HTML DOM Validation + if: always() && steps.deps.outcome == 'success' continue-on-error: true env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} run: pnpm check:html - name: 🔒 Security Headers Scan + if: always() && steps.deps.outcome == 'success' continue-on-error: true env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} run: pnpm check:security - name: 🔗 Lychee Deep Link Crawl + if: always() && steps.deps.outcome == 'success' continue-on-error: true env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} run: pnpm check:links - name: 🖼️ Dynamic Asset & Image Integrity Scan + if: always() && steps.deps.outcome == 'success' continue-on-error: true env: NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx index 81627ff5..9994b8e5 100644 --- a/app/[locale]/contact/page.tsx +++ b/app/[locale]/contact/page.tsx @@ -24,9 +24,9 @@ export async function generateMetadata({ params }: ContactPageProps): Promise mapSlugToFileSlug(s, locale))); + const getLocalizedPath = async (lang: string) => { + const parts = await Promise.all([ + mapFileSlugToTranslated('products', lang), + ...fileSlugs.map((fs) => mapFileSlugToTranslated(fs, lang)), + ]); + return parts.join('/'); + }; + const product = await getProductBySlug(productSlug, locale); if (!product) return {}; @@ -72,9 +81,9 @@ export async function generateMetadata({ params }: ProductPageProps): Promise { const text = node.text; - if (text && (text.includes('<') || text.includes('data-start'))) { - return ; - } - // Handle markdown-style lists embedded in text nodes from MDX migration if (text && text.includes('\n- ')) { - const parts = text.split('\n- ').filter(Boolean); + const parts = text.split('\n- ').filter((p: string) => p.trim() !== ''); // If first part doesn't start with "- ", it's a prefix paragraph const startsWithDash = text.trimStart().startsWith('- '); const prefix = startsWithDash ? null : parts.shift(); return ( <> - {prefix && {prefix}} + {prefix && ( + + {!prefix.includes('<') ? prefix : undefined} + + )}
    - {parts.map((item: string, i: number) => ( -
  • {item.trim()}
  • - ))} + {parts.map((item: string, i: number) => { + const cleanItem = item.trim(); + if (cleanItem.includes('<')) { + return
  • ; + } + return
  • {cleanItem}
  • ; + })}
); } + if (text && (text.includes('<') || text.includes('data-start'))) { + return ; + } + // Handle markdown-style links [text](url) from MDX migration if (text && /\[([^\]]+)\]\(([^)]+)\)/.test(text)) { const parts: React.ReactNode[] = [];