Next.js

The whole integration of a Next.js site by hand — model, read, refresh on publish, preview, forms — ending with a green doctor.

From a Next.js site with hardcoded content to a site its editors run from Mapled, step by step, with no AI agent. An agent does all of this for you after one prompt (Connect with AI); this is the same integration done by hand. Plan on an hour for a small site.

You need a Mapled project where you can change the structure (the builder plan), a Next.js 14+ site on the App Router, and Node 18 or newer. The example is a site with a homepage and a blog; the keys (homepage, articles) are whatever you choose in step 1.

1. Model the content

In Mapled, open the project and press the wrench in the header (Structure) → Collections. Create a single for every one-off part of the site (homepage, footer) and a collection for every list of alike things (articles, team), then Add field for each text, image and link the code hardcodes today.

  • Keys are what the code reads — getRecords("articles"), data.title. A key can be renamed later and the old one keeps working as an alias, but it is a migration: choose them with care.
  • A collection whose records have pages of their own needs a Slug field — getRecordBySlug finds records by it.
  • Write a Help text for every field. Editors see it under the field; write it for someone who has never seen the code.
  • Mark a field Sensitive when editors keep it but the site must never receive it.

2. Fill it in and publish once

Open Content, move the hardcoded texts and images into records, then Review & publish. The site only ever reads published releases: before the first publish the delivery API answers 404 “Nothing published yet.” — getSingle and getRecord read that as null, getRecords throws.

3. Link the repository and generate types

npx @mapled/cli auth login
npx @mapled/cli project link
npx @mapled/cli types generate
npx @mapled/cli schema pull

Your browser opens Mapled, you pick the project and authorize; project link writes mapled.json, types generate writes mapled-types.ts — an interface per collection and single, tied together by MapledSchema — and schema pull records the schema the site is built against in mapled/schema.json, which schema diff and doctor compare with Mapled later. (CLI reference has the details of every command used here.)

4. Create the client

Install the package, and a Markdown renderer for rich text (step 5 uses react-markdown):

npm install @mapled/next react-markdown

and put the delivery key — the project's settings (the gear in its header) → Integrations → Your site — into .env.local and your hosting's environment:

MAPLED_KEY=mk_live_…

Then one client for the whole site:

// lib/mapled.ts
import { createClient } from "@mapled/next";
import type { MapledSchema } from "../mapled-types";

export const mapled = createClient<MapledSchema>({ key: process.env.MAPLED_KEY! });

With MapledSchema every read is typed by its key, and a misspelled collection or field is a build error.

5. Replace the hardcoded content with reads

Read in Server Components — no client-side fetching, no loading states. A single:

// app/page.tsx
import { assetUrl } from "@mapled/next";
import { mapled } from "../lib/mapled";

export default async function Home() {
  const home = await mapled.getSingle("homepage");
  if (!home) return null; // nothing published yet

  return (
    <main>
      <h1>{home.data.headline}</h1>
      <p>{home.data.intro}</p>
      {home.data.cover ? <img src={assetUrl(home.data.cover, { width: 1280 })} alt="" /> : null}
    </main>
  );
}

A list:

// app/blog/page.tsx
import Link from "next/link";
import { mapled } from "../../lib/mapled";

export default async function Blog() {
  const { records } = await mapled.getRecords("articles", { sort: "-date", limit: 20 });

  return (
    <ul>
      {records.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.data.slug}`}>{post.data.title}</Link>
        </li>
      ))}
    </ul>
  );
}

A page per record — new records get their page without a deploy:

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import Markdown from "react-markdown";
import { mapled } from "../../../lib/mapled";

export async function generateStaticParams() {
  const { records } = await mapled.getRecords("articles", { fields: ["slug"] });
  return records.map((post) => ({ slug: post.data.slug }));
}

export default async function Article({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await mapled.getRecordBySlug("articles", slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.data.title}</h1>
      <Markdown>{post.data.body}</Markdown>
    </article>
  );
}

(params is a Promise since Next.js 15; on 14 it is the plain object.) Rich text is Markdown — render it with a component such as react-markdown, never as raw HTML. Leave in the code what editors shouldn't touch: layout, navigation structure, anything that is logic rather than content.

6. Refresh the site on publish

Reads are cached (revalidate: 3600) and tagged; a webhook refreshes them the moment someone publishes.

// app/api/mapled/revalidate/route.ts
import { createRevalidateHandler } from "@mapled/next/server";

export const POST = createRevalidateHandler({
  secret: process.env.MAPLED_WEBHOOK_SECRET!,
});

The route builds and deploys before the secret exists: until MAPLED_WEBHOOK_SECRET is set it answers 503 and revalidates nothing.

Deploy, then in Mapled → Integrations → Your site save https://your-site.com/api/mapled/revalidate as the Revalidation webhook, put the Signing secret that first save shows into MAPLED_WEBHOOK_SECRET (hosting env and .env.local) — it shows once; later, Rotate secret gives a new one — redeploy, and press Send test ping: the delivery shows up under Recent deliveries as Delivered. The URL has to be public — Mapled refuses addresses that resolve to a private network.

Optional: pages that are already open can follow a publish too, without a reload — a route and one component, see Live updates.

7. Preview drafts

// app/api/mapled/preview/route.ts
import { createPreviewHandler } from "@mapled/next/server";

export const GET = createPreviewHandler();
// app/api/mapled/preview/exit/route.ts
import { createExitPreviewHandler } from "@mapled/next/server";

export const GET = createExitPreviewHandler();

The Preview button in Mapled opens the site at the origin of the webhook URL from step 6, on exactly this path — /api/mapled/preview — so keep the route there. It turns on Next.js draft mode, and every read of the client switches to live drafts, uncached, with no changes in your components.

8. Forms

A contact or signup form posts straight to Mapled; submissions land in Forms, with e-mail notifications and spam protection. Create the form once:

npx @mapled/cli forms create "Contact"

and post to it from the site — see Forms for the component.

9. Live data

Orders, bookings, votes — records the site's backend writes and reads immediately, without Publish — go to collections created with the mode Live data and are reached with the server key: see Live data.

10. Record the integration

npx @mapled/cli scan --write
npx @mapled/cli bindings push
npx @mapled/cli md pull

scan reads the code and writes mapled/manifest.json: which page reads which collection and field. bindings push sends it to Mapled, so editors see where a field appears and get a warning before a change would break the site (Structure → Bindings). md pull writes MAPLED.md, the guide for whoever works on the site next — person or AI agent. Commit mapled.json, mapled-types.ts, mapled/schema.json, mapled/manifest.json and MAPLED.md; never commit an env file.

11. Check everything

npx @mapled/cli doctor

It checks the integration from both sides — the repository and what Mapled sees of the deployed site. The list below is what an agent's setup has to pass before Mapled calls it complete; doctor answers all of it except the last two lines, which are yours to check:

  • the site reads content from Mapled (“Site reads content — Last read …”);
  • a publish refreshes it (“Publish webhook — Delivered …”);
  • Preview in Mapled opens the site with drafts (“Preview on the site — Responds at …”);
  • bindings are pushed and healthy, and the integration is in sync (“Bindings”, “Integration”);
  • no env file or secret is tracked by git (“Secrets in git”);
  • every field has a help text (Structure → Collections);
  • the site builds.

Later, when the structure changes in Mapled, npx @mapled/cli schema diff says what changed and whether it breaks the site; after updating the code, run types generate, scan --write, bindings push and md pull again.