@spoolcms/nextjs
Version:
The beautiful headless CMS for Next.js developers
125 lines (124 loc) • 4.73 kB
JavaScript
'use client';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpoolContentRenderer = SpoolContentRenderer;
exports.SpoolContentSecure = SpoolContentSecure;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = __importDefault(require("react"));
/**
* Safe renderer for Spool CMS content that prevents XSS attacks
* and properly handles code blocks without execution.
*
* This component safely renders HTML content from Spool CMS by:
* 1. Escaping HTML entities inside code blocks to prevent execution
* 2. Basic HTML sanitization to remove dangerous elements
* 3. Preventing script execution while preserving formatting
* 4. Ensuring forms and other HTML in code examples display as text
*
* @example
* ```tsx
* import { SpoolContentRenderer } from '@spoolcms/nextjs';
*
* export function BlogPost({ post }) {
* return (
* <article>
* <h1>{post.title}</h1>
* <SpoolContentRenderer content={post.body} />
* </article>
* );
* }
* ```
*/
function SpoolContentRenderer({ content, className, as: Component = 'div' }) {
const sanitizedContent = react_1.default.useMemo(() => {
if (!content)
return '';
let processedContent = content;
// Fix code blocks: escape HTML entities inside <code> and <pre><code> blocks
processedContent = processedContent.replace(/(<pre[^>]*>)?<code[^>]*>([\s\S]*?)<\/code>(<\/pre>)?/gi, (match, preOpen, codeContent, preClose) => {
// Escape HTML entities inside code blocks
const escapedContent = codeContent
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
return `${preOpen || ''}<code>${escapedContent}</code>${preClose || ''}`;
});
// Basic HTML sanitization - remove script tags and event handlers
processedContent = processedContent
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/javascript:/gi, '');
return processedContent;
}, [content]);
return ((0, jsx_runtime_1.jsx)(Component, { className: className, dangerouslySetInnerHTML: { __html: sanitizedContent } }));
}
/**
* Alternative approach using iframe for maximum security
* Use this if you need the highest level of security
*/
function SpoolContentSecure({ content, className }) {
const iframeRef = react_1.default.useRef(null);
react_1.default.useEffect(() => {
if (iframeRef.current && content) {
const iframe = iframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc) {
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: inherit;
margin: 0;
padding: 16px;
line-height: 1.6;
}
pre {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
code {
background: #f5f5f5;
padding: 2px 4px;
border-radius: 2px;
font-family: 'Monaco', 'Menlo', monospace;
}
blockquote {
border-left: 4px solid #ddd;
margin: 0;
padding-left: 16px;
color: #666;
}
</style>
</head>
<body>${content}</body>
</html>
`);
doc.close();
// Auto-resize iframe to content
const resizeIframe = () => {
if (doc.body) {
iframe.style.height = doc.body.scrollHeight + 'px';
}
};
setTimeout(resizeIframe, 100);
iframe.addEventListener('load', resizeIframe);
}
}
}, [content]);
return ((0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, className: className, style: {
width: '100%',
border: 'none',
minHeight: '100px',
}, sandbox: "allow-same-origin", title: "Spool Content" }));
}
;