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 choices | localStorage works, per page, per browser. |
| Show numbers you update on a schedule | Page data: docpin.data() / docpin.onData(). |
| Link to another site | A normal <a href>. It opens in a new tab automatically. |
| Load a library | From a public CDN with a <script src>, or inline it. |
| Look good when shared | A <title>, a <meta name="description">, and optionally a favicon and og:image. See link previews. |
| Avoid | Cookies, 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:
- Scripts, forms, dialogs (
alert,confirm), downloads, fullscreen, copy to clipboard, andwindow.open. - Fetching from any API that allows cross-origin requests (CORS). Your page's origin is opaque (
null), so the API must allow*. - Your own files from the same upload, with relative links (
styles.css,img/logo.png,about.html).
What doesn't:
- Cookies and IndexedDB. They're blocked. Use
localStorage(below) or the URL. - Navigating the whole tab to another site from a script.
location.href = "https://…"andwindow.top.locationfail. Use a link orwindow.open. - Reading the DocPin page around you, its cookies or its storage.
- Camera, microphone, location, payment and USB.
Links
- Links to other sites open in a new tab, so your page stays open. DocPin adds
target="_blank"andrel="noopener"when someone clicks them. mailto:andtel:links also open in a new tab, which hands them to the mail or phone app.- Links to
#idsscroll within the page, as normal. - Links between files in your upload (
about.html) open inside the frame. The address bar keeps the page's own link.
URL state
DocPin connects your page's ?query and #hash to the link in the address bar, in both directions:
- In: opening
https://acme.docpin.it/plan?tab=pricing#faqloads your page withlocation.search === "?tab=pricing"andlocation.hash === "#faq". If#faqmatches an element's id, the browser scrolls to it. - Out: when your page changes its query or hash, through
history.replaceState,history.pushState, settinglocation.hash, or a#link, DocPin updates the address bar to match. Copying the link then reopens that exact view.
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:
- Use
replaceStatefor UI state like tabs and filters, so the back button isn't flooded. UsepushStateonly for things that feel like separate pages. - Keep state small: up to about 4,000 characters of query plus hash.
- Use the URL for state people share. Use storage for state that's personal (a visitor's draft, a dismissed banner).
- Don't put anything secret in the URL. It's shown in the address bar and stays in browser history.
Browser storage
localStorage and sessionStorage work, even though the sandbox normally blocks them:
localStorageis kept for this page in the visitor's browser, under the page's link (for exampleacme.docpin.it). It survives reloads and new versions of the page.- Each page has its own storage. Pages don't see each other's.
- Up to about 1 MB per page. Past that,
setItemthrowsQuotaExceededError. - It's still the visitor's browser: clearing site data removes it, and it doesn't follow them to another device.
sessionStoragelasts for the current visit only.
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.
Link previews and icons
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:
| Member | Description |
|---|---|
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.updatedAt | When 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:
- Send one complete, self-contained HTML document, or
index.htmlplus its files. - Keep any state worth sharing in the URL (
replaceState), and read it on load. - Use
localStorageonly for per-visitor state, and wrap it in try/catch. - Read changing numbers from
docpin.data()/docpin.onData()and show an empty state when there's no data. - Use plain
<a href>links to other sites, neverlocation.hrefto another site. - Avoid cookies, IndexedDB, service workers, and anything that needs the camera or location.
- Keep secrets out of the HTML, the URL and the page data.
- Give the page a clear
<title>and<meta name="description">for link previews, and optionally a favicon.