ResytechResytech Docs

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

PropertyTypeDefaultDescription
classPrefixstring'rsy-blog'CSS class prefix for all generated elements
injectStylesbooleantrueAutomatically inject default styles into <head>
customCssstring''Custom CSS appended after default styles
sanitizebooleanfalseSanitize 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): void
const 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): string
const 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): string
const 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(): string
const 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).

PropTypeDefaultDescription
textstring''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.

PropTypeDefaultDescription
contentstring''HTML content (paragraphs, links, bold, etc.)
alignment'left' | 'center' | 'right''left'Text alignment

list

Renders an ordered or unordered list.

PropTypeDefaultDescription
itemsstring[][]List item strings (can contain HTML)
listType'ordered' | 'unordered''unordered'List style

quote

Renders a blockquote with optional attribution.

PropTypeDefaultDescription
textstring''Quote text
attributionstringundefinedAttribution / citation text
style'default' | 'large' | 'bordered''default'Visual style variant

image

Renders an image with optional caption inside a <figure> element.

PropTypeDefaultDescription
imageUrlstringundefinedImage source URL (required)
altTextstringcaptionAlt text for accessibility
captionstringundefinedCaption 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).

PropTypeDefaultDescription
imageUrlstringundefinedImage source URL
imageAltstring''Image alt text
contentstring''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.

PropTypeDefaultDescription
headersstring[][]Column header labels
rowsstring[][][]Row data (array of arrays)
stripedbooleantrueAlternate row background colors

video

Embeds a YouTube or Vimeo video in a responsive 16:9 container. Supports standard URLs, short URLs, and embed URLs.

PropTypeDefaultDescription
urlstring''YouTube or Vimeo URL
captionstringundefinedCaption displayed below the video

Supported URL formats:

  • https://www.youtube.com/watch?v=VIDEO_ID
  • https://youtu.be/VIDEO_ID
  • https://www.youtube.com/embed/VIDEO_ID
  • https://www.youtube.com/shorts/VIDEO_ID
  • https://vimeo.com/VIDEO_ID
  • https://player.vimeo.com/video/VIDEO_ID

alert

Renders a styled alert box with an icon.

PropTypeDefaultDescription
alertType'info' | 'warning' | 'tip' | 'success''info'Alert variant (determines color and icon)
titlestringundefinedOptional bold title
contentstring''Alert body content (HTML)

accordion

Renders an interactive accordion (FAQ-style). Click handlers are automatically wired up by render().

PropTypeDefaultDescription
itemsAccordionItem[][]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.

PropTypeDefaultDescription
textstring'Learn More'Button label
urlstring'#'Link URL
buttonStyle'filled' | 'outline''filled'Button style variant
colorstring'#0d9488'Button color (CSS color value)
alignment'left' | 'center' | 'right''center'Button alignment
openInNewTabbooleanfalseOpen link in new tab

divider

Renders a horizontal rule.

PropTypeDefaultDescription
style'solid' | 'dashed' | 'dotted''solid'Line style
colorstring'#e5e7eb'Line color
width'full' | 'medium' | 'short''full'Line width (100%, 66%, 33%)

spacer

Renders a vertical spacer.

PropTypeDefaultDescription
height'small' | 'medium' | 'large''medium'Spacer height (16px, 32px, 64px)

html

Renders raw HTML content directly. Use with caution.

PropTypeDefaultDescription
htmlstring''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, always rsy-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 needed

Targeting 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} &middot; ${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.

On this page