inmake DOCS
Docs / Generative / Load more

Load more

The "load-more" rule fragment — composed into the CRM & Studio agents' prompts and served as drop-core://rules/load-more.

LOAD MORE (paged listings): a listing that grows on demand ("load more", next page, infinite scroll) is three parts that must line up — the field's envelope, the page-1 markup, and the fetch script. The envelope is the single contract: the renderer resolves page 1 from it AND the server derives every later page from the SAME envelope, so there is nothing else to configure.

  1. THE FIELD — a kind:'reference' field with reference_type:"collection". Its stored VALUE is the ENVELOPE, e.g. {"type":"collection","collection":"recipes","limit":12,"order_by":{"title":"asc"},"query":{"category":"soup"},"ref":"/data/{locale}/recipes"}.
  • Set limit to the page size and ALWAYS set order_by — paging without an explicit stable order skips/repeats documents. order_by must name a field the schema declares and EVERY document populates — the sort key doubles as a filter (documents missing it are silently excluded; a field no document has returns an empty page 1 with no error).
  • query narrows the listing; later pages inherit it automatically.
  • query on one field PLUS order_by (or a range) on ANOTHER runs on a QUERY KEY on the collection. The common shapes are BUILT IN and need nothing: status + newest (timestamp), status + a datetime field, the first enum field (alone or with status) + its date / newest; on products status + price / name / newest and the category / tag pages (readRule("data-collections") QUERY KEYS, readRule("commerce-products") LISTINGS). Only a shape beyond them — a number sort like prep_time, a second enum — needs registerCollection keys: {"status_prep_time": ["status", "prep_time"]}, the equality fields first and the sort field last; declare it BEFORE shipping the listing: without a key the listing sorts in memory only while ≤ 500 documents match the filter; above that page 1 and every /content page answer 400 query_key_required {collection, suggest} — declare exactly suggest as the key, do not shrink the listing. A sort-only envelope (no query) needs no key. The after cursor (the server's offset) works unchanged under a key — nothing in the envelope or the script changes.
  • NEVER write resolved documents into the field. A materialized array cannot page, goes stale the moment content changes, and the load-more endpoint refuses it ("not a collection reference"). The renderer resolves the envelope at render time — that is the point.
  1. PAGE 1 — the template renders the resolved field as usual. Give the container the FIELD KEY and every card its doc id, so the script knows what to ask for and where it stopped:
<div class="rec-grid" data-field="recipes_list">
  {{#each recipes_list}}<article class="rec-card" data-id="{{ this.id }}">…</article>{{/each}}
</div>
<button type="button" class="rec-more">{{t "list.load_more"}}</button>

The button label is the same on every listing — a translations key, never a field each listing document must fill.

  1. MORE PAGES — POST ${DROP_BASE}/content/${DROP_LOCALE} with {path, field, after} (drop-core-server route; both globals are injected into every rendered page):
var grid = document.querySelector('[data-field]');
var btn = document.querySelector('.rec-more');
btn?.addEventListener('click', async function () {
  var cards = grid.querySelectorAll('[data-id]');
  var res = await fetch(window.DROP_BASE + '/content/' + window.DROP_LOCALE, {
    method: 'POST', headers: {'content-type': 'application/json'},
    body: JSON.stringify({
      path: location.pathname,                              // this page - the server reads ITS envelope
      field: grid.dataset.field,                            // the reference field's key (dot-path for nested)
      after: cards[cards.length - 1]?.dataset.id,           // last id already shown
    }),
  });
  if (!res.ok) return;
  var body = await res.json();
  body.documents.forEach(function (doc) { /* append a card; keep data-id="{doc.id}" */ });
  if (!body.next) btn.hidden = true;                        // short/absent next = collection exhausted
});
  • The response is {count, limit, documents, next?} — documents are RAW collection docs (the same data page 1 rendered from; the one exception: kind:"link" fields inside them arrive already resolved to URL STRINGS, so a card script reads doc.href as a plain string exactly as page 1 did), next is the new cursor and is OMITTED when the collection is exhausted.
  • The request carries NO filters — collection/query/order_by/limit all come from the page's envelope, so the browser can never list anything the page doesn't already show; a limit in the request may only SHRINK the page.
  • Build the appended card's markup to MATCH the {{#each}} block (same classes/fields), or the page visibly degrades after the first click.
  • WHERE THE SCRIPT LIVES: when the listing is a library ELEMENT, the fetch script goes in that element's js — it then loads only on pages placing the listing (readRule("elements") for the channel contract). When the listing is plain template markup, it goes in that template's js. Never a standalone store file loaded site-wide via headTags.

SMALL COLLECTIONS: when everything fits in one render (up to a few dozen docs), skip all of this — omit limit (or set it high) and filter/hide client-side. Paging is for collections that actually grow.