Mentionwell

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-host wrapper. Every .wb-callout, .wb-youtube, .wb-quote, <thead>, etc. style is scoped to this class. If you render post.html inside a wrapper without className="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. The wb-article-host wrapper 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-quote and 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 !important on TL;DR / CTA / table-header colors, but every other wb-* element is fair game for accidental overrides.
  • Duplicate chrome blocks. post.html already 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 from post.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 from post.html before 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 near floor(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 needs top: ~6.5rem (or whatever the masthead height is), max-height: calc(100vh - 8rem); overflow-y: auto;, and z-index: 1 (below the masthead). Otherwise the masthead overlaps the TOC label every time the page scrolls.
  • next/image rejecting hostnames. AI-generated featured images come from *.fal.media, stock photos from images.unsplash.com, uploaded assets from *.cloudinary.com / *.supabase.co, YouTube poster frames from i.ytimg.com. Add ALL of them to next.config.ts images.remotePatterns up front — section "Configure image hostnames" of the Next.js quickstart has the canonical block to paste.
  • Build hangs on cold Mentionwell API. Wrap fetch calls inside your reader helper with a 10s AbortController timeout, and add export 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/:slug returning 404 with getPost returning null is almost always missing MENTIONWELL_API_BASE / MENTIONWELL_API_KEY on 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.html is 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 featuredImage and <img src> inside prepareArticleHtml.

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.