# 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](/docs/page-data.md): `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](#link-previews-and-icons). |
| 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**, and **`window.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://…"` and `window.top.location` fail. Use a link or `window.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"` and `rel="noopener"` when someone clicks them.
- **`mailto:` and `tel:` links** also open in a new tab, which hands them to the mail or phone app.
- **Links to `#ids`** scroll 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#faq` loads your page with `location.search === "?tab=pricing"` and `location.hash === "#faq"`. If `#faq` matches an element's id, the browser scrolls to it.
- **Out:** when your page changes its query or hash, through `history.replaceState`, `history.pushState`, setting `location.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.

```html
<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 `replaceState` for UI state like tabs and filters, so the back button isn't flooded. Use `pushState` only 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:
- **`localStorage`** is kept for this page in the visitor's browser, under the page's link (for example `acme.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, `setItem` throws `QuotaExceededError`.
  - It's still the visitor's browser: clearing site data removes it, and it doesn't follow them to another device.
- **`sessionStorage`** lasts 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](/docs/page-data.md).

## 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.

```html
<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](/docs/page-data.md). |
| `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:
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.
