Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d75a83ccf2 | |||
| 5991bd8392 | |||
| 6207e04bf5 | |||
| 8ffb5967d3 | |||
| 8ba1c7ea38 | |||
| a546ffe69c | |||
| 15740db51e | |||
| 13ab755857 |
17
Dockerfile
17
Dockerfile
@@ -1,5 +1,9 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base
|
FROM node:20-alpine AS base
|
||||||
|
RUN apk add --no-cache libc6-compat curl
|
||||||
|
|
||||||
|
# Enable pnpm
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.3.0 --activate
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Arguments for build-time configuration
|
# Arguments for build-time configuration
|
||||||
@@ -52,12 +56,17 @@ ENV UV_THREADPOOL_SIZE=3
|
|||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Stage 2: Runner
|
# Stage 2: Runner
|
||||||
FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install curl for health checks
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
||||||
USER root
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
RUN chown -R nextjs:nodejs /app
|
adduser --system --uid 1001 nextjs && \
|
||||||
|
chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getAdjacentPosts,
|
getAdjacentPosts,
|
||||||
getReadingTime,
|
getReadingTime,
|
||||||
extractLexicalHeadings,
|
extractLexicalHeadings,
|
||||||
|
getPostSlugs,
|
||||||
} from '@/lib/blog';
|
} from '@/lib/blog';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -33,12 +34,21 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
|
|
||||||
if (!post) return {};
|
if (!post) return {};
|
||||||
|
|
||||||
|
const slugs = await getPostSlugs(slug, locale);
|
||||||
|
const deSlug = slugs?.de || post.slug;
|
||||||
|
const enSlug = slugs?.en || post.slug;
|
||||||
|
|
||||||
const description = post.frontmatter.excerpt || '';
|
const description = post.frontmatter.excerpt || '';
|
||||||
return {
|
return {
|
||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
description: description,
|
description: description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
|
languages: {
|
||||||
|
de: `${SITE_URL}/de/blog/${deSlug}`,
|
||||||
|
en: `${SITE_URL}/en/blog/${enSlug}`,
|
||||||
|
'x-default': `${SITE_URL}/en/blog/${enSlug}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${post.frontmatter.title} | KLZ Cables`,
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
||||||
@@ -134,13 +144,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
<span>{getReadingTime(rawTextContent)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
||||||
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
||||||
Draft Preview
|
Draft Preview
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,13 +181,13 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
<span>{getReadingTime(rawTextContent)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
||||||
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
<span className="px-2 py-0.5 border border-orange-500/50 text-orange-600 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
||||||
Draft Preview
|
Draft Preview
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ export async function generateMetadata(props: {
|
|||||||
},
|
},
|
||||||
metadataBase: new URL(baseUrl),
|
metadataBase: new URL(baseUrl),
|
||||||
manifest: '/manifest.webmanifest',
|
manifest: '/manifest.webmanifest',
|
||||||
alternates: {
|
|
||||||
canonical: `${baseUrl}/${locale}`,
|
|
||||||
languages: {
|
|
||||||
de: `${baseUrl}/de`,
|
|
||||||
en: `${baseUrl}/en`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon.ico', sizes: 'any' },
|
{ url: '/favicon.ico', sizes: 'any' },
|
||||||
@@ -132,11 +125,7 @@ export default async function Layout(props: {
|
|||||||
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang={safeLocale} className={`overflow-x-hidden ${inter.variable}`}>
|
||||||
lang={safeLocale}
|
|
||||||
className={`scroll-smooth overflow-x-hidden ${inter.variable}`}
|
|
||||||
data-scroll-behavior="smooth"
|
|
||||||
>
|
|
||||||
<head>
|
<head>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
|
|||||||
54
lib/blog.ts
54
lib/blog.ts
@@ -136,6 +136,60 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPostSlugs(slug: string, locale: string): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
// First, find the post in the current locale to get its ID
|
||||||
|
let { docs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!docs || docs.length === 0) {
|
||||||
|
// Fallback: search across all locales
|
||||||
|
const { docs: crossLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
docs = crossLocaleDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docs || docs.length === 0) return {};
|
||||||
|
|
||||||
|
const postId = docs[0].id;
|
||||||
|
|
||||||
|
// Fetch the post with locale 'all' to get all localized fields
|
||||||
|
const { docs: allLocalesDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
id: { equals: postId },
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allLocalesDocs || allLocalesDocs.length === 0) return {};
|
||||||
|
return (allLocalesDocs[0].slug as unknown as Record<string, string>) || {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Payload] getPostSlugs failed for ${slug}:`, error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
|
||||||
Document,
|
|
||||||
Page,
|
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
Image,
|
|
||||||
StyleSheet,
|
|
||||||
Font,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
// Register fonts (using system fonts for now, can be customized)
|
// Standard fonts like Helvetica are built-in to PDF and don't require registration
|
||||||
Font.register({
|
// unless we want to use specific TTF files. Using built-in Helvetica for maximum stability.
|
||||||
family: 'Helvetica',
|
|
||||||
fonts: [
|
|
||||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
|
||||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
|
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -302,10 +288,7 @@ const getLabels = (locale: 'en' | 'de') => {
|
|||||||
return labels[locale];
|
return labels[locale];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) => {
|
||||||
product,
|
|
||||||
locale,
|
|
||||||
}) => {
|
|
||||||
const labels = getLabels(locale);
|
const labels = getLabels(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -317,9 +300,7 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
<View>
|
<View>
|
||||||
<Text style={styles.logoText}>KLZ</Text>
|
<Text style={styles.logoText}>KLZ</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.docTitle}>
|
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
||||||
{labels.productDatasheet}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.productRow}>
|
<View style={styles.productRow}>
|
||||||
@@ -328,7 +309,8 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
<View style={styles.categories}>
|
<View style={styles.categories}>
|
||||||
{product.categories.map((cat, index) => (
|
{product.categories.map((cat, index) => (
|
||||||
<Text key={index} style={styles.productMeta}>
|
<Text key={index} style={styles.productMeta}>
|
||||||
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
|
{cat.name}
|
||||||
|
{index < product.categories.length - 1 ? ' • ' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -337,12 +319,8 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.productImageCol}>
|
<View style={styles.productImageCol}>
|
||||||
{product.featuredImage ? (
|
{product.featuredImage ? (
|
||||||
<Image
|
<Image src={product.featuredImage} style={styles.heroImage} />
|
||||||
src={product.featuredImage}
|
|
||||||
style={styles.heroImage}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<Text style={styles.noImage}>{labels.noImage}</Text>
|
<Text style={styles.noImage}>{labels.noImage}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -356,7 +334,11 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
||||||
<View style={styles.sectionAccent} />
|
<View style={styles.sectionAccent} />
|
||||||
<Text style={styles.description}>
|
<Text style={styles.description}>
|
||||||
{stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
|
{stripHtml(
|
||||||
|
product.applicationHtml ||
|
||||||
|
product.shortDescriptionHtml ||
|
||||||
|
product.descriptionHtml,
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -372,17 +354,14 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
key={index}
|
key={index}
|
||||||
style={[
|
style={[
|
||||||
styles.specsTableRow,
|
styles.specsTableRow,
|
||||||
index === product.attributes.length - 1 &&
|
index === product.attributes.length - 1 && styles.specsTableRowLast,
|
||||||
styles.specsTableRowLast,
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View style={styles.specsTableLabelCell}>
|
<View style={styles.specsTableLabelCell}>
|
||||||
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
|
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.specsTableValueCell}>
|
<View style={styles.specsTableValueCell}>
|
||||||
<Text style={styles.specsTableValueText}>
|
<Text style={styles.specsTableValueText}>{attr.options.join(', ')}</Text>
|
||||||
{attr.options.join(', ')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
||||||
|
|
||||||
// Register fonts (using system fonts for now, can be customized)
|
// Standard fonts like Helvetica are built-in to PDF and don't require registration
|
||||||
Font.register({
|
// unless we want to use specific TTF files. Using built-in Helvetica for maximum stability.
|
||||||
family: 'Helvetica',
|
|
||||||
fonts: [
|
|
||||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
|
||||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
|
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
|
||||||
const C = {
|
const C = {
|
||||||
@@ -41,11 +35,12 @@ const styles = StyleSheet.create({
|
|||||||
hero: {
|
hero: {
|
||||||
backgroundColor: C.white,
|
backgroundColor: C.white,
|
||||||
paddingTop: 24,
|
paddingTop: 24,
|
||||||
paddingBottom: 0,
|
paddingBottom: 20,
|
||||||
paddingHorizontal: MARGIN,
|
paddingHorizontal: MARGIN,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: C.gray200,
|
||||||
},
|
},
|
||||||
|
|
||||||
header: {
|
header: {
|
||||||
@@ -71,17 +66,15 @@ const styles = StyleSheet.create({
|
|||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Content Area
|
productHero: {
|
||||||
content: {
|
marginTop: 0,
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: C.navyDeep,
|
color: C.navyDeep,
|
||||||
marginBottom: 8,
|
marginBottom: 0,
|
||||||
marginTop: 10,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
},
|
},
|
||||||
@@ -90,10 +83,15 @@ const styles = StyleSheet.create({
|
|||||||
width: 30,
|
width: 30,
|
||||||
height: 3,
|
height: 3,
|
||||||
backgroundColor: C.accent,
|
backgroundColor: C.accent,
|
||||||
marginBottom: 20,
|
marginTop: 8,
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Content Area
|
||||||
|
content: {
|
||||||
|
paddingHorizontal: MARGIN,
|
||||||
|
},
|
||||||
|
|
||||||
// Lexical Elements
|
// Lexical Elements
|
||||||
paragraph: {
|
paragraph: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -304,12 +302,14 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|||||||
</View>
|
</View>
|
||||||
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.productHero}>
|
||||||
|
<Text style={styles.pageTitle}>{page.title}</Text>
|
||||||
|
<View style={styles.accentBar} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.pageTitle}>{page.title}</Text>
|
|
||||||
<View style={styles.accentBar} />
|
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
{page.content?.root?.children?.map((node: any, i: number) =>
|
||||||
renderLexicalNode(node, i),
|
renderLexicalNode(node, i),
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.12",
|
"version": "2.2.14",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
@@ -38,11 +38,21 @@ function getExpectedTranslation(
|
|||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
sourceLocale: string,
|
sourceLocale: string,
|
||||||
targetLocale: string,
|
targetLocale: string,
|
||||||
): string {
|
alternates: { hreflang: string; href: string }[],
|
||||||
|
): string | null {
|
||||||
const segments = sourcePath.split('/').filter(Boolean);
|
const segments = sourcePath.split('/').filter(Boolean);
|
||||||
// First segment is locale
|
|
||||||
segments[0] = targetLocale;
|
segments[0] = targetLocale;
|
||||||
|
|
||||||
|
// Blog posts have dynamic slugs. If it's a blog post, trust the alternate tag
|
||||||
|
// if the href is present in the sitemap.
|
||||||
|
// The Smoke Test's primary job is ensuring the alternate links point to valid pages.
|
||||||
|
if (segments[1] === (targetLocale === 'de' ? 'blog' : 'blog') && segments.length > 2) {
|
||||||
|
const altLink = alternates.find((a) => a.hreflang === targetLocale);
|
||||||
|
if (altLink) {
|
||||||
|
return new URL(altLink.href).pathname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const map = sourceLocale === 'de' ? SLUG_MAP : REVERSE_SLUG_MAP;
|
const map = sourceLocale === 'de' ? SLUG_MAP : REVERSE_SLUG_MAP;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,7 +60,7 @@ function getExpectedTranslation(
|
|||||||
segments
|
segments
|
||||||
.map((seg, i) => {
|
.map((seg, i) => {
|
||||||
if (i === 0) return seg; // locale
|
if (i === 0) return seg; // locale
|
||||||
return map[seg] || seg; // translate or keep (product names like n2x2y stay the same)
|
return map[seg] || seg; // translate or keep
|
||||||
})
|
})
|
||||||
.join('/')
|
.join('/')
|
||||||
);
|
);
|
||||||
@@ -118,7 +128,7 @@ async function main() {
|
|||||||
if (alt.hreflang === locale) continue; // Same locale, skip
|
if (alt.hreflang === locale) continue; // Same locale, skip
|
||||||
|
|
||||||
// 1. Check slug translation is correct
|
// 1. Check slug translation is correct
|
||||||
const expectedPath = getExpectedTranslation(path, locale, alt.hreflang);
|
const expectedPath = getExpectedTranslation(path, locale, alt.hreflang, alternates);
|
||||||
const actualPath = new URL(alt.href).pathname;
|
const actualPath = new URL(alt.href).pathname;
|
||||||
|
|
||||||
if (actualPath !== expectedPath) {
|
if (actualPath !== expectedPath) {
|
||||||
|
|||||||
Reference in New Issue
Block a user