@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-kitUse 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 />
}| Entry | Use |
|---|---|
| Root | Types only, including Article, ArticleEditorialPhoto, ArticleAuthor |
/articles | Client widgets, newsletter signup and synchronous presentation helpers |
/articles/share | Lightweight public article share and copy controls |
/articles/server | Server-only article reads, metadata, static params, schemas, feeds and news sitemap |
/articles/server-ui | Server-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_htmlkeeps ordinary article markup: headings (withidanchors), paragraphs, lists, blockquotes,preandcode(withlanguage-*classes), tables, figures and captions,imgoverhttps, inline emphasis, line breaks, rules, andspanordivwith a plain class name. Links keephttp,https,mailtoandtelURLs. It dropsscriptandstyleelements, everyon*handler andstyleattribute,javascript:anddata:URLs, forms and form controls,object,embed,svg,mathand 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'shref,relandtarget.
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
< or >.
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.