initial migration
This commit is contained in:
282
lib/data.ts
Normal file
282
lib/data.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Data utilities for Next.js WordPress migration
|
||||
*/
|
||||
|
||||
import wordpressData from '../data/processed/wordpress-data.json';
|
||||
|
||||
export interface SiteInfo {
|
||||
title: string;
|
||||
description: string;
|
||||
baseUrl: string;
|
||||
defaultLocale: string;
|
||||
locales: string[];
|
||||
}
|
||||
|
||||
export interface TranslationReference {
|
||||
locale: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
id: number;
|
||||
translationKey: string;
|
||||
locale: string;
|
||||
slug: string;
|
||||
path: string;
|
||||
title: string;
|
||||
titleHtml: string;
|
||||
contentHtml: string;
|
||||
excerptHtml: string;
|
||||
featuredImage: number | null;
|
||||
updatedAt: string;
|
||||
translation: TranslationReference | null;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
id: number;
|
||||
translationKey: string;
|
||||
locale: string;
|
||||
slug: string;
|
||||
path: string;
|
||||
title: string;
|
||||
titleHtml: string;
|
||||
contentHtml: string;
|
||||
excerptHtml: string;
|
||||
featuredImage: number | null;
|
||||
datePublished: string;
|
||||
updatedAt: string;
|
||||
translation: TranslationReference | null;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
translationKey: string;
|
||||
locale: string;
|
||||
slug: string;
|
||||
path: string;
|
||||
name: string;
|
||||
shortDescriptionHtml: string;
|
||||
descriptionHtml: string;
|
||||
images: string[];
|
||||
featuredImage: string | null;
|
||||
sku: string;
|
||||
regularPrice: string;
|
||||
salePrice: string;
|
||||
currency: string;
|
||||
stockStatus: string;
|
||||
categories: Array<{ id: number; name: string; slug: string }>;
|
||||
attributes: any[];
|
||||
variations: any[];
|
||||
updatedAt: string;
|
||||
translation: TranslationReference | null;
|
||||
}
|
||||
|
||||
export interface ProductCategory {
|
||||
id: number;
|
||||
translationKey: string;
|
||||
locale: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
path: string;
|
||||
description: string;
|
||||
count: number;
|
||||
translation: TranslationReference | null;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
id: number;
|
||||
filename: string;
|
||||
url: string;
|
||||
localPath: string;
|
||||
alt: string;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
mimeType: string | null;
|
||||
}
|
||||
|
||||
export interface Redirect {
|
||||
source: string;
|
||||
destination: string;
|
||||
permanent: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface WordPressData {
|
||||
site: SiteInfo;
|
||||
content: {
|
||||
pages: Page[];
|
||||
posts: Post[];
|
||||
products: Product[];
|
||||
categories: ProductCategory[];
|
||||
};
|
||||
assets: {
|
||||
media: Media[];
|
||||
map: Record<string, string>;
|
||||
};
|
||||
redirects: Redirect[];
|
||||
exportDate: string;
|
||||
}
|
||||
|
||||
// Load data
|
||||
// Use type assertion to handle the JSON import properly
|
||||
const data = wordpressData as unknown as WordPressData;
|
||||
|
||||
// Data access functions
|
||||
export const getSiteInfo = (): SiteInfo => data.site;
|
||||
|
||||
export const getAllPages = (): Page[] => data.content.pages;
|
||||
|
||||
export const getAllPosts = (): Post[] => data.content.posts;
|
||||
|
||||
export const getAllProducts = (): Product[] => data.content.products;
|
||||
|
||||
export const getAllCategories = (): ProductCategory[] => data.content.categories;
|
||||
|
||||
export const getMediaById = (id: number): Media | undefined => {
|
||||
return data.assets.media.find(m => m.id === id);
|
||||
};
|
||||
|
||||
export const getMediaByUrl = (url: string): Media | undefined => {
|
||||
const localPath = data.assets.map[url];
|
||||
if (!localPath) return undefined;
|
||||
return data.assets.media.find(m => m.localPath === localPath);
|
||||
};
|
||||
|
||||
export const getAssetMap = (): Record<string, string> => {
|
||||
return data.assets?.map || {};
|
||||
};
|
||||
|
||||
export const getRedirects = (): Redirect[] => data.redirects;
|
||||
|
||||
// Locale-specific queries
|
||||
export const getPagesByLocale = (locale: string): Page[] => {
|
||||
return data.content.pages.filter(p => p.locale === locale);
|
||||
};
|
||||
|
||||
export const getPostsByLocale = (locale: string): Post[] => {
|
||||
return data.content.posts.filter(p => p.locale === locale);
|
||||
};
|
||||
|
||||
export const getProductsByLocale = (locale: string): Product[] => {
|
||||
return data.content.products.filter(p => p.locale === locale);
|
||||
};
|
||||
|
||||
export const getCategoriesByLocale = (locale: string): ProductCategory[] => {
|
||||
return data.content.categories.filter(c => c.locale === locale);
|
||||
};
|
||||
|
||||
// Single item queries
|
||||
export const getPageBySlug = (slug: string, locale: string): Page | undefined => {
|
||||
return data.content.pages.find(p => p.slug === slug && p.locale === locale);
|
||||
};
|
||||
|
||||
export const getPostBySlug = (slug: string, locale: string): Post | undefined => {
|
||||
return data.content.posts.find(p => p.slug === slug && p.locale === locale);
|
||||
};
|
||||
|
||||
export const getProductBySlug = (slug: string, locale: string): Product | undefined => {
|
||||
return data.content.products.find(p => p.slug === slug && p.locale === locale);
|
||||
};
|
||||
|
||||
export const getCategoryBySlug = (slug: string, locale: string): ProductCategory | undefined => {
|
||||
return data.content.categories.find(c => c.slug === slug && c.locale === locale);
|
||||
};
|
||||
|
||||
// Translation helpers
|
||||
export const getTranslation = <T extends { translationKey: string; locale: string }>(
|
||||
item: T,
|
||||
targetLocale: string
|
||||
): T | undefined => {
|
||||
const collection = [
|
||||
...getAllPages(),
|
||||
...getAllPosts(),
|
||||
...getAllProducts(),
|
||||
...getAllCategories()
|
||||
];
|
||||
const result = collection.find(
|
||||
(i: any) => i.translationKey === item.translationKey && i.locale === targetLocale
|
||||
);
|
||||
return result as unknown as T | undefined;
|
||||
};
|
||||
|
||||
// Asset URL replacement
|
||||
export const replaceAssetUrls = (html: string): string => {
|
||||
let result = html;
|
||||
Object.entries(data.assets.map).forEach(([wpUrl, localPath]) => {
|
||||
result = result.replace(new RegExp(wpUrl, 'g'), localPath);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Additional functions for product categories
|
||||
export const getProductCategory = (slug: string, locale: string): ProductCategory | undefined => {
|
||||
return data.content.categories.find(c => c.slug === slug && c.locale === locale);
|
||||
};
|
||||
|
||||
export const getProductsByCategory = (categoryId: number, locale: string): Product[] => {
|
||||
return data.content.products.filter(p =>
|
||||
p.locale === locale && p.categories.some(c => c.id === categoryId)
|
||||
);
|
||||
};
|
||||
|
||||
// Get products by category slug
|
||||
export const getProductsByCategorySlug = (categorySlug: string, locale: string): Product[] => {
|
||||
const category = getCategoryBySlug(categorySlug, locale);
|
||||
if (!category) return [];
|
||||
return getProductsByCategory(category.id, locale);
|
||||
};
|
||||
|
||||
// Get related products (same category, excluding current product)
|
||||
export const getRelatedProducts = (product: Product, locale: string, limit: number = 4): Product[] => {
|
||||
if (product.categories.length === 0) return [];
|
||||
|
||||
// Get first category
|
||||
const firstCategory = product.categories[0];
|
||||
const categoryProducts = getProductsByCategory(firstCategory.id, locale);
|
||||
|
||||
// Filter out current product and limit results
|
||||
return categoryProducts
|
||||
.filter(p => p.id !== product.id)
|
||||
.slice(0, limit);
|
||||
};
|
||||
|
||||
// Get categories by slugs
|
||||
export const getCategoriesBySlugs = (slugs: string[], locale: string): ProductCategory[] => {
|
||||
return data.content.categories.filter(c =>
|
||||
slugs.includes(c.slug) && c.locale === locale
|
||||
);
|
||||
};
|
||||
|
||||
// Locale-specific queries for static generation
|
||||
export const getAllCategorySlugsForLocale = (locale: string): string[] => {
|
||||
return [...new Set(data.content.categories.filter(c => c.locale === locale).map(c => c.slug))];
|
||||
};
|
||||
|
||||
export const getAllPageSlugsForLocale = (locale: string): string[] => {
|
||||
return [...new Set(data.content.pages.filter(p => p.locale === locale).map(p => p.slug))];
|
||||
};
|
||||
|
||||
export const getAllPostSlugsForLocale = (locale: string): string[] => {
|
||||
return [...new Set(data.content.posts.filter(p => p.locale === locale).map(p => p.slug))];
|
||||
};
|
||||
|
||||
export const getAllProductSlugsForLocale = (locale: string): string[] => {
|
||||
return [...new Set(data.content.products.filter(p => p.locale === locale).map(p => p.slug))];
|
||||
};
|
||||
|
||||
// Get items for locale
|
||||
export const getCategoriesForLocale = (locale: string): ProductCategory[] => {
|
||||
return data.content.categories.filter(c => c.locale === locale);
|
||||
};
|
||||
|
||||
export const getPagesForLocale = (locale: string): Page[] => {
|
||||
return data.content.pages.filter(p => p.locale === locale);
|
||||
};
|
||||
|
||||
export const getPostsForLocale = (locale: string): Post[] => {
|
||||
return data.content.posts.filter(p => p.locale === locale);
|
||||
};
|
||||
|
||||
export const getProductsForLocale = (locale: string): Product[] => {
|
||||
return data.content.products.filter(p => p.locale === locale);
|
||||
};
|
||||
115
lib/html-compat.ts
Normal file
115
lib/html-compat.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* HTML Compatibility Layer
|
||||
* Handles HTML entities and formatting from WordPress exports
|
||||
*/
|
||||
|
||||
export function processHTML(html: string | null | undefined): string {
|
||||
if (!html) return '';
|
||||
|
||||
// Replace common HTML entities
|
||||
let processed = html;
|
||||
|
||||
const entities: Record<string, string> = {
|
||||
'\u00A0': ' ', // Non-breaking space
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
'¢': '¢',
|
||||
'£': '£',
|
||||
'¥': '¥',
|
||||
'€': '€',
|
||||
'©': '©',
|
||||
'®': '®',
|
||||
'™': '™',
|
||||
'°': '°',
|
||||
'±': '±',
|
||||
'×': '×',
|
||||
'÷': '÷',
|
||||
'µ': 'µ',
|
||||
'¶': '¶',
|
||||
'§': '§',
|
||||
'á': 'á',
|
||||
'é': 'é',
|
||||
'í': 'í',
|
||||
'ó': 'ó',
|
||||
'ú': 'ú',
|
||||
'Á': 'Á',
|
||||
'É': 'É',
|
||||
'Í': 'Í',
|
||||
'Ó': 'Ó',
|
||||
'Ú': 'Ú',
|
||||
'ñ': 'ñ',
|
||||
'Ñ': 'Ñ',
|
||||
'ü': 'ü',
|
||||
'Ü': 'Ü',
|
||||
'ö': 'ö',
|
||||
'Ö': 'Ö',
|
||||
'ä': 'ä',
|
||||
'Ä': 'Ä',
|
||||
'ß': 'ß',
|
||||
'—': '—',
|
||||
'–': '–',
|
||||
'…': '…',
|
||||
'«': '«',
|
||||
'»': '»',
|
||||
'‘': "'",
|
||||
'’': "'",
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
'•': '•',
|
||||
'·': '·'
|
||||
};
|
||||
|
||||
// Replace entities
|
||||
for (const [entity, char] of Object.entries(entities)) {
|
||||
processed = processed.replace(new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), char);
|
||||
}
|
||||
|
||||
// Remove script tags
|
||||
processed = processed.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||
|
||||
// Remove style tags
|
||||
processed = processed.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
|
||||
|
||||
// Remove inline event handlers
|
||||
processed = processed.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
||||
|
||||
// Remove dangerous attributes
|
||||
processed = processed.replace(/\s+(href|src)\s*=\s*["']\s*javascript:/gi, '');
|
||||
|
||||
// Remove any remaining WordPress shortcode-like content (e.g., [vc_row...])
|
||||
processed = processed.replace(/\[[^\]]*\]/g, '');
|
||||
|
||||
// Keep HTML structure from processed data - allow divs with our classes
|
||||
// Allow: <p>, <br>, <h1-6>, <strong>, <b>, <em>, <i>, <ul>, <ol>, <li>, <a>, <div>, <span>, <img>
|
||||
// Also allow our vc-row/vc-column classes
|
||||
processed = processed.replace(/<\/?(?!\/?(p|br|h[1-6]|strong|b|em|i|ul|ol|li|a|div|span|img|small)(\s|>))[^>]*>/gi, '');
|
||||
|
||||
// Clean up empty paragraphs and extra spaces
|
||||
processed = processed.replace(/<p>\s*<\/p>/g, '');
|
||||
processed = processed.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
export function stripHTML(html: string | null | undefined): string {
|
||||
if (!html) return '';
|
||||
return html.replace(/<[^>]*>/g, '');
|
||||
}
|
||||
|
||||
export function extractTextFromHTML(html: string | null | undefined): string {
|
||||
if (!html) return '';
|
||||
return processHTML(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dictionary for translations
|
||||
* This is a compatibility function for the i18n system
|
||||
*/
|
||||
export function getDictionary(locale: string): Record<string, string> {
|
||||
// For now, return empty dictionary
|
||||
// In a real implementation, this would load translation files
|
||||
return {};
|
||||
}
|
||||
8
lib/i18n-config.ts
Normal file
8
lib/i18n-config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Locale } from './i18n';
|
||||
|
||||
export const i18n = {
|
||||
defaultLocale: 'en' as Locale,
|
||||
locales: ['en', 'de'] as Locale[],
|
||||
} as const;
|
||||
|
||||
export type LocaleParam = 'en' | 'de';
|
||||
299
lib/i18n.ts
Normal file
299
lib/i18n.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
export type Locale = 'en' | 'de';
|
||||
|
||||
export const defaultLocale: Locale = 'en';
|
||||
export const locales: Locale[] = ['en', 'de'];
|
||||
|
||||
// Simple translation dictionary
|
||||
const translations = {
|
||||
en: {
|
||||
site: {
|
||||
title: 'Kabel-Konfigurator',
|
||||
description: 'Professional cable solutions - configure and order custom cables',
|
||||
},
|
||||
nav: {
|
||||
home: 'Home',
|
||||
blog: 'Blog',
|
||||
products: 'Products',
|
||||
contact: 'Contact',
|
||||
privacy: 'Privacy Policy',
|
||||
legal: 'Legal Notice',
|
||||
terms: 'Terms & Conditions',
|
||||
},
|
||||
home: {
|
||||
hero: 'Professional Cable Solutions',
|
||||
heroSubtitle: 'Configure your custom cables online',
|
||||
cta: 'Configure Now',
|
||||
featuredPosts: 'Latest News',
|
||||
featuredProducts: 'Featured Products',
|
||||
},
|
||||
blog: {
|
||||
title: 'Blog',
|
||||
readMore: 'Read more',
|
||||
noPosts: 'No posts available.',
|
||||
backToBlog: '← Back to Blog',
|
||||
},
|
||||
products: {
|
||||
title: 'Products',
|
||||
categories: 'Categories',
|
||||
noProducts: 'No products available.',
|
||||
noCategories: 'No categories available.',
|
||||
inStock: 'In Stock',
|
||||
outOfStock: 'Out of Stock',
|
||||
price: 'Price',
|
||||
sku: 'SKU',
|
||||
viewAll: 'View All Products',
|
||||
},
|
||||
product: {
|
||||
backToProducts: '← Back to Products',
|
||||
description: 'Description',
|
||||
specifications: 'Specifications',
|
||||
price: 'Price',
|
||||
sku: 'SKU',
|
||||
stock: 'Stock Status',
|
||||
inStock: 'In Stock',
|
||||
outOfStock: 'Out of Stock',
|
||||
},
|
||||
productCategory: {
|
||||
backToCategories: '← Back to Categories',
|
||||
productsInCategory: 'Products in this category',
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact Us',
|
||||
name: 'Your Name',
|
||||
email: 'Your Email',
|
||||
message: 'Your Message',
|
||||
submit: 'Send Message',
|
||||
success: 'Message sent successfully!',
|
||||
error: 'Failed to send message. Please try again.',
|
||||
processing: 'Sending...',
|
||||
phone: 'Phone (optional)',
|
||||
company: 'Company (optional)',
|
||||
},
|
||||
consent: {
|
||||
title: 'Cookie & Analytics Consent',
|
||||
description: 'We use analytics cookies to improve our website. Please accept to continue.',
|
||||
accept: 'Accept',
|
||||
decline: 'Decline',
|
||||
analytics: 'Analytics',
|
||||
analyticsDesc: 'Help us understand how visitors use our site',
|
||||
},
|
||||
footer: {
|
||||
rights: 'All rights reserved.',
|
||||
madeWith: 'Made with Next.js',
|
||||
},
|
||||
common: {
|
||||
readMore: 'Read more',
|
||||
back: 'Back',
|
||||
loading: 'Loading...',
|
||||
noContent: 'No content available.',
|
||||
date: 'Date',
|
||||
updated: 'Updated',
|
||||
},
|
||||
form: {
|
||||
success: 'Message sent successfully!',
|
||||
error: {
|
||||
submit: 'Failed to send message. Please try again.',
|
||||
network: 'Network error. Please try again.',
|
||||
},
|
||||
sending: 'Sending...',
|
||||
name: 'Your Name',
|
||||
email: 'Your Email',
|
||||
message: 'Your Message',
|
||||
submit: 'Send Message',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
site: {
|
||||
title: 'Kabel-Konfigurator',
|
||||
description: 'Professionelle Kabel-Lösungen - konfigurieren und bestellen Sie maßgeschneiderte Kabel',
|
||||
},
|
||||
nav: {
|
||||
home: 'Startseite',
|
||||
blog: 'Blog',
|
||||
products: 'Produkte',
|
||||
contact: 'Kontakt',
|
||||
privacy: 'Datenschutz',
|
||||
legal: 'Impressum',
|
||||
terms: 'AGB',
|
||||
},
|
||||
home: {
|
||||
hero: 'Professionelle Kabel-Lösungen',
|
||||
heroSubtitle: 'Konfigurieren Sie Ihre maßgeschneiderten Kabel online',
|
||||
cta: 'Jetzt konfigurieren',
|
||||
featuredPosts: 'Aktuelle Neuigkeiten',
|
||||
featuredProducts: 'Empfohlene Produkte',
|
||||
},
|
||||
blog: {
|
||||
title: 'Blog',
|
||||
readMore: 'Weiterlesen',
|
||||
noPosts: 'Keine Beiträge verfügbar.',
|
||||
backToBlog: '← Zurück zum Blog',
|
||||
},
|
||||
products: {
|
||||
title: 'Produkte',
|
||||
categories: 'Kategorien',
|
||||
noProducts: 'Keine Produkte verfügbar.',
|
||||
noCategories: 'Keine Kategorien verfügbar.',
|
||||
inStock: 'Auf Lager',
|
||||
outOfStock: 'Nicht auf Lager',
|
||||
price: 'Preis',
|
||||
sku: 'Artikelnummer',
|
||||
viewAll: 'Alle Produkte anzeigen',
|
||||
},
|
||||
product: {
|
||||
backToProducts: '← Zurück zu Produkten',
|
||||
description: 'Beschreibung',
|
||||
specifications: 'Spezifikationen',
|
||||
price: 'Preis',
|
||||
sku: 'Artikelnummer',
|
||||
stock: 'Lagerbestand',
|
||||
inStock: 'Auf Lager',
|
||||
outOfStock: 'Nicht auf Lager',
|
||||
},
|
||||
productCategory: {
|
||||
backToCategories: '← Zurück zu Kategorien',
|
||||
productsInCategory: 'Produkte in dieser Kategorie',
|
||||
},
|
||||
contact: {
|
||||
title: 'Kontakt',
|
||||
name: 'Ihr Name',
|
||||
email: 'Ihre E-Mail',
|
||||
message: 'Ihre Nachricht',
|
||||
submit: 'Nachricht senden',
|
||||
success: 'Nachricht erfolgreich gesendet!',
|
||||
error: 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es erneut.',
|
||||
processing: 'Wird gesendet...',
|
||||
phone: 'Telefon (optional)',
|
||||
company: 'Firma (optional)',
|
||||
},
|
||||
consent: {
|
||||
title: 'Cookie- & Analyse-Einwilligung',
|
||||
description: 'Wir verwenden Analyse-Cookies, um unsere Website zu verbessern. Bitte akzeptieren Sie zur Fortsetzung.',
|
||||
accept: 'Akzeptieren',
|
||||
decline: 'Ablehnen',
|
||||
analytics: 'Analyse',
|
||||
analyticsDesc: 'Helfen Sie uns zu verstehen, wie Besucher unsere Seite nutzen',
|
||||
},
|
||||
footer: {
|
||||
rights: 'Alle Rechte vorbehalten.',
|
||||
madeWith: 'Erstellt mit Next.js',
|
||||
},
|
||||
common: {
|
||||
readMore: 'Weiterlesen',
|
||||
back: 'Zurück',
|
||||
loading: 'Wird geladen...',
|
||||
noContent: 'Kein Inhalt verfügbar.',
|
||||
date: 'Datum',
|
||||
updated: 'Aktualisiert',
|
||||
},
|
||||
form: {
|
||||
success: 'Nachricht erfolgreich gesendet!',
|
||||
error: {
|
||||
submit: 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es erneut.',
|
||||
network: 'Netzwerkfehler. Bitte versuchen Sie es erneut.',
|
||||
},
|
||||
sending: 'Wird gesendet...',
|
||||
name: 'Ihr Name',
|
||||
email: 'Ihre E-Mail',
|
||||
message: 'Ihre Nachricht',
|
||||
submit: 'Nachricht senden',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function t(key: string, locale: Locale = 'en'): string {
|
||||
const keys = key.split('.');
|
||||
let value: any = translations[locale];
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
// Fallback to English
|
||||
value = translations.en;
|
||||
for (const k2 of keys) {
|
||||
if (value && typeof value === 'object' && k2 in value) {
|
||||
value = value[k2];
|
||||
} else {
|
||||
return key; // Return the key itself if translation not found
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we always return a string
|
||||
return typeof value === 'string' ? value : key;
|
||||
}
|
||||
|
||||
export function getLocaleFromPath(path: string): Locale {
|
||||
if (path.startsWith('/de/')) {
|
||||
return 'de';
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
export function getLocalizedPath(path: string, locale: Locale): string {
|
||||
if (locale === 'en') {
|
||||
return path.replace('/de/', '/');
|
||||
}
|
||||
if (locale === 'de') {
|
||||
if (path === '/') return '/de';
|
||||
return path.startsWith('/de/') ? path : `/de${path}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function getPathWithoutLocale(path: string): string {
|
||||
if (path.startsWith('/de/')) {
|
||||
return path.substring(3) || '/';
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export const languageNames: Record<Locale, string> = {
|
||||
en: 'English',
|
||||
de: 'Deutsch',
|
||||
};
|
||||
|
||||
export function getSiteInfo(locale?: Locale) {
|
||||
const loc = locale || defaultLocale;
|
||||
return {
|
||||
title: t('site.title', loc),
|
||||
description: t('site.description', loc),
|
||||
locale: loc,
|
||||
baseUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com',
|
||||
locales: ['en', 'de'],
|
||||
};
|
||||
}
|
||||
|
||||
// Hook for client components (simplified version)
|
||||
export function useTranslation(namespace?: string) {
|
||||
// This would be used in client components
|
||||
// For now, return a simple t function
|
||||
return {
|
||||
t: (key: string) => t(namespace ? `${namespace}.${key}` : key, defaultLocale)
|
||||
};
|
||||
}
|
||||
|
||||
// Get alternate URLs for SEO
|
||||
export function getAlternateUrls(path: string) {
|
||||
return [
|
||||
{ locale: 'en', url: path.replace('/de/', '/') },
|
||||
{ locale: 'de', url: path.startsWith('/de') ? path : `/de${path}` },
|
||||
];
|
||||
}
|
||||
|
||||
// Hook for client components - returns current locale
|
||||
export function useLocale(): Locale {
|
||||
// This is a simplified version for build purposes
|
||||
// In a real app, this would use next/navigation to get the current path
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
// Get dictionary for client components
|
||||
export function getDictionary(locale: Locale) {
|
||||
return {
|
||||
t: (key: string) => t(key, locale)
|
||||
};
|
||||
}
|
||||
130
lib/seo.ts
Normal file
130
lib/seo.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Metadata } from 'next';
|
||||
import { getSiteInfo } from './i18n';
|
||||
import type { Locale } from './i18n';
|
||||
|
||||
export interface SEOParams {
|
||||
title?: string;
|
||||
description?: string;
|
||||
locale?: Locale;
|
||||
canonical?: string;
|
||||
ogType?: 'website' | 'article' | 'product';
|
||||
ogImages?: string[];
|
||||
publishedTime?: string;
|
||||
updatedTime?: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export function generateSEOMetadata({
|
||||
title,
|
||||
description,
|
||||
locale = 'en',
|
||||
canonical,
|
||||
ogType = 'website',
|
||||
ogImages = [],
|
||||
publishedTime,
|
||||
updatedTime,
|
||||
author,
|
||||
}: SEOParams): Metadata {
|
||||
const site = getSiteInfo(locale);
|
||||
|
||||
const pageTitle = title ? `${title} | ${site.title}` : site.title;
|
||||
const pageDescription = description || site.description;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || site.baseUrl;
|
||||
const path = canonical || '/';
|
||||
const fullUrl = `${baseUrl}${path}`;
|
||||
|
||||
// Generate alternate URLs for both locales
|
||||
const alternates = {
|
||||
canonical: fullUrl,
|
||||
languages: {
|
||||
'en': `${baseUrl}${path.replace('/de/', '/')}`,
|
||||
'de': `${baseUrl}${path.startsWith('/de') ? path : `/de${path}`}`,
|
||||
},
|
||||
};
|
||||
|
||||
const openGraph = {
|
||||
title: pageTitle,
|
||||
description: pageDescription,
|
||||
url: fullUrl,
|
||||
siteName: site.title,
|
||||
locale: locale === 'de' ? 'de_DE' : 'en_US',
|
||||
type: ogType,
|
||||
...(ogImages.length > 0 && { images: ogImages }),
|
||||
...(publishedTime && { publishedTime }),
|
||||
...(updatedTime && { updatedTime }),
|
||||
...(author && { authors: [author] }),
|
||||
};
|
||||
|
||||
const twitter = {
|
||||
card: 'summary_large_image',
|
||||
title: pageTitle,
|
||||
description: pageDescription,
|
||||
...(ogImages.length > 0 && { images: ogImages }),
|
||||
};
|
||||
|
||||
return {
|
||||
title: pageTitle,
|
||||
description: pageDescription,
|
||||
alternates,
|
||||
openGraph,
|
||||
twitter,
|
||||
authors: author ? [{ name: author }] : undefined,
|
||||
metadataBase: new URL(baseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper for blog posts
|
||||
export function getPostSEO(post: any, locale: Locale): Metadata {
|
||||
return generateSEOMetadata({
|
||||
title: post.title,
|
||||
description: post.excerptHtml?.replace(/<[^>]*>/g, '') || '',
|
||||
canonical: post.path,
|
||||
locale: locale,
|
||||
ogType: 'article',
|
||||
ogImages: post.featuredImage ? [post.featuredImage] : [],
|
||||
publishedTime: post.datePublished,
|
||||
updatedTime: post.updatedAt,
|
||||
author: 'KLZ Cables Team',
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for products
|
||||
export function getProductSEO(product: any, locale: Locale): Metadata {
|
||||
return generateSEOMetadata({
|
||||
title: product.name,
|
||||
description: product.shortDescriptionHtml?.replace(/<[^>]*>/g, '') || '',
|
||||
canonical: product.path,
|
||||
locale: locale,
|
||||
ogType: 'product',
|
||||
ogImages: product.images || [],
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for categories
|
||||
export function getCategorySEO(category: any, locale: Locale): Metadata {
|
||||
return generateSEOMetadata({
|
||||
title: category.name,
|
||||
description: category.description || `Products in ${category.name}`,
|
||||
canonical: category.path,
|
||||
locale: locale,
|
||||
ogType: 'website',
|
||||
});
|
||||
}
|
||||
|
||||
export function generateSitemapItem({
|
||||
path,
|
||||
lastmod,
|
||||
priority = 0.7,
|
||||
}: {
|
||||
path: string;
|
||||
lastmod?: string;
|
||||
priority?: number;
|
||||
}) {
|
||||
return {
|
||||
url: path,
|
||||
lastmod: lastmod || new Date().toISOString().split('T')[0],
|
||||
changefreq: 'weekly',
|
||||
priority,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user