Docs
Dashboard

Concepts

When an address changes

Routes match on values your team can edit. We keep what those values used to be, so your site can answer a stale link instead of losing the reader.

Your site routes on something in your content: a slug, a code, a key. That value belongs to the people writing the content, and sooner or later one of them changes it. They are allowed to. A page gets renamed, a product gets repositioned, a typo gets fixed three months after launch.

Every link anybody saved, sent or indexed then points at a value nothing publishes. Your site cannot tell that apart from an address that never existed, so both come back as a 404 and the reader leaves.

ZevContent keeps the previous values, and tells you when you ask.

Mark what your code reads

Only fields you declare are kept. Set frontend_contract on the field your routes match:

{ "key": "slug", "label": "Address", "type": "text", "required": true,
  "frontend_contract": true,
  "help_text": "Your routes match on this." }

That one flag does two jobs. It makes the dashboard present the field as a value with a pen rather than an open input, so nobody edits it by accident (see Navigation and mega menus). And it is how we know the value is worth remembering.

Ask what an address used to be

curl -G https://api.zevcontent.net/v1/content/page/lookup \
  --data-urlencode "field=slug" \
  --data-urlencode "value=/services/managed-it/networks-and-connectivity" \
  -H "Authorization: Bearer $ZEVCONTENT_KEY"

Three answers, and your router branches on match.

It is still live. Render it.

{ "match": "current",
  "entry": { "id": "ent_68f...", "type": "page",
             "url": "/v1/content/page/ent_68f..." } }

It moved. current_value is where it lives now, so you can redirect without a second request.

{ "match": "previous",
  "replaced_at": "2026-09-13T15:33:46.914Z",
  "current_value": "/services/managed-it/network-operations",
  "entry": { "id": "ent_68f...", "type": "page",
             "url": "/v1/content/page/ent_68f..." } }

Nothing ever published it. 404, and this one really is a 404.

In a router

const res = await fetch(
  `${API}/v1/content/page/lookup?field=slug&value=${encodeURIComponent(path)}`,
  { headers: { Authorization: `Bearer ${key}` } },
);

if (res.status === 404) return notFound();

const { data } = await res.json();
if (data.match === 'previous') {
  return redirect(data.current_value, 301);
}
return render(data.entry);

Call it only when your normal lookup missed. A request that matched a live page has nothing to ask.

What is kept, and when

Only on publish. A draft has never been served, so a value that only existed in an unpublished draft is not history, it is typing.

Only the fields you marked. Keeping every previous value of every field would be a second copy of your project, to answer a question nobody asks of a headline.

The most recent claim wins. A value can be handed around: a slug one page gives up can be taken by another and given up again. The lookup answers with the entry that gave it up most recently, which is the one a reader following an old link was most likely looking at.

History starts when you mark the field. We cannot record what a value used to be before we were asked to watch it, so set frontend_contract when you define the field rather than after the first rename.

Updated at, Sunday, September 20, 2026