npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mentra-intelligence/react

v0.4.1

Published

React renderer for Mentra headless CMS content

Readme

@mentra-intelligence/react

React renderer for Mentra headless CMS content.

Renders TipTap/ProseMirror JSON documents returned by the Mentra Public API — so you don't have to understand the underlying document format.

Install

npm install @mentra-intelligence/react

The MentraPage example below also uses isomorphic-dompurify (npm install isomorphic-dompurify), or any sanitiser of your own.

Published on the public npm registry, so no token or .npmrc setup is needed. If your project's .npmrc still points @mentra-intelligence at GitHub Packages (@mentra-intelligence:registry=https://npm.pkg.github.com), remove that line and its _authToken line.

Quick Start

import { MentraContent } from "@mentra-intelligence/react";

function ArticlePage({ content }) {
  return (
    <article>
      <MentraContent doc={content.contentJson} />
    </article>
  );
}

Image Sizing & Alignment

Content creators set image size and alignment in the Mentra editor. Rendered automatically:

| Size | Width | Alignment | Layout | |------|-------|-----------|--------| | small | 25% | left | Float left | | medium | 50% | center | Block centered | | large | 75% | right | Float right | | xl | 100% | | |

Alt text and caption are edited separately in Mentra. An image with a caption and no alt text uses the caption as its alt, as Mentra's contentHtml does.

Internal Links

A link from one of your articles to another carries the target's id. The API resolves it on every read: the link's href is the target's current slug, as /{slug}, so the link follows the article when its slug changes. A link to an article that is not published (or not yours) arrives as plain text; a preview read (publishedOnly=false) also resolves links to drafts.

{ "type": "link", "attrs": { "href": "/pricing-guide", "contentId": "abc123", "slug": "pricing-guide" } }

Internal links render at that href, in the same tab. If your articles live under another path, map them with resolveInternalLink; return undefined to render the text without a link:

<MentraContent
  doc={content.contentJson}
  resolveInternalLink={({ slug }) => (slug ? `/blog/${slug}` : undefined)}
/>

External links (no contentId) open in a new tab, as before. A link entry in markOverrides replaces both.

Custom Overrides

<MentraContent
  doc={content.contentJson}
  nodeOverrides={{
    heading: ({ node, children }) => (
      <h2 className="text-2xl font-serif mb-4">{children}</h2>
    ),
  }}
  markOverrides={{
    link: ({ mark, children }) => (
      <a href={mark.attrs?.href} className="text-blue-600 underline">{children}</a>
    ),
  }}
/>

Using contentHtml?

import "@mentra-intelligence/react/styles";
<div dangerouslySetInnerHTML={{ __html: content.contentHtml }} />

Pages: MentraPage

Render a published page from GET /v1/pages/{slug} with one component:

import { MentraPage } from "@mentra-intelligence/react";
import DOMPurify from "isomorphic-dompurify"; // plain `dompurify` has no `sanitize` in Node

const res = await fetch(`${API}/v1/pages/home`, { headers });
if (!res.ok) throw new Error(`GET /v1/pages/home failed: ${res.status}`); // 404: unknown or unpublished slug
const page = await res.json();

<MentraPage page={page} sanitizeHtml={(html) => DOMPurify.sanitize(html)} />;

Every block type has an unstyled default (semantic HTML, mentra-block-* classes). Replace any of them with blocks:

import { MentraPage, type BlockComponent, type BlockContentMap } from "@mentra-intelligence/react";

const Hero: BlockComponent<BlockContentMap["hero"]> = ({ content, context }) => {
  const href = context.resolveLink(content.ctaLink);
  return (
    <header className="my-hero">
      <h1>{content.heading}</h1>
      {href && content.ctaText && <a href={href}>{content.ctaText}</a>}
    </header>
  );
};

<MentraPage page={page} blocks={{ hero: Hero, testimonials: null /* hide */ }} />;

| Prop | Default | | |------|---------|---| | blocks | defaults | Components by block type. null hides a type. A key the package doesn't know adds a type. | | jsonLd | true | Render the page's jsonLd as <script type="application/ld+json">. | | sanitizeHtml | none | Sanitiser for rich_text HTML. Without one, rich_text renders as plain text. | | resolveLink | external URLs with a safe scheme | href for a link. Internal links carry a target id, so they render as text until you map them to your routes. Pages are fetched by slug: map a page id to its slug with GET /v1/pages, whose summaries carry both. | | resolveItemHref | none | href for a dynamic_content or related_content item: a standalone article, or a Section node (with its nodeTypeKey) when the block's source is section or a related_content block lists a node's siblings. Gets (item, block). | | renderContextBound | false | Render blocks the response marks contextBound. | | contentProps | {} | Overrides forwarded to MentraContent for article bodies. | | theme | {} | Colours, fonts and shape, exposed as CSS variables (--mentra-color-primary, --mentra-font-heading, --mentra-radius, …). |

Block types: hero dynamic_hero rich_text image video embed features_grid testimonials faq pricing cta columns dynamic_content content_body meta_info context_navigation related_content form. A block whose type has no component is skipped, so a page using a type newer than your package version still renders.

Video and embed blocks

video and embed render only what /v1/pages returns in the block's resolved, which Mentra builds after checking the URL against its allowlist:

  • A provider (YouTube, Vimeo, Loom, Spotify, Google Maps) renders an <iframe> whose src is resolved.embedUrl and whose sandbox, allow and referrerPolicy are copied from resolved unchanged, with loading="lazy". The frame is never built from content.url. An embed takes its shape from aspectRatio (16:9, the default, 4:3, 1:1 or 9:16); a video frame is 16:9.
  • A video file (resolved.provider === "file") renders <video controls> from resolved.fileUrl, with poster, a captions <track>, and muted autoPlay playsInline / loop when the block asks for them.
  • resolved: null (the stored URL failed the check) renders nothing.

The default <video> sets no crossOrigin, because CORS mode would stop the video loading from a host that sends no CORS headers. A captions file on another origin therefore isn't loaded, and the video plays without captions. To load cross-origin captions, override video with a client component that sets crossOrigin="anonymous" and drops it on the video's error event, as Mentra's own frontend/src/components/VideoBlock.tsx does.

If your site sends a Content-Security-Policy, allow the player hosts in frame-src:

frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://www.loom.com https://open.spotify.com https://www.google.com

Mentra keeps this list as FRAME_HOSTS in backend/app/libs/page_block_media.py. Video files can be on any https host, so media-src needs https:.

Form blocks

A form block is a newsletter sign-up whose submission goes from the visitor's browser straight to the tenant's own service: Mentra never receives or stores what a visitor types. /v1/pages puts how to submit in the block's resolved (type, submitUrl, fieldNames, honeypotField, preview); the block's content holds the labels and only public identifiers, never an API key.

The default FormBlock is a native HTML form, so it needs no client script:

  • webhook posts to resolved.submitUrl, and the page navigates to the endpoint's reply.
  • mailchimp posts to resolved.submitUrl in a new tab, where Mailchimp shows its own confirmation.
  • hubspot takes only a JSON body, which a native form can't send, so the default renders nothing on a live read. Override form (below).
  • A preview read (resolved.preview: true, from a draft or preview-token request) draws the form with the button disabled and says submitting is off; it never gets the live URL. So does any resolved without an https submitUrl.
  • resolved: null (no destination, or one Mentra's checks refuse) renders nothing.

Every form carries a honeypot: a hidden input named resolved.honeypotField, off-screen in an aria-hidden wrapper with tabIndex={-1}. People leave it empty and many bots fill it. A custom form must add one too, and must send nothing when it is filled.

To stay on the page and show successMessage, or to support HubSpot, override form with a client component that calls submitMentraForm. It posts a webhook form-encoded with mode: "no-cors" (so "sent" means the browser sent it; the reply can't be read), uses Mailchimp's JSONP endpoint (your script-src must allow the list-manage.com host), and posts HubSpot JSON. It throws on a preview read and returns "skipped" without sending when the honeypot is filled.

"use client";
import { useState } from "react";
import { submitMentraForm, type BlockComponent, type BlockContentMap } from "@mentra-intelligence/react";

export const SignupForm: BlockComponent<BlockContentMap["form"]> = ({ block, content }) => {
  const r = block.resolved;
  const [state, setState] = useState<"idle" | "sending" | "done" | "error">("idle");
  if (!r?.fieldNames || !r.honeypotField) return null;
  const live = r.preview === false;
  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const f = new FormData(e.currentTarget);
        setState("sending");
        try {
          await submitMentraForm(r, {
            email: String(f.get("email") ?? ""),
            name: String(f.get("name") ?? ""),
            consent: f.get("consent") === "on",
            honeypot: String(f.get("hp") ?? ""),
          });
          setState("done");
        } catch {
          setState("error"); // the typed values stay in the inputs
        }
      }}
    >
      {content.heading && <h2>{content.heading}</h2>}
      {content.collectName && <label>{content.nameLabel || "Name"} <input name="name" autoComplete="name" /></label>}
      <label>{content.emailLabel || "Email"} <input name="email" type="email" required autoComplete="email" /></label>
      <div aria-hidden="true" style={{ position: "absolute", left: "-10000px" }}>
        <input name="hp" tabIndex={-1} autoComplete="off" defaultValue="" />
      </div>
      {content.consentText && <label><input name="consent" type="checkbox" required /> {content.consentText}</label>}
      <button type="submit" disabled={!live || state === "sending"}>{content.buttonText || "Subscribe"}</button>
      {!live && <p>Preview: submitting is off.</p>}
      <p role="status">
        {state === "done" && (content.successMessage || "Thanks for subscribing!")}
        {state === "error" && "That didn't go through. Please try again."}
      </p>
    </form>
  );
};

// <MentraPage page={page} blocks={{ form: SignupForm }} />

If your site sends a Content-Security-Policy, the destination host must be allowed in form-action (native form) or connect-src (submitMentraForm).

A block type's content type, default component and registry entry live in src/page/blocks.tsx; a type whose resolved object has fields of its own (like form) also adds them to PageBlock in src/page/types.ts.

Supported Nodes

doc paragraph heading bulletList orderedList listItem blockquote codeBlock hardBreak horizontalRule image table tableRow tableHeader tableCell embed callout keyTakeaways pullQuote

A table puts its leading all-tableHeader rows in <thead> and the rest in <tbody>, as Mentra's published HTML does.

An embed node ({"type": "embed", "attrs": {"url": "…"}}) renders a sandboxed <iframe> only for a YouTube, Vimeo, Loom, Spotify or Google Maps URL. The frame src is built from the parsed id by the same allowlist Mentra enforces, never taken from url. Any other https URL renders as a link, and anything else renders nothing. The frames need the same frame-src hosts as the page video and embed blocks above.

The article blocks match Mentra's published HTML: a callout renders <aside data-callout="note|tip|warning|info"> (an unknown variant falls back to note); keyTakeaways renders <section data-key-takeaways> with an optional <h2> title; pullQuote renders a <figure> with a <blockquote> and an optional <figcaption> attribution, linked to sourceUrl only when that URL is relative or http, https, mailto or tel.

Supported Marks

bold italic underline strike code link

TypeScript

import type { TipTapNode, TipTapMark, ImageAttrs, InternalLink, NodeRenderer, MarkRenderer, MentraContentProps } from "@mentra-intelligence/react";

License

MIT \u00a9 Mentra Intelligence