# Blog Content Renderer (/developers/blog-renderer)



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 [#quick-start]

```typescript
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 [#constructor]

```typescript
const renderer = new ResytechBlogRenderer(config?: BlogRendererConfig);
```

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. |

```typescript
const renderer = new ResytechBlogRenderer({
  classPrefix: 'my-blog',
  injectStyles: true,
  customCss: '.my-blog-container { max-width: 960px; }',
  sanitize: false,
});
```

Methods [#methods]

render(container, content) [#rendercontainer-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.

```typescript
render(container: HTMLElement, content: string): void
```

```typescript
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 [#tohtmlcontent---instance-method]

Converts content to an HTML string without touching the DOM. Useful for server-side rendering or when you need the raw HTML.

```typescript
toHtml(content: string): string
```

```typescript
const html = renderer.toHtml(post.content);
// Use the HTML string however you need
element.innerHTML = html;
```

toHtml(content, config?) - Static Method [#tohtmlcontent-config---static-method]

Static convenience method that creates a renderer internally. Useful for one-off conversions.

```typescript
static toHtml(content: string, config?: BlogRendererConfig): string
```

```typescript
const html = ResytechBlogRenderer.toHtml(post.content, {
  classPrefix: 'my-blog',
});
```

getStyles() [#getstyles]

Returns the default CSS as a string. Useful when you need to manage style injection yourself (e.g., SSR or shadow DOM).

```typescript
getStyles(): string
```

```typescript
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 [#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 [#content-format]

```json
{
  "version": 2,
  "blocks": [
    { "id": "abc123", "type": "heading", "props": { "text": "Hello", "level": "h2" } },
    { "id": "def456", "type": "text", "props": { "content": "<p>Some text</p>" } }
  ]
}
```

Block Reference [#block-reference]

heading [#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 [#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 [#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 [#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 [#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 [#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 [#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 [#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_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 [#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 [#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                            |

```typescript
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 [#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 [#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 [#spacer]

Renders a vertical spacer.

| Prop     | Type                             | Default    | Description                      |
| -------- | -------------------------------- | ---------- | -------------------------------- |
| `height` | `'small' \| 'medium' \| 'large'` | `'medium'` | Spacer height (16px, 32px, 64px) |

html [#html]

Renders raw HTML content directly. Use with caution.

| Prop   | Type     | Default | Description     |
| ------ | -------- | ------- | --------------- |
| `html` | `string` | `''`    | Raw HTML string |

Default Styling [#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 [#css-class-structure]

Every block gets two classes and two data attributes:

```html
<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 [#custom-styling]

Using customCss [#using-customcss]

Pass custom CSS in the constructor. It is appended after the default styles:

```typescript
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 [#using-a-custom-class-prefix]

Change the prefix to avoid collisions with existing styles:

```typescript
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 [#external-stylesheets]

Disable automatic style injection and write your own CSS:

```typescript
const renderer = new ResytechBlogRenderer({
  injectStyles: false,
});
```

Then add your own styles targeting the default `rsy-blog-*` classes, or use `getStyles()` as a starting point:

```typescript
const baseCss = renderer.getStyles();
// Modify and inject as needed
```

Targeting Specific Block Types [#targeting-specific-block-types]

Use `data-block-type` for styling specific blocks:

```css
[data-block-type="heading"] { margin-top: 3rem; }
[data-block-type="image"] { margin: 2rem 0; }
[data-block-type="alert"] { border-radius: 8px; }
```

Integration with ResytechApi [#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 [#fetch-and-render-a-blog-post]

```typescript
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 [#blog-listing-page]

```typescript
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 [#category-filtering]

```typescript
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 [#server-side-rendering]

Use the static `toHtml()` method when you do not have a DOM:

```typescript
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.
