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": [], "assets": [],
"env": { "env": {
"__NEXT_BUILD_ID": "development", "__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": {}, "node": {},
"edge": {}, "edge": {},
"encryptionKey": "FV9R3duv3e6hZD53ocfPgBrQwGX7rHfUyZRgkqwqFtk=" "encryptionKey": "0R3AeKRiCnSumpEO3wgP/k8sKq7OCUs1SBp+q3dmhMw="
} }

View File

@@ -125,7 +125,7 @@
/******/ /******/
/******/ /* webpack/runtime/getFullHash */ /******/ /* webpack/runtime/getFullHash */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.h = () => ("f11be5ba913d1b5f") /******/ __webpack_require__.h = () => ("c80591bbb933a4a4")
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ /* 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 { t } from '@/lib/i18n';
import { SEO } from '@/components/SEO'; import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher'; import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps { interface PageProps {
params: { params: {
@@ -211,10 +212,12 @@ export default async function BlogDetailPage({ params }: PageProps) {
})()} })()}
{/* Article Content */} {/* Article Content */}
<div <div className="mb-12">
className="prose prose-lg prose-blue max-w-none mb-12" <ContentRenderer
dangerouslySetInnerHTML={{ __html: processedContent }} content={post.contentHtml}
/> className="prose prose-lg prose-blue"
/>
</div>
{/* Article Footer */} {/* Article Footer */}
<footer className="border-t border-gray-200 pt-8 mt-12"> <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 { getSiteInfo, t, getLocalizedPath } from '@/lib/i18n';
import { SEO } from '@/components/SEO'; import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher'; import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { processHTML } from '@/lib/html-compat'; import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps { interface PageProps {
params: { params: {
@@ -140,10 +140,12 @@ export default async function BlogPage({ params }: PageProps) {
{post.title} {post.title}
</Link> </Link>
</h3> </h3>
<div <div className="text-gray-600 line-clamp-3 text-sm mb-4">
className="text-gray-600 line-clamp-3 text-sm mb-4" <ContentRenderer
dangerouslySetInnerHTML={{ __html: processHTML(post.excerptHtml) }} content={post.excerptHtml}
/> className="text-gray-600 line-clamp-3 text-sm"
/>
</div>
<Link <Link
href={getLocalizedPath(`/blog/${post.slug}`, locale as 'en' | 'de')} 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" 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} {post.title}
</Link> </Link>
</h3> </h3>
<div <div className="text-gray-600 mb-3">
className="text-gray-600 mb-3" <ContentRenderer
dangerouslySetInnerHTML={{ __html: processHTML(post.excerptHtml) }} content={post.excerptHtml}
/> className="text-gray-600 mb-3"
/>
</div>
<Link <Link
href={getLocalizedPath(`/blog/${post.slug}`, locale as 'en' | 'de')} 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" 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 { FeaturedImage } from '@/components/content/FeaturedImage';
import { Container } from '@/components/ui/Container'; import { Container } from '@/components/ui/Container';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps { interface PageProps {
params: { params: {
@@ -117,9 +118,9 @@ export default async function Page({ params }: PageProps) {
{page.title} {page.title}
</h1> </h1>
{page.excerptHtml && ( {page.excerptHtml && (
<div <ContentRenderer
content={page.excerptHtml}
className="text-lg sm:text-xl text-gray-600 leading-relaxed" className="text-lg sm:text-xl text-gray-600 leading-relaxed"
dangerouslySetInnerHTML={{ __html: processHTML(page.excerptHtml) }}
/> />
)} )}
</ResponsiveWrapper> </ResponsiveWrapper>
@@ -127,9 +128,9 @@ export default async function Page({ params }: PageProps) {
{processedContent && ( {processedContent && (
<ResponsiveWrapper className="bg-white rounded-lg shadow-sm p-6 sm:p-8" container={true} maxWidth="full"> <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" className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: processedContent }}
/> />
</ResponsiveWrapper> </ResponsiveWrapper>
)} )}

View File

@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'
import { getAllCategories, getProductsByCategory } from '@/lib/data' import { getAllCategories, getProductsByCategory } from '@/lib/data'
import { ProductList } from '@/components/ProductList' import { ProductList } from '@/components/ProductList'
import { Metadata } from 'next' import { Metadata } from 'next'
import { ContentRenderer } from '@/components/content/ContentRenderer'
interface PageProps { interface PageProps {
params: { params: {
@@ -48,9 +49,9 @@ export default async function ProductCategoryPage({ params }: PageProps) {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-6">{category.name}</h1> <h1 className="text-4xl font-bold mb-6">{category.name}</h1>
{category.description && ( {category.description && (
<div <ContentRenderer
content={category.description}
className="mb-8 prose max-w-none" 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 { processHTML } from '@/lib/html-compat';
import { SEO } from '@/components/SEO'; import { SEO } from '@/components/SEO';
import { LocaleSwitcher } from '@/components/LocaleSwitcher'; import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { ContentRenderer } from '@/components/content/ContentRenderer';
interface PageProps { interface PageProps {
params: { params: {
@@ -187,9 +188,9 @@ export default async function ProductDetailPage({ params }: PageProps) {
<h3 className="text-sm font-medium text-gray-900 mb-3"> <h3 className="text-sm font-medium text-gray-900 mb-3">
{t('products.description', locale as 'en' | 'de')} {t('products.description', locale as 'en' | 'de')}
</h3> </h3>
<div <ContentRenderer
content={product.descriptionHtml || ''}
className="prose prose-sm max-w-none text-gray-600" className="prose prose-sm max-w-none text-gray-600"
dangerouslySetInnerHTML={{ __html: processedDescription }}
/> />
</div> </div>
)} )}

View File

@@ -89,8 +89,16 @@ export const ContentRenderer: React.FC<ContentRendererProps> = ({
/** /**
* Parse HTML string to React elements * Parse HTML string to React elements
* This is a safe parser that only allows specific tags and attributes * 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 { 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 // Define allowed tags and their properties
const allowedTags = { const allowedTags = {
div: ['className', 'id', 'style'], div: ['className', 'id', 'style'],

View File

@@ -113,8 +113,8 @@ function sanitizeHTML(html: string): string {
// Remove dangerous attributes // Remove dangerous attributes
processed = processed.replace(/\s+(href|src)\s*=\s*["']\s*javascript:/gi, ''); processed = processed.replace(/\s+(href|src)\s*=\s*["']\s*javascript:/gi, '');
// Remove any remaining WordPress shortcode-like content (e.g., [vc_row...]) // Note: Shortcode removal is handled in processShortcodes function
processed = processed.replace(/\[[^\]]*\]/g, ''); // Don't remove shortcodes here as they need to be processed first
// Allow safe HTML tags // Allow safe HTML tags
const allowedTags = [ const allowedTags = [
@@ -170,23 +170,72 @@ function processVcRowShortcodes(html: string): string {
const bgImage = extractAttribute(attrs, 'bg_image'); const bgImage = extractAttribute(attrs, 'bg_image');
const bgColor = extractAttribute(attrs, 'bg_color'); const bgColor = extractAttribute(attrs, 'bg_color');
const colorOverlay = extractAttribute(attrs, 'color_overlay'); const colorOverlay = extractAttribute(attrs, 'color_overlay');
const colorOverlay2 = extractAttribute(attrs, 'color_overlay_2');
const overlayStrength = extractAttribute(attrs, 'overlay_strength'); const overlayStrength = extractAttribute(attrs, 'overlay_strength');
const enableGradient = extractAttribute(attrs, 'enable_gradient'); const enableGradient = extractAttribute(attrs, 'enable_gradient');
const gradientDirection = extractAttribute(attrs, 'gradient_direction'); const gradientDirection = extractAttribute(attrs, 'gradient_direction');
const topPadding = extractAttribute(attrs, 'top_padding'); const topPadding = extractAttribute(attrs, 'top_padding');
const bottomPadding = extractAttribute(attrs, 'bottom_padding'); const bottomPadding = extractAttribute(attrs, 'bottom_padding');
const fullScreen = extractAttribute(attrs, 'full_screen_row_position'); 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 // Build style string
let style = ''; let style = '';
let wrapperClasses = [...classes]; 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 // Handle background image
if (bgImage) { 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-size: cover; `;
style += `background-position: center; `; style += `background-position: center; `;
wrapperClasses.push('bg-cover', 'bg-center');
} }
// Handle background color // Handle background color
@@ -194,21 +243,63 @@ function processVcRowShortcodes(html: string): string {
style += `background-color: ${bgColor}; `; style += `background-color: ${bgColor}; `;
} }
// Handle color overlay // Handle video background
if (colorOverlay) { if (videoBg === 'use_video' && (videoMp4 || videoWebm)) {
const opacity = overlayStrength ? parseFloat(overlayStrength) : 0.5; // 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; `; style += `position: relative; `;
wrapperClasses.push('relative'); wrapperClasses.push('relative');
// Create overlay div let overlayStyle = '';
const overlayStyle = `background-color: ${colorOverlay}; opacity: ${opacity};`; 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}"> return `<div class="${wrapperClasses.join(' ')}" style="${style}">
<div class="absolute inset-0" style="${overlayStyle}"></div> <div class="absolute inset-0" style="${overlayStyle}"></div>
<div class="relative flex flex-wrap -mx-4 w-full">${content}</div> <div class="relative flex flex-wrap -mx-4 w-full">${content}</div>
</div>`; </div>`;
} }
// Handle gradient // Handle gradient (without overlay)
if (enableGradient === 'true' || enableGradient === '1') { if (enableGradient === 'true' || enableGradient === '1') {
const gradientClass = getGradientClass(gradientDirection); const gradientClass = getGradientClass(gradientDirection);
wrapperClasses.push(gradientClass); wrapperClasses.push(gradientClass);
@@ -216,9 +307,11 @@ function processVcRowShortcodes(html: string): string {
// Handle padding // Handle padding
if (topPadding || bottomPadding) { if (topPadding || bottomPadding) {
// Convert percentage values to Tailwind arbitrary values
const pt = topPadding ? `pt-[${topPadding}]` : ''; const pt = topPadding ? `pt-[${topPadding}]` : '';
const pb = bottomPadding ? `pb-[${bottomPadding}]` : ''; const pb = bottomPadding ? `pb-[${bottomPadding}]` : '';
wrapperClasses.push(pt, pb); if (pt) wrapperClasses.push(pt);
if (pb) wrapperClasses.push(pb);
} }
// Handle full screen // Handle full screen