Datalens html pages
Skill datalens-tech/datalens-skills/skills/datalens-html-pages
Use this skill to build or fix a standalone HTML page or report in DataLens. DataLens takes a whole self-contained HTML document — often an AI-generated report with tables, charts, and a CSV/download button — and renders it in a sandboxed, CSP-locked iframe, so normal web code silently breaks. Reach for it when generating such a page from scratch, and when one already in DataLens misbehaves: images, fonts, scripts, or CDN libraries blocked or throwing Content-Security-Policy errors; fetch/XHR/localStorage failing; charts blank; a download or Export button doing nothing; an upload rejected as too large or the wrong encoding; or the page needing to match the user's DataLens theme (light/dark) and language (ru/en). Also covers which CDNs and hosts are allowed and making the page fully self-contained. Not for DataLens chart cells or HTML-markup table columns.From its SKILL.md
npx -y skills add datalens-tech/datalens-skills --skill datalens-html-pagesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.7 KB, as published. Nobody here has run it
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 noallow-same-origin— the page runs in an opaque origin (parent also sendsCross-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 forscript/style/font/img/media, andconnect-src,form-action,frame-src,object-src,worker-src,base-uriall'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):
| Resource | Allowed from |
|---|---|
| Scripts | cdn.jsdelivr.net, cdnjs.cloudflare.com, cdn.tailwindcss.com, yastatic.net (+ inline) |
| Styles | the above + fonts.googleapis.com (+ inline) |
| Fonts | cdn.jsdelivr.net, cdnjs.cloudflare.com, fonts.gstatic.com, data: |
| Images | yastatic.net, data:, blob: |
| Media | data:, blob: only |
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.
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—connect-srcis'none'. 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 throughOPEN_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:
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
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
| Symptom | Cause | Fix |
|---|---|---|
| Library never loads / CSP error in console | script/style host off the allowlist | serve it from jsdelivr / cdnjs / tailwind / yastatic |
| Data never appears | code calls fetch/XHR | inline the dataset into the page at generation time |
| "localStorage is not available" / throws | storage API in sandbox | keep state in memory |
| Fonts don't render | font host off-allowlist | Google Fonts (fonts.googleapis.com CSS + fonts.gstatic.com files) or jsdelivr/cdnjs |
| Image is blank | http:// or off-allowlist host | use yastatic.net, data:, or blob: |
| Download button does nothing | download APIs blocked | post {code:'EXPORT', data:{name,mime,data}} to the parent |
| Clicking a link does nothing | links don't navigate in the sandbox | intercept the click and post {code:'OPEN_URL', data:{url}} |
| Page renders as plain text / broken head | wrapping markdown code fences | remove the ``` fences |
| Upload rejected | > 10 MB or invalid UTF-8 | shrink assets; ensure UTF-8 + early <meta charset> |