llms.txt

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.