feat: RichText component for safe PocketBase HTML rendering

Add html-react-parser-backed RichText component that converts HTML
strings from PocketBase rich-text fields into React elements without
dangerouslySetInnerHTML. Replace raw <p> render in IntroSection and
BioSection, and drop the invalid slug filters those collections lacked.
This commit is contained in:
Ilia Mashkov
2026-05-12 13:58:17 +03:00
parent 0a99a37bca
commit 301e7a2555
8 changed files with 201 additions and 17 deletions
+1
View File
@@ -0,0 +1 @@
export { RichText } from './ui/RichText';
@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import { RichText } from './RichText';
describe('RichText', () => {
describe('rendering', () => {
it('renders a paragraph from <p> tag', () => {
render(<RichText html="<p>Hello world</p>" />);
expect(screen.getByText('Hello world').tagName).toBe('P');
});
it('renders bold text from <strong> tag', () => {
render(<RichText html="<strong>Bold</strong>" />);
expect(screen.getByText('Bold').tagName).toBe('STRONG');
});
it('renders a link from <a> tag', () => {
render(<RichText html='<a href="https://example.com">Link</a>' />);
const link = screen.getByRole('link', { name: 'Link' });
expect(link).toHaveAttribute('href', 'https://example.com');
});
it('renders nested tags', () => {
render(<RichText html="<p>Text with <em>emphasis</em></p>" />);
expect(screen.getByText('emphasis').tagName).toBe('EM');
});
it('renders nothing for empty string', () => {
const { container } = render(<RichText html="" />);
expect(container.firstChild).toBeNull();
});
it('renders multiple sibling elements', () => {
render(<RichText html="<p>First</p><p>Second</p>" />);
expect(screen.getByText('First')).toBeInTheDocument();
expect(screen.getByText('Second')).toBeInTheDocument();
});
});
describe('className passthrough', () => {
it('applies className to the wrapper', () => {
const { container } = render(<RichText html="<p>text</p>" className="prose" />);
expect(container.firstChild).toHaveClass('prose');
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import parse from 'html-react-parser';
type Props = {
/**
* HTML string from PocketBase rich-text editor
*/
html: string;
/**
* CSS classes applied to the wrapper div
*/
className?: string;
};
/**
* Renders a PocketBase rich-text HTML string as React elements.
*/
export function RichText({ html, className }: Props) {
if (!html) {
return null;
}
const parsed = parse(html);
if (className) {
return <div className={className}>{parsed}</div>;
}
return <>{parsed}</>;
}
+1 -1
View File
@@ -6,7 +6,7 @@ export type { CardBackground } from './Card';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card';
export { Input, Textarea } from './Input';
export { RichText } from './RichText';
export type { ContainerSize, SectionBackground } from './Section';
export { Container, Section } from './Section';
export { TechStackBrick, TechStackGrid } from './TechStack';