mcpField
D
datalens-html-pages

datalens-html-pages

github.com/datalens-techDevelopment1245 installsUpdated 3w ago
Repository

About datalens-html-pages

A skill for authoring standalone HTML reports and pages served by DataLens in a restricted sandbox. It explains resource, security, and interaction constraints, and includes a validator for checking pages before upload.

Key features

  1. 01Author standalone HTML pages
  2. 02Follow sandbox and CSP constraints
  3. 03Use allowlisted external resources
  4. 04Handle exports and links via host messages
  5. 05Validate pages with a Python script
  6. 06Support themes, languages, and mobile

Use cases

  1. 01Create a DataLens report page
  2. 02Check HTML before uploading
  3. 03Add maps using Yandex Maps JS API 2.1
  4. 04Prepare a page for mobile viewing

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/datalens-tech/datalens-skills --skill datalens-html-pages

After installing, type/datalens-html-pagesin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

assets/
dl-dashboard.css
dl-theme-tokens.css
report.template.html
references/
authoring-constraints.md
csp-and-sandbox.md
design-principles.md
publication.md
scripts/
validate_page.py

Details

From the project README

DataLens HTML pages

DataLens serves standalone HTML pages — whole documents, usually AI-generated reports — by storing them in a private bucket and rendering them in a locked-down sandboxed iframe from a short-TTL presigned URL, with a Content-Security-Policy injected as a <meta> tag at upload. You author a complete <!DOCTYPE html> page; scripts run, but only from an allowlist of CDNs, and most browser APIs are blocked.

Not this skill: HTML inside a chart — markup functions, HTML-markup table cells, ChartEditor.generateHtml. That is a different feature with a strict tag/attribute allowlist and no scripts at all. This skill is only for standalone HTML pages.

The runtime in one picture

  • Sandboxed iframe: sandbox="allow-scripts", allow="", referrerpolicy="no-referrer", and crucially no allow-same-origin — the page runs in an opaque origin (parent also sends Cross-Origin-Opener-Policy: same-origin). Same-origin isolation depends on this; never assume same-origin access.
  • Injected CSP (you do not write it — the platform prepends it, idempotently): default-src 'none' with a small allowlist for script/style/font/img/media, connect-src opened only to the Yandex Maps hosts, and form-action, frame-src, object-src, worker-src, base-uri all 'none'. Full policy in references/csp-and-sandbox.md.
  • Served from a presigned GET that expires in 10–30 seconds, after a server-side permission check. Theme/language ride along as signed query params.

The net effect: make the page fully self-contained. Inline your own CSS/JS (the CSP allows 'unsafe-inline'/'unsafe-eval'), pull libraries only from allowlisted CDNs, and never rely on network, storage, or the parent page.

Authoring rules

Allowed external resources (everything else is blocked by the CSP):

ResourceAllowed from
Scriptscdn.jsdelivr.net, cdnjs.cloudflare.com, cdn.tailwindcss.com, yastatic.net, Yandex Maps (api-maps.yandex.ru, *.api-maps.yandex.ru, *.maps.yandex.net, suggest-maps.yandex.ru) (+ inline)
Stylesthe CDNs above + fonts.googleapis.com, blob: (+ inline)
Fontscdn.jsdelivr.net, cdnjs.cloudflare.com, fonts.gstatic.com, data:
Imagesyastatic.net, the Yandex Maps hosts, data:, blob:
Mediadata:, blob: only
Network (connect-src)the Yandex Maps hosts only — for the Maps API itself, not for your data

Load Google Fonts the standard way (<link> CSS from fonts.googleapis.com, font files from fonts.gstatic.com). Mirror off-allowlist libraries (unpkg, plot.ly, d3js.org, jQuery) through jsdelivr/cdnjs.

Yandex Maps JS API 2.1 works out of the box: load it from api-maps.yandex.ru (with the user's API key when they have one) and render into a fixed-height container. Scripts, tiles and the API's own requests are allowed. Use 2.1, not v3 — v3 refuses to load without a key and needs workers the sandbox blocks. Everything else about the sandbox still holds — your own data is inlined, not fetched.

<script src="https://api-maps.yandex.ru/2.1/?apikey=YOUR_KEY&lang=ru_RU"></script>
<div id="map" style="height: 400px"></div>
<script>ymaps.ready(() => new ymaps.Map('map', {center: [55.76, 37.64], zoom: 10}));</script>

Blocked — do not use (they throw or silently fail):

  • Storage: localStorage, sessionStorage, indexedDB, document.cookie, Cache API → keep all state in memory.
  • Network: fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon to anything except the Yandex Maps hosts — connect-src allows nothing else. Inline your data into the page instead of fetching it.
  • Frames/objects/forms: nested <iframe>, <object>, <embed>, <form> submission, <base>.
  • Workers, popups, dialogs, downloads, camera/geolocation/fullscreen, navigating the parent frame, and ordinary link navigation — an <a href> won't open on its own; route clicks through OPEN_URL (see Opening links below).

Theme & language — read them from the query string:

const theme = new URLSearchParams(location.search).get('theme'); // 'light' | 'dark' | 'system'
const lang  = new URLSearchParams(location.search).get('lang');  // 'ru' | 'en'

When designing and generating HTML, try to create a page that supports available themes, languages and also adaptive: some users will view them via mobile web version of the DataLens.

Exporting a file — the download APIs are blocked; hand data to the host instead. The host verifies event.source, applies a MIME allowlist + size cap, sanitizes the filename, and creates the download:

parent.postMessage({ code: 'EXPORT', data: { name: 'report.csv', mime: 'text/csv', data: csvString } }, '*');

Opening links — an ordinary <a href> does not navigate inside the sandbox (opaque origin; even target="_blank" only reaches about:blank). To open a URL, ask the host: intercept the click, preventDefault(), and post OPEN_URL. One delegated listener covers every link — gate it on being framed so the file still works opened top-level or previewed locally.

if (window.parent !== window) {
  document.addEventListener('click', (e) => {
    const a = e.target.closest('a[href]');
    if (!a) return;
    const href = a.getAttribute('href');
    if (!href || href.startsWith('#')) return; // in-page anchors scroll normally — leave them
    e.preventDefault();
    parent.postMessage({ code: 'OPEN_URL', data: { url: a.href } }, '*');
  });
}

Encoding & size (enforced at upload):

  • Valid UTF-8; declare <meta charset="utf-8"> within the first 1024 bytes. The server detects mojibake but never repairs it.
  • Do not wrap the document in markdown code fences (```html) — they prematurely close <head>.
  • Page size up to ~10 MB, ideally keep below ~5 MB.

See references/authoring-constraints.md for the exhaustive lists, and assets/report.template.html for a compliant starting point.

Design — before building a dashboard or report page, ask the user whether they want it styled per the DataLens design principles (unless they already said so). If you cannot ask — a non-interactive run with no way to get an answer — default to not applying them: style the page plainly and say in your output that the design principles were skipped. If they agree, follow references/design-principles.md — layout grid, chart-type selection, colors, number formatting, and theming. It relies on the DataLens design tokens in assets/dl-theme-tokens.css and base dashboard styles in assets/dl-dashboard.css — inline both files' contents into the page's <style> (the page must be self-contained) and set g-root g-root_theme_light|dark on the root element per the theme query param. If the user declines, skip these three files and style the page however they ask; the sandbox and CSP rules above still apply.

Validate before you upload

Lint the page against the exact CSP allowlist and sandbox constraints — a clean run means nothing will be blocked or stripped at render:

python scripts/validate_page.py report.html          # errors + warnings
python scripts/validate_page.py --strict report.html # warnings fail too (CI mode)
cat report.html | python scripts/validate_page.py -   # from stdin

It flags off-allowlist resource hosts, blocked APIs (storage/fetch/workers/…), blocked tags, missing charset, mojibake, wrapping code fences, and oversize pages. Run python scripts/validate_page.py --self-test to check the linter itself.

Serve & publish

To upload or publish a page and return its permanent link, follow references/publication.md.

Serving is server-side; know the shape so you author correctly:

  • Presigned GET, 10–30 s TTL, minted only after a permission check (no TOCTOU gap). Don't build flows that assume a long-lived URL.
  • Object stored with Content-Type: text/html; charset=utf-8, Content-Disposition: inline, Cache-Control: no-store. Upload is server-side JSON-RPC (no presigned PUTs — the server owns CSP injection and metadata).
  • The iframe carries the sandbox above; the parent sends COOP: same-origin.

Accepted risks to design around (details in references/csp-and-sandbox.md): a URL opened top-level (within its TTL) loses the iframe sandbox but keeps the injected CSP; a page can self-navigate via <meta http-equiv="refresh">; allowlisted CDNs are not SRI-pinned. Because the frame is same-origin-isolated and network-less, do not put viewer data, parameters, or live LLM output that must stay private into the page — it is a report, not a trusted app surface.

Common pitfalls → fixes

SymptomCauseFix
Library never loads / CSP error in consolescript/style host off the allowlistserve it from jsdelivr / cdnjs / tailwind / yastatic
Data never appearscode calls fetch/XHRinline the dataset into the page at generation time
"localStorage is not available" / throwsstorage API in sandboxkeep state in memory
Fonts don't renderfont host off-allowlistGoogle Fonts (fonts.googleapis.com CSS + fonts.gstatic.com files) or jsdelivr/cdnjs
Image is blankhttp:// or off-allowlist hostuse yastatic.net, data:, or blob:
Yandex map is blankMaps v3 used, API loaded from a mirror, or the container has no heightuse JS API 2.1 from api-maps.yandex.ru and give the container an explicit height
Download button does nothingdownload APIs blockedpost {code:'EXPORT', data:{name,mime,data}} to the parent
Clicking a link does nothinglinks don't navigate in the sandboxintercept the click and post {code:'OPEN_URL', data:{url}}
Page renders as plain text / broken headwrapping markdown code fencesremove the ``` fences
Upload rejected> 10 MB or invalid UTF-8shrink assets; ensure UTF-8 + early <meta charset>

Comments

Markdown supported