# seaglass
HTML sanitizer for stored rich text: a bluemonday allowlist policy that returns clean HTML byte for byte and reports what it removed from the rest.
`import "git.golem15.com/golem15/summercms/modules/seaglass"`
**Name:** Glass the sea has worn smooth; rough HTML goes in and only the safe, smooth part comes out.
## Overview
`seaglass` is the framework's one HTML policy for rich text an administrator stores, such as cabana's `type: richeditor` field. It wraps [bluemonday](https://github.com/microcosm-cc/bluemonday) v1.0.27 with the policy the Journal plugin uses for imported WinterCMS content, without that plugin's editor-specific rules. A tokenizing sanitizer checks attributes and URLs the way a browser parses them, so it does not share the bypasses of regex gates (` `, `javascript:`, ``).
The comparison is byte-stable: when the input and the policy's output tokenize to the same HTML tokens, the input itself is returned, so a value that only differs in escaping is never rewritten. Otherwise the policy's output is returned with a report of the removed elements, attributes and comments, which cabana shows to the administrator as save notices.
## Role in the framework
A leaf utility with no SummerCMS dependency. [cabana](../cabana/README.md) runs every richeditor value through it on create and update, top level and inside repeater rows; plugins that serve stored HTML in a public API can run it again on output, for content that was imported without passing the admin save.
Use cases:
- Sanitize an HTML field on save and tell the editor what was removed.
- Check that stored HTML is already clean (`seaglass.Check`), for example in an import report.
- Sanitize HTML on output in a public API.
## Features
- bluemonday's UGC policy with links kept as written (no `rel="nofollow"`), `tel:` links next to `http`, `https`, `mailto` and relative URLs, and the `class`, `dir`, microdata, footnote `role` and inline `color`, `font-size`, `font-weight` and `text-align` styles imported WinterCMS content uses.
- Everything else is removed: `script`, `style`, `iframe`, `object`, `embed`, `form`, `svg`, `math`, comments, every `on*` attribute, `target`, `rel`, and URLs with any other scheme.
- `seaglass.SanitizeReport` returns HTML the policy keeps whole byte for byte with a zero `seaglass.Removals`; a tag that repeats an attribute is always a change.
- `seaglass.Removals` names the removed elements and attributes (at most ten distinct names each, in first-seen order) and counts every removal and every removed comment.
## Usage
```go
clean, report := seaglass.SanitizeReport(`Hi
`)
// clean == "Hi
"
// report.Elements == []string{"script"}, report.Attributes == []string{"onclick"}
if err := seaglass.Check(stored); err != nil {
// stored HTML would change under the policy
}
```
## API reference
| Identifier | Description |
|------------|-------------|
| `seaglass.Sanitize` | Returns the HTML filtered through the policy: the input itself when the policy keeps all of it. |
| `seaglass.SanitizeReport` | `seaglass.Sanitize` with a `seaglass.Removals` report of what was removed; a zero report exactly when the input is returned. |
| `seaglass.Check` | Returns an error when the policy would change the HTML, nil when it keeps it whole. |
| `seaglass.Removals` | What was removed: `Elements` and `Attributes` (distinct names), `ElementCount` and `AttributeCount` (every removal), `Comments`, and `Changed` (the HTML differs from the input). |
## Dependencies
- SummerCMS modules: none.
- Third-party: `github.com/microcosm-cc/bluemonday` v1.0.27 (the policy; decision note `.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md`), `golang.org/x/net/html` (the token comparison and the removal report).
- Standard library: `errors`, `fmt`, `io`, `regexp`, `slices`, `strings`.
## Testing
```sh
go test ./modules/seaglass/...
```
The tests run in memory and need no external services.