@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
226 lines (225 loc) • 8.39 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from 'react';
import { CmsImage } from './OptimizedImage';
import styles from './DefaultTemplate.module.css';
/**
* ContentBody component for rendering content body with different strategies
*/
export const ContentBody = ({ content, contentType, customRenderer, className = '', }) => {
// Use custom renderer if provided
if (customRenderer) {
return (_jsx("div", { className: `${styles.contentBody} ${className}`, children: customRenderer(content) }));
}
// Handle different content types
const renderContent = () => {
if (!content) {
return (_jsx("div", { className: styles.errorContainer, children: _jsx("p", { className: styles.errorMessage, children: "No content available to display." }) }));
}
// Handle string content
if (typeof content === 'string') {
return renderStringContent(content);
}
// Handle array content (Portable Text or array of blocks)
if (Array.isArray(content)) {
return renderArrayContent(content);
}
// Handle object content
if (typeof content === 'object') {
return renderObjectContent(content);
}
// Fallback for unknown content types
return (_jsx("div", { className: styles.errorContainer, children: _jsxs("p", { className: styles.errorMessage, children: ["Unsupported content format for type: ", contentType] }) }));
};
return (_jsx("div", { className: `${styles.contentBody} ${className}`, children: renderContent() }));
};
/**
* Render string content with basic HTML support
*/
const renderStringContent = (content) => {
// Check if content looks like HTML
const htmlRegex = /<[^>]*>/;
if (htmlRegex.test(content)) {
// For HTML content, use dangerouslySetInnerHTML with caution
return (_jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeHtml(content) } }));
}
// For plain text, preserve line breaks
return (_jsx("div", { children: content.split('\n').map((line, index) => (_jsxs(React.Fragment, { children: [line, index < content.split('\n').length - 1 && _jsx("br", {})] }, index))) }));
};
/**
* Render array content (Portable Text blocks)
*/
const renderArrayContent = (content) => {
return (_jsx("div", { children: content.map((block, index) => renderBlock(block, index)) }));
};
/**
* Render object content
*/
const renderObjectContent = (content) => {
// Handle Portable Text single block
if (content._type && content._type === 'block') {
return renderBlock(content, 0);
}
// Handle objects with specific content fields
if (content.html) {
return (_jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeHtml(content.html) } }));
}
if (content.text) {
return renderStringContent(content.text);
}
if (content.markdown) {
// Basic markdown support could be added here
return renderStringContent(content.markdown);
}
// Fallback: try to extract any string values
const textContent = extractTextFromObject(content);
if (textContent) {
return renderStringContent(textContent);
}
return (_jsx("div", { className: styles.errorContainer, children: _jsx("p", { className: styles.errorMessage, children: "Unable to render content object" }) }));
};
/**
* Render a Portable Text block
*/
const renderBlock = (block, index) => {
if (!block || !block._type) {
return null;
}
const key = block._key || index;
switch (block._type) {
case 'block':
return renderTextBlock(block, key);
case 'image':
return renderImageBlock(block, key);
case 'code':
return renderCodeBlock(block, key);
default: {
// For unknown block types, try to extract text
const text = extractTextFromObject(block);
if (text) {
return _jsx("p", { children: text }, key);
}
return null;
}
}
};
/**
* Render a text block with style support
*/
const renderTextBlock = (block, key) => {
const { style = 'normal', children = [] } = block;
// Handle different block styles
const content = children.map((child, childIndex) => renderInlineContent(child, `${key}-${childIndex}`));
switch (style) {
case 'h1':
return _jsx("h1", { children: content }, key);
case 'h2':
return _jsx("h2", { children: content }, key);
case 'h3':
return _jsx("h3", { children: content }, key);
case 'h4':
return _jsx("h4", { children: content }, key);
case 'h5':
return _jsx("h5", { children: content }, key);
case 'h6':
return _jsx("h6", { children: content }, key);
case 'blockquote':
return _jsx("blockquote", { children: content }, key);
default:
return _jsx("p", { children: content }, key);
}
};
/**
* Render inline content with mark support
*/
const renderInlineContent = (child, key) => {
if (!child || typeof child !== 'object') {
return String(child || '');
}
const { text = '', marks = [] } = child;
if (marks.length === 0) {
return text;
}
// Apply marks (bold, italic, etc.)
let element = text;
marks.forEach((mark) => {
switch (mark) {
case 'strong':
element = _jsx("strong", { children: element });
break;
case 'em':
element = _jsx("em", { children: element });
break;
case 'code':
element = _jsx("code", { children: element });
break;
case 'underline':
element = _jsx("u", { children: element });
break;
default:
// For unknown marks, just return the text
break;
}
});
return _jsx(React.Fragment, { children: element }, key);
};
/**
* Render an image block
*/
const renderImageBlock = (block, key) => {
const { asset, alt, caption } = block;
if (!asset) {
return null;
}
// Handle both Sanity asset objects and direct URLs
const imageAsset = typeof asset === 'string' ? asset : asset;
return (_jsxs("figure", { children: [_jsx(CmsImage, { image: imageAsset, alt: alt || 'Content image', width: 800, height: 600, quality: 85, showFallback: true, sizes: "(max-width: 768px) 100vw, (max-width: 1024px) 80vw, 800px" }), caption && _jsx("figcaption", { children: caption })] }, key));
};
/**
* Render a code block
*/
const renderCodeBlock = (block, key) => {
const { code, language } = block;
if (!code) {
return null;
}
return (_jsx("pre", { children: _jsx("code", { className: language ? `language-${language}` : undefined, children: code }) }, key));
};
/**
* Extract text content from an object
*/
const extractTextFromObject = (obj) => {
if (typeof obj === 'string') {
return obj;
}
if (typeof obj !== 'object' || obj === null) {
return '';
}
// Try common text fields
const textFields = ['text', 'content', 'body', 'description', 'title'];
for (const field of textFields) {
if (obj[field] && typeof obj[field] === 'string') {
return obj[field];
}
}
// If it's an array, try to extract text from items
if (Array.isArray(obj)) {
return obj.map(item => extractTextFromObject(item)).filter(Boolean).join(' ');
}
// Try to get text from children
if (obj.children && Array.isArray(obj.children)) {
return obj.children.map((child) => child.text || extractTextFromObject(child)).filter(Boolean).join('');
}
return '';
};
/**
* Basic HTML sanitization (remove dangerous tags/attributes)
*/
const sanitizeHtml = (html) => {
// This is a basic sanitization - in production, use a proper library like DOMPurify
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/on\w+="[^"]*"/gi, '') // Remove event handlers
.replace(/javascript:/gi, ''); // Remove javascript: URLs
};
export default ContentBody;