Skip to content

Embeddable post grid (widget)

A <script> tag that renders a grid of your published posts inside a page you do not control — a partner’s site, a landing page, an app shell. It reads the public read API and builds the DOM itself. No iframe.

The bundle is 4.4 KB gzipped and pulls in nothing else.

  1. Create a public read key.

    In the admin, open Configurações → Integrações API → Criar chave, pick Pública de leitura (pk_), and register the origins where the widget may run (for example https://parceiro.com and https://www.parceiro.com). The pk_… token is shown once.

  2. Paste the snippet.

    <div data-canverly-grid data-key="pk_YOURKEY" data-limit="6"></div>
    <script src="https://docs.canverly.com/widget/v1/grid.js"
    integrity="sha384-pHV9vZdn2P2/wjNayBRVklmrBLFKkPT5Hz6ZAHiodvsxUo4C9Vr5pbxdIbje3MEF"
    crossorigin="anonymous" defer></script>

    Keep the integrity and crossorigin attributes. They are what makes the browser refuse the file if a single byte of it ever differs from what you audited.

  3. Check the origin.

    If the grid does not appear, open the console. [canverly-grid] origem não está na allowlist da chave means the page’s origin is not registered on the key — add it in the admin. The API answers 403 in that case: the key is valid, the page it is used from is not.

Attribute Default Meaning
data-key — Required. The pk_… key. data-site is accepted as an alias.
data-limit 6 1–50. Out-of-range values are clamped locally with a console warning.
data-category — Category slug.
data-tag — Tag slug.
data-language — BCP-47 tag, e.g. pt-BR.
data-locale <html lang> or pt-BR Locale for dates and labels.
data-link-target _self Use _blank to open posts in a new tab (links always carry rel="noopener noreferrer").
data-empty-text localized Message shown when the site has no matching posts.
data-api https://api.canverly.com API base. Only https: (or localhost) is accepted.

Several grids can live on the same page; each [data-canverly-grid] element is rendered independently. The current state is exposed as data-state: loading, ready, empty or error.

If the host page sends a CSP, it needs three sources:

script-src https://docs.canverly.com
connect-src https://api.canverly.com
img-src https://canverly.b-cdn.net

Styles need no CSP change: the widget ships its CSS inside the bundle and applies it through a constructable stylesheet, which style-src 'self' does not block. Only the legacy fallback loads grid.css as a <link>, and that needs style-src https://docs.canverly.com. Every class is prefixed cvw-, so nothing leaks into the host page’s styles.

The widget never assigns HTML. Every value coming from the API goes in through createElement + textContent, and every URL is scheme-checked before it reaches the DOM. A post titled <script>alert(1)</script> renders as that literal text, visibly — it does not execute, and it does not silently disappear. This is enforced by tests that assert on the resulting DOM (no <script> node, no extra <img>, the title node has zero child elements) plus a mutation test that fails if anyone swaps textContent for innerHTML.

  • 600 requests/minute per key, plus 60 requests/minute per (key, IP) for public keys. The per-IP dimension exists so a single hostile visitor cannot drain the quota of every page that embeds your grid.
  • One page view = one request. The widget renders a single page of results and ignores next_cursor on purpose.
  • On any failure the widget renders nothing visible and logs to the console. It never throws into the host page.

The version is in the path, and the bytes at a published path never change. /widget/v1/grid.js will always be the file whose hash is above; a new build is published under a new path. That is what lets the snippet pin integrity and what lets us serve the file with a one-year immutable cache. If we ever ship a breaking change, it becomes /widget/v2/grid.js and this page gets a new snippet — the one you pasted keeps working.

Current hashes are published next to the file at /widget/v1/INTEGRITY.txt.