# @sonordev/agency-site-kit

Case studies for agency websites built on [Sonor](https://sonor.io). 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](https://sonor.dev/agency-site-kit).

## Install

```bash
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`:

```bash
SONOR_API_KEY=sonor_xxxxxxxx_xxxxxxxxxxxx
```

It'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:

```bash
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](https://sonor.dev/agency-site-kit/quickstart)
walks through every flag and file.

## A case study page

```tsx
// 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](https://sonor.dev/agency-site-kit/proof)                                          |
| Sections          | `orderedSections`, `sectionData`, `projectBrandColor`, and the one-laptop-or-three-devices decision                                         | [Sections and devices](https://sonor.dev/agency-site-kit/sections)                                       |
| Images and labels | `resolvePortfolioImage`, category, service and date formatting, path sanitizing                                                             | [Images, labels and paths](https://sonor.dev/agency-site-kit/images)                                     |
| Structured data   | `buildPortfolioJsonLd` and `jsonLdString`                                                                                                   | [JSON-LD](https://sonor.dev/agency-site-kit/json-ld)                                                     |
| Data              | The server fetchers, metadata, static params and sitemap helpers, and the error contract                                                    | [Fetching](https://sonor.dev/agency-site-kit/fetching), [Types](https://sonor.dev/agency-site-kit/types) |
| Brand             | `AgencySiteKitLayout`, which publishes your brand as `--sk-*` CSS variables                                                                 | [Brand layout](https://sonor.dev/agency-site-kit/layout)                                                 |
| Live updates      | `createRevalidateHandler` and `createPortfolioPreviewHandler`                                                                               | [Live updates and preview](https://sonor.dev/agency-site-kit/live-updates)                               |
| Device frames     | Showing a client's live site inside a device frame                                                                                          | [Live device frames](https://sonor.dev/agency-site-kit/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, including `AgencySiteKitLayout`.
  Server only.

The [Subpath map](https://sonor.dev/agency-site-kit/subpaths) 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](https://sonor.dev/agency-site-kit/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
