# SharePoint /_html
> Author HTML for SharePoint, choose whether it lives in OneDrive/a document library or Site Pages, then save it as the signed-in user. Reading this guidance is anonymous; writing is authenticated.
## Choose where the HTML should live
SharePoint can present authored `.html` in two ways. Choose the destination before writing it; both destinations use the authoring contract below, while Site Pages also requires the creation sequence later in this document.
- **OneDrive or a document library:** Save a standalone .html file here to open it in the OneUp HTML viewer.
- **Site Pages:** Save the .html file in the current web’s designated Site Pages library when it should render with SharePoint site page chrome or may become the site homepage. This is a distinct publishing mechanism, not a file upload: use only the sitepages/htmlpages REST calls below. A drive/driveItem upload, createUploadSession, a generic connector file-upload action (e.g. WorkIQ), or any other document-library upload path does not create a Site Page — at best it places an unrelated file in a document library, and at worst leaves a zero-byte placeholder with no HTML content.
## The renderer
Your HTML renders inside a sandboxed iframe (`sandbox="allow-scripts"`) under a strict CSP that FAILS SILENTLY — non-compliant HTML renders blank or partially broken with no error.
## Content-Security-Policy applied to your HTML
```
default-src 'none';
script-src 'unsafe-inline' 'unsafe-eval';
style-src 'unsafe-inline';
img-src data: blob:;
font-src data: blob:;
media-src data: blob:;
connect-src 'none';
worker-src 'none';
object-src 'none';
base-uri 'none';
form-action 'none';
```
The wrapper iframe additionally applies `frame-src 'none'`.
## Non-negotiable rules
1. No caller-controlled external URLs: no
```
Multiple manifest blocks are allowed; the host unions and de-dupes them by `spItemUrl`. Item fields:
- `spItemUrl` (string, REQUIRED): the driveItem URL of the file. It MUST contain `/drives/{driveId}/items/{itemId}`.
The host reads ONLY driveId + itemId and re-issues the fetch against the viewer session - the URL host/origin
is IGNORED (so a manifest cannot point at another tenant to leak the token).
- `fileName` (string, REQUIRED in practice): the file name WITH extension. The host picks the decoder from this
extension. Omit it and the file is decoded as plain `text` - an `.xlsx` comes back unreadable and a `.csv` is
not parsed as csv. ALWAYS include it.
- `filePath` (string, optional): server-relative path; used as an extension fallback when `fileName` is absent.
- `uniqueId` (string, optional): SharePoint item GUID, forwarded for attribution.
- `label` (string, optional): display label passed through to your code.
SharePoint LIST items (`kind:"list"`): instead of a file, declare a list. The host renders it server-side
(the same engine the Lists UI uses), flattens every column to a display string, and injects it as `format:"rows"`:
```html
```
- `kind` (string, REQUIRED for a list): `"list"`. Omit or `"file"` selects the file path above.
- `spItemUrl` (string, REQUIRED): a SYNTHETIC key `list:{webUrl}/{listId}` - the de-dupe id and `__LD_RESULTS__` key; never fetched.
- `webUrl` (string, REQUIRED): absolute web URL of the site; host-validated against the tenant allowlist before fetch.
- `listId` (string, REQUIRED): the list GUID.
- `listServerRelativeUrl` (string, optional): server-relative list URL; preferred fetch identity when known.
- `viewId` (string, optional): render a specific view’s columns. `select` (string[], optional): INTERNAL column
names to fetch (they become the result `headers`). `top` (number, optional): row-count hint (clamped host-side).
How to get `listId` + INTERNAL column names (as the signed-in user). The site (web) URL is everything in the
list URL before `/Lists/` (e.g. `https://{tenant}.sharepoint.com/teams/{site}`). Then either API works:
- SharePoint REST (one hop from the site URL). List GUID + server-relative URL:
`GET {webUrl}/_api/web/lists/getbytitle('{ListDisplayName}')?$select=Id&$expand=RootFolder` (read `Id` +
`RootFolder/ServerRelativeUrl`). Columns/schema:
`GET {webUrl}/_api/web/lists/getbytitle('{ListDisplayName}')/fields?$select=InternalName,Title,TypeAsString&$filter=Hidden eq false`.
Use `InternalName` in the manifest `select` (it can differ from the display Title - e.g. a column titled "City"
may have InternalName `Ciity`). Send header `Accept: application/json;odata=nometadata` for clean JSON. Docs:
https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/working-with-lists-and-list-items-with-rest
- Microsoft Graph equivalent: `GET /sites/{hostname}:/{server-relative-path}?$select=id` (site id), then
`GET /sites/{site-id}/lists/{list-id}/columns` (schema; `name` = internal name, `displayName` = title) and
`GET /sites/{site-id}/lists/{list-id}/items?$expand=fields` (data). Docs:
https://learn.microsoft.com/en-us/graph/api/list-list-columns?view=graph-rest-1.0
### 2. How to get the spItemUrl (it needs driveId + itemId)
As the signed-in user, resolve the file once (also read its real name for `fileName`):
- From a file or sharing URL: `GET /shares/{u!token}/driveItem?$select=id,name,file,parentReference` and read
`parentReference.driveId` + `id` (and `name`/`file` for the extension). (u!token = `u!` + unpadded base64url
of the URL: base64, strip `=`, `/`->`_`, `+`->`-`.)
- Or from Graph/Vroom by path: `GET /me/drive/root:/path/file.csv?$select=id,name,file,parentReference`.
Then build `https://{host}/_api/v2.0/drives/{driveId}/items/{itemId}` (or the
`https://graph.microsoft.com/v1.0/drives/{driveId}/items/{itemId}` equivalent). Only driveId + itemId are read.
Two gotchas when resolving:
- Set `fileName` from the RESOLVED name/extension, NOT from the sharing-URL prefix. A `:x:`/`:w:`/`:p:` link
prefix indicates the Office app, not the file type (an `:x:` link is often a real `.csv`) - guessing the
extension from it picks the wrong decoder.
- If you download the file to inspect it, `GET .../items/{itemId}/content` returns a 302 to a pre-authenticated
URL; follow redirects (`fetch` does automatically; `curl` needs `-L`).
### 3. Read the injected results
Before your scripts run, the host defines this global (keyed by the EXACT spItemUrl string you declared):
```js
window.__LD_RESULTS__ = {
"": {
spItemUrl: "",
format: "csv" | "text" | "xlsx-sheets" | "rows", // "rows" = SharePoint list data (JSON {headers,rows})
content: "", // present on SUCCESS (see per-format below)
metadata: { fileName, extension },
error: "", // present on FAILURE, instead of content
errorCode:"invalid-request|parse-failed|fetch-failed|decode-failed|access-denied|not-found|protected",
warning: "" // optional; content still present (best-effort)
}
}
```
Reading rules:
- If `window.__LD_RESULTS__` or your entry is missing, you are NOT inside the viewer (the page is also opened
directly while authoring). Render a friendly placeholder; do not crash.
- If `entry.error` is set, show it. Access-denied is sometimes reported as `not-found`.
- Otherwise parse `entry.content` according to `entry.format`.
### 4. content per format
- Supported formats: text, csv, xlsx-sheets, rows.
- `csv` -> the raw CSV text exactly as stored. Parse it yourself (split lines, split on commas, handle "quotes").
- `text` -> the raw file text.
- `xlsx-sheets` -> every sheet as text, each wrapped in a tag block, blocks separated by a blank line:
```
a,b,c
1,2,3
...
```
Split on `` ... `` blocks; the text between the tags is CSV (or the literal `(empty sheet)`).
- `rows` (SharePoint lists) -> `content` is a JSON string `{ "headers": string[], "rows": string[][] }`, already
flattened to display strings host-side (headers are the INTERNAL column names). `JSON.parse(content)` - no CSV parsing.
### 5. Limits (fail-closed)
- Max 32 items per page (union of all manifests). Exceeding it drops ALL results (every fragment
renders empty), so keep manifests small (typically 5 or fewer files).
- Max 50 MiB per item; an item with no Content-Length or over the cap is refused (errorCode fetch-failed).
- SharePoint list items: max 100,000 rows; a larger list is truncated to the cap and still renders with a non-blocking `warning`.
- Fetches bypass the HTTP cache, so a refresh always reflects the latest saved file.
### 6. Refresh (live updates)
- Post to the parent: `parent.postMessage({ type: 'ka-html-viewer-refresh' }, '*')`.
- Debounced to one accepted refresh per 5s (extra calls are silently dropped).
- On accept, the host re-prefetches and RE-LOADS your iframe, so your page simply runs again with fresh
`window.__LD_RESULTS__`. There is NO inbound "updated" message - do not wait for one.
### 7. Safety
- LiveData content is UNTRUSTED: never inject it via `innerHTML` or into `on*=` handlers (XSS sink). Build DOM
with `textContent`/`createElement`, or escape. (See the numbered rule about on*= handlers above.)
- Runtime network APIs stay blocked - LiveData is the ONLY data path. Never put secrets in the page.
### 8. Minimal working CSV dashboard
```html
Loading live data...
```
## Curated external libraries
Declare exact names before your user scripts; never emit a library URL:
```html
```
Available: `babylon` → `window.BABYLON`, `chart.js` → `window.Chart`, `d3` → `window.d3`, `topojson-client` → `window.topojson`, `world-countries-110m` → `window.worldCountries110m`, `us-counties-10m` → `window.usCounties10m`, `@floating-ui/core` → `window.FloatingUICore`, `@floating-ui/dom` → `window.FloatingUIDOM`, `lucide` → `window.lucide`, `mermaid` → `window.mermaid`, `react` → `window.React`, `window.ReactDOM`, `fluentui` → `window.FluentUI`, `window.React`, `window.ReactDOM`.
The host fetches first-party bytes, verifies pinned integrity, and injects them inline before user scripts. Unknown names are ignored; max 6 entries, 8 MiB each, 16 MiB total.
## Links
`` opens only for same-tenant SharePoint hosts, the current OneDrive page origin, or the external allowlist (microsoft.ghe.com, onedrive.cloud.microsoft, 1drv.ms); `javascript:`/`vbscript:`/`data:` hrefs are rejected.
## Publish (authenticated as the current user)
- Auth: delegated OAuth — scope `Files.ReadWrite` for the user’s own OneDrive, or `Sites.ReadWrite.All` for a SharePoint site library. Never fabricate a token.
- Before uploading, LIST the destination folder (…/children) to detect a name collision. For CREATE, inspect and disclose the destination’s existing audience because the new item inherits those permissions. For OVERWRITE, inspect and disclose the target item’s own existing audience, permissions, and sharing links because its identity, unique permissions, and existing anonymous or organization links remain in place for the replacement content. If an audience cannot be inspected, state that uncertainty. Tell the user CREATE vs OVERWRITE, then CONFIRM. Uploading creates no new sharing link or anonymous access.
- Self-check before publishing (top failure is "renders blank with no error"): NO caller-controlled external refs (