npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rebilder/render-md

v0.2.0

Published

Source-of-truth to markdown/JSON-LD renderer for commerce pages (PDP, policies, catalog). Pure format transformation: every substantive value is injected from the source object.

Readme

@rebilder/render-md

Source-of-truth → markdown/JSON-LD renderer for any page: commerce (PDP, policies, catalog) and everything else (services, locations, plans, articles, listings, profiles, government records). This is the markdown path of the gateway: when an agent asks for Accept: text/markdown, this package turns the merchant's canonical data into clean, front-loaded markdown and schema.org JSON-LD.

Contract

Pure format transformation. Framework-agnostic, zero runtime dependencies, pure functions, fully deterministic: same input → same output, everywhere, every time. No LLM, no network, no clock, no locale (Intl is not used).

Injection-only guarantee (Hard Rule 2). Every substantive value in the output — prices, availability, discounts, shipping and returns text, policy bodies, attributes, facts, hours, contact details, prose — is injected verbatim from the source object. This package never invents, estimates, or rewords a price, stock state, claim, or policy. The only text it adds is structural scaffolding, and that scaffolding is enumerated in full below. Non-integer money amounts throw rather than round: silently altering a price is never acceptable.

Same substance across formats (Hard Rule 3). Markdown output is a format transformation of the same substance as the canonical HTML page. Rendering must never produce different prices, claims, or availability than the page a human sees; the renderer has no inputs (requester identity, headers, UA) that could even make that possible.

Malformed input is dropped, never repaired. A malformed date, a non-finite number, a wrapping time interval, a URL with a scheme we do not allow — each is omitted from the output. Repairing a value invents one the merchant never stated; dropping it only loses information, which the agent can still get from the canonical page.

API

import {
  // commerce
  renderProductMarkdown, // ProductSource → markdown PDP (facts first, variants table, description, attributes, image links)
  renderProductJsonLd,   // ProductSource → schema.org Product with offers (decimal price strings, schema.org availability URLs)
  renderPolicyMarkdown,  // PolicySource[] → linked headings + verbatim bodies
  renderCatalogMarkdown, // CatalogItemSource[] → markdown table (linked title, price, availability)
  // universal
  renderDocumentMarkdown,   // DocumentSource   → any single page: facts, hours, contact, actions, prose
  renderCollectionMarkdown, // CollectionSource → any listing page: link list or fact table
  renderCollectionJsonLd,   // CollectionSource → schema.org ItemList of positioned ListItems
  formatMoney,           // { amount: 8900, currency: 'USD' } → "$89.00" (minor units in, deterministic, zero-decimal aware: ¥4,900)
} from '@rebilder/render-md'

Options for all render*Markdown functions: { maxBytes?: number; headingLevel?: number }.

Source types: ProductSource, ProductVariantSource, ShippingSource, ReturnsSource, PolicySource, CatalogItemSource, DocumentSource, CollectionSource, CollectionItemSource, Fact, FactValue, HoursSpec, ContactSource, ActionSource, LinkSource, Money, Availability. Only fields present in the source appear in the output — absent optionals render nothing.

The fixed label maps (BOOLEAN_LABELS, WEEKDAY_LABELS, PERIOD_SUFFIX, ACCESS_LABELS, CLOSED_LABEL, NOT_STATED_LABEL, SEE_PAGE_LABEL, WEEKDAY_ORDER, ALLOWED_URL_SCHEMES) are exported. They are part of the contract: a consumer rendering the same source objects on another surface must be able to reproduce our output exactly.

Rendering order (product)

Title (linked to canonical URL) → brand → price (~~$120.00~~ $89.00 when a compare-at price exists in source) → availability → shipping → returns → - **Updated:** → variants table (id / title / options / price / availability) → description → attributes → images as markdown links. An agent reading top-down gets every buying fact in the first ~15 lines.

updated sits at the end of the fact block rather than the start on purpose: it is metadata about the buying facts rather than one of them, and every line above - **Availability:** pushes a core fact further from the top of the document, which is a byte offset ARS D4 scores.

Freshness and language

| Source type | updated | language | |---|---|---| | ProductSource | dateModified in JSON-LD, - **Updated:** in markdown | inLanguage in JSON-LD | | CollectionSource | dateModified in JSON-LD | inLanguage in JSON-LD | | DocumentSource | - **Updated:** in markdown | — |

updated is an ISO 8601 date or datetime; language is a BCP 47 tag (en, pt-BR, zh-Hant, zh-Hant-TW, es-419).

DocumentSource has no language field on purpose. There is no document JSON-LD renderer, so the only thing that consumes language would never see it, and a source field that silently renders nothing is a worse contract than an absent one. It arrives with renderDocumentJsonLd, which is unbuilt because choosing a schema.org @type per DocumentKind is an inference this package does not make — DocumentKind is an open union of merchant-supplied strings.

Both are gates, never transforms. A value that passes is emitted byte-for-byte as supplied — casing included, because BCP 47 is case-insensitive and re-casing a merchant's tag would make our JSON-LD disagree with their own <html lang>. A value that fails is dropped from every representation at once, so a fact can never be good enough for the markdown and not the JSON-LD.

The date check is a real-calendar check, not a Date.parse call: Date.parse rolls a day-of-month overflow silently forward (2026-02-31 → March 3, 2026-02-29 → March 1 in a non-leap year), which is tolerable in a markdown line and not tolerable as dateModified in a product feed.

renderProductJsonLd feeds /acp/v0/feed and the MCP product tool as well as the canonical page, so anything it emits is published as an authoritative fact about the catalog.

Collections as structured data

renderCollectionJsonLd renders a CollectionSource as a schema.org ItemList of positioned ListItems. Three refusals are part of the contract:

  • name is omitted when the source has no title. The markdown renderer falls back to the fixed Contents heading because a markdown document needs a heading; JSON-LD does not, and emitting the fallback would assert a name the merchant never wrote.
  • Item URLs pass the same scheme allowlist as the markdown path. A rejected URL drops the link, not the item — the item still renders with its name, because dropping it would misreport the length of the listing.
  • numberOfItems counts what was emitted, not items.length. Past the 500-row cap, a list announcing 900 while carrying 500 would send a paging agent after items that are not in the document.

Item facts are deliberately not projected into the list: ListItem has no honest slot for an arbitrary labelled fact, and inventing property names for merchant labels is the kind of inference this package does not do. The facts are in the markdown table.

The universal surface

Every non-commerce vertical evaluated reduces to document with facts. A dentist's location page, a SaaS plan, a law firm's practice area, a news article, and a government service record differ in vocabulary, not in structure — so they share two types and two renderers.

Block order and truncation (document)

| # | Block | Truncatable | |---|---|---| | 1 | # [title](url) | no | | 2 | > summary | no | | 3 | - **Updated:** + scalar facts (text/list/number/boolean/money/date/url) | no | | 4 | ## <fact label> hours table per hours fact, in fact order | no | | 5 | ## Contact | no | | 6 | ## Actions | no | | 7 | ## <section heading> prose — only when access === 'free' | yes | | 8 | ## Related | yes — first to go |

Truncation is strictly from the bottom and always on whole lines, so related drops before prose and prose is never cut mid-sentence. A truncated document ends with the fixed truncation note and nothing renders after it.

Two ceilings, not one. maxBytes (default 5120) governs the truncatable tail. An absolute ceiling at 4 × maxBytes governs the required blocks: "facts are never sacrificed" is a safe promise for a PDP with a dozen fields and an unbounded one for a government page with sixty, and without the second ceiling the merchant's data shape decides our response size. Further hard caps: 60 facts, 20 actions, 500 collection rows, 12 collection fact columns.

Fact values

| FactValue.type | Rendered as | Dropped when | |---|---|---| | text | verbatim, newlines collapsed to spaces | — | | list | items joined with , | empty | | number | String(value), unit appended verbatim | non-finite, or exponential form (1e+21) | | boolean | Yes / No | — | | money | formatMoney; maxValue$900.00-$2,500.00; period → fixed suffix; per → verbatim | never — a contradictory range (currency mismatch, maxValue < value) throws | | date | verbatim | not a valid ISO 8601 date/datetime | | url | [label](url), or the bare URL when there is no label | scheme not allowlisted | | hours | its own ## <fact label> block with a weekday table | missing/blank timeZone, or any malformed interval |

Fact.note renders as an indented bullet under its fact line.

Hours. All seven weekdays always render. A weekday absent from weekly is Not stated; a weekday present with intervals: [] is Closed. Those are different facts and collapsing them would assert a closure the merchant never stated. An interval where closes <= opens is malformed — overnight spans are expressed as two intervals on two days — and one malformed interval drops the whole hours fact, because a table missing exactly the day that was broken reads as authoritative and is not. Nothing here computes "open now": that needs a clock and a timezone database, and a wrong answer to "are they open" is worse than no answer.

Access gate. sections is emitted only when access === 'free'. registered, metered, and subscriber emit summary + facts + contact + actions + a fixed access notice, and never touch sections. This is mechanical, not a prose rule: no renderer bug can leak a paywalled body and no merchant can accidentally over-serve one.

Dedup. If DocumentSource.updated is set and valid, a fact labelled Updated is dropped. Two "Updated" lines disagreeing is worse than one that might be stale.

Collections

Two deterministic modes and no third:

  • No item carries facts → a link list, one bullet per item, summary indented beneath.
  • Any item carries facts → a table. Columns are Title + the ordered union of fact labels by first appearance, plus a Note column carrying item summaries. The union is accumulated in the same streaming pass that spends the byte budget, so the header can never advertise a column belonging only to rows the budget then dropped. If not one row fits, no table is emitted at all.

An hours fact is not a valid table cell; it renders as See page linked to the item's own URL.

URL scheme allowlist

https:, http:, mailto:, tel:. Anything else — and any relative URL, since this package has no base URL to resolve one against — drops the item: the action, the related link, or the url fact. The commerce path only ever emitted merchant-controlled product and policy URLs; the universal path emits arbitrary merchant-supplied link targets, and [Click here](javascript:…) handed to an agent under our formatting authority is a phishing primitive. A document whose own url fails the allowlist still renders, with an unlinked title.

Escaping

], (, and ) are escaped in every markdown link label — without it, source text can close the label early and forge a link target. | and newlines are escaped in every table cell and in every column header derived from a Fact.label. A | inside a fact line is left alone; it only breaks tables.

Scaffolding whitelist

The complete list of renderer-authored text. Nothing else is ever added to the output.

  • Structure: **<label>:** bolding, # headings, - bullets, > blockquote markers, table pipes and --- separators, , between list items, - between the two ends of a money or time range.
  • Headings: Exceptions, Contact, Actions, Related, Contents.
  • Labels: Updated, Phone, Email, Address, Yes, No, Closed, Not stated, Day, Date, Hours, Note, Title, See page.
  • Fixed enum mappings: weekday names MondaySunday; billing-period suffixes one-time, per hour, per day, per week, per month, per quarter, per year; access words free, registration required, metered, subscription required (used only inside the access notice). These are format mappings of closed enums, the same class as Yes/No and the commerce path's In stock.
  • Fixed sentences: All times are local to <timeZone>., *Full text is not served to agents at this URL (access: <level>).*, and the truncation note.

ContactSource.url is rendered as a bare bullet with no label, because there is no whitelisted word for "the contact URL" and inventing one would put renderer prose in the output. DocumentKind and ActionKind are advisory routing/telemetry labels and are never rendered.

When a vertical earns its own type

Stated here so it is not relitigated per merchant:

A vertical earns its own source type and renderer only when (a) the generic Fact union loses machine-parseability for its core facts, and (b) a downstream protocol surface needs the structure, and (c) it is top-5 by observed merchant demand. Otherwise it ships as a profile — vocabulary, which is data.

Nothing clears the bar today. The closest call is article (normalised author/publisher for attribution); revisit that first if citation data justifies it.

Security and privacy notes

A snapshot is not a source of truth. DocumentSource.provenance is set by hosted extraction only. When present, the gateway refuses to serve the document past ttlSeconds and falls through to HTML, and a scheduled recrawl auto-unpublishes on content drift. Without that, a merchant changes their hours and agents keep receiving the approved bundle — different substance to agents and humans at the same URL, which is a Hard Rule 3 breach manufactured by our own pipeline. This package renders provenance documents like any other; enforcement is the gateway's job, and shipping extraction without it is not permitted.

Prompt injection. DocumentSectionSource.body is untrusted third-party prose delivered inside our structured envelope. A merchant page — or anything a merchant's CMS lets a third party publish — can contain text addressed to the agent reading it. We do not sanitise it, because rewriting merchant prose is exactly what this package must never do; we render it verbatim and it stays the consuming agent's responsibility to treat page content as data, not instructions. This is a known and deliberate boundary, not an oversight.

PII. ContactSource, combined with kind: 'profile' or 'job', makes it trivial to serve named individuals' contact details in bulk and in a machine-readable form. The merchant is the data controller. render-md must not be used to expose contact data that is not already public on the canonical page — mirroring ARCHITECTURE § Security.

Testing

pnpm --filter @rebilder/render-md test.

  • commerce-frozen.test.ts pins the three commerce renderers byte-exactly with inline string literals, not snapshots: vitest -u can silently regenerate a snapshot, it cannot regenerate a string literal. A change to that file is a deliberate output change, never a mechanical update. The same applies to tests/__snapshots__/ — a diff there is a build break, not a snapshot refresh.
  • documents-integrity.test.ts is structural, not a corpus scan, and this is the important part: a bare-number scan is not a validator. A number fact renders through String(value), so a fact of 3 renders 3, and from that moment any 3 anywhere in the output passes a scan — including a fabricated one. The test instead (a) pins every fact to the exact line - **<label>:** <expected>, with expected computed independently from the same fixed label maps, and (b) asserts that the output, minus the scaffolding whitelist, is a token-for-token subsequence of the concatenated source strings. Subsequence, not subset: a token in the wrong place fails too. The whitelist is restated as literals in that file rather than imported, so new renderer-authored text cannot whitelist itself.
  • documents-access.test.ts proves the access gate holds at every budget, against sentinel bodies that exist for no other purpose.
  • documents-budget.test.ts asserts the truncation ordering over a sweep of budgets rather than at hand-picked byte counts.
  • integrity.test.ts keeps the commerce price-token scan, which works there because a price has a recognisable shape.