Mentionwell's writer emits a small, stable set of class names on its HTML. You have three options for styling:
1. Use the default theme (fastest)
import "mentionwell-reader/styles";
Drops in a tasteful default that matches a typical content blog. Inherits your existing font, but provides spacing, code blocks, callouts, image captions.
2. Override the defaults
The default stylesheet uses CSS custom properties:
:root {
--wb-text: #111;
--wb-text-muted: #555;
--wb-accent: #2a72ff;
--wb-bg-code: #f6f8fa;
--wb-radius: 12px;
--wb-line-height: 1.7;
/* ... */
}
Override these in your global CSS to match your brand without forking the stylesheet.
3. Write your own (most flexible)
Don't import mentionwell-reader/styles. Instead, target the class names directly. The complete list:
.wb-article outermost wrapper
.wb-header title + meta block
.wb-section major content section
.wb-tldr TL;DR card
.wb-toc table of contents
.wb-faq FAQ accordion
.wb-figure image + caption
.wb-callout inline callout
.wb-code code block wrapper
.wb-cta inline CTA button
Plus standard <h2>, <h3>, <p>, <ul>, <ol>, <blockquote>, <table>, <a>, <img> — style them however your site already does.
Dark mode
The default theme respects prefers-color-scheme. To force light or dark, override the relevant custom properties inside a [data-theme="dark"] (or your own scope) block.
Tailwind
Apply @apply to the .wb-* classes in a global stylesheet, or use the official @tailwindcss/typography plugin and wrap rendered content in <article class="prose">.
Common pitfalls
- Missing
wb-article-hostwrapper. Every.wb-callout,.wb-youtube,.wb-quote,<thead>, etc. style is scoped to this class. If you renderpost.htmlinside a wrapper withoutclassName="wb-article-host", callouts appear as bare asides, YouTube cards have no border, and table headers lose their dark background. Always wrap:<div className="wb-article-host" dangerouslySetInnerHTML={{ __html: post.html }} />. You can layer your own classes alongside (className="wb-article-host blog-content"). - Forgetting to import
mentionwell-reader/styles. Thewb-article-hostwrapper is useless without the stylesheet. In your blog layout:import "mentionwell-reader/styles";BEFORE any destination CSS so destination overrides win on shared selectors. - Generic CSS selectors clobbering platform blocks. Rules like
.blog-content aside { border-left: red },.blog-content [class*="callout"] { background: cream },.blog-content th { background: cream }, or.blog-content blockquote { border-left: red }will hit the platform's.wb-callout,<thead>, and.wb-quoteand either stamp red bars on top of the styled callouts or crash the table-header contrast (cream-on-cream invisible text). Add:not()guards:.blog-content aside:not([class*="wb-"]),.blog-content blockquote:not(.wb-quote),.blog-content:not(.wb-article-host) th. The platform's contrast safety floor uses!importanton TL;DR / CTA / table-header colors, but every other wb-* element is fair game for accidental overrides. - Duplicate chrome blocks.
post.htmlalready contains<aside class="wb-tldr">,<section class="wb-faq">,<aside class="wb-author">,<aside class="wb-cta">,<aside class="wb-footer-cta">,<nav class="wb-toc">,<header class="wb-header">, and<figure class="wb-hero">. If you ALSO render bespoke versions frompost.tldr,post.faqs, etc., everything renders twice (the live "Key takeaways shown twice" / "FAQ shown twice" bug). Either drop the bespoke versions, or strip the platform versions frompost.htmlbefore render with a regex pass — see the duplicate chrome guide below. - Inline CTA placed too early. A common mistake is
html.indexOf("</h2>")to inject a CTA after the first H2. The first</h2>is the TL;DR's "Key takeaways" h2 — your CTA renders inside the takeaways box with broken contrast. Worse: a CTA right after section 1's heading interrupts the reader before they're invested. Inject the CTA as a peer block AFTER the closing</section>of the section nearfloor(n/2)of the body sections. Count<section class="wb-section">blocks first, find the middle one, insert after its</section>. - Sticky in-article elements (TOC, share rail) covered by the masthead. If the destination has a sticky site header at
top: 0, your in-article sticky element needstop: ~6.5rem(or whatever the masthead height is),max-height: calc(100vh - 8rem); overflow-y: auto;, andz-index: 1(below the masthead). Otherwise the masthead overlaps the TOC label every time the page scrolls. next/imagerejecting hostnames. AI-generated featured images come from*.fal.media, stock photos fromimages.unsplash.com, uploaded assets from*.cloudinary.com/*.supabase.co, YouTube poster frames fromi.ytimg.com. Add ALL of them tonext.config.tsimages.remotePatternsup front — section "Configure image hostnames" of the Next.js quickstart has the canonical block to paste.- Build hangs on cold Mentionwell API. Wrap
fetchcalls inside your reader helper with a 10sAbortControllertimeout, and addexport const dynamic = "force-dynamic"on the sitemap route. Without these guards a slow API cold start can exceed Next.js's 60s per-route prerender budget and fail the entire Vercel deploy. - Env vars set locally but not on the host. Posting to
/blog/:slugreturning 404 withgetPostreturning null is almost always missingMENTIONWELL_API_BASE/MENTIONWELL_API_KEYon the production host. Check Vercel/Netlify/Railway environment-variable settings, not just.env.local. Redeploy after adding env vars — they only apply to NEW builds. - Double sanitisation.
post.htmlis already safe-to-inject. Don't run it through DOMPurify a second time — you'll strip your own headings. - Heading collision. If your page already has an
<h1>, Mentionwell's article<h1>will be a second one. Two options: drop your page-level<h1>(the article title is already an h1), or post-process the HTML to demote — e.g.html.replace(/<h([1-5])\b/g, (_m, n) =><h${Number(n) + 1}). - Image hosting. Featured images are hosted on Mentionwell's CDN by default. If you mirror them to your own CDN, rewrite
featuredImageand<img src>insideprepareArticleHtml.
Duplicate chrome — strip platform versions if you build bespoke ones
Two valid render strategies:
A. Render post.html as-is. Simplest. Drop in <div className="wb-article-host" dangerouslySetInnerHTML={{ __html: post.html }} /> and you get the platform's TL;DR, FAQ, CTA, author block, etc. styled out of the box.
B. Build bespoke versions from structured fields, strip the platform's versions from post.html. Use this when you want the chrome to use your design system's components (your own breadcrumb, your own newsletter form). Run this strip pass before render:
function stripPlatformChrome(html: string): string {
return html
.replace(/<aside\b[^>]*class="[^"]*\bwb-tldr\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
.replace(/<section\b[^>]*class="[^"]*\bwb-faq\b[^"]*"[^>]*>[\s\S]*?<\/section>/gi, "")
.replace(/<aside\b[^>]*class="[^"]*\bwb-(?:cta|footer-cta)\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
.replace(/<aside\b[^>]*class="[^"]*\bwb-author\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
.replace(/<nav\b[^>]*class="[^"]*\bwb-toc\b[^"]*"[^>]*>[\s\S]*?<\/nav>/gi, "")
.replace(/<header\b[^>]*class="[^"]*\bwb-header\b[^"]*"[^>]*>[\s\S]*?<\/header>/gi, "")
.replace(/<figure\b[^>]*class="[^"]*\bwb-hero\b[^"]*"[^>]*>[\s\S]*?<\/figure>/gi, "");
}
Now render { __html: stripPlatformChrome(post.html) } and use post.tldr, post.faqs, post.author, etc. for your bespoke blocks.
Never strip .wb-callout, .wb-youtube, .wb-quote, .wb-citations, .wb-steps, or tables. Those have no structured-field equivalents in the API — stripping them removes content the article needs.
Never mix strategies for the same block. If you render bespoke FAQ AND leave the platform's wb-faq in post.html, the FAQ section appears twice on the page.