OpinlySDK

Rendering content

@opinly/shared's renderer — renderToHtml, createRenderer, the node/mark coverage, and customizing output.

Your post body is Tiptap JSON (OpinlyNode). @opinly/shared walks that tree and renders it. The framework packages (@opinly/react, @opinly/vue, @opinly/svelte) wrap it as an <OpinlyContent> component — that's the surface you'll usually use.

<OpinlyContent> (React / Vue / Svelte)

<OpinlyContent
  content={post.content}                       // OpinlyNode (required)
  config={{ imagesPrefix: '/images' }}         // OpinlyConfig (required)
  classNames={{ heading: 'font-display' }}     // per-node-type classes (optional)
  components={{ image: MyImage }}              // override node renderers (React/Vue; optional)
/>
  • config — at minimum imagesPrefix (where images resolve). Add siteUrl/blogPrefix/ siteName if your nodes need absolute URLs.
  • classNames — attach a class to every node of a type without replacing its markup.
  • components — replace how a node type renders. Each receives { node, children }:
<OpinlyContent
  content={content}
  config={config}
  components={{
    image: ({ node }) => (
      <img src={`/images/${node.attrs?.fileKey}`} alt={node.attrs?.alt ?? ''} loading="lazy" />
    ),
    // e.g. route links through next/link, Nuxt <NuxtLink>, etc.
  }}
/>

Node & mark coverage

The renderer handles the full content schema out of the box:

  • Block nodes: paragraph, heading, image, bulletList, orderedList, listItem, blockquote, codeBlock, horizontalRule, hardBreak, and the table family (table, tableRow, tableHeader, tableCell).
  • Marks: bold, italic, strike, underline, code, link, textStyle (color).

Unknown node/mark types are skipped safely. Links are sanitized (javascript: and other unsafe URIs are dropped) and all text is HTML-escaped.

Lower-level helpers (@opinly/shared)

If you're not in React/Vue/Svelte — or you want an HTML string — use the core functions:

import { renderToHtml, createRenderer } from '@opinly/shared'

// 1. HTML string (RSS, email, plain SSR):
const html = renderToHtml(content, { config: { imagesPrefix: '/images' } })

// 2. Generic element walker — inject your framework's createElement:
const render = createRenderer({
  config: { imagesPrefix: '/images' },
  renderFn: (type, props, children) => /* React.createElement / h / … */,
})
const elements = render(content)

renderToHtml is what @opinly/svelte uses internally; createRenderer is what @opinly/react and @opinly/vue wrap.

Content utilities

@opinly/shared also exports pure helpers you can use anywhere:

  • imageUrl(fileKey, config) — build a CDN image URL.
  • extractHeadings(content) — pull headings for a table of contents.
  • calculateReadingTime(content) / countWords(content).
  • blogPath/blogUrl, postPath/postUrl, categoryPath/categoryUrl, authorPath/authorUrl — URL builders (*Path = relative, for in-app links; *Url = absolute, for canonicals). All honour categoryPrefix / authorPrefix from your OpinlyConfig.