Getting Started#

In this tutorial we're going to walkthrough using r3ply from start to finish with an example. We will be installing the re CLI tool, generating a config, simulating a comment, and then discussing next steps. Follow the the steps below from within your project's top-level directory.

#

Table of Contents

──𓆝𓆟𓆞𓆝𓆟 𓆝𓆟𓆞──

Installation & Setup#

For this tutorial you will need to install the r3ply CLI tool called re.

# use npm -D @r3ply/cli for per project installations
npm -g @r3ply/cli
re --help

You should see the usage statement print.

Next initialize a new r3ply project at the top-level directory of your project.

re init

You should see output similar to this (but not exactly the same):

Initialized empty r3ply project at /Users/demo/Developer/r3ply/site

Add the following site entry to your config:

[[site]]
domain = "site.local.test"
r3ply = "cli.r3ply.test"
signet = "6Be8MUKnqpXZ73MDbX2u2g"
issued = "2025-11-08"
label = "CLI"

Help: You can generate a config with `re generate config` if you have not already.

This is a site entry, and in r3ply there's one site entry per domain x r3ply pair.

In this case, the domain is "site.local.test" and r3ply is "cli.r3ply.test". These values are special cases used by the r3ply CLI.

The signet is a special cryptographic envelope (docs) that signifies that unique domain x r3ply pair. In this case, the signet is issued by the r3ply CLI to your local project (as indicated by label).

Great, now our r3ply project is initialized. Don't worry about saving the initialization output above. We will see it again in the next step.

Generating a Config#

Now let's generate a config so we can use r3ply.

re generate config

You should see output similar to this:

version = "0.0.1"
enabled = true

[[site]]
domain = "site.local.test"
r3ply = "cli.r3ply.test"
signet = "6Be8MUKnqpXZ73MDbX2u2g"
issued = "2025-11-08"
label = "CLI"

[comments.email]
enabled = true

[moderation]
enabled = true
github = [ ]
webhook = [ ]

  [[moderation.local]]
  "file_path_{}" = "comment_{{ comment.id[:8] }}.json"
  enabled = true
  "allow*" = [ ]

(If you look closely you can see that the [[site]] entry here is identical to the one from the re init command we ran earlier)

Copy the above output to a file named config.toml, and save the file in a place where it can be accessed from your website. r3ply will look at the following paths in this order:

# Priority from highest to lowest:

https://${domain}/.well-known/r3ply/config.toml
https://${domain}/.well-known/r3ply.config.toml
https://${domain}/r3ply.config.toml
https://${domain}/r3ply.toml

(The r3ply website itself stores the config at static/.well-known/r3ply/config.toml and can be reached online at https://r3ply.com/.well-known/r3ply/config.toml.)

Finally, let's set the path of your config as the default config path:

re config set-default <your-config-path>

We can run re config validate to verify that our config is well formed. If there's no output then you're ready to proceed to the next section and begin simulating email comments.

⚠️

If you do see output from re config validate then you will need to fix it before you move on.

Simulating a Comment#

re simulate email --moderate

You should see a large amount of text representing each stage of the comment processing pipeline. The docs cover output more in-depth, but it's basically the entire email to comment pipeline broken into stages.

💡

re simulate email generates a pretty huge amount of text. Normally it will be filtered. The CLI docs cover how to do that.

The --moderate flag told re to to send the comment for moderation. Towards the bottom of the output you should see something similar to this (but not exactly the same):

1# === Moderation: Local[0] ===
2
3#################################
4# Request portion of moderation #
5#################################
6
7# `bypass` asks to skip moderation altogether. For local moderation it has no effect.
8[request]
9type = "local"
10bypass = false
11
12 # `relative_path` is relative to project root.
13 [request.args]
14 relative_path = "content/comments/49aa56d3.json"
15 # `comment` variable elided, see comment output from earlier steps in docs
16 comment = '[elided... see "Comment: Processed" above]'
17
18################################
19# Ticket portion of moderation #
20################################
21
22# `ticket.local` is the response to a request for local moderation.
23[ticket.local]
24absolute_path = "/Users/demo/Developer/r3ply/site/content/comments/49aa56d3.json"

At the bottom we see the absolute_path the comment was written to. You can change this path by editing your config (file_path_{} under [[moderation.local]]).

What's Inside a Comment#

Now that we can simulate the receiving comments based on a real configuration, we need to understand what's inside a comment object. Open the file that was at the absolute_path from the last step. You can also expand the one that was used during the making of this tutorial.

Expand to See File
1{
2 "r3ply": {
3 "config_version": "0.0.1",
4 "server": "cli.r3ply.test",
5 "site": "site.local.test",
6 "signet": "wWM5hk4DKr1xVRhVq-7aog",
7 "issued": "2025-10-16"
8 },
9 "author": {
10 "pseudonym": "30e991c8dd7ef21de607f346d063d68033338049778be8aee61410c8a96a4d13",
11 "token": "qQ3KhRG_ZTbBOBZ9vFWk2MSWhHeWJ8ZBSKXbwwyRdp6auUPlq0MavGiVo0q0P2QWQzf-S7a4KrFioEmyag_6EbxHeXXsZzxElT85e68Hb4Be5p75BdClBeVOCqqHONjRB6R9KxXcjW4V313HVTBHG8iH0H4IwJ0iYoPov_b3Tk-OULtHrNS1rpzdP_1s2atMqm02qhPvWDneC2D-dwXdC0YBMoRvonBI40UPBKT5g_ukpf_GI9T3r8Q-Is_9kjPM8hJ2AQ9vKRME3a3qxH6-139UtCVjgdNhvb5S1qyUWy7afZvv0RZNFS8qLJwy63czZR1rGvT8Jx9fvfrKt-zYYjt8BnggopaWqecSTfqzfPCHHZ-SFhvbhzvUPpnmmrsafSRHR2k0-77lI9LKT9jWiBd5bGykNtS-OO4ggRjKd7iii4ofqM7WywQxQVlylmbkSt4hxq1s7Rdn8KV9"
12 },
13 "comment": {
14 "id": "49aa56d3ab184f67a4643437d0837aef",
15 "ts_rcvd": "1762680510",
16 "subject": {
17 "url": "https://site.local.test/docs/getting-started/",
18 "origin": "https://site.local.test",
19 "protocol": "https:",
20 "hostname": "site.local.test",
21 "path": "/docs/getting-started/"
22 },
23 "txt": "If you think about it... commenting systems have been a sort of a [great filter](https://en.wikipedia.org/wiki/Great_Filter) for websites, since at least the 1990s.\r\n",
24 "md": "<p>If you think about it... commenting systems have been a sort of a <a href=\"https://en.wikipedia.org/wiki/Great_Filter\">great filter</a> for websites, since at least the 1990s.</p>\n",
25 "html": "<p>If you think about it... commenting systems have been a sort of a <a href=\"https://en.wikipedia.org/wiki/Great_Filter\" rel=\"noopener noreferrer\">great filter</a> for websites, since at least the 1990s.</p>\n"
26 },
27 "email": {
28 "to": "[email protected]",
29 "subject": "/docs/getting-started/",
30 "date": "2025-11-09T09:28:30+00:00",
31 "text": "If you think about it... commenting systems have been a sort of a [great filter](https://en.wikipedia.org/wiki/Great_Filter) for websites, since at least the 1990s.\r\n",
32 "auth": {
33 "dkim": false,
34 "spf": false,
35 "dmarc": false,
36 "pass": false
37 },
38 "from": {
39 "pseudonym": "acae7e02620773047964ab4e7e6af86278d93582f3e6bd67640936f7e51229c3",
40 "signet": "wWM5hk4DKr1xVRhVq-7aog",
41 "issued": "2025-10-16",
42 "token": "qQ3KhRG_ZTbBOBZ9vFWk2MSWhHeWJ8ZBSKXbwwyRdp6auUPlq0MavGiVo0q0P2QWQzf-S7a4KrFioEmyag_6EbxHeXXsZzxElT85e68Hb4Be5p75BdClBeVOCqqHONjRB6R9KxXcjW4V313HVTBHG8iH0H4IwJ0iYoPov_b3Tk-OULtHrNS1rpzdP_1s2atMqm02qhPvWDneC2D-dwXdC0YBMoRvonBI40UPBKT5g_ukpf_GI9T3r8Q-Is_9kjPM8hJ2AQ9vKRME3a3qxH6-139UtCVjgdNhvb5S1qyUWy7afZvv0RZNFS8qLJwy63czZR1rGvT8Jx9fvfrKt-zYYjt8BnggopaWqecSTfqzfPCHHZ-SFhvbhzvUPpnmmrsafSRHR2k0-77lI9LKT9jWiBd5bGykNtS-OO4ggRjKd7iii4ofqM7WywQxQVlylmbkSt4hxq1s7Rdn8KV9"
43 }
44 }
45}

Let's look more closely at individual items to get a better understanding.

  ...
  "r3ply": {
    "config_version": "0.0.1",
    "server": "cli.r3ply.test",
    "site": "site.local.test",
    "signet": "wWM5hk4DKr1xVRhVq-7aog",
    "issued": "2025-10-16"
  },
  ...

This is just metadata about concerning the site, r3ply server, etc... that serviced this comment.

Next let's look at author:

  ...
  "author": {
    "pseudonym": "30e991c8dd7ef21de607f346d063d68033338049778be8aee61410c8a96a4d13",
    "token": "..."
  }
  ...

Here we see details about the comment's author. Their email address has been anonymized to a stable pseudonym that can be used like an ID. There's also a long token which isn't shown fully here (You can read more about these in the docs, but it isn't necessary right now).

  ...
  "comment": {
    "id": "49aa56d3ab184f67a4643437d0837aef",
    "ts_rcvd": "1762680510",
    "subject": {
      "url": "https://site.local.test/docs/getting-started/",
      "origin": "https://site.local.test",
      "protocol": "https:",
      "hostname": "site.local.test",
      "path": "/docs/getting-started/"
    },
    "txt": "If you think about it... commenting systems have been a sort of a [great filter](https://en.wikipedia.org/wiki/Great_Filter) for websites, since at least the 1990s.\r\n",
    "md": "<p>If you think about it... commenting systems have been a sort of a <a href=\"https://en.wikipedia.org/wiki/Great_Filter\">great filter</a> for websites, since at least the 1990s.</p>\n",
    "html": "<p>If you think about it... commenting systems have been a sort of a <a href=\"https://en.wikipedia.org/wiki/Great_Filter\" rel=\"noopener noreferrer\">great filter</a> for websites, since at least the 1990s.</p>\n"
  },
  ...

Here's the actual comment object. There're three nearly identical versions of the comment body: txt, md, and html. This is because r3ply supports text written as markdown, as well as converting that markdown to HTML, but it will also strip out malicious html tags. You can configure this further within [comment.sanitize_html] (docs).

⚠️

It is strongly advised to only use the .html content when you render your comments. Out of the three, only .html is sanitized. Otherwise it's possible malicious comments could be crafted.

There's also the subject field of the comment object, which tells us the URL of what the comment was in response to. Using this you should be able to identify the page the comment belongs on.

Integrating Comments#

To build comments into your site you just treat them like you would do any other content. Since everyone's websites are built differently, specific advice can't be given, however r3ply allows you to customize how comments look using a templating language (docs). Therefore you have a few options at your disposal.

  1. You can just save comments as plain json files and build your site from those
  2. or you can customize it in a way that works with how you would like them to be built into your site.
💡

It's recommended to store the original JSON somewhere in your comment, even if you do template it. This is in case you ever need to migrate old comments to a new look.

The variable __tera_context is how you access the whole object.

Let's look at a quick example though, using the comment from above. We could render that comment as HTML as follows:

<article data-comment-id="48ec61da69b743cda2d6747efe6dca80">
  <header>
    <time datetime="2025-11-08T14:58:08+00:00">11 November, 2025</time>
    <span> - </span>
    <strong>30e991c8</strong> 🗣️
  </header>
  <section>
    <blockquote>
      <p>If you think about it... commenting systems have been a sort of a <a href="https://en.wikipedia.org/wiki/Great_Filter" rel="noopener noreferrer">great filter</a> for websites, since at least the 1990s.</p>
    </blockquote>
  </section>
  <hr>
  <nav>
    <a href="/docs/getting-started/">View related post</a>
    <a href="/commenters/30e991c8/">More posts by user</a>
  </nav>
</article>

And that same comment above would render like this (with a little added styling):

To get something like the above example you can add the comment_{} variable (under [comments.email]):

1version = "0.0.1"
2enabled = true
3
4[[site]]
5domain = "site.local.test"
6r3ply = "cli.r3ply.test"
7signet = "6Be8MUKnqpXZ73MDbX2u2g"
8issued = "2025-11-08"
9label = "CLI"
10
11[comments.email]
12enabled = true
13"comment_{}" = """
14<article data-comment-id="{{ comment.id }}">
15 <header>
16 <time datetime="{{ email.date }}">{{ email.date | date(format="%d %B, %Y") }}</time>
17 <span> - </span>
18 <strong>{{ author.pseudonym[:7] }}</strong> 🗣️
19 </header>
20 <section>
21 <blockquote>
22 {{ comment.html }}
23 </blockquote>
24 </section>
25 <hr>
26 <nav>
27 <a href="{{ comment.subject.path }}">View related post</a>
28 <a href="/commenters/{{ author.pseudonym[:8] }}/">More posts by user</a>
29 </nav>
30</article>
31"""
32
33[moderation]
34enabled = true
35github = [ ]
36webhook = [ ]
37
38 [[moderation.local]]
39 "file_path_{}" = "comment_{{ comment.id[:8] }}.json"
40 enabled = true
41 "allow*" = [ ]
💡

If your comment_{} gets too long then you can put it in a separate file and reference that file with &comment_{} (docs).

There is much more you can do with building your comment section using templating. To get some ideas check out the demo section.

Summary & Next Steps#

You should now able to simulate comments via email with re simulate email, and render them using your site's build pipeline.

The development process from here is going to be just iterating on the steps above, using the re CLI tool, until you come up with something that you like. There's a lot of helpful functionality waiting to be discovered in the config and CLI sections of the docs.


When you're ready, you can deploy your site online to receive comments publicly, via email.

To do that you're going to need to add a new site entry for your site's public domain:

# each (site x r3ply) pair has an entry
[[site]]
domain = "example.com"
r3ply = "r3ply.com"
signet = "iSQIIBcF7ka2UURJpFDkYw"
issued = 2025-08-26

Next, you'll want to add another moderation channel to your r3ply config. In this tutorial we only covered the local moderation channel. For example you can add GitHub moderation like so:

# E.g. add moderation by GitHub PR
[[moderation.github]]
owner = "<ACCOUNT>"
repo = "<REPO>"
"file_path_{}" = "content/comments/{{ comment.id[:8] }}.md"
💡

To use GitHub moderation with a private repo, you have to give the r3ply GitHub bot permission to access your repo.

With GitHub moderation you should see incoming comments like this:

Screenshot of a comment waiting for GitHub Moderation

The GitHub bot helps keep your commit history clean.

The details (PR title, commit message, etc...) are all customizable, as are many things and features in r3ply.

Read the config and CLI docs to take full advantage of all the features.

──𓆝𓆟𓆞𓆝𓆟 𓆝𓆟𓆞──

Comments (1) #

Comment [+] Expand

Pending Comments (0)

Trying it out! .