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

@stratum-core/presentation

v0.4.2

Published

The presentation-mode contract a Stratum consumer site implements: the message channel, the record annotations, and the hover/select overlay.

Readme

@stratum-core/presentation

npm license

The consumer half of Stratum's presentation mode. A site embeds itself in the operational app, and an editor gets three things on their real pages: hovering any content shows which record produced it, clicking it opens that record in the editor, and typing in the form updates the page in place.

This package is the site's side of that contract — the message channel, the record annotations, and the hover/select overlay. Everything in it is plain DOM and plain postMessage with zero runtime dependencies; React and Next are optional peers used only by the convenience entry points.

Install

npm  install @stratum-core/presentation
pnpm add     @stratum-core/presentation
yarn add     @stratum-core/presentation

Requires Node 18+ or any modern browser bundler. react >= 18 and next >= 15 are optional peers — you need them only for /react, /next and /field.

Entry points

There is no root export; import the entry point you need.

| Entry point | What it is | Needs | | ---------------------------------------- | ------------------------------------------------- | ------------ | | @stratum-core/presentation/annotate | Attributes that mark a record and its fields | — | | @stratum-core/presentation/overlay | The hover/select overlay, as plain DOM | — | | @stratum-core/presentation/protocol | Message types, version, and readers for the wire | — | | @stratum-core/presentation/react | <PresentationOverlay> for any React app | react | | @stratum-core/presentation/next | <PresentationOverlay> that refreshes the router | react, next | | @stratum-core/presentation/field | <Field>, an inline span marking one field | react |

Next.js (App Router)

A complete integration is five files. Every snippet below is the shape a working consumer actually has, not a sketch.

1. Configure the host origin

# .env
STRATUM_OPERATIONAL_ORIGIN=https://app.stratum.example

One variable, and it is the on-switch. Unset means presentation is off, not open — the route must 404 rather than render a frame nobody is allowed to embed. Never read the origin off the incoming message; an origin you accept from the embedder is an origin any page can claim.

2. The route

// app/presentation/[collection]/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { PresentationOverlay } from '@stratum-core/presentation/next'

import { PageView } from '@/components/page-view'
import { fetchPage } from '@/lib/fetch-page'

// Never cached: it renders a draft that changes while the editor works.
// Never indexed: it is not a page of the site.
export const dynamic = 'force-dynamic'
export const metadata: Metadata = { robots: { index: false, follow: false } }

export default async function PresentationPage({
  params,
  searchParams,
}: {
  params: Promise<{ collection: string; slug: string }>
  searchParams: Promise<{ token?: string }>
}) {
  const { collection, slug } = await params
  const { token } = await searchParams
  const hostOrigin = process.env.STRATUM_OPERATIONAL_ORIGIN

  if (!hostOrigin || !token) notFound()

  const page = await fetchPage({ collection, slug, token })

  // A rejected token and an absent draft are the same 404 here: the host built
  // this URL and can say which, and the frame has no chrome to say it in.
  if (!page) notFound()

  return (
    <PageView
      page={page}
      // Its presence is what puts record ids into the DOM, so the published
      // page never carries them. `parent` is the sequence these records sit
      // in, written `collection:id:field`.
      presentation={{ parent: `${collection}:${page.id}:sections` }}
      overlay={<PresentationOverlay hostOrigin={hostOrigin} accent="#ee7623" />}
    />
  )
}

The route renders the same components as the public route. If it renders anything else, presentation is lying to the editor about their own site.

3. Read content with the token

// lib/fetch-page.ts
export async function fetchPage({ collection, slug, token }) {
  const res = await fetch(`${process.env.STRATUM_API_URL}/graphql`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      // Forwarded verbatim, and never verified: the site holds no secret and
      // must not pretend to.
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ query: pageQuery(collection), variables: { slug } }),
    // A cached response is by definition the published one, so the editor
    // would be shown their own draft's absence and told it was a preview.
    cache: 'no-store',
  })

  if (res.status === 401) return null   // say the session ended...
  // ...and do NOT fall back to published content, or the editor concludes
  // their draft was lost.

  const { data } = await res.json()
  return data?.[collection]?.[0] ?? null
}

4. Annotate — only while previewing

This is the part to get right. Annotations must exist on the presentation route and nowhere else: on a published page they disclose your collection names and record ids, and the field wrappers change the DOM your audience gets. So the flag from step 2 gates them, rather than the components emitting them always.

// components/render-section.tsx
import { sectionAttributes } from '@stratum-core/presentation/annotate'
import { Field } from '@stratum-core/presentation/field'

// The collection each rendered record came from — the inverse of the type tag
// Stratum sends. Presentation opens the editor by collection name, so a wrong
// entry here opens the wrong record.
const COLLECTION_BY_TYPE = {
  Banner: 'cms_banners',
  Statement: 'cms_statements',
} as const

export function renderSection(section, presentation, index = 0) {
  // Spread onto the section's root element. Absent when not previewing.
  const marks = presentation
    ? sectionAttributes({
        collection: COLLECTION_BY_TYPE[section.__typename],
        id: section.id,
        parent: presentation.parent,
        index,
      })
    : undefined

  // Wraps a value so the overlay can outline that field, and leaves it exactly
  // as it was otherwise: an annotated page and a published one must lay out
  // identically.
  const f = (name: string, value: React.ReactNode) =>
    presentation ? <Field name={name}>{value}</Field> : value

  switch (section.__typename) {
    case 'Banner':
      return (
        <Hero
          key={section.id}
          {...marks}
          title={f('title', section.title)}
          lede={section.lede ? f('lede', section.lede) : undefined}
        />
      )
    // ...
  }
}

Your components must forward unknown props to their root element, or marks lands nowhere and the overlay sees an unannotated page.

5. Headers

Two rules, and both traps below are load-bearing.

// next.config.ts
const stratumOrigin = process.env.STRATUM_OPERATIONAL_ORIGIN?.trim()

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        // The embedded editing surface: framed by Stratum and by nothing else.
        source: '/presentation/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: `frame-ancestors ${stratumOrigin || "'none'"}`,
          },
          { key: 'X-Robots-Tag', value: 'noindex, nofollow' },
          // The token is in this page's URL, so without this every image, font
          // and script it loads would carry a replayable session in its Referer.
          { key: 'Referrer-Policy', value: 'no-referrer' },
        ],
      },
      {
        // Every page except /presentation, and nothing under /_next.
        //
        // Excluding /presentation matters because two CSP headers on one
        // response are enforced as their intersection, so a blanket 'none'
        // would win and blank the frame with nothing in any log.
        //
        // Excluding /_next matters more: these headers would otherwise be added
        // to the dev server's HMR websocket upgrade, which breaks the handshake,
        // so the client runtime never boots and *no page on the site hydrates*.
        // Server-rendered HTML still looks perfect, which is why only driving a
        // browser catches it.
        source: '/((?!presentation|_next/).*)',
        headers: [
          { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
        ],
      },
    ]
  },
}

6. Keep it out of the index

// app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      // /presentation renders unpublished drafts for one embedded editor.
      disallow: ['/presentation'],
    },
  }
}

7. Verify it

The route must refuse an unauthenticated request, carry the two headers, annotate its records, and mount the overlay. Check all four against a running deployment before you call it done — a rule nobody can run is a rule nobody follows.

The contract, in any framework

  1. A dynamic route/presentation/<collection>/<slug>?token=<jwt> — that reads its content with the token and renders the public components.
  2. noindex, plus a robots.txt disallow.
  3. Content-Security-Policy: frame-ancestors <stratum origin> on that route, and 'none' everywhere else.
  4. Referrer-Policy: no-referrer on that route — the token is in the URL and would otherwise ride out in the Referer of every image and font request.
  5. Annotations in its render path, emitted only on that route. On a published page they would disclose your collection names and record ids, and the field wrappers would change the DOM your audience gets.
  6. The overlay, mounted only on that route.

Without React, mount the overlay yourself and pass your own refresh — the two non-React entry points are all you need:

import { createOverlay } from '@stratum-core/presentation/overlay'
import { sectionAttributes, fieldAttributes } from '@stratum-core/presentation/annotate'

const overlay = createOverlay({
  hostOrigin: HOST_ORIGIN,
  onRefresh: () => location.reload(),  // or however your framework re-reads content
})
// on teardown
overlay.destroy()

Whatever the framework, exclude its dev-server asset path (/_next on Next) from the site-wide frame-ancestors rule. Applied to an HMR websocket upgrade it breaks the handshake, the client runtime never boots, and no page on the site hydrates — with perfect server HTML and a green test suite.

The token

Short-lived and read-only: the platform denies every mutation from a preview caller above the resolver layer. Forward it verbatim as Authorization: Bearer on the content read and never verify it — the site holds no secret and must not pretend to. If it is rejected, say the session has ended; do not fall back to published content, or the editor concludes their draft was lost.

A cross-site frame receives no cookies, which is why the token travels in the URL rather than in a session.

API

/annotate

sectionAttributes({ collection, id, parent, index }): Record<string, string>
fieldAttributes(field: string): Record<string, string>
targetFrom(element: Element | null): Target | null

parent is the sequence the record sits in, written collection:id:field. targetFrom walks up from an element to the record — and the field, when the record still contains it — that produced it.

A field element must always be a descendant of its record's element. That containment is how the overlay decides which record a field belongs to; a field found above the record belongs to an ancestor record, not to this one.

The attributes are also exported by name: ID_ATTR, COLLECTION_ATTR, FIELD_ATTR, PARENT_ATTR, INDEX_ATTR.

/overlay

createOverlay(options: OverlayOptions): Overlay

| Option | Type | Notes | | ------------ | ------------ | ------------------------------------------------------------ | | hostOrigin | string | The one origin it talks to and takes orders from. Required. | | onRefresh | () => void | Re-read the page's content. Required. | | accent | string | Outline and label colour. Defaults to a high-contrast orange. | | document | Document | Injected for tests; defaults to the ambient document. | | window | Window | Injected for tests; defaults to the ambient window. |

Returns { destroy() }, which removes every listener and the outline element and is safe to call twice.

onRefresh is yours because only your framework knows how its data is fetched — router.refresh() on Next, a query invalidation elsewhere, a reload at worst.

The overlay never writes. A preview token is read-only by contract, so every change is made by the host with the operator's own session.

/react and /next

<PresentationOverlay hostOrigin={string} onRefresh={() => void} accent={string} />

Renders nothing — the overlay owns its own element outside the React tree. The /react version defaults onRefresh to a full page reload, which always works and costs the scroll position. The /next version takes no onRefresh: it uses router.refresh(), which re-renders the server component in place so the editor keeps their scroll position. That route must be dynamic and its content read uncached, or the refresh returns the same cached payload and the change appears not to have happened.

/field

<Field name="title">{section.title}</Field>

An inline span carrying nothing but the field's name, with no styling of its own: an annotated page and a published one must lay out identically. It is a separate module without "use client" on purpose — exporting it from /react would put a client boundary around every annotated string on the page.

/protocol

PROTOCOL_VERSION: number
CHANNEL: 'stratum.presentation'
type Target = { collection: string; id: string; field?: string }
type FrameMessage  // ready | hover | select | input
type HostMessage   // hello | editable | highlight | patch | scrollTo | refresh
envelope<M>(message: M): Envelope<M>
readFrameMessage(data: unknown): FrameMessage | null
readHostMessage(data: unknown): HostMessage | null
sameTarget(a: Target | null, b: Target | null): boolean

Both sides announce PROTOCOL_VERSION on connect; a mismatch is reported to the operator rather than guessed at. Every message rides in an envelope tagged with CHANNEL, so unrelated postMessage traffic is ignorable.

Messages are addressed to one origin and accepted from one origin. A target read off the wire is rebuilt from its three known keys, so a sender cannot smuggle extra properties into an object the receiver may spread into a mutation input.

For coding agents

The package ships an agent skill covering the whole integration — the procedure, the traps that produce correct-looking source, and a verification checklist:

mkdir -p .claude/skills
cp -r node_modules/@stratum-core/presentation/skills/stratum-presentation .claude/skills/

Claude Code picks it up from there. The traps in it are the ones that fail silently — a blank frame with nothing in any log, a site where no page hydrates, record ids leaking onto published pages — so an agent wiring this up unaided is likely to ship at least one of them.

Versioning

0.x: the wire protocol may change with a minor bump. PROTOCOL_VERSION is the thing to check — the host and the site must agree on it, and a mismatch surfaces to the operator rather than degrading silently.

License

Apache-2.0. See LICENSE.