docpin
View as Markdown

How published pages run

A DocPin page is your HTML, served inside a DocPin page at your link (https://acme.docpin.it/plan, https://docs.acme.com/plan, or a short link like https://docpin.it/Ab3dE). Knowing how that works lets you build interactive pages that behave well.

The short version

You want to…Do this
Keep state shareable (tabs, filters, selected item)Read location.search / location.hash, write with history.replaceState. DocPin mirrors it into the link.
Link to a section<a href="#pricing"> and an element with id="pricing".
Remember a visitor's own choiceslocalStorage works, per page, per browser.
Show numbers you update on a schedulePage data: docpin.data() / docpin.onData().
Link to another siteA normal <a href>. It opens in a new tab automatically.
Load a libraryFrom a public CDN with a <script src>, or inline it.
Look good when sharedA <title>, a <meta name="description">, and optionally a favicon and og:image. See link previews.
AvoidCookies, IndexedDB, location.href = "https://other.site", reading the parent page.

The frame

Your page runs in a sandboxed frame on its own address under docpinusercontent.com. Each published version gets its own address. The DocPin page around it shows the link, the "Pinned with DocPin" badge, and the password or sign-in screen for private pages.

What works inside the frame:

What doesn't:

URL state

DocPin connects your page's ?query and #hash to the link in the address bar, in both directions:

The query and hash also survive the password screen, people sign-in, and redirects from an old link after a page is renamed or moved.

<nav>
  <button data-tab="overview">Overview</button>
  <button data-tab="pricing">Pricing</button>
</nav>
<script>
  const params = new URLSearchParams(location.search);
  function show(tab) {
    document.querySelectorAll("[data-tab]").forEach((b) => b.setAttribute("aria-pressed", b.dataset.tab === tab));
    // …show the matching section…
  }
  show(params.get("tab") || "overview");
  document.querySelectorAll("[data-tab]").forEach((b) => b.addEventListener("click", () => {
    const q = new URLSearchParams(location.search);
    q.set("tab", b.dataset.tab);
    history.replaceState(null, "", "?" + q + location.hash);
    show(b.dataset.tab);
  }));
</script>

Tips:

Browser storage

localStorage and sessionStorage work, even though the sandbox normally blocks them:

The standard API works: getItem, setItem, removeItem, clear, key(i), length, and localStorage.name = value. Values are strings, so use JSON.stringify and JSON.parse for objects.

Storage is per visitor. To show the same data to everyone, use page data.

When someone shares a page's link in Slack, iMessage, email or social media, the preview comes from DocPin, using what your page says about itself:

Your page's <head>Used for
<title>The preview title and the browser tab.
<meta name="description">The preview's description.
<meta property="og:image">The preview image. Use a file in your upload (cover.png) or an https:// URL.
<link rel="icon">The browser tab icon. A file in your upload, an https:// URL, or a small data: image (an emoji SVG works).
<meta name="theme-color">The browser's UI colour on mobile.

Without an og:image, DocPin draws a share card from the page's title and link. Private pages (password or people-only) always show a "Private page" card, with no title, description or image, because a preview shouldn't reveal what's inside. Secret links show the generated card but not your description or image.

<title>Q4 proposal for Acme</title>
<meta name="description" content="Pricing, timeline and next steps for the Q4 rollout.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">

The docpin object

DocPin's script adds a global docpin before your own scripts run:

MemberDescription
docpin.data()Promise of the page's data object. See page data.
docpin.onData(cb, { every })Calls cb(data) now and whenever the data changes, checking every every seconds (default 30, minimum 10). Returns a stop function.
docpin.updatedAtWhen the data last changed (ISO 8601), after a read.

If your page might also be opened outside DocPin (for example, as a local file), check before using it: if (window.docpin) { … }.

Checklist for AI-generated pages

When an AI app writes a page for DocPin, it should:

  1. Send one complete, self-contained HTML document, or index.html plus its files.
  2. Keep any state worth sharing in the URL (replaceState), and read it on load.
  3. Use localStorage only for per-visitor state, and wrap it in try/catch.
  4. Read changing numbers from docpin.data() / docpin.onData() and show an empty state when there's no data.
  5. Use plain <a href> links to other sites, never location.href to another site.
  6. Avoid cookies, IndexedDB, service workers, and anything that needs the camera or location.
  7. Keep secrets out of the HTML, the URL and the page data.
  8. Give the page a clear <title> and <meta name="description"> for link previews, and optionally a favicon.