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 minimumimagesPrefix(where images resolve). AddsiteUrl/blogPrefix/siteNameif 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 honourcategoryPrefix/authorPrefixfrom yourOpinlyConfig.