migration wip

This commit is contained in:
2025-12-29 18:45:02 +01:00
parent f86785bfb0
commit 3efbac78cb
33 changed files with 164 additions and 52 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -18,7 +18,7 @@
"assets": [],
"env": {
"__NEXT_BUILD_ID": "development",
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY": "FV9R3duv3e6hZD53ocfPgBrQwGX7rHfUyZRgkqwqFtk="
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY": "0R3AeKRiCnSumpEO3wgP/k8sKq7OCUs1SBp+q3dmhMw="
}
}
},

View File

@@ -1,5 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "FV9R3duv3e6hZD53ocfPgBrQwGX7rHfUyZRgkqwqFtk="
"encryptionKey": "0R3AeKRiCnSumpEO3wgP/k8sKq7OCUs1SBp+q3dmhMw="
}

View File

@@ -125,7 +125,7 @@
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("f11be5ba913d1b5f")
/******/ __webpack_require__.h = () => ("c80591bbb933a4a4")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ import { getLocalizedPath } from '@/lib/i18n';
import { t } from '@/lib/i18n';
import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps {
params: {
@@ -211,10 +212,12 @@ export default async function BlogDetailPage({ params }: PageProps) {
})()}
{/* Article Content */}
<div
className="prose prose-lg prose-blue max-w-none mb-12"
dangerouslySetInnerHTML={{ __html: processedContent }}
/>
<div className="mb-12">
<ContentRenderer
content={post.contentHtml}
className="prose prose-lg prose-blue"
/>
</div>
{/* Article Footer */}
<footer className="border-t border-gray-200 pt-8 mt-12">

View File

@@ -4,7 +4,7 @@ import { getPostsByLocale, getCategoriesByLocale, getMediaById } from '@/lib/dat
import { getSiteInfo, t, getLocalizedPath } from '@/lib/i18n';
import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { processHTML } from '@/lib/html-compat';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps {
params: {
@@ -140,10 +140,12 @@ export default async function BlogPage({ params }: PageProps) {
{post.title}
</Link>
</h3>
<div
className="text-gray-600 line-clamp-3 text-sm mb-4"
dangerouslySetInnerHTML={{ __html: processHTML(post.excerptHtml) }}
/>
<div className="text-gray-600 line-clamp-3 text-sm mb-4">
<ContentRenderer
content={post.excerptHtml}
className="text-gray-600 line-clamp-3 text-sm"
/>
</div>
<Link
href={getLocalizedPath(`/blog/${post.slug}`, locale as 'en' | 'de')}
className="inline-flex items-center text-sm font-medium text-blue-600 hover:text-blue-700"
@@ -205,10 +207,12 @@ export default async function BlogPage({ params }: PageProps) {
{post.title}
</Link>
</h3>
<div
className="text-gray-600 mb-3"
dangerouslySetInnerHTML={{ __html: processHTML(post.excerptHtml) }}
/>
<div className="text-gray-600 mb-3">
<ContentRenderer
content={post.excerptHtml}
className="text-gray-600 mb-3"
/>
</div>
<Link
href={getLocalizedPath(`/blog/${post.slug}`, locale as 'en' | 'de')}
className="inline-flex items-center text-sm font-medium text-blue-600 hover:text-blue-700"

View File

@@ -9,6 +9,7 @@ import { ResponsiveSection, ResponsiveWrapper, ResponsiveGrid } from '@/componen
import { FeaturedImage } from '@/components/content/FeaturedImage';
import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps {
params: {
@@ -117,9 +118,9 @@ export default async function Page({ params }: PageProps) {
{page.title}
</h1>
{page.excerptHtml && (
<div
<ContentRenderer
content={page.excerptHtml}
className="text-lg sm:text-xl text-gray-600 leading-relaxed"
dangerouslySetInnerHTML={{ __html: processHTML(page.excerptHtml) }}
/>
)}
</ResponsiveWrapper>
@@ -127,9 +128,9 @@ export default async function Page({ params }: PageProps) {
{processedContent && (
<ResponsiveWrapper className="bg-white rounded-lg shadow-sm p-6 sm:p-8" container={true} maxWidth="full">
<div
<ContentRenderer
content={contentToDisplay || ''}
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: processedContent }}
/>
</ResponsiveWrapper>
)}

View File

@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'
import { getAllCategories, getProductsByCategory } from '@/lib/data'
import { ProductList } from '@/components/ProductList'
import { Metadata } from 'next'
import { ContentRenderer } from '@/components/content/ContentRenderer'
interface PageProps {
params: {
@@ -48,9 +49,9 @@ export default async function ProductCategoryPage({ params }: PageProps) {
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-6">{category.name}</h1>
{category.description && (
<div
<ContentRenderer
content={category.description}
className="mb-8 prose max-w-none"
dangerouslySetInnerHTML={{ __html: category.description }}
/>
)}

View File

@@ -6,6 +6,7 @@ import { getSiteInfo, t, getLocaleFromPath, getLocalizedPath } from '@/lib/i18n'
import { processHTML } from '@/lib/html-compat';
import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps {
params: {
@@ -187,9 +188,9 @@ export default async function ProductDetailPage({ params }: PageProps) {
<h3 className="text-sm font-medium text-gray-900 mb-3">
{t('products.description', locale as 'en' | 'de')}
</h3>
<div
<ContentRenderer
content={product.descriptionHtml || ''}
className="prose prose-sm max-w-none text-gray-600"
dangerouslySetInnerHTML={{ __html: processedDescription }}
/>
</div>
)}

View File

@@ -89,8 +89,16 @@ export const ContentRenderer: React.FC<ContentRendererProps> = ({
/**
* Parse HTML string to React elements
* This is a safe parser that only allows specific tags and attributes
* Works in both server and client environments
*/
function parseHTMLToReact(html: string): React.ReactNode {
// For server-side rendering, use a simple approach with dangerouslySetInnerHTML
// The HTML has already been sanitized by processHTML, so it's safe
if (typeof window === 'undefined') {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// Client-side: use DOMParser for proper parsing
// Define allowed tags and their properties
const allowedTags = {
div: ['className', 'id', 'style'],

View File

@@ -113,8 +113,8 @@ function sanitizeHTML(html: string): string {
// 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, '');
// Note: Shortcode removal is handled in processShortcodes function
// Don't remove shortcodes here as they need to be processed first
// Allow safe HTML tags
const allowedTags = [
@@ -170,23 +170,72 @@ function processVcRowShortcodes(html: string): string {
const bgImage = extractAttribute(attrs, 'bg_image');
const bgColor = extractAttribute(attrs, 'bg_color');
const colorOverlay = extractAttribute(attrs, 'color_overlay');
const colorOverlay2 = extractAttribute(attrs, 'color_overlay_2');
const overlayStrength = extractAttribute(attrs, 'overlay_strength');
const enableGradient = extractAttribute(attrs, 'enable_gradient');
const gradientDirection = extractAttribute(attrs, 'gradient_direction');
const topPadding = extractAttribute(attrs, 'top_padding');
const bottomPadding = extractAttribute(attrs, 'bottom_padding');
const fullScreen = extractAttribute(attrs, 'full_screen_row_position');
const videoBg = extractAttribute(attrs, 'video_bg');
const videoMp4 = extractAttribute(attrs, 'video_mp4');
const videoWebm = extractAttribute(attrs, 'video_webm');
const textAlign = extractAttribute(attrs, 'text_align');
const textColor = extractAttribute(attrs, 'text_color');
const overflow = extractAttribute(attrs, 'overflow');
const equalHeight = extractAttribute(attrs, 'equal_height');
const contentPlacement = extractAttribute(attrs, 'content_placement');
const columnDirection = extractAttribute(attrs, 'column_direction');
const rowBorderRadius = extractAttribute(attrs, 'row_border_radius');
const rowBorderRadiusApplies = extractAttribute(attrs, 'row_border_radius_applies');
// Build style string
let style = '';
let wrapperClasses = [...classes];
// Handle text alignment
if (textAlign === 'center') wrapperClasses.push('text-center');
if (textAlign === 'right') wrapperClasses.push('text-right');
if (textAlign === 'left') wrapperClasses.push('text-left');
// Handle text color
if (textColor === 'light') wrapperClasses.push('text-white');
// Handle overflow
if (overflow === 'visible') wrapperClasses.push('overflow-visible');
// Handle equal height
if (equalHeight === 'yes') {
wrapperClasses.push('items-stretch');
wrapperClasses.push('flex');
}
// Handle content placement
if (contentPlacement === 'bottom') wrapperClasses.push('justify-end');
if (contentPlacement === 'middle') wrapperClasses.push('justify-center');
// Handle column direction
if (columnDirection === 'column') wrapperClasses.push('flex-col');
// Handle border radius
if (rowBorderRadius === 'none' && rowBorderRadiusApplies === 'bg') {
wrapperClasses.push('rounded-none');
}
// Handle background image
if (bgImage) {
style += `background-image: url(/media/${bgImage}.webp); `;
// Try to get media by ID first
const mediaId = parseInt(bgImage);
if (!isNaN(mediaId)) {
// This will be handled by ContentRenderer with data attributes
wrapperClasses.push('bg-cover', 'bg-center');
style += `background-image: url(/media/${bgImage}.webp); `;
} else {
// Assume it's a direct URL
style += `background-image: url(${bgImage}); `;
}
style += `background-size: cover; `;
style += `background-position: center; `;
wrapperClasses.push('bg-cover', 'bg-center');
}
// Handle background color
@@ -194,21 +243,63 @@ function processVcRowShortcodes(html: string): string {
style += `background-color: ${bgColor}; `;
}
// Handle color overlay
if (colorOverlay) {
const opacity = overlayStrength ? parseFloat(overlayStrength) : 0.5;
// Handle video background
if (videoBg === 'use_video' && (videoMp4 || videoWebm)) {
// Mark for ContentRenderer to handle
wrapperClasses.push('relative', 'overflow-hidden');
style += `position: relative; `;
// Create video background structure
const videoAttrs = [];
if (videoMp4) videoAttrs.push(`data-video-mp4="${videoMp4}"`);
if (videoWebm) videoAttrs.push(`data-video-webm="${videoWebm}"`);
videoAttrs.push('data-video-bg="true"');
return `<div class="${wrapperClasses.join(' ')}" style="${style}" ${videoAttrs.join(' ')}>
<div class="relative flex flex-wrap -mx-4 w-full h-full">${content}</div>
</div>`;
}
// Handle color overlay (single or gradient)
if (colorOverlay || colorOverlay2 || enableGradient === 'true' || enableGradient === '1') {
style += `position: relative; `;
wrapperClasses.push('relative');
// Create overlay div
const overlayStyle = `background-color: ${colorOverlay}; opacity: ${opacity};`;
let overlayStyle = '';
if (colorOverlay2 && enableGradient === 'true') {
// Gradient overlay
const gradientDir = gradientDirection || 'left_to_right';
let gradientCSS = '';
switch(gradientDir) {
case 'left_to_right':
gradientCSS = `linear-gradient(to right, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'right_to_left':
gradientCSS = `linear-gradient(to left, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'top_to_bottom':
gradientCSS = `linear-gradient(to bottom, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'bottom_to_top':
gradientCSS = `linear-gradient(to top, ${colorOverlay}, ${colorOverlay2})`;
break;
default:
gradientCSS = `linear-gradient(to right, ${colorOverlay}, ${colorOverlay2})`;
}
overlayStyle = `background: ${gradientCSS}; opacity: 0.32;`;
} else if (colorOverlay) {
// Solid color overlay
const opacity = overlayStrength ? parseFloat(overlayStrength) : 0.5;
overlayStyle = `background-color: ${colorOverlay}; opacity: ${opacity};`;
}
return `<div class="${wrapperClasses.join(' ')}" style="${style}">
<div class="absolute inset-0" style="${overlayStyle}"></div>
<div class="relative flex flex-wrap -mx-4 w-full">${content}</div>
</div>`;
}
// Handle gradient
// Handle gradient (without overlay)
if (enableGradient === 'true' || enableGradient === '1') {
const gradientClass = getGradientClass(gradientDirection);
wrapperClasses.push(gradientClass);
@@ -216,9 +307,11 @@ function processVcRowShortcodes(html: string): string {
// Handle padding
if (topPadding || bottomPadding) {
// Convert percentage values to Tailwind arbitrary values
const pt = topPadding ? `pt-[${topPadding}]` : '';
const pb = bottomPadding ? `pb-[${bottomPadding}]` : '';
wrapperClasses.push(pt, pb);
if (pt) wrapperClasses.push(pt);
if (pb) wrapperClasses.push(pb);
}
// Handle full screen