diff --git a/.gitignore b/.gitignore
index 8d8d9655..50d6cce1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Mkdocs build directory
/site/
+/.cache/
/.idea
.vscode
__pycache__/
diff --git a/docs/html-pages/example.md b/docs/html-pages/example.md
new file mode 100644
index 00000000..fce5619f
--- /dev/null
+++ b/docs/html-pages/example.md
@@ -0,0 +1,177 @@
+---
+description: A complete, minimal HTML Page example — initialize the SDK, read rows from the base, render them, and write new linked rows back on submit.
+---
+
+# Example: a form that reads and writes
+
+This page ties the SDK together into one working flow: load data on start, render it, and write new rows back on submit. It is a distilled version of the [simple form template](https://github.com/seatable/seatable-html-page-template-simple-form) — a form that lists products from the base, lets the user pick quantities, and stores the result as an order.
+
+The data flow is always the same four steps:
+
+```mermaid
+flowchart LR
+ A[init SDK] --> B[listRows: load data]
+ B --> C[render UI]
+ C --> D[on submit: batchAddRows + addRow]
+```
+
+It assumes the three tables described in [Getting Started](getting-started.md): **Products**, **OrderItems** (link to Products), and **Orders** (link to OrderItems).
+
+## Single-file version (non-modular)
+
+This is the complete page. It loads the SDK from a CDN, so everything lives in one `index.html` — the non-modular style. Styling is omitted for clarity.
+
+```html
+
+
+
+
+ Product Order
+
+
+
+
+
+
+
+
+
+
+```
+
+!!! tip "Linking rows"
+
+ Notice the two-step write: first create the `OrderItems` rows with `batchAddRows`, then read their `_id` values from `res.data.rows` and pass them as an array to the `Orders` row's link column. Link columns always take an array of linked row IDs — see [`addRow`](sdk/rows.md#add-rows).
+
+## Modular version
+
+For anything beyond a simple page, split the logic into modules and import the SDK from the npm package. The template's `src/esm` directory does exactly this. A common pattern is to wrap all base access in a single `Context` class, keeping SDK calls out of your UI code:
+
+```js
+import { HTMLPageSDK } from "seatable-html-page-sdk";
+
+export default class Context {
+ async init(options) {
+ this.sdk = new HTMLPageSDK(options);
+ await this.sdk.init();
+ }
+
+ async loadProducts() {
+ const res = await this.sdk.listRows({ tableName: "Products" });
+ return {
+ columns: res.data.metadata,
+ rows: res.data.results,
+ };
+ }
+
+ async submitOrder(orderItemsData) {
+ const itemsRes = await this.sdk.batchAddRows({
+ tableName: "OrderItems",
+ rowsData: orderItemsData,
+ });
+ const orderItemIds = itemsRes.data.rows.map((row) => row._id);
+
+ await this.sdk.addRow({
+ tableName: "Orders",
+ rowData: { OrderItems: orderItemIds },
+ });
+ }
+}
+```
+
+The entry point reads the injected dev config and starts the app:
+
+```js
+// index.js
+import Context from "./context";
+
+document.addEventListener("DOMContentLoaded", async () => {
+ const context = new Context();
+ await context.init(window.__HTML_PAGE_DEV_CONFIG__ || null);
+ const { rows } = await context.loadProducts();
+ // ... render rows, wire up submit -> context.submitOrder(...)
+});
+```
+
+## Next steps
+
+- [SDK Reference: Rows](sdk/rows.md) — full parameters and return shapes for every row method.
+- [SDK Reference: Files & Images](sdk/files.md) — add file and image uploads to your form.
diff --git a/docs/html-pages/getting-started.md b/docs/html-pages/getting-started.md
new file mode 100644
index 00000000..b4c1fcf8
--- /dev/null
+++ b/docs/html-pages/getting-started.md
@@ -0,0 +1,113 @@
+---
+description: Set up, develop, build and upload a SeaTable HTML Page. Walks through the simple form template with Vite, the SDK, and the uploadable ZIP package.
+---
+
+# Getting Started
+
+This guide walks through building an HTML Page from the official [simple form template](https://github.com/seatable/seatable-html-page-template-simple-form): a form that writes submitted records into a base. By the end you will have a packaged ZIP that you can upload to a Universal App.
+
+The template ships the build configuration and project structure you need. It supports two development styles:
+
+- **Modular** — HTML, JavaScript and CSS split across multiple files. Suited for complex pages.
+- **Non-modular** — everything in a single `index.html`. Suited for simple pages.
+
+Choose the approach that fits the complexity of your page. Both produce the same kind of uploadable package.
+
+!!! note "Base requirements for this example"
+
+ The form reads products from the base and writes orders back, so it expects three tables:
+
+ - **Products** — with `Product_name` (text) and `Unit_price` (number)
+ - **OrderItems** — with `Product` (link to Products) and `Quantity` (number)
+ - **Orders** — with `OrderItems` (link to OrderItems)
+
+ Create them in your base before running the page, or adapt the table and field names in the template's source.
+
+## Before you start: add an HTML Page in the app
+
+In your Universal App, add a new page and choose **Add HTML page**. This creates the empty page you will later upload your build to, and it surfaces the `server`, `appUuid` and `pageId` values you need for local development ([step 3](#3-configure-local-development)).
+
+
+
+## 1. Get the template
+
+```shell
+git clone https://github.com/seatable/seatable-html-page-template-simple-form.git
+cd seatable-html-page-template-simple-form
+npm install
+```
+
+## 2. Choose a development style
+
+The active style is controlled by the `ESM` variable in `.env`. Vite derives its `root` from it automatically (`src/esm` when `true`, `src/classic` when `false`) — you do not edit `vite.config.js`.
+
+=== "Modular"
+
+ JavaScript and CSS are split into multiple files under `src/esm`, and the entry file is imported into `index.html` with `type="module"`. The SDK is used via the npm package (`import { HTMLPageSDK } from "seatable-html-page-sdk"`).
+
+ Enable it by setting `ESM=true`. Either edit `.env` directly, or copy `.env` to `.env.local` (git-ignored) and set it there.
+
+=== "Non-modular"
+
+ All logic lives in a single `index.html` under `src/classic`, with the SDK loaded from a CDN. This is the template default (`ESM=false`), so no extra configuration is required.
+
+## 3. Configure local development
+
+During local development, the page needs to know which base to talk to. Create `src/setting.js` based on `setting.template.js`:
+
+```js
+export default {
+ server: "",
+ appUuid: "",
+ pageId: "",
+ accountToken: "",
+};
+```
+
+These values are registered to `window.__HTML_PAGE_DEV_CONFIG__` and picked up by the SDK when you call `init()` in development. See [Initialization](sdk/initialization.md) for how the SDK consumes them.
+
+!!! note "Where the values come from"
+
+ - `accountToken` — an API token you generate in the base in advance. See [how to create API tokens](https://seatable.com/help/create-api-tokens/).
+ - `server`, `appUuid` and `pageId` — shown under **HTML Page development information** in the page's configuration (see below).
+
+The HTML Page configuration shows these values ready to copy, alongside the upload area you will use later:
+
+
+
+## 4. Run the development server
+
+```shell
+npm run dev
+```
+
+Open the local development URL in your browser to preview and test the page. Changes reload live.
+
+## 5. Build and package
+
+Generate the uploadable ZIP:
+
+```shell
+npm run build-page
+```
+
+This single command cleans the output, runs the Vite build (into `dist`), and packages it into a ZIP under the `page-zip` directory. The ZIP is named after the project, for example `seatable-html-page-template-simple-form-0.0.1.zip` — bump the `version` in `package.json` before building so you can tell uploads apart.
+
+!!! note "Inspecting the build"
+
+ `npm run build-page` already runs the build, so you do not call `npm run build` separately. Run `npm run build` on its own only if you want to inspect the unpackaged output in `dist`, or `npm run dev` to preview it live.
+
+## 6. Upload and test
+
+1. In SeaTable, open the configuration of the HTML Page in your Universal App.
+2. Drag the generated ZIP into the **Upload HTML page file** area (the same configuration shown in [step 3](#3-configure-local-development)).
+3. Open the app preview, fill in the form, and submit it.
+4. Return to the base and confirm that the corresponding records were created.
+
+Once the submitted records appear in the base, the page is fully integrated and ready to use.
+
+## Next steps
+
+- [SDK Reference: Initialization](sdk/initialization.md)
+- [SDK Reference: Rows](sdk/rows.md)
+- [SDK Reference: Files & Images](sdk/files.md)
diff --git a/docs/html-pages/index.md b/docs/html-pages/index.md
new file mode 100644
index 00000000..c7327551
--- /dev/null
+++ b/docs/html-pages/index.md
@@ -0,0 +1,44 @@
+---
+description: Build interactive HTML Pages for SeaTable Universal Apps. Learn how a page reads and writes base data through the seatable-html-page-sdk.
+---
+
+# HTML Pages
+
+Starting with SeaTable 6.2, a Universal App can contain a new page type: the **HTML Page**. You upload a packaged HTML/JavaScript/CSS bundle, and SeaTable renders it as a full page inside the app.
+
+A static bundle on its own can only display fixed content. To turn an HTML Page into a real application — a custom form, a dashboard, a calculator — it needs to read from and write to the base. That data exchange runs through the [`seatable-html-page-sdk`](https://www.npmjs.com/package/seatable-html-page-sdk).
+
+This section is written for developers. It covers how to set up a project, how to develop and package a page, and the full SDK reference.
+
+## Architecture
+
+An HTML Page is rendered in a sandboxed context inside the Universal App. It never talks to the base directly. Instead, the SDK provides a messaging bridge to the app, which performs the actual data operations.
+
+```mermaid
+flowchart LR
+ A[HTML Page] -->|seatable-html-page-sdk| B[Universal App]
+ B -->|API| C[(Base)]
+ C --> B
+ B --> A
+```
+
+The SDK offers:
+
+- **Data APIs** — list, add, update and delete rows; upload files and images.
+- **Event propagation** — bidirectional events (mouse, keyboard, drag-and-drop) between the page and the app.
+
+## Prerequisites
+
+- SeaTable 6.2 or later.
+- A Universal App with an HTML Page added to it.
+- An API token generated in the base (used during local development).
+- Node.js and npm for building the page.
+
+## Where to start
+
+- [Getting Started](getting-started.md) — set up the project, develop locally, build and upload a page. We follow the official [simple form template](https://github.com/seatable/seatable-html-page-template-simple-form) end to end.
+- [SDK Reference](sdk/initialization.md) — installation, initialization, and the full API for rows, files and images.
+
+!!! tip "Example project"
+
+ The [`seatable-html-page-template-simple-form`](https://github.com/seatable/seatable-html-page-template-simple-form) repository contains a complete, buildable form page. It is the running example used throughout this section.
diff --git a/docs/html-pages/sdk/files.md b/docs/html-pages/sdk/files.md
new file mode 100644
index 00000000..26246ecd
--- /dev/null
+++ b/docs/html-pages/sdk/files.md
@@ -0,0 +1,69 @@
+---
+description: seatable-html-page-sdk reference for uploading files and images from an HTML Page into file and image columns of a base.
+---
+
+# Files & Images
+
+Upload files and images from an HTML Page. The returned data is used to populate a file or image column — typically by passing it into `updateRow` or `addRow` (see [Rows](rows.md)). The `sdk` instance below is created and initialized as shown in [Initialization](initialization.md).
+
+!!! note "Return value"
+
+ Unlike the row methods, the upload methods resolve to the result object **directly** — there is no `.data` wrapper.
+
+!!! abstract "uploadFile"
+
+ Upload a file for use in a **file** column.
+
+ ```js
+ sdk.uploadFile({ file });
+ ```
+
+ __Parameters__
+
+ `file`
+ : object — the file to upload (for example, a `File` from an ``)
+
+ __Returns__ `{ name, size, type, url }` — pass this object as the value of a file column.
+
+ __Example__
+ ```js
+ fileInput.addEventListener("change", async (e) => {
+ const file = e.target.files[0];
+ const { name, size, type, url } = await sdk.uploadFile({ file });
+
+ await sdk.updateRow({
+ tableName: "Documents",
+ rowId: "fcHIocncTsOygA3FjL-toQ",
+ rowData: { Attachment: [{ name, size, type, url }] },
+ });
+ });
+ ```
+
+!!! abstract "uploadImage"
+
+ Upload an image for use in an **image** column.
+
+ ```js
+ sdk.uploadImage({ file });
+ ```
+
+ __Parameters__
+
+ `file`
+ : object — the image file to upload
+
+ __Returns__ `{ name, size, type, url }` — same shape as `uploadFile`, with `type` set to `"image"`. Pass the URL as the value of an image column.
+
+ __Example__
+ ```js
+ fileInput.addEventListener("change", async (e) => {
+ const file = e.target.files[0];
+ const { url } = await sdk.uploadImage({ file });
+
+ await sdk.updateRow({
+ tableName: "Products",
+ rowId: "fcHIocncTsOygA3FjL-toQ",
+ rowData: { Photo: [url] },
+ });
+ });
+ ```
diff --git a/docs/html-pages/sdk/initialization.md b/docs/html-pages/sdk/initialization.md
new file mode 100644
index 00000000..641eb38c
--- /dev/null
+++ b/docs/html-pages/sdk/initialization.md
@@ -0,0 +1,101 @@
+---
+description: Install and initialize the seatable-html-page-sdk. Set up the SDK via npm or CDN and connect an HTML Page to its SeaTable base.
+---
+
+# Initialization
+
+The `seatable-html-page-sdk` is the bridge between an HTML Page and its Universal App. It exposes APIs for data interaction and event subscription. This page covers installation and initialization. For data operations, see [Rows](rows.md) and [Files & Images](files.md).
+
+## Installation
+
+=== "Package manager"
+
+ Install with npm or yarn — recommended for the [modular development style](../getting-started.md#2-choose-a-development-style).
+
+ ```shell
+ # npm
+ npm install seatable-html-page-sdk --save
+
+ # yarn
+ yarn add seatable-html-page-sdk
+ ```
+
+ ```js
+ import { HTMLPageSDK } from "seatable-html-page-sdk";
+ ```
+
+=== "CDN"
+
+ Load the SDK via CDN — convenient for the non-modular style, where everything lives in `index.html`. `HTMLPageSDK` becomes available as a global.
+
+ ```html
+
+
+
+
+
+
+
+ ```
+
+ !!! tip "Pin a version"
+
+ `@latest` always resolves to the newest release. For production pages, pin an explicit version (for example `seatable-html-page-sdk@1.0.0`) so a new release cannot change behavior unexpectedly.
+
+## Initialize the SDK
+
+Create an instance, then call `init()` before making any data calls. Write the initialization **once** — the same code works in both development and production:
+
+```js
+const options = window.__HTML_PAGE_DEV_CONFIG__ || null;
+const sdk = new HTMLPageSDK(options);
+await sdk.init();
+```
+
+How `options` is resolved differs by environment, but your code does not change:
+
+=== "Production"
+
+ Inside the Universal App, `window.__HTML_PAGE_DEV_CONFIG__` is not defined, so `options` is `null`. The SDK derives the connection context from the host app automatically — no credentials in your code.
+
+=== "Development"
+
+ There is no host app locally, so the SDK needs connection details. You do **not** hardcode them: the Vite dev server reads [`src/setting.js`](../getting-started.md#3-configure-local-development) and injects the values into `window.__HTML_PAGE_DEV_CONFIG__` at serve time.
+
+ The injected object has this shape:
+
+ ```js
+ {
+ server: "your-html-page-server",
+ accountToken: "your-account-token",
+ appUuid: "your-app-uuid",
+ pageId: "your-app-page-id",
+ }
+ ```
+
+ Because `setting.js` is git-ignored and never bundled, your account token stays out of the build.
+
+## Basic usage
+
+Once initialized, call the data APIs on the instance:
+
+```js
+const sdk = new HTMLPageSDK(options);
+await sdk.init();
+
+const res = await sdk.listRows({
+ tableName: "Employees",
+ start: 0,
+ limit: 100,
+});
+const rows = res.data.results;
+```
+
+All SDK methods are asynchronous and return promises — always `await` them. Row methods resolve to the HTTP response, so the payload is under `.data` (see [Rows](rows.md)). Upload methods are the exception and return the result object directly (see [Files & Images](files.md)).
+
+## Next steps
+
+- [Rows](rows.md) — list, add, update and delete rows, including batch operations.
+- [Files & Images](files.md) — upload files and images for file and image columns.
diff --git a/docs/html-pages/sdk/rows.md b/docs/html-pages/sdk/rows.md
new file mode 100644
index 00000000..b33dcefd
--- /dev/null
+++ b/docs/html-pages/sdk/rows.md
@@ -0,0 +1,238 @@
+---
+description: seatable-html-page-sdk reference for row operations — list, add, update, delete, and batch-modify rows in a base from an HTML Page.
+---
+
+# Rows
+
+Row operations on the `seatable-html-page-sdk`. All methods are asynchronous — `await` them. The `sdk` instance below is created and initialized as shown in [Initialization](initialization.md).
+
+!!! warning "Response format"
+
+ Row methods resolve to the underlying HTTP response — the payload is under `.data`, not the return value itself. For example, `listRows` gives you the rows via `res.data.results` and the column metadata via `res.data.metadata`. The `Returns` sections below describe the shape of `.data`.
+
+!!! tip "Single- and multi-select values"
+
+ Single-select and multi-select fields — including those returned via linked records and lookup formulas — return the **option names**, not their internal IDs.
+
+## List rows
+
+!!! abstract "listRows"
+
+ List rows from a specific table, with pagination.
+
+ ```js
+ sdk.listRows({ tableName, start, limit });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `start`
+ : number — starting index for pagination
+
+ `limit`
+ : number — number of rows to retrieve
+
+ __Returns__ `.data` holds `{ metadata, results }` — `results` is the array of row objects, `metadata` the column definitions.
+
+ __Example__
+ ```js
+ const res = await sdk.listRows({ tableName: "Employees", start: 0, limit: 50 });
+ const rows = res.data.results;
+ const columns = res.data.metadata;
+ ```
+
+## Add rows
+
+!!! abstract "addRow"
+
+ Add a new row to the specified table.
+
+ ```js
+ sdk.addRow({ tableName, rowData });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowData`
+ : object — key-value pairs of column names and values
+
+ __Returns__ `.data` holds `{ row }` — the newly created row object.
+
+ __Example__
+ ```js
+ const res = await sdk.addRow({
+ tableName: "Employees",
+ rowData: {
+ Name: "Jane Smith",
+ Age: 28,
+ Department: "Engineering",
+
+ // IDs of records linked in the Manager field
+ Manager: ["NSPa_fd4SEqRESqOZzRqyg", "eA6rQDuxQyGITmD1hrfyzw"],
+ },
+ });
+ ```
+
+ !!! tip "Linked record fields"
+
+ Set a link column with `{ "link_column_name": ["linked_row_id1", "linked_row_id2", ...] }`.
+
+!!! abstract "batchAddRows"
+
+ Add multiple rows to the specified table in one call.
+
+ ```js
+ sdk.batchAddRows({ tableName, rowsData });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowsData`
+ : array — an array of row objects
+
+ __Returns__ `.data` holds `{ rows }` — the list of created row objects. Each row carries its new `_id`, which you can use to populate link fields in a follow-up call.
+
+ __Example__
+ ```js
+ await sdk.batchAddRows({
+ tableName: "Employees",
+ rowsData: [
+ {
+ Name: "Jane Smith",
+ Age: 28,
+ Department: "Engineering",
+ Manager: ["NSPa_fd4SEqRESqOZzRqyg", "eA6rQDuxQyGITmD1hrfyzw"],
+ },
+ {
+ Name: "Tom",
+ Age: 24,
+ Department: "Product Management",
+ Manager: ["QgK2KMf8Sxad8duPcq6gQA", "bBDbhbzXReSPWpcxq225xA"],
+ },
+ ],
+ });
+ ```
+
+## Update rows
+
+!!! abstract "updateRow"
+
+ Update an existing row.
+
+ ```js
+ sdk.updateRow({ tableName, rowId, rowData });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowId`
+ : string — the unique ID of the row to update
+
+ `rowData`
+ : object — the fields to update
+
+ __Returns__ `.data` holds `{ success: true, row: {...} }`
+
+ __Example__
+ ```js
+ await sdk.updateRow({
+ tableName: "Employees",
+ rowId: "fcHIocncTsOygA3FjL-toQ",
+ rowData: { Age: 18 },
+ });
+ ```
+
+!!! abstract "batchUpdateRows"
+
+ Update multiple rows in one call.
+
+ ```js
+ sdk.batchUpdateRows({ tableName, rowsData });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowsData`
+ : array — objects containing a `row_id` and the updated `row` data
+
+ __Returns__ `.data` holds `{ success: true, rows: [{...}, ...] }`
+
+ __Example__
+ ```js
+ await sdk.batchUpdateRows({
+ tableName: "Employees",
+ rowsData: [
+ { row_id: "fcHIocncTsOygA3FjL-toQ", row: { Age: 18 } },
+ { row_id: "BIXJ_dUMS1OW8Lyoxrx4Fw", row: { Age: 24 } },
+ ],
+ });
+ ```
+
+## Delete rows
+
+!!! abstract "deleteRow"
+
+ Delete a single row.
+
+ ```js
+ sdk.deleteRow({ tableName, rowId });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowId`
+ : string — the unique ID of the row to delete
+
+ __Returns__ `.data` holds `{ success: true }`
+
+ __Example__
+ ```js
+ await sdk.deleteRow({
+ tableName: "Employees",
+ rowId: "fcHIocncTsOygA3FjL-toQ",
+ });
+ ```
+
+!!! abstract "batchDeleteRows"
+
+ Delete multiple rows in one call.
+
+ ```js
+ sdk.batchDeleteRows({ tableName, rowsIds });
+ ```
+
+ __Parameters__
+
+ `tableName`
+ : string — name of the target table
+
+ `rowsIds`
+ : array — the row IDs to delete
+
+ __Returns__ `.data` holds `{ success: true }`
+
+ __Example__
+ ```js
+ await sdk.batchDeleteRows({
+ tableName: "Employees",
+ rowsIds: ["fcHIocncTsOygA3FjL-toQ", "BIXJ_dUMS1OW8Lyoxrx4Fw"],
+ });
+ ```
diff --git a/docs/media/html-page-add.png b/docs/media/html-page-add.png
new file mode 100644
index 00000000..3932cc6b
Binary files /dev/null and b/docs/media/html-page-add.png differ
diff --git a/docs/media/html-page-config.png b/docs/media/html-page-config.png
new file mode 100644
index 00000000..af3c44ba
Binary files /dev/null and b/docs/media/html-page-config.png differ
diff --git a/mkdocs.yml b/mkdocs.yml
index 8b7989a5..b9c45045 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -240,3 +240,11 @@ nav:
- plugins/index.md
- Environments: plugins/environments.md
- Available methods: plugins/methods.md
+ - HTML Pages:
+ - html-pages/index.md
+ - Getting Started: html-pages/getting-started.md
+ - Example: html-pages/example.md
+ - SDK Reference:
+ - Initialization: html-pages/sdk/initialization.md
+ - Rows: html-pages/sdk/rows.md
+ - Files & Images: html-pages/sdk/files.md