Blog Content Renderer
Render Resytech CMS blog content as styled HTML using the ResytechBlogRenderer class.
The ResytechBlogRenderer converts block-based JSON content from the Resytech CMS into semantic HTML with scoped styles. It supports 14 block types, automatic style injection, and interactive elements like accordions.
Quick Start
import { ResytechApi, ResytechBlogRenderer } from 'resytech.js';
const api = new ResytechApi();
const renderer = new ResytechBlogRenderer();
// After initialization, the blog controller gets the location GUID automatically
await api.initialization.initialize({ identifier: 'my-session' });
// Fetch and render a blog post
const response = await api.blog.getPost('my-post-slug');
if (response.success) {
const container = document.getElementById('blog-content');
renderer.render(container, response.post.content);
}Constructor
const renderer = new ResytechBlogRenderer(config?: BlogRendererConfig);BlogRendererConfig
| Property | Type | Default | Description |
|---|---|---|---|
classPrefix | string | 'rsy-blog' | CSS class prefix for all generated elements |
injectStyles | boolean | true | Automatically inject default styles into <head> |
customCss | string | '' | Custom CSS appended after default styles |
sanitize | boolean | false | Sanitize HTML in text/list blocks. Set to false when content comes from your own CMS. |
const renderer = new ResytechBlogRenderer({
classPrefix: 'my-blog',
injectStyles: true,
customCss: '.my-blog-container { max-width: 960px; }',
sanitize: false,
});Methods
render(container, content)
Renders blog content into a DOM element. Injects default styles on first call (if injectStyles is true), sets innerHTML, and initializes interactive elements like accordions.
render(container: HTMLElement, content: string): voidconst container = document.getElementById('blog-content');
renderer.render(container, post.content);The container element gets the rsy-blog-container class added automatically.
toHtml(content) - Instance Method
Converts content to an HTML string without touching the DOM. Useful for server-side rendering or when you need the raw HTML.
toHtml(content: string): stringconst html = renderer.toHtml(post.content);
// Use the HTML string however you need
element.innerHTML = html;toHtml(content, config?) - Static Method
Static convenience method that creates a renderer internally. Useful for one-off conversions.
static toHtml(content: string, config?: BlogRendererConfig): stringconst html = ResytechBlogRenderer.toHtml(post.content, {
classPrefix: 'my-blog',
});getStyles()
Returns the default CSS as a string. Useful when you need to manage style injection yourself (e.g., SSR or shadow DOM).
getStyles(): stringconst renderer = new ResytechBlogRenderer({ injectStyles: false });
const css = renderer.getStyles();
// Inject into a shadow root
const style = document.createElement('style');
style.textContent = css;
shadowRoot.appendChild(style);Supported Block Types
The renderer supports version 2 block-based JSON content. Each block has a type, id, and props object. Legacy raw HTML content is passed through as-is.
Content Format
{
"version": 2,
"blocks": [
{ "id": "abc123", "type": "heading", "props": { "text": "Hello", "level": "h2" } },
{ "id": "def456", "type": "text", "props": { "content": "<p>Some text</p>" } }
]
}Block Reference
heading
Renders a heading element (h2, h3, or h4).
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | '' | Heading text content |
level | 'h2' | 'h3' | 'h4' | 'h2' | Heading level |
alignment | 'left' | 'center' | 'right' | 'left' | Text alignment |
text
Renders rich text content. The content prop accepts HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
content | string | '' | HTML content (paragraphs, links, bold, etc.) |
alignment | 'left' | 'center' | 'right' | 'left' | Text alignment |
list
Renders an ordered or unordered list.
| Prop | Type | Default | Description |
|---|---|---|---|
items | string[] | [] | List item strings (can contain HTML) |
listType | 'ordered' | 'unordered' | 'unordered' | List style |
quote
Renders a blockquote with optional attribution.
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | '' | Quote text |
attribution | string | undefined | Attribution / citation text |
style | 'default' | 'large' | 'bordered' | 'default' | Visual style variant |
image
Renders an image with optional caption inside a <figure> element.
| Prop | Type | Default | Description |
|---|---|---|---|
imageUrl | string | undefined | Image source URL (required) |
altText | string | caption | Alt text for accessibility |
caption | string | undefined | Caption displayed below image |
size | 'full' | 'medium' | 'small' | 'full' | Image width constraint |
alignment | 'left' | 'center' | 'right' | 'center' | Image alignment |
image-text
Renders a side-by-side layout with an image and rich text content. Collapses to a stacked layout on small screens (below 640px).
| Prop | Type | Default | Description |
|---|---|---|---|
imageUrl | string | undefined | Image source URL |
imageAlt | string | '' | Image alt text |
content | string | '' | HTML content for the text side |
imagePosition | 'left' | 'right' | 'left' | Which side the image appears on |
verticalAlign | 'top' | 'center' | 'bottom' | 'top' | Vertical alignment of image and text |
table
Renders a responsive HTML table with optional striped rows.
| Prop | Type | Default | Description |
|---|---|---|---|
headers | string[] | [] | Column header labels |
rows | string[][] | [] | Row data (array of arrays) |
striped | boolean | true | Alternate row background colors |
video
Embeds a YouTube or Vimeo video in a responsive 16:9 container. Supports standard URLs, short URLs, and embed URLs.
| Prop | Type | Default | Description |
|---|---|---|---|
url | string | '' | YouTube or Vimeo URL |
caption | string | undefined | Caption displayed below the video |
Supported URL formats:
https://www.youtube.com/watch?v=VIDEO_IDhttps://youtu.be/VIDEO_IDhttps://www.youtube.com/embed/VIDEO_IDhttps://www.youtube.com/shorts/VIDEO_IDhttps://vimeo.com/VIDEO_IDhttps://player.vimeo.com/video/VIDEO_ID
alert
Renders a styled alert box with an icon.
| Prop | Type | Default | Description |
|---|---|---|---|
alertType | 'info' | 'warning' | 'tip' | 'success' | 'info' | Alert variant (determines color and icon) |
title | string | undefined | Optional bold title |
content | string | '' | Alert body content (HTML) |
accordion
Renders an interactive accordion (FAQ-style). Click handlers are automatically wired up by render().
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItem[] | [] | Array of { question: string, answer: string } |
style | 'default' | 'bordered' | 'default' | Visual style variant |
interface AccordionItem {
question: string; // Trigger text
answer: string; // Panel content (HTML)
}When using toHtml() instead of render(), accordion panels are rendered with hidden attributes but click handlers are not attached. You will need to initialize them yourself.
button
Renders a styled link button.
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | 'Learn More' | Button label |
url | string | '#' | Link URL |
buttonStyle | 'filled' | 'outline' | 'filled' | Button style variant |
color | string | '#0d9488' | Button color (CSS color value) |
alignment | 'left' | 'center' | 'right' | 'center' | Button alignment |
openInNewTab | boolean | false | Open link in new tab |
divider
Renders a horizontal rule.
| Prop | Type | Default | Description |
|---|---|---|---|
style | 'solid' | 'dashed' | 'dotted' | 'solid' | Line style |
color | string | '#e5e7eb' | Line color |
width | 'full' | 'medium' | 'short' | 'full' | Line width (100%, 66%, 33%) |
spacer
Renders a vertical spacer.
| Prop | Type | Default | Description |
|---|---|---|---|
height | 'small' | 'medium' | 'large' | 'medium' | Spacer height (16px, 32px, 64px) |
html
Renders raw HTML content directly. Use with caution.
| Prop | Type | Default | Description |
|---|---|---|---|
html | string | '' | Raw HTML string |
Default Styling
The renderer injects a <style> tag into <head> with id {classPrefix}-styles. Styles are scoped using the class prefix to avoid conflicts.
Default layout:
- Max width: 768px, centered
- Font: system font stack (-apple-system, BlinkMacSystemFont, Segoe UI, Roboto)
- Line height: 1.7
- Block margin-bottom: 1.5rem
CSS Class Structure
Every block gets two classes and two data attributes:
<div class="rsy-blog-block rsy-blog-heading rsy-block-heading"
data-block-type="heading"
data-block-id="abc123">
<h2>...</h2>
</div>The three classes are:
{prefix}-block-- shared by all blocks{prefix}-{type}-- type-specific (prefixed)rsy-block-{type}-- type-specific (unprefixed, alwaysrsy-block-)
Custom Styling
Using customCss
Pass custom CSS in the constructor. It is appended after the default styles:
const renderer = new ResytechBlogRenderer({
customCss: `
.rsy-blog-container { max-width: 960px; }
.rsy-blog-heading h2 { color: #1a365d; }
.rsy-blog-alert-info { background: #eef6ff; }
`,
});Using a Custom Class Prefix
Change the prefix to avoid collisions with existing styles:
const renderer = new ResytechBlogRenderer({
classPrefix: 'company-blog',
});This changes all generated classes to use company-blog- (e.g., company-blog-container, company-blog-heading).
External Stylesheets
Disable automatic style injection and write your own CSS:
const renderer = new ResytechBlogRenderer({
injectStyles: false,
});Then add your own styles targeting the default rsy-blog-* classes, or use getStyles() as a starting point:
const baseCss = renderer.getStyles();
// Modify and inject as neededTargeting Specific Block Types
Use data-block-type for styling specific blocks:
[data-block-type="heading"] { margin-top: 3rem; }
[data-block-type="image"] { margin: 2rem 0; }
[data-block-type="alert"] { border-radius: 8px; }Integration with ResytechApi
The blog controller on ResytechApi provides the data that the renderer consumes. After calling initialization.initialize(), the blog controller automatically receives the location GUID.
Fetch and Render a Blog Post
import { ResytechApi, ResytechBlogRenderer } from 'resytech.js';
const api = new ResytechApi({ debug: true });
const renderer = new ResytechBlogRenderer();
async function loadPost(slug: string) {
// Initialize to get auth token and location GUID
const init = await api.initialization.initialize({ identifier: 'blog-session' });
if (!init.success) {
console.error('Initialization failed:', init.message);
return;
}
// Fetch the post
const response = await api.blog.getPost(slug);
if (!response.success) {
console.error('Failed to load post:', response.message);
return;
}
const post = response.post;
// Render into the page
document.querySelector('h1').textContent = post.title;
document.querySelector('.author').textContent = post.author;
document.querySelector('.date').textContent =
new Date(post.publishedAt).toLocaleDateString();
if (post.featuredImageUri) {
const img = document.querySelector('.featured-image') as HTMLImageElement;
img.src = post.featuredImageUri;
img.alt = post.title;
}
const container = document.getElementById('blog-content');
renderer.render(container, post.content);
}
loadPost('my-first-blog-post');Blog Listing Page
async function loadBlogListing(page = 1) {
const response = await api.blog.getPosts(page, 10);
if (!response.success) return;
const listEl = document.getElementById('blog-list');
listEl.innerHTML = response.posts.map(post => `
<article>
<img src="${post.featuredImageThumbnailUri}" alt="${post.title}" />
<h2><a href="/blog/${post.slug}">${post.title}</a></h2>
<p>${post.excerpt}</p>
<span>${post.author} · ${new Date(post.publishedAt).toLocaleDateString()}</span>
<div class="categories">
${post.categories.map(c => `<span class="tag">${c.name}</span>`).join('')}
</div>
</article>
`).join('');
// Pagination
const totalPages = Math.ceil(response.totalCount / response.pageSize);
document.getElementById('page-info').textContent =
`Page ${response.page} of ${totalPages}`;
}Category Filtering
async function loadCategories() {
const response = await api.blog.getCategories();
if (!response.success) return;
const nav = document.getElementById('category-nav');
nav.innerHTML = response.categories.map(cat => `
<button onclick="filterByCategory('${cat.slug}')">${cat.name}</button>
`).join('');
}
async function filterByCategory(categorySlug: string, page = 1) {
const response = await api.blog.getPostsByCategory(categorySlug, page, 10);
if (!response.success) return;
// Render post list (same as above)
}Server-Side Rendering
Use the static toHtml() method when you do not have a DOM:
import { ResytechBlogRenderer } from 'resytech.js';
// In your SSR handler
const html = ResytechBlogRenderer.toHtml(post.content);
const css = new ResytechBlogRenderer().getStyles();
const page = `
<html>
<head><style>${css}</style></head>
<body>
<div class="rsy-blog-container">${html}</div>
</body>
</html>
`;Note: Interactive elements (accordions) will not work without client-side JavaScript. Use render() on the client to hydrate them, or attach your own click handlers.
