al.ink

Work requirements

A work is either a content file alink renders for you — markdown, PDF, video, audio, image, or a script animation — or a static web bundle: an index.html plus the files it references, served exactly as uploaded, with no build step and no server-side code.

Content works

Upload one content file at the root, plus any assets it uses: paper.md with its images, film.mp4 with a .vtt caption track, thesis.pdf, poster.png, main.js for a canvas animation. alink writes the page that presents it. A bundle holding two candidates (two .md files, two PDFs) is rejected rather than guessed at.

A script work is loaded as an ES module and gets a full-viewport canvas at #stage, already sized for devicePixelRatio; window.AlinkStage holds { canvas, shell, size }. Import libraries from the allowed CDNs below, or ship them in the bundle.

Bundle format

  • A web bundle needs index.html at its root; the other kinds must not have one (that is what makes a bundle a web page).
  • Reference all other files by relative path: ./app.js, assets/hero.png. Absolute paths are not supported — each upload is served from a new address, and a leading / resolves outside the bundle.
  • Path segments may contain letters, digits, dot, underscore and hyphen, may not start with a dot, and may be at most 5 directories deep.
  • Allowed file types: HTML, CSS, JS, JSON, images, video, audio, fonts, WebAssembly. Upload a single .html file, a directory, or a zip; zip archives are unpacked in the browser.

Script tags require crossorigin or type="module"

A work is served into a sandbox with an opaque origin, so requests for its own files are cross-origin. A plain <script src="./app.js"> is fetched in no-cors mode, which leaves the script without a base URL: any dynamic import("./other.js") inside it resolves against about:blank and fails.

✗ Dynamic import() fails
<script src="./app.js"></script>
✓ Both of these work
<script src="./app.js" crossorigin></script>
<script type="module" src="./app.js"></script>

Not affected: scripts inlined in a single HTML file, and scripts that never call import(). Generated prototypes typically emit a plain <script src> and need this change.

Allowed requests

  • Files inside the bundle, plus these static CDNs: cdnjs, jsDelivr, unpkg, esm.sh, Google Fonts. All other origins are blocked.
  • fetch() for bundled files is allowed, including Range requests for video. Requests to any other origin fail.

Sandbox limits

These limits are fixed. They are not configurable, on any plan:

  • No visitor identity is available: no name, no account, no address.
  • No network access outside the bundle: no third-party requests, no form submissions, no beacons. Visitor input never leaves the work.
  • No storage, no cookies, and no shared state with the rest of alink.

Optional: sdk

The sdk is optional. Outside the shell, connect() returns null and the work runs unchanged. Inside the shell, five calls are available:

<script type="module" src="https://works.al.ink/-/sdk/v1.js"></script>
<script>
  const alink = await AlinkWork.connect({ timeoutMs: 800 })
  if (alink) {
    alink.context            // { work, owner, viewer: { signedIn, locale, theme } }
    alink.on('themechange', (theme) => { /* 'light' | 'night' */ })
    alink.requestFullscreen()
    alink.openDoor()         // takes the visitor to your requests room
    alink.share()
  }
</script>
  • context — the work title and address, the publisher’s public name, and the viewer’s signed-in state, language and theme.
  • requestFullscreen() / exitFullscreen() — the shell toggles fullscreen for the stage. Esc always exits.
  • openDoor() — navigates the visitor to the publisher’s requests room, tagged with the originating work.
  • share() — opens the share sheet for this work’s address.
  • on("themechange") — fires when the viewer switches between light and dark.

Current sdk version is v1, served from works.al.ink/-/sdk/v1.js. A future v2 will use a new address; existing references keep working.