APIOnly() — content types without a public URL

Not every content type belongs on the public web. APIOnly() is a single module option that makes a content type invisible to browsers while keeping every MCP tool and CLI command intact. No HTML route, no JSON leak, no crawlable admin surface.

Amendment A102 · Smeldr core v1.24.0

Some content types have no business being on the public web. A HomePage managed entirely via MCP tools, a PlatformConfig set once at launch, a NavTree built from a CMS dashboard — these are admin objects, not pages. Yet Smeldr registered HTML routes for all modules, which meant a browser visiting /home-pages would receive a JSON blob instead of a 404. Confusing for humans, potentially confusing for crawlers.

smeldr.APIOnly() fixes this with a single option:

smeldr.NewModule((*HomePage)(nil),
    smeldr.At("/home-pages"),
    smeldr.Repo(repo),
    smeldr.MCP(smeldr.MCPWrite),
    smeldr.APIOnly(),
)

Now:

  • GET /home-pages with Accept: text/html404
  • GET /home-pages with Accept: application/json200 JSON (unchanged)
  • All MCP tools present — create_home_page, update_home_page, publish_home_page, etc.
  • smeldr-cli works via the REST API without any changes
  • Preview token bypass still works for agents (they use JSON)

The response is 404, not 406. 404 says "this URL has no browsable surface" — search engines won't attempt to index it. 406 would say "I can't produce HTML" which leaks the fact that content is there. The defensive posture matches how Smeldr already handles Draft content: 404 intentionally hides existence.

One guard is added: APIOnly() and SingleInstance() cannot be combined — SingleInstance serves HTML at the module prefix, which is exactly what APIOnly forbids. NewModule panics at startup if both are supplied.

Routing variants summary (v1.24.0):

OptionHTML surfaceMCP tools
*(default)*FullFull set
SingleInstance()GET /{prefix} onlylist_{type}s suppressed
Standalone()GET /{slug}Full set
APIOnly()NoneFull set