Next.js
Add Specify to a Next.js app.
Set up with the wizard
npx @specify-sh/wizard --publisherThe wizard runs through the coding agent you already use (Claude Code, Codex etc.). It inspects your codebase, proposes an integration plan and implements it as a reviewable diff.
Prefer to do it by hand? The manual steps below are the same setup.
Manual setup
Runnable example
nextjs-typescript is this guide as a working App Router app.
Next.js can serve ads two ways:
| Where you serve | Import | Identity comes from |
|---|---|---|
| Client component | @specify-sh/publisher-sdk | The visitor's cookie, plus wallets you pass |
| Server component, route handler, server action (Not recommended) | @specify-sh/publisher-sdk/server | Wallets you pass |
Only the browser can hold the Specify cookie, so only client components can fill a placement for a visitor who has no wallet connected. We recommend that you serve from the client when possible as this will give you higher fill rates.
1. Install
npm i @specify-sh/publisher-sdk2. Set your keys
You will need a publisher key. See Publisher keys.
The environment variable needs a NEXT_PUBLIC_ variable which is fine: publisher keys are public by design.
NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY=spk_your_key_here3. Serve an ad
Create the client in its own module and import it where you need it.
import Specify from '@specify-sh/publisher-sdk';
export const specify = new Specify({
publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY!
});useSpecifyAd() serves on mount, tries again when the identity improves, and keeps the first ad it gets.
'use client';
import { ImageFormat } from '@specify-sh/publisher-sdk';
import { useSpecifyAd } from '@specify-sh/publisher-sdk/react';
import { specify } from '@/lib/specify';
export function AdSlot() {
const ad = useSpecifyAd({
specify,
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'sidebar'
});
if (!ad) return null;
return (
<a href={ad.ctaUrl} target="_blank" rel="noopener noreferrer">
{ad.imageUrl && <img src={ad.imageUrl} alt="" />}
<h3>{ad.headline}</h3>
<p>{ad.content}</p>
<span>{ad.ctaLabel}</span>
</a>
);
}Importing it from a Server Component fails the build with a message telling you to add 'use client' or use the server entry instead.
The slot takes no addresses. Wallets registered through identify() ride along, so nothing has to be passed down the tree.
Available image formats and the fields returned in the ad can be found in the Browser SDK reference.
4. Connect consent
Wire your consent platform to the SDK, high in the component tree. Consent starts false and the SDK never stores it.
'use client';
import { useEffect } from 'react';
import { specify } from '@/lib/specify';
export function ConsentBridge() {
useEffect(() => cmp.onChange((consent) => {
specify.setCookieConsent(consent.targetedAdvertising);
}), []);
return null;
}With consent granted, Specify recognises returning visitors across every site in our network, so a placement can fill even when nobody has connected a wallet to your app.
5. Register connected wallets
If your app has a wallet connection flow, notify the SDK when a wallet is connected. Every later serve() includes it, so components deeper in the tree do not need the address passed down.
'use client';
import { useEffect } from 'react';
import { useAccount } from 'wagmi';
import { specify } from '@/lib/specify';
export function WalletBridge() {
const { address } = useAccount();
useEffect(() => {
if (address) specify.identify(address);
}, [address]);
return null;
}With this in place, an ad slot needs no wallet address at all.
specify.serve({ imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'sidebar' })Serving from a server component
import { serve, ImageFormat } from '@specify-sh/publisher-sdk/server';
export default async function Page() {
const ad = await serve({
publisherKey: process.env.SPECIFY_PUBLISHER_KEY!,
walletAddresses: ['0x1111111111111111111111111111111111111111'],
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'sidebar'
});
if (!ad) return null;
return <a href={ad.ctaUrl}>{ad.headline}</a>;
}walletAddresses is required here.
Importing the wrong entrypoint
Importing @specify-sh/publisher-sdk from a server component fails immediately, with a message telling you to use @specify-sh/publisher-sdk/server.