docs
    press-kit: Overview
    v1.4.0.md

    @sonordev/press-kit

    Articles, publication pages, author profiles, topic clusters, newsletter signup, RSS/Atom feeds and Google News sitemaps for Sonor-powered Next.js sites. Press Kit owns the publication UI and data helpers; Sonor's app and APIs own editorial planning, Broadcast artwork and publishing.

    pnpm add @sonordev/press-kit @sonordev/site-kit

    Use Site Kit 8.4 or later in the 8.x series, React 19 and Next.js 16. Keep SiteKitLayout in your root layout and SONOR_API_KEY in .env.local. Press Kit uses the site's existing credential, transport, shared styling tokens and managed forms. It needs no extra key or provider.

    import { Article } from '@sonordev/press-kit/articles/server-ui'
    import { generateArticleMetadata, generateArticleStaticParams } from '@sonordev/press-kit/articles/server'
    
    export const generateStaticParams = generateArticleStaticParams
    export async function generateMetadata({ params }) {
      const { slug } = await params
      return generateArticleMetadata(slug, { siteName: 'Our journal' })
    }
    export default async function Page({ params }) {
      const { slug } = await params
      return <Article slug={slug} showRelated showToc />
    }
    EntryUse
    RootTypes only, including Article, ArticleEditorialPhoto, ArticleAuthor
    /articlesClient widgets, newsletter signup and synchronous presentation helpers
    /articles/shareLightweight public article share and copy controls
    /articles/serverServer-only article reads, metadata, static params, schemas, feeds and news sitemap
    /articles/server-uiServer-only publication, article, category and cluster components; table/photo insertion

    Never import asynchronous article components from the client entry. Server reads stay behind server-only; client calls use Site Kit's shared transport. The article flow uses the same photo placement contract as Sonor's planning API, including stable heading anchors and exact missing-placement diagnostics.

    See Articles for component props and feed/sitemap examples. Existing @sonordev/site-kit/articles imports remain compatible in Site Kit 8.3, built from this exact source; migrate by replacing only the package name. Removal belongs to a future Site Kit major release. Neither installation nor migration changes a published article or schedules a broadcast.

    Reliable reads and public archives

    Press Kit 1.1 uses Site Kit 8.4 or later. getArticle(slug) returns null only for a confirmed 404. An outage, invalid credential or malformed upstream response throws ArticleUnavailableError; let your route error boundary offer a retry instead of caching a false “not found.” getAllArticles({ maxPosts: Infinity }) reads the whole archive and throws on incomplete reads. getArticleResult offers an explicit ready | not_found | unavailable result for custom interfaces.

    Topic-cluster reads follow the same rule: getTopicClusters() returns [] only for a valid empty list, and getTopicCluster(slug) returns null only for a confirmed 404. Unavailable or incomplete cluster data throws ArticleUnavailableError.

    insertArticlePhotos accepts an optional imageAttributes adapter for responsive page images, including Next.js getImageProps. Feeds and MCP keep the original two-argument call and canonical image URLs. Approved captions, dimensions and source identities survive optimization; see the supporting photo example.

    getPublicationIssues({ site, limit, cursor }) and getPublicationIssue(id, { site }) return that same result shape. A ready list contains { issues, nextCursor? }; a ready issue is the immutable sent snapshot { id, title, subject, sentAt, articles, html?, text? }. Draft campaigns and recipient data never belong in this public archive. Keep a sample honestly labeled until a sent issue exists; don't present an unavailable archive as an empty one. Render archived HTML in an iframe with an empty sandbox (or allow-popups allow-popups-to-escape-sandbox for its read links), never inject email HTML into your site's document.

    Managed newsletter forms acknowledge the accepted signup request. They don't claim a visitor is subscribed before the asynchronous enrollment completes, or reveal whether an address is already subscribed. useForm from Site Kit exposes submissionResult.publicationEnrollment and reload(); retries preserve entered values.

    Article HTML and what Sonor sanitizes

    The article HTML Press Kit renders arrives already cleaned. Sonor's public article reads (every post list and detail response, the blog alias, and the private draft preview) pass content_html and the answers in faq_items through an allowlist before they leave the API. The stored body isn't changed; only the copy a site receives is. This has been live since 2026-10-20.

    • content_html keeps ordinary article markup: headings (with id anchors), paragraphs, lists, blockquotes, pre and code (with language-* classes), tables, figures and captions, img over https, inline emphasis, line breaks, rules, and span or div with a plain class name. Links keep http, https, mailto and tel URLs. It drops script and style elements, every on* handler and style attribute, javascript: and data: URLs, forms and form controls, object, embed, svg, math and all iframes.
    • FAQ answers get a narrower list: bold, italic, underline, paragraphs, line breaks, lists, links and span, with no attributes other than a link's href, rel and target.

    Press Kit adds no sanitizer of its own. insertArticlePhotos, wrapArticleTables and the external-link rewrite are layout transforms that trust their input, and the stock Article renders the HTML it's given.

    Those transforms, the heading anchors and the table of contents find tags the way a browser's HTML tokenizer does. A quote opens an attribute value only right after =, so a stray quote in an attribute name or an unquoted value doesn't pair with the next one. A tag that never closes is left alone, along with all the text after it, because a browser reads that text as part of the tag. Text that doesn't start a tag, such as 1 < 2, is left as written. Ordinary article HTML comes out byte for byte as it went in, apart from the attributes the transforms add. The text of an RSS description or an Atom summary made from the article body carries no < or >: a bracket that is left once the tags are gone is written as &lt; or &gt;.

    content_html is the only article field Press Kit renders as HTML. content is the source text, not a render field: Sonor doesn't clean it, so the stock Article, the RSS feed and the Atom feed never render it, not even as a fallback. A post with a blank content_html renders an empty body. Article doesn't convert content_html from Markdown either; the body is the HTML Sonor sent. Keep content_html populated. If a custom renderer reads content, treat it as text and escape it.

    HTML that reaches your site any other way sits outside the guarantee too, such as a sent-issue archive's html or anything from your own database or another API. So don't render fields Press Kit doesn't expose, or the fields above when they come from somewhere else, as page HTML. Don't add dangerouslySetInnerHTML paths of your own for article content, and don't build markup from article text or URLs in a custom renderer without escaping it. A strict Content-Security-Policy stays worth having as a second layer.

    Private previews on the actual site

    Sonor issues a short-lived token for one project, site, article and saved revision. No additional credential is needed. Configure the entry route at app/api/press-preview/route.ts:

    import { createPublicationPreviewHandler } from '@sonordev/press-kit/articles/server'
    export const dynamic = 'force-dynamic'
    export const GET = createPublicationPreviewHandler({
      siteUrl: 'https://example.com', previewPath: '/journal/preview',
    })

    Give the preview route a synchronous server layout returning <div data-sk-private-preview>{children}</div>. The marker is present even while the draft is streaming; Site Kit suppresses analytics snapshots, SitemapSync, chat/popups and reporting for the whole private document.

    At the fixed preview page, read the scoped HttpOnly cookie and exchange it on the server for every render. Use your ordinary article rendering component with the returned post. Never pass the token to a client component or log it.

    import { cookies } from 'next/headers'
    import { getArticlePreview, PUBLICATION_PREVIEW_COOKIE } from '@sonordev/press-kit/articles/server'
    export const dynamic = 'force-dynamic'
    export const metadata = { robots: { index: false, follow: false }, referrer: 'no-referrer' }
    export default async function Preview() {
      const token = (await cookies()).get(PUBLICATION_PREVIEW_COOKIE)?.value || ''
      const result = await getArticlePreview(token, { site: 'example.com' })
      if (result.state !== 'ready') return <p>Return to Sonor to open a current preview.</p>
      return <YourArticleRenderer post={result.value.post} preview />
    }

    Set both the token entry route and preview page response headers to PUBLICATION_PREVIEW_HEADERS (using Next's headers() configuration, after any broad global header rule): private/no-store, noindex/nofollow/noarchive, and no-referrer. On Netlify, also set Netlify-CDN-Cache-Control: no-store. Exclude this route from the sitemap and public feed. The helper strips the token from the browser URL via a fixed redirect; changed revisions and expired tokens fail closed. Previewing never publishes or schedules anything.

    Discovery images, freshness and agent reading

    resolveRepresentativeImage(post) prefers the API's approved representative_image, then editorial art, then the legacy featured image. Known asset dimensions are preserved; missing sizes are omitted. Metadata lists the representative image first and keeps the social card available; Twitter retains the composed card. Schemas and image/news sitemaps use the same representative image. Article metadata enables max-image-preview:large. An editor-approved editorial_verification note may include reviewedAt and reviewDueAt; an ordinary updated_at is never presented as fact verification.

    The site's MCP should use the same routing and readers:

    articles: {
      path: '/journal', routing: { includeCategoryInPath: true },
      readMany: getAllArticles, readOne: getArticle,
    }

    list_articles searches the complete archive and body text, returns canonical URLs and supports offset. get_article returns next_offset so agents can continue past the first 12,000 characters instead of mistaking an excerpt for the whole article.

    Launch from a brand-owned definition

    definePublication from /articles/server validates a versioned definition for your publication identity, sections, newsletter form and brand. The newsroom, journal and briefing starters use distinct grid densities, reading widths and typography. Preview generated routes with npx sonor-setup publication --config publication.json --json, then add --write to create them. The command preserves existing files and never copies another customer's content or credentials. See the CLI launch guide for the definition and the remaining content, preview and launch checks.

    Approved publication_cta: { label, url, goal } data renders a call to action in the stock article. Custom links should carry data-sk-article-id and data-sk-article-goal; Site Kit's analytics provider sends article_cta_click with { article_id, goal, href } through the site's existing gated transport. Clicks measure interest, not completed conversions.