Diane, note for the file. What the box takes, and what it does with it.

Docs.

Embedding the widget takes two lines of HTML. Everything after the first section is optional: settings for the cases where the defaults are not what your site needs.

Setting it up

  1. Sign in at admin.diane.to with Google.
  2. Add your domain. One row per hostname: www.example.com and example.com are the same site, and subdomains such as blog.example.com are registered separately.
  3. Paste these two lines into your page:
<script async defer src="https://diane.to/w.js"></script>
<to-diane></to-diane>

The script can sit in the head or at the end of the body. Place the custom element where the widget should render.

There is no key in that snippet, no site id and nothing per-page. Your site is identified by the Origin header and the page by its own URL, so there is nothing to paste into the markup and nothing to keep in sync.

Which site, which page

The site is identified by the Origin header, which the browser sets on every cross-origin request and page JavaScript cannot forge. It is matched exactly against the domain you registered, except that one leading www. is stripped. All other subdomains require separate registration.

The page is identified by checking three places, in order:

  1. the post-slug attribute on the element,
  2. the pathname of the <link rel="canonical"> tag,
  3. the current location.pathname.

The resulting identifier is cleaned up the same way in the widget and on the server: query strings, fragments, doubled slashes and trailing slashes are removed. So /post, /post/ and /post?utm_source=rss resolve to the same thread. Keep an identifier under 200 characters — the widget ignores a longer post-slug and falls back to the canonical link, then to location.pathname.

Two elements on one page with different post-slug attributes keep two separate threads. A canonical link pointing elsewhere gives syndicated copies of an article one shared thread.

Attributes

AttributeDescription
post-slugThe thread identifier, used when the thread is not derived from the address. Up to 200 characters.
langThe language of the widget's labels, overriding <html lang>.
<to-diane post-slug="/blog/twin-peaks"></to-diane>

Both attributes are optional. There are no others.

Sites with client-side routing

The widget detects route changes on its own: it listens for popstate, uses the Navigation API, and otherwise polls location.href once a second. A standard single-page application needs no further configuration.

If the thread is not identified by the address, update the attribute when the route changes:

document.querySelector("to-diane").setAttribute("post-slug", nextSlug)

A server response that arrives after the reader has navigated away is discarded, and an unsent comment is cleared on navigation.

Appearance

The widget derives its palette from the text colour of your page. It ignores prefers-color-scheme so that it matches the page it is embedded in rather than the visitor's operating system, which is why most sites need no theming at all.

To customize it, apply CSS custom properties to the element:

to-diane {
  --td-font: Georgia, serif;
  --td-radius: 4px;
  --td-gap: 24px;

  /* A solid brand button instead of the default tinted one */
  --td-button-bg: #7c2d12;
  --td-button-fg: #fff;
  --td-button-border: transparent;
}
PropertyDefaultDescription
--td-fontsystem sans stackThe typeface for all text.
--td-gap16pxThe spacing between elements.
--td-radius8pxThe border radius for fields and the button.
--td-bubble-radius14pxThe border radius for comment bubbles.
--td-surface11% of your text colourThe comment background colour.
--td-border24% of your text colourThe border colour for fields and the button.
--td-muted62% of your text colourThe colour for names, dates and secondary text.
--td-button-bg13% of your text colourThe button background colour.
--td-button-bg-hover20% of your text colourThe button background colour on hover.
--td-button-fgyour text colourThe button label colour.
--td-button-bordersame as --td-borderThe button border colour; transparent hides it.
--td-danger#e5484dThe error text colour.

Three parts are exposed for styling: ::part(root) targets the whole container, ::part(heading) the comment-count heading and ::part(powered) the “Powered by Diane.to” label below the form. You can restyle that label, or hide it:

to-diane::part(powered) {
  display: none;
}

No other internal element can be targeted from the page. The element uses display: block; apply a max-width to control its size.

Language

The widget takes its language from the <html lang> attribute of your page. An attribute on the element overrides it:

<to-diane lang="cs"></to-diane>

English and Czech are supported. An unsupported language falls back to English labels, while dates follow the page's own locale. To request a language, write to hello@diane.to.

Limits

A free account holds one hundred live comments per site. Above that the server rejects new submissions and the widget reports that the box is full; comments already written stay visible. Deleted comments do not count towards the limit, and a deletion can be undone.

A name is limited to 80 characters and a comment to 4000.

Comments are checked for spam, without a CAPTCHA. A rejected comment gets a message in the visitor's own language telling them what to do next. Write to hello@diane.to if legitimate comments are being blocked.

Moderation

Moderate in the console at admin.diane.to. It provides one inbox with every comment, newest first, and a per-site view grouped by thread. You can delete and restore comments, show deleted ones, rename a site, and silence a site entirely — silencing hides the widget on that domain without showing visitors an error.

A new site holds comments until you approve them. They arrive in the console marked waiting, and Approve puts one on the page. Refusing a comment is deleting it — the same button, and it can be undone, which puts the comment back in the queue. Until you approve it, the only person who sees it on the page is whoever wrote it, greyed out and marked as waiting. The site's settings switch this to Publish immediately, and comments then appear as soon as they are posted.

Switching the mode does not reach backwards: comments already waiting go on waiting until you approve them, and published ones stay published. Comments that are waiting do not count towards the limit of a hundred published ones — the queue has a ceiling of its own, also a hundred. Once it is full the site refuses new comments until you approve or delete something.

You can answer from the console. Your reply appears as the newest comment in the thread, marked author so a reader can tell the owner is speaking. It is signed with the Reply as name on the site's screen, which is filled in with your own name when you register the site and can be changed at any time. Changing it also changes the replies you have already posted — it is still one person, not two. A reply counts towards the limit like any other comment, and the same button deletes it.

What is stored

Diane stores the name the visitor typed, the comment text, the timestamp, and an unsalted SHA-256 hash of their IP address. The hash is only ever compared for equality, as part of the spam checks, and no endpoint returns it. On your site the widget sets no cookies and loads no third-party resources.

While a comment of theirs is waiting to be approved, the widget keeps its id in localStorage on your own domain, under the key to-diane:pending — the id and the page it was written on, never the text. It is what lets someone reload the page and still see what they wrote. It is readable only by your site, it never reaches us, and it is dropped as soon as the comment is published or refused.

Comments are stored as submitted and rendered as plain text: no HTML, no Markdown, no auto-linking. Further details are on the privacy page.

Content Security Policy

If your site sends a Content Security Policy, three directives concern the widget:

script-src   https://diane.to
connect-src  https://diane.to
style-src    'unsafe-inline'

script-src permits the script, connect-src the API requests, and style-src the widget's own stylesheet, which is a <style> element inside its shadow root that a nonce cannot reach. The widget loads no fonts, images or third-party resources.

When nothing shows up

The widget never shows a configuration error to your visitors. It renders nothing and writes to the browser console with a [to-diane] prefix, so open the console first.

What Diane does not do

No reply threads, no votes, no editing after posting, no Markdown, no avatars, no reader accounts, and nothing that notifies a visitor — nobody is told their comment was answered. You are told: the console can push a new comment to your own devices. If one of these decides whether you can use Diane, write to hello@diane.to.