Skip to content

markdown route: return lastModified (and title) alongside the markdown #105

Description

@yumike

Problem

The markdown route added in #104 returns a bare PageMarkdownResponse:

GET /site/:namespace/:kind/:name/markdown?sectionRef=&subpath=
→ { markdown: "…" }

There is no way for a consumer to learn when the page last changed.

That matters most for the use case the route was built for. An AI agent reading a runbook should be able to caveat a two-year-old one — "this doc was last updated in 2024, verify before following it" — and to prefer a fresh page over a stale one when two hits look equally relevant. Freshness is the difference between an agent quoting a stale runbook confidently and quoting it with a warning.

Why frontmatter doesn't solve it

getPageMarkdown includes frontmatter, so title, description and kind are recoverable by parsing the body. lastModified is not — it's a git author-time / storage mtime, not something an author writes in the file. Core surfaces it on PageMetaResponse.lastModified and PageEntryResponse.lastModified, but neither is reachable from this route.

Why the workaround is self-defeating

The only way to get it today is to also call:

GET /site/:namespace/:kind/:name/pages/:path

which runs a full HTML render of the page just to read one timestamp off its metadata. That reimposes exactly the cost getPageMarkdown was designed to avoid — the core commit is explicit that it "is a single storage read, so it costs nothing for callers who only want HTML". A markdown route that forces a render back onto the caller gives that saving straight back.

It's also two round-trips and two failure modes for one logical read.

Ask

Return cheap metadata alongside the markdown:

{
  "meta": { "title": "", "lastModified": "", "sectionRef": "", "subpath": "" },
  "markdown": "---\ntitle: …\n---\n\n# …"
}

This is additive{ markdown }{ markdown, meta } — so it doesn't break the route shipped in #104, and it can land whenever.

Where to get it without rendering

You know the internals better than I do, but two render-free sources already exist:

  • listPages() returns PageEntryResponse { sectionRef, subpath, title, lastModified } — keyed by exactly the (sectionRef, subpath) identity this route already takes. That looks like a direct lookup.
  • rw-backend's own site-index registry already stores PageRow { site_ref, section_ref, subpath, title, last_modified }, written by the scan worker from listPages.

Either way, the point is: a markdown request should not have to pay for an HTML render to learn when the page changed.

If a per-page metadata read is genuinely expensive and only listPages() (a whole-site listing) can supply it, say so and I'll adapt — but then it's probably worth a getPageMeta(path) on core instead.

Context

Consumer: an MCP read-page tool for AI agents (Backstage side). It returns a page as Markdown, windowed to the agent's token budget, inside a metadata envelope — { title, lastModified, url, content_markdown, … }. lastModified is the one field of that envelope I currently cannot populate without rendering the page to HTML and throwing the HTML away.

Follows from #103 (where this was the main ask) and #104.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions