@sonordev/agency-site-kit
Case studies for agency websites built on Sonor. The kit fetches your portfolio from Sonor and gives your site the rules every case study follows: which numbers it may claim and the credit a client's number wears, the order its sections read in, JSON-LD, image sizes, the device-stage decision, and what happens when the API can't be reached. How your case studies look is up to you: the kit doesn't render any markup, and the setup CLI scaffolds a starter renderer that's yours to restyle.
Built for the Next.js App Router. Full docs live at sonor.dev/agency-site-kit.
Install
pnpm add @sonordev/agency-site-kit @sonordev/site-kit@sonordev/site-kit (>= 6.5.0) is a peer dependency. It carries the shared
Sonor data plane and the portfolio contract the kit's proof rules build on.
Next.js >= 14 and React >= 18 are peers too.
You need one environment variable, in .env.local:
SONOR_API_KEY=sonor_xxxxxxxx_xxxxxxxxxxxxIt's read on the server only. There's no NEXT_PUBLIC_ variant and no
project ID: Sonor works out the project from the key.
Quickstart
From your Next.js project root:
npx agency-site-kit-setup --site-url https://youragency.com --agency-name "Your Agency"That writes the /work index, category pages, case study pages, a starter
renderer, the webhook Sonor calls when content changes and the dashboard's
draft preview route. It never overwrites a file, so it's safe to run again.
Then wrap your root layout in AgencySiteKitLayout and add your case studies
to the sitemap. The Quickstart
walks through every flag and file.
A case study page
// app/work/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { curatePortfolioProof, formatMetricValue, metricNote, sectionData } from '@sonordev/agency-site-kit/portfolio';
import { getPortfolioItem } from '@sonordev/agency-site-kit/portfolio/server';
export default async function CaseStudyPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const item = await getPortfolioItem(slug);
if (!item) notFound(); // null means it doesn't exist; an outage throws instead
const hero = sectionData(curatePortfolioProof(item), 'portfolioHero');
return (
<article>
<h1>{hero?.headline ?? item.title}</h1>
<dl>
{hero?.kpis.map((kpi) => {
const note = metricNote(kpi, 'short');
return (
<div key={kpi.label}>
<dt>{kpi.label}</dt>
<dd>{formatMetricValue(kpi)}</dd>
{note ? <dd>{note}</dd> : null}
</div>
);
})}
</dl>
</article>
);
}metricNote is the line a number wears when you didn't measure it: per Northwind Engineering on a client-reported number, Estimated on an
estimate, nothing on a measured one.
What's in the box
| Area | What you get | Docs |
|---|---|---|
| Proof | curatePortfolioProof, metricNote, headlineKpi, gateMetricsDeltas, formatMetricValue and the measured / reported / estimated model | Proof and provenance |
| Sections | orderedSections, sectionData, projectBrandColor, and the one-laptop-or-three-devices decision | Sections and devices |
| Images and labels | resolvePortfolioImage, category, service and date formatting, path sanitizing | Images, labels and paths |
| Structured data | buildPortfolioJsonLd and jsonLdString | JSON-LD |
| Data | The server fetchers, metadata, static params and sitemap helpers, and the error contract | Fetching, Types |
| Brand | AgencySiteKitLayout, which publishes your brand as --sk-* CSS variables | Brand layout |
| Live updates | createRevalidateHandler and createPortfolioPreviewHandler | Live updates and preview |
| Device frames | Showing a client's live site inside a device frame | Live device frames |
Where to import from
@sonordev/agency-site-kit/portfolio: the rules and the types. Pure, so it imports from a server component, a client component, a plain module or a test.@sonordev/agency-site-kit/portfolio/server: the fetchers and Next.js helpers, plus the same rules. Server only.@sonordev/agency-site-kit: everything, includingAgencySiteKitLayout. Server only.
The Subpath map lists every entry and what it exports.
Keeping up to date
The kit is pre-1.0, so breaking changes can land in a minor release. The changelog says when one does.
A new release doesn't reach your site on its own. Your lockfile pins the
version and your host installs from it, so run
pnpm update @sonordev/agency-site-kit and commit the new lockfile. The build
succeeds either way and the site looks the same, so an update that never
landed is easy to miss. Install from the npm registry, never as a link:
dependency: a link works locally and fails on a fresh clone.
License
MIT