Integrating Snaps
If you already have a website and want it to render as a snap when shared on Farcaster, you need to serve snap JSON from the same URL as your site when the client requests it. You can use HTTP headers to serve snaps and HTML from the same URL. See HTTP Headers for details.
Dynamic sites (Node.js, Hono, Express)
If your site has a server, add middleware that checks for the snap media type in
Accept before your existing routes.
With Hono
import { Hono } from "hono";
const app = new Hono();
// Snap handler -- runs before your existing routes
app.get("/", async (c, next) => {
const accept = c.req.header("Accept") || "";
if (!accept.includes("application/vnd.farcaster.snap+json")) {
return next(); // Not a snap request, continue to normal site
}
// Return snap JSON
return c.json(
{
version: "1.0",
theme: { accent: "purple" },
ui: {
root: "page",
elements: {
page: {
type: "stack",
props: {},
children: ["title", "body", "cta"],
},
title: {
type: "text",
props: { content: "My Site", weight: "bold" },
},
body: {
type: "text",
props: { content: "Welcome to my site on Farcaster.", size: "sm" },
},
cta: {
type: "button",
props: { label: "Visit site", variant: "primary" },
on: {
press: {
action: "open_url",
params: { target: "https://example.com" },
},
},
},
},
},
},
200,
{
"Content-Type": "application/vnd.farcaster.snap+json",
Vary: "Accept",
},
);
});
// Your existing routes continue to work
app.get("/", (c) => c.html("<h1>My normal website</h1>"));
With Express
app.get("/", (req, res, next) => {
const accept = req.headers.accept || "";
if (!accept.includes("application/vnd.farcaster.snap+json")) {
return next();
}
res.set("Content-Type", "application/vnd.farcaster.snap+json");
res.set("Vary", "Accept");
res.json({
version: "1.0",
theme: { accent: "blue" },
ui: {
root: "page",
elements: {
page: {
type: "stack",
props: {},
children: ["title", "body"],
},
title: {
type: "text",
props: { content: "My Site", weight: "bold" },
},
body: {
type: "text",
props: { content: "Check us out on Farcaster.", size: "sm" },
},
},
},
});
});
Static sites (GitHub Pages, Netlify, S3)
Static hosts can't do server-side content negotiation. Use one of these approaches to honor HTTP Headers at the edge:
Option 1: Cloudflare Worker proxy
Put a Cloudflare Worker in front of your static site. It inspects Accept and routes
snap requests to a separate snap server.
export default {
async fetch(request: Request): Promise<Response> {
const accept = request.headers.get("Accept") || "";
const url = new URL(request.url);
if (accept.includes("application/vnd.farcaster.snap+json")) {
// Forward to your snap server (e.g. deployed on host.neynar.app)
const snapUrl = "https://my-snap.host.neynar.app" + url.pathname;
return fetch(snapUrl, {
method: request.method,
headers: request.headers,
body: request.body,
});
}
// Forward to your static site
return fetch(request);
},
};
Deploy the snap server separately (e.g. using the snap template on host.neynar.app) and point the worker at it.
Option 2: Vercel Edge Middleware
If your static site is on Vercel, add a middleware.ts at the project root:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const accept = request.headers.get("Accept") || "";
if (accept.includes("application/vnd.farcaster.snap+json")) {
// Rewrite to your snap API route or external snap server
return NextResponse.rewrite(
new URL("https://my-snap.host.neynar.app" + request.nextUrl.pathname),
);
}
return NextResponse.next();
}
Option 3: Separate snap URL
The simplest approach -- deploy your snap to a different URL entirely and share that URL in casts. Your website stays untouched.
- Website:
https://example.com - Snap:
https://example-snap.host.neynar.app
Users who click the link in a browser go to the snap's fallback page (which previews the snap and links to your site). Farcaster clients render the snap inline.
Handling POST interactions
If your snap has buttons with submit actions, the Farcaster client sends signed POST
requests to the button's target URL. For static site setups (Options 1-3 above), these
POSTs go to your snap server, not the static site.
Make sure your snap's button targets point to the snap server URL, not the static site:
{
"type": "button",
"props": { "label": "Vote", "variant": "primary" },
"on": {
"press": {
"action": "submit",
"params": { "target": "https://my-snap.host.neynar.app/vote" }
}
}
}
Testing
Test content negotiation with curl:
# Should return your normal HTML
curl -sS https://example.com/
# Should return snap JSON
curl -sS -H 'Accept: application/vnd.farcaster.snap+json' https://example.com/
Then test interactively at farcaster.xyz/~/developers/snaps -- enter your URL and click Load snap. The emulator sends real signed POST requests, so buttons work exactly like in the feed.