# breakwater Markdown renderer and HTML sanitizer for user-submitted content: GitHub Flavored Markdown through goldmark, then a fixed bluemonday allow-list that adds nofollow and target=_blank to links. `import "git.golem15.com/golem15/summercms/modules/breakwater"` **Name:** A breakwater takes the force of the open sea; user Markdown and HTML break on the policy and only calm, allow-listed HTML reaches the shore. ## Overview `breakwater` is the gate for content a site user submits and a plugin stores and serves: forum posts, comments, and any other Markdown or HTML that does not come from an administrator. `breakwater.Render` converts Markdown to HTML and `breakwater.Purify` sanitizes HTML that is already HTML; both return the policy's output, which the plugin stores on write and serves as is. The policy is the PHP `golem15/forum` HTMLPurifier config 1:1 (decision note `.planning/notes/2026-10-18-bluemonday-forum-policy.md`): a short element list, links with `http`, `https` or `mailto` only, `rel="nofollow"` on every link and `target="_blank"` on fully-qualified ones. The Markdown pass is [goldmark](https://github.com/yuin/goldmark) with GFM, in its default safe mode: raw HTML in the Markdown is stripped and a link with an unsafe destination is dropped before the policy runs, the analog of the PHP `html_input=strip` and `allow_unsafe_links=false`. **Not this module.** [seaglass](../seaglass/README.md) is the policy for rich text an administrator stores through cabana's `richeditor` field: bluemonday's UGC policy, which keeps images, relative and `tel:` links, and links as written without `nofollow`. `cabana.RenderMarkdown` is the admin Markdown field preview: CommonMark without GFM behind a regex gate that refuses unsafe output instead of removing it. Plugins must not use either for user content. ## Features - Allowed elements: `p`, `br`, `strong`, `em`, `b`, `i`, `h1`–`h6`, `ul`, `ol`, `li`, `blockquote`, `pre`, `code`, `a` with `href`, `rel` and `target`, `table`, `thead`, `tbody`, `tr`, `th`, `td`, `hr`, `del`, `ins`, `sub`, `sup`. Everything else, including `img`, `figure`, `script`, `style`, `iframe`, `form`, `svg`, every `on*` attribute, `class` and `id`, is removed. - URL schemes: `http`, `https` and `mailto`. No `tel:` and no relative URLs; a link whose `href` is refused loses its tag and keeps its text. - Every link gets `rel="nofollow"`; every fully-qualified `http` or `https` link also gets `target="_blank"` and `rel="noopener"`. A caller-supplied `rel` or `target` with any other value is dropped and rewritten. - Raw HTML in Markdown is stripped, not escaped into visible text, and HTML comments are removed. - Blank or whitespace-only input renders and purifies to `""`. - Safe for concurrent use: the Markdown engine and the policy are built once and shared. ## Usage ```go // A forum post: store the rendered HTML on write and serve it as is. html := breakwater.Render(post.Content) // Content stored as HTML: the same allow-list, no Markdown pass. clean := breakwater.Purify(training.ContentHTML) ``` `breakwater.Render("See [the docs](https://example.com/docs) and this.")` gives `
See the docs and this.
`. ## API reference | Identifier | Description | |------------|-------------| | `breakwater.Render` | Converts user Markdown (GFM) to HTML with raw HTML stripped and unsafe links dropped, then returns it filtered through the policy; `""` for blank input. | | `breakwater.Purify` | Returns HTML filtered through the same policy with no Markdown pass, for content stored as HTML; `""` for blank input. | ## Dependencies - SummerCMS modules: none. - Third-party: `github.com/microcosm-cc/bluemonday` v1.0.27 (the policy; decision note `.planning/notes/2026-10-18-bluemonday-forum-policy.md`), `github.com/yuin/goldmark` v1.8.6 with `extension.GFM` (the Markdown pass). - Standard library: `bytes`, `regexp`, `strings`. ## Testing ```sh go test ./modules/breakwater/... ``` The tests run in memory and need no external services. They cover the PHP `ForumMarkdownRenderTest` corpus (XSS cases, GFM table and fenced code), links and autolinks per scheme, blank input, the `Purify` allow-list and concurrent use.