chore(ci/routing): merge v1.1.7 fixes into main (routing fix & pipeline optimizations)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 29s
Build & Deploy / 🏗️ Build (push) Successful in 2m52s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / ♿ WCAG (push) Has been cancelled
Build & Deploy / 🛡️ Quality Gates (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled

This commit is contained in:
2026-02-23 12:49:04 +01:00
parent 477a3bb8ce
commit 6a0269facc
5 changed files with 67 additions and 40 deletions

View File

@@ -34,6 +34,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm'
- name: 🔐 Configure Private Registry - name: 🔐 Configure Private Registry
run: | run: |

View File

@@ -45,7 +45,7 @@ jobs:
run: | run: |
echo "Purging old build layers and dangling images..." echo "Purging old build layers and dangling images..."
docker image prune -f docker image prune -f
docker builder prune -f --filter "until=6h" docker builder prune -f --filter "until=24h"
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -165,6 +165,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm'
- name: 🔐 Registry Auth - name: 🔐 Registry Auth
run: | run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -193,7 +194,7 @@ jobs:
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
build: build:
name: 🏗️ Build name: 🏗️ Build
needs: [prepare, qa] needs: [prepare]
if: needs.prepare.outputs.target != 'skip' if: needs.prepare.outputs.target != 'skip'
runs-on: docker runs-on: docker
container: container:
@@ -392,7 +393,7 @@ jobs:
name: 🧪 Smoke Test name: 🧪 Smoke Test
needs: [prepare, deploy] needs: [prepare, deploy]
continue-on-error: true continue-on-error: true
if: needs.deploy.result == 'success' && github.ref_type != 'tag' if: needs.deploy.result == 'success' && needs.prepare.outputs.target != 'branch'
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -425,7 +426,7 @@ jobs:
name: ⚡ Lighthouse name: ⚡ Lighthouse
needs: [prepare, deploy] needs: [prepare, deploy]
continue-on-error: true continue-on-error: true
if: success() && needs.prepare.outputs.target != 'skip' && github.ref_type != 'tag' if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -440,6 +441,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm'
- name: 🔐 Registry Auth - name: 🔐 Registry Auth
run: | run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -500,7 +502,7 @@ jobs:
name: ♿ WCAG name: ♿ WCAG
needs: [prepare, deploy, smoke_test] needs: [prepare, deploy, smoke_test]
continue-on-error: true continue-on-error: true
if: success() && needs.prepare.outputs.target != 'skip' && github.ref_type != 'tag' if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -522,6 +524,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm'
- name: 🔐 Registry Auth - name: 🔐 Registry Auth
run: | run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -582,7 +585,7 @@ jobs:
name: 🛡️ Quality Gates name: 🛡️ Quality Gates
needs: [prepare, deploy, smoke_test] needs: [prepare, deploy, smoke_test]
continue-on-error: true continue-on-error: true
if: success() && needs.prepare.outputs.target != 'skip' && github.ref_type != 'tag' if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
runs-on: docker runs-on: docker
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -597,6 +600,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm'
- name: 🔐 Registry Auth - name: 🔐 Registry Auth
run: | run: |
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
@@ -630,6 +634,11 @@ jobs:
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
steps: steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: 🔔 Gotify - name: 🔔 Gotify
run: | run: |
STATUS="${{ needs.deploy.result }}" STATUS="${{ needs.deploy.result }}"

View File

@@ -53,11 +53,11 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: categoryTitle, title: categoryTitle,
description: categoryDesc, description: categoryDesc,
alternates: { alternates: {
canonical: `${SITE_URL}/${locale}/products/${productSlug}`, canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${productSlug}`,
languages: { languages: {
de: `${SITE_URL}/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`, de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
en: `${SITE_URL}/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`, en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
'x-default': `${SITE_URL}/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`, 'x-default': `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
}, },
}, },
openGraph: { openGraph: {
@@ -81,11 +81,11 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: product.frontmatter.title, title: product.frontmatter.title,
description: product.frontmatter.description, description: product.frontmatter.description,
alternates: { alternates: {
canonical: `${SITE_URL}/${locale}/products/${slug.join('/')}`, canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
languages: { languages: {
de: `${SITE_URL}/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`, de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
en: `${SITE_URL}/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`, en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
'x-default': `${SITE_URL}/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`, 'x-default': `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
}, },
}, },
openGraph: { openGraph: {
@@ -179,6 +179,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
setRequestLocale(locale); setRequestLocale(locale);
const productSlug = slug[slug.length - 1]; const productSlug = slug[slug.length - 1];
const t = await getTranslations('Products'); const t = await getTranslations('Products');
const productsSlug = await mapFileSlugToTranslated('products', locale);
// Check if it's a category page // Check if it's a category page
const categories = [ const categories = [
@@ -220,7 +221,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
<div className="max-w-4xl animate-slide-up"> <div className="max-w-4xl animate-slide-up">
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest"> <nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
<Link <Link
href={`/${locale}/${await mapFileSlugToTranslated('products', locale)}`} href={`/${locale}/${productsSlug}`}
className="hover:text-accent transition-colors" className="hover:text-accent transition-colors"
> >
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'} {t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
@@ -242,7 +243,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
{productsWithTranslatedSlugs.map((product) => ( {productsWithTranslatedSlugs.map((product) => (
<Link <Link
key={product.slug} key={product.slug}
href={`/${locale}/products/${productSlug}/${product.translatedSlug}`} href={`/${locale}/${productsSlug}/${productSlug}/${product.translatedSlug}`}
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5" className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
> >
<Card tag="article" className="premium-card-reset"> <Card tag="article" className="premium-card-reset">
@@ -381,14 +382,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
<div className="max-w-4xl animate-slide-up"> <div className="max-w-4xl animate-slide-up">
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]"> <nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
<Link <Link
href={`/${locale}/${await mapFileSlugToTranslated('products', locale)}`} href={`/${locale}/${productsSlug}`}
className="hover:text-accent transition-colors" className="hover:text-accent transition-colors"
> >
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'} {t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
</Link> </Link>
<span className="mx-4 opacity-20">/</span> <span className="mx-4 opacity-20">/</span>
<Link <Link
href={`/${locale}/products/${categorySlug}`} href={`/${locale}/${productsSlug}/${categorySlug}`}
className="hover:text-accent transition-colors" className="hover:text-accent transition-colors"
> >
{categoryTitle} {categoryTitle}
@@ -511,7 +512,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
'@type': 'Offer', '@type': 'Offer',
availability: 'https://schema.org/InStock', availability: 'https://schema.org/InStock',
priceCurrency: 'EUR', priceCurrency: 'EUR',
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`, url: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
itemCondition: 'https://schema.org/NewCondition', itemCondition: 'https://schema.org/NewCondition',
}, },
additionalProperty: technicalItems.map((item: any) => ({ additionalProperty: technicalItems.map((item: any) => ({
@@ -522,7 +523,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
category: product.frontmatter.categories.join(', '), category: product.frontmatter.categories.join(', '),
mainEntityOfPage: { mainEntityOfPage: {
'@type': 'WebPage', '@type': 'WebPage',
'@id': `${SITE_URL}/${locale}/products/${slug.join('/')}`, '@id': `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
}, },
} as any } as any
} }

View File

@@ -2,6 +2,7 @@ import { getAllProducts } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import Image from 'next/image'; import Image from 'next/image';
import { RelatedProductLink } from './RelatedProductLink'; import { RelatedProductLink } from './RelatedProductLink';
import { mapFileSlugToTranslated } from '@/lib/slugs';
interface RelatedProductsProps { interface RelatedProductsProps {
currentSlug: string; currentSlug: string;
@@ -16,6 +17,7 @@ export default async function RelatedProducts({
}: RelatedProductsProps) { }: RelatedProductsProps) {
const products = await getAllProducts(locale); const products = await getAllProducts(locale);
const t = await getTranslations('Products'); const t = await getTranslations('Products');
const productsSlug = await mapFileSlugToTranslated('products', locale);
// Filter products: same category, not current product // Filter products: same category, not current product
const related = products const related = products
@@ -27,6 +29,34 @@ export default async function RelatedProducts({
if (related.length === 0) return null; if (related.length === 0) return null;
// Pre-calculate translated slugs for related products
const relatedWithTranslatedSlugs = await Promise.all(
related.map(async (product) => {
// Find the category slug for the link
const categorySlugs = [
'low-voltage-cables',
'medium-voltage-cables',
'high-voltage-cables',
'solar-cables',
];
const catFileSlug =
categorySlugs.find((slug) => {
const key = slug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const title = t(`categories.${key}.title`);
return product.frontmatter.categories.some(
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug || cat === title,
);
}) || 'low-voltage-cables';
const catSlug = await mapFileSlugToTranslated(catFileSlug, locale);
return {
...product,
catSlug,
};
}),
);
return ( return (
<div className=""> <div className="">
<div className="flex items-end justify-between mb-12"> <div className="flex items-end justify-between mb-12">
@@ -39,29 +69,11 @@ export default async function RelatedProducts({
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{related.map((product) => { {relatedWithTranslatedSlugs.map((product) => {
// Find the category slug for the link
const categorySlugs = [
'low-voltage-cables',
'medium-voltage-cables',
'high-voltage-cables',
'solar-cables',
];
const catSlug =
categorySlugs.find((slug) => {
const key = slug
.replace(/-cables$/, '')
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const title = t(`categories.${key}.title`);
return product.frontmatter.categories.some(
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug || cat === title,
);
}) || 'low-voltage-cables';
return ( return (
<RelatedProductLink <RelatedProductLink
key={product.slug} key={product.slug}
href={`/${locale}/products/${catSlug}/${product.slug}`} href={`/${locale}/${productsSlug}/${product.catSlug}/${product.slug}`}
productSlug={product.slug} productSlug={product.slug}
productTitle={product.frontmatter.title} productTitle={product.frontmatter.title}
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5" className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"

View File

@@ -405,6 +405,10 @@ const nextConfig = {
source: '/de/produkte', source: '/de/produkte',
destination: '/de/products', destination: '/de/products',
}, },
{
source: '/de/produkte/:path*',
destination: '/de/products/:path*',
},
{ {
source: '/cms/:path*', source: '/cms/:path*',
destination: `${directusUrl}/:path*`, destination: `${directusUrl}/:path*`,