diff --git a/docs/features/html-import.md b/docs/features/html-import.md
index 7cb7bf493..831914e82 100644
--- a/docs/features/html-import.md
+++ b/docs/features/html-import.md
@@ -98,8 +98,8 @@ Callers splice the fragment into the page tree via `insertImportedNodes(parentId
| `instatic-outlet` | `base.outlet` | none (the CMS content outlet) | **No** |
| `instatic-loop` | `base.loop` | `sourceId`, `filters.tableId`, `orderBy`, `direction`, `limit`, `offset`, `pagination`, `pageSize`, optional `tag` / `customTag` from `data-*` attrs | Yes |
| `h1`–`h6`, `p`, `span`, `small`, `strong`, `em` | `base.text` | `text` = `el.textContent`, `tag` = tag name | No |
-| `a` with class `btn` | `base.button`, or `base.link` when it wraps element children | `label` (`text` on `base.link`) = `el.textContent`, `href`, `target` | No for text-only; yes when it wraps elements |
-| `a` (no `btn` class) | `base.link` | `text` = `el.textContent`, `href`, `target` | No for text-only; yes when it wraps elements |
+| `a` with class `btn` | `base.button`, or `base.link` when it wraps element children | `label` (`text` on `base.link`) = `el.textContent`, `href`, `target` (normalised, see below) | No for text-only; yes when it wraps elements |
+| `a` (no `btn` class) | `base.link` | `text` = `el.textContent`, `href`, `target` (normalised, see below) | No for text-only; yes when it wraps elements |
| `img` | `base.image` | `src`, plus `loading` / `decoding` / `fetchPriority` when the attribute holds a value the module offers; `alt` is reported in `imageAlts` for the media record, not stored as a prop | No |
| `form` | `base.form` | `mode`, `formId`, CMS data attrs, custom `action` / `method` | Yes |
| `label` | `base.label` unless wrapping elements, then `base.container` | `text`, `targetMode`, `targetId` | No for plain labels; yes for wrapper labels |
@@ -122,6 +122,7 @@ Callers splice the fragment into the page tree via `insertImportedNodes(parentId
- **Direct text inside a recursing container is preserved.** The walker iterates `childNodes` (not just `children`): element children route through the rules, and each significant text node becomes a synthesized `base.text` child with `tag: 'none'` in document order. That no-wrapper text mode publishes back to bare text, so `
98%
` and `
Buy milk
` import as containers holding their original text without adding selector-visible wrapper elements. Whitespace-only text (indentation between tags) is skipped; internal whitespace runs collapse to single spaces, and boundary spaces are kept when the text run sits between element siblings.
- **`` metadata is preserved separately.** Classes, safe HTML attributes (`id`, ARIA, `data-*`, etc.), and harvested inline styles on `` are returned as `fragment.body` rather than inserted into `rootIds`. Full-site import applies them to `base.body`; paste-style HTML import can ignore them without changing the fragment structure.
- `base.link` uses the prop `text` (not `label`). `base.button` uses `label` (not `text`). These match the module source.
+- **`target` is normalised to the module vocabulary.** `base.link` and `base.button` persist only `_self`, `_blank` and `_parent` (`AnchorTargetSchema` in `@core/htmlAttributes`). Other authored values map to the one that navigates the same way outside a frameset: an empty `target=""`, a bare `target` and `_top` import as `_self`; a named browsing context (`target="sidebar"`, `_new`), which opens a new tab when no frame has that name, imports as `_blank`. Keywords match case-insensitively. Copying it verbatim would leave a prop the publisher's schema rejects, and the whole node would then render with module defaults instead of its authored `href` and text.
- **Button-like elements keep what they wrap.** `base.button` is `canHaveChildren: false`, so an `a.btn` or a `