Help us shape the future of Go! Take the 2026 Go Developer Survey

comments

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 24 Imported by: 0

README

Comments

Comments below posts, events or pages, and guestbooks. New messages wait for approval unless moderation is off, and staff can reply from the admin.

Every message belongs to a subject: a text the template chooses, such as a post ID, a page ID or a fixed word. Add an instance per kind of conversation; each has its own messages, settings and admin section.

Add it to the site

app.Use(
    comments.Module("guestbook", "Guestbook"),
    comments.Module("remarks", "Comments"),
)

The key (guestbook) is the instance's name in URLs and template functions and must be unique on the site. The title (Guestbook) is what editors see in the admin navigation and the page editor.

Comments below content

Comments attach to anything with an ID through the template functions; the instance needs no page of its own. Below a post in news/post.gohtml:

{{commentThread "remarks" .ID}}

commentThread renders the approved comments on that subject, oldest first with their replies, followed by the form. {{commentForm "remarks" .ID}} renders only the form, and {{commentCount "remarks" .ID}} counts the approved comments and replies, for example next to titles in a list.

Both come with plain, accessible default markup. A theme replaces them with remarks/thread.gohtml and remarks/form.gohtml, or comments/thread.gohtml and comments/form.gohtml for every instance; task templates -- remarks writes the defaults to start from. They use stable wt-* classes (wt-comments, wt-comment, wt-comment--staff, …), laid out by the optional stylesheet webthings.css, which the layout links with {{styles "webthings.css" "site.css"}} (see The base stylesheet). Their texts go through tl, so the site's translations translate them.

For markup of your own, comments returns the approved comments, commentURL the address the form posts to, commentInput the form's model for a subject, commentResult the outcome of the visitor's last post and commentsClosed whether new comments are refused:

{{range comments "remarks" .ID}}
    <article>
        <strong>{{.Name}}</strong> {{date .CreatedAt}}
        {{.HTML}}

        {{range .Replies}}
            <blockquote>{{if .Staff}}Reply from the site:{{else}}{{.Name}}:{{end}} {{.HTML}}</blockquote>
        {{end}}
    </article>
{{end}}

<div id="remarks">
    {{with commentResult "remarks"}}<p class="notice">{{.}}</p>{{end}}

    {{if not (commentsClosed "remarks")}}
        {{with commentInput "remarks" .ID}}
            {{form_open .}}
                {{form_field . "subject" nil}}
                {{form_field . "name" nil}}
                {{form_field . "email" nil}}
                {{form_field . "body" nil}}
                <button>Send</button>
            {{form_close}}
        {{end}}
    {{end}}
</div>

HTML is the message escaped, with its paragraphs and line breaks. CreatedAt is in UTC; {{date .CreatedAt}} shows it in the page's language and the site's timezone.

form_open writes the form tag, posting to commentURL, with the spam trap and the page to return to: the element whose id is the instance key, here remarks. form_field renders one field of the form by its key, with its label, its limits (required, maxlength) and its help text, translated with the site's translations and in the site's form theme; subject is a hidden field with the subject. The fields' ids start with the key, such as remarks-name. See Template functions. Inputs written by hand with the same names work too.

Guestbook page

In the admin, open a page and choose the instance under Shows on this page. The page renders guestbook/list.gohtml, or comments/list.gohtml for every instance without a design of its own, with a comments.ListView: the approved messages of the instance, newest first, 20 per page. .Form is the form, with .Subject, the instance key, as its subject: {{form_open .Form}} with form_field per field, subject included, returns the visitor to the element whose id is .Key. The pager's .NewerURL and .OlderURL are the page's address with ?page=, such as /en/guestbook?page=2. Generate a starting point with:

task templates -- guestbook

The form component works here too:

{{/* @dot github.com/donseba/go-webthings/modules/comments.ListView */}}
<h1>{{.Title}}</h1>

{{commentForm .Key .Subject}}

{{range .Comments}}
    <article>
        <p><strong>{{.Name}}</strong> · {{date .CreatedAt}}</p>
        {{.HTML}}
    </article>
{{end}}

{{with .NewerURL}}<a href="{{.}}">Newer</a>{{end}}
{{with .OlderURL}}<a href="{{.}}">Older</a>{{end}}

The page lists the messages of every subject of the instance, so keep a guestbook apart from the comments below posts.

Visitors

The form posts to /_/<key>/post ({{commentURL "remarks"}}) with the fields of comments.PostInput:

Field
subject required, one line, at most 200 characters
name required, one line, at most 100 characters
email optional; never shown on the website
body required, plain text, at most 5,000 characters
parent optional: the ID of the comment to answer; a reply to a reply joins the same thread

form_open adds the spam trap and the page to return to, the element whose id is the instance key. The visitor comes back with ?<key>=<result>, which commentResult and the form component read:

  • sent: the message is shown right away;
  • pending: it waits for approval;
  • invalid: a field is missing, too long or not on one line;
  • closed: the instance was closed for new messages when the visitor posted; the form component says so even after it opens again;
  • throttled: the visitor posted more than 5 times in 10 minutes.

With invalid, the form shows what the visitor wrote again, with a message in the page's language at each field to correct. The components and form_field do this without a change to the template; inputs written by hand do not. Of several comment forms on one page, only the one for the subject the visitor wrote on shows it. See A post with mistakes. A post that cannot be saved, such as when the database cannot be reached, gets the site's error page, 500.gohtml, also with HTMX.

Bots that fill in the spam trap get the answer a person would get; nothing is stored. A post that stores nothing, spam, invalid, closed or throttled, leaves the pages in the site's output cache as they are; see Modules and the output cache. The message's language is the language of the page it was written on. Visitors' IP addresses are never stored: a hash that changes daily lets moderators spot a flood from one sender.

Moderation

The instance's admin section (/admin/remarks; /admin is the default admin path) lists the messages newest first, grouped by subject, with tabs for pending, approved and all messages. A group is headed by the title of the published content on the page its messages were written on, such as a post's title, with the page's address below it; when messages were written in several languages, the page in the admin's language wins. Without published content there the heading is the address, and a guestbook's is the instance's title. Each message shows the name, the e-mail address, the language, the page it was written on and a short sender code. The badge in the navigation counts the messages that are not shown.

  • Approve shows a message, Hide takes it off the website again, after the question "Take this off the website?". Both need the publish right.
  • Reply adds a staff reply, shown at once and marked as Staff. It is signed with the site title in the comment's language (or the organisation, or the editor's name) and approves the comment it answers. It needs the create right.
  • Move to trash moves a message and its replies to the trash, where they can be restored or deleted for good; restoring the message brings its replies back with it. It needs the delete right. A reply deleted on its own cannot be restored while the message it answers is in the trash.

Under Settings, editors with the edit right switch Approve messages before they are shown (on by default), close the instance with Closed for new messages, and set a Notification address.

E-mail

With a notification address, every new message from a visitor is mailed there in the admin's language: the name and address, the message, the page it was written on, whether it waits for approval, and a link to the admin. Replying to the mail answers the visitor when they gave an address.

The mail is the template notify, with a comments.NotifyView. A theme replaces it with mail/remarks/notify.gohtml, or mail/comments/notify.gohtml for every instance; task templates -- remarks writes the default there.

From Go

comments.Of(app, "remarks") returns the instance once the app has started:

remarks := comments.Of(app, "remarks")

list, err := remarks.List(ctx, postID)
count, err := remarks.Count(ctx, postID)

Add stores a message after the same checks as the form, for example in an import; Reply, Approve and Delete do what the admin does, and Undelete and Purge bring a deleted message back or remove it for good. What they change shows on the site at once, also on pages in the site's output cache; see Modules and the output cache. CreatedAt keeps the original date; zero means now. A reply passes the ID of the comment Add returned as ParentID:

comment, err := remarks.Add(ctx, comments.Entry{
    Subject:   postID,
    Name:      "Anna",
    Body:      "Great to see the playground open again.",
    Locale:    "en",
    Approved:  true,
    CreatedAt: written,
})

reply, err := remarks.Add(ctx, comments.Entry{
    Subject:  postID,
    ParentID: comment.ID,
    Name:     "Ben",
    Body:     "The swings are new too.",
    Locale:   "en",
    Approved: true,
})
  • Publishing and Events give comments a subject: a post's or event's ID.
  • Registrations attaches sign-ups to events in the same way.
  • Forms receives messages for the site's staff only, with an inbox.

Documentation

Overview

Package comments lets visitors leave messages on the website: comments below news posts or pages, and guestbooks. Messages belong to a subject, a string the template chooses, such as a post ID, a page path or a fixed word. New messages wait for approval in the admin unless moderation is switched off; staff can reply from the admin. Add an instance per kind of conversation:

app.Use(
	comments.Module("remarks", "Comments"),
	comments.Module("guestbook", "Guestbook"),
)

Comments in any template

The template functions work in every template, for example below a post of a Publishing instance:

{{range comments "remarks" .ID}}
  <article>
    <strong>{{.Name}}</strong> {{date .CreatedAt}}
    {{.HTML}}
    {{range .Replies}}
      <blockquote>{{if .Staff}}Reply from the site:{{else}}{{.Name}}:{{end}} {{.HTML}}</blockquote>
    {{end}}
  </article>
{{end}}

<div id="remarks">
  {{with commentResult "remarks"}}<p class="notice">{{.}}</p>{{end}}

  {{if not (commentsClosed "remarks")}}
    {{with commentInput "remarks" .ID}}
      {{form_open .}}
        {{form_field . "subject" nil}}
        {{form_field . "name" nil}}
        {{form_field . "email" nil}}
        {{form_field . "body" nil}}
        <button>Send</button>
      {{form_close}}
    {{end}}
  {{end}}
</div>

Functions, all with the instance key first:

  • comments key subject: the approved comments on subject, oldest first, each with its approved Replies (see Comment).
  • commentCount key subject: the number of approved comments and replies.
  • commentURL key: the address the form posts to.
  • commentInput key subject: the form's model (PostInput) for go-form's functions, as above.
  • commentResult key: the outcome of the visitor's last post (see below), or "".
  • commentsClosed key: whether the instance accepts no new comments.
  • commentThread key subject: the thread component, rendered (below).
  • commentForm key subject: the form component, rendered (below).

Components

Instead of writing the markup above, a template can include the module's components (see webthings.Module.Components), which come with plain, accessible default markup:

{{commentThread "remarks" .ID}}   thread: the comments with their replies, then the form
{{commentForm "remarks" .ID}}     form: the form with the result of the last post

thread gets a ThreadView, form a FormView. A theme replaces them with <key>/thread.gohtml and <key>/form.gohtml, or comments/thread.gohtml and comments/form.gohtml for every instance; App.WriteTemplates(dir, key) writes the defaults there to start from. The defaults use the classes wt-comments, wt-comments__title, wt-comments__list, wt-comments__empty, wt-comment, wt-comment--reply, wt-comment--staff, wt-comment__header, wt-comment__name, wt-comment__badge, wt-comment__date, wt-comment__body, wt-comment__replies and wt-comment-form, and the shared wt-form, wt-form__title, wt-form__field, wt-form__label, wt-form__input, wt-form__help, wt-notice, wt-meta and wt-button, styled by the optional {{asset "webthings.css"}}; their texts go through tl, so the site's translations translate them.

Form fields

The form posts subject (required), name (required, at most 100 characters), email (optional, never shown on the website), body (required, plain text, at most 5000 characters) and optionally parent, the ID of the comment to reply to: PostInput, whose fields form_field renders with these limits. form_open adds the spam trap and the page to return to: the element with the instance key as its ID, and the fields' IDs start with the key, such as remarks-name. Each visitor can post 5 times per 10 minutes.

After a post the visitor returns to the page with ?<key>=<result>, which commentResult reads: "sent" (shown right away), "pending" (waiting for approval), "invalid" (a field is missing or too long), "closed" (the instance accepted no comments at that moment) or "throttled" (too many posts).

Importing

Of(app, key).Add stores comments from another system: set Approved, and CreatedAt to keep their date; a reply has the ID of the Comment Add returned for its parent as ParentID.

Comment.CreatedAt is in UTC; {{date .CreatedAt}} and {{time .CreatedAt}} show it in the page's language and the site's timezone.

Guestbook page

Placed on a page, an instance renders comments/list.gohtml (or <key>/list.gohtml) with a ListView: every approved message of the instance, newest first, 20 per page, and the details for a form posting with subject ListView.Subject (the instance key).

Settings

The admin's settings screen per instance switches moderation (on by default) and closes the instance for new comments. With a notification address set, every new message sends an e-mail with a link to the admin: the mail template notify (see webthings.Module.Mail) with NotifyView as data, in the admin's language. A theme replaces it with mail/<key>/notify.gohtml or mail/comments/notify.gohtml.

Visitors are told apart for a day by an HMAC of their address with a key of the instance (webthings.Env.Secret), so the address is never stored.

Index

Constants

View Source
const (
	// ResultSent is a comment that shows at once.
	ResultSent = "sent"
	// ResultPending is a comment that waits for moderation.
	ResultPending = "pending"
	// ResultInvalid is a post with a missing or too long field; nothing
	// was saved.
	ResultInvalid = "invalid"
	// ResultClosed is a post to a post whose comments are closed.
	ResultClosed = "closed"
	// ResultThrottled is the answer to a visitor who posted too often.
	ResultThrottled = frontend.ResultThrottled
)

Results of a visitor's post, added to the page as ?<key>=<result>.

Variables

View Source
var ErrInvalid = errors.New("comments_invalid")

ErrInvalid is returned for a message with a missing or too long field, or a parent that does not exist.

View Source
var ErrParentDeleted = errors.New("comments_parent_deleted")

ErrParentDeleted is returned when restoring a reply whose comment is in the trash: restore the comment instead.

Functions

func Module

func Module(key, title string) webthings.Module

Module adds a Comments instance. key is used in URLs of the admin and in the template functions and must be unique on the site; title is shown in the admin and the page editor.

Types

type Comment

type Comment struct {
	ID      string
	Subject string
	Name    string
	// Body is the plain text as written.
	Body string
	// HTML is Body escaped, with paragraphs and line breaks.
	HTML template.HTML
	// Staff is set for replies written in the admin.
	Staff  bool
	Locale string
	// CreatedAt is in UTC; {{date .CreatedAt}} shows it in the site's timezone.
	CreatedAt time.Time
	// Replies are the approved replies, oldest first. Replies have none.
	Replies []Comment
}

Comment is an approved message as templates see it. The e-mail address is never part of it.

type Entry

type Entry struct {
	Subject string
	// ParentID makes the entry a reply. A reply to a reply is attached to the
	// top-level comment, so threads stay one level deep.
	ParentID string
	Name     string
	Email    string
	Body     string
	Locale   string
	// Path is the page the message was written on, for the admin.
	Path     string
	Approved bool
	Staff    bool
	// IPHash identifies the sender for a day; see Service.visitor.
	IPHash string
	// CreatedAt is when the message was written, for imports; zero is now.
	CreatedAt time.Time
}

Entry is a new message, from a visitor or an import.

type FormView

type FormView struct {
	Key     string
	Subject string
	// Action is the address the form posts to.
	Action string
	// Form is the form's model (PostInput) for go-form's template
	// functions, such as {{form_open .Form}} and {{form_field .Form "name"
	// nil}}; it returns the visitor to the element with the id Key.
	Form form.RenderModel
	// Result is "sent", "pending", "invalid", "closed", "throttled" or "".
	Result string
	// Closed is set when the instance accepts no new comments.
	Closed bool
	// Moderated is set when new comments wait for approval.
	Moderated bool
}

FormView is the data of the form component: the comment form of a subject with the result of the visitor's last post.

type InboxGroup

type InboxGroup struct {
	Subject string
	// Title names the subject for people: the instance title for the
	// instance's own subject (a guestbook); otherwise the title of the
	// published content on the page the messages were written on, in the
	// admin's language when a message was written in it, or that page's
	// address when no published content is there; the subject key when no
	// message knows its page.
	Title string
	// Address is the page whose content Title names, shown with it.
	Address string
	Rows    []InboxRow
}

InboxGroup is the messages on one subject.

type InboxRow

type InboxRow struct {
	Date   string
	Name   string
	Email  string
	Locale string
	Body   string
	// PageURL is the page the message was written on.
	PageURL  string
	ReplyTo  string
	Staff    bool
	Approved bool
	// Sender is a short code that is equal for messages from one address on
	// one day.
	Sender   string
	Approve  *form.RenderModel
	Hide     *form.RenderModel
	Delete   *form.RenderModel
	ReplyURL string
}

InboxRow is one message in the admin list.

type InboxView

type InboxView struct {
	Title   string
	Pending int
	// Filters switch between pending, approved and all messages.
	Filters     admin.Tabs
	Groups      []InboxGroup
	SettingsURL string
	TrashURL    string
	Pager       admin.Pager
}

InboxView is the data of templates/admin/list.gohtml.

type ListView

type ListView struct {
	// Title is the title of the page the module is placed on.
	Title string
	// Key is the instance key, for the template functions.
	Key string
	// Comments are the approved messages of every subject, newest first, with
	// their replies.
	Comments []Comment
	// Action is the address the form posts to.
	Action string
	// Subject is the value of the form's subject field.
	Subject string
	// Form is the form's model (PostInput) for go-form's template
	// functions, such as {{form_open .Form}} and {{form_field .Form "name"
	// nil}}; it returns the visitor to the element with the id Key.
	Form form.RenderModel
	// Result is the outcome of the visitor's last post: "sent", "pending",
	// "invalid", "closed", "throttled" or "".
	Result string
	// Closed is set when the instance accepts no new messages.
	Closed bool
	// Moderated is set when new messages wait for approval.
	Moderated bool
	// NewerURL and OlderURL page through the messages; empty at either end.
	NewerURL string
	OlderURL string
}

ListView is the data of comments/list.gohtml, the instance placed on a page.

type NotifyView

type NotifyView struct {
	Comment
	// Email is the visitor's address; empty when they gave none.
	Email string
	// Title is the instance's title, for example "Comments".
	Title string
	// Pending is set when the message waits for approval.
	Pending bool
	// PageURL is the page the message was written on; empty when unknown.
	PageURL string
	// ReviewURL opens the messages in the admin.
	ReviewURL string
}

NotifyView is the data of the mail template notify, sent to the notification address in the admin's language. Replying answers the visitor when they gave an address.

type PostInput

type PostInput struct {
	Subject string `form:"input,hidden" name:"subject" required:"true" maxLength:"200"  line:"true"`
	Parent  string `form:"input,hidden" name:"parent"                  maxLength:"64"   line:"true"`
	Name    string `form:"input,text"   name:"name"    required:"true" maxLength:"100"  line:"true" label:"Name"           autocomplete:"name"`
	Email   string `` /* 191-byte string literal not displayed */
	Body    string `form:"textarea"     name:"body"    required:"true" maxLength:"5000"             label:"Message"`
}

PostInput is the form a visitor posts to /_/<key>/post: the subject the message belongs to, optionally the message it answers, a name, an optional e-mail address and the message. Themes render its fields with form_field, with their limits ({{form_field .Form "name" nil}}, see ListView, FormView and commentInput), or write inputs with these names.

type ReplyView

type ReplyView struct {
	Title   string
	BackURL string
	Name    string
	Date    string
	Body    string
	Form    form.RenderModel
	Errors  form.FieldErrors
}

ReplyView is the data of templates/admin/reply.gohtml.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is one Comments instance, for the website's own code.

func Of

func Of(app *webthings.App, key string) *Service

Of returns the Comments instance with key from a started app, or nil.

func (*Service) Add

func (s *Service) Add(ctx context.Context, entry Entry) (Comment, error)

Add stores a message after checking its fields, for visitors and for imports: an import sets Approved, and CreatedAt for the original date, and passes the ID of the returned Comment as ParentID of its replies. It returns ErrInvalid for missing or too long fields and for a parent that is not a comment of this instance on the same subject.

func (*Service) Approve

func (s *Service) Approve(ctx context.Context, id string, approved bool) error

Approve shows (true) or hides (false) the message with id on the website.

func (*Service) Count

func (s *Service) Count(ctx context.Context, subject string) (int, error)

Count returns the number of approved comments on subject, including replies to approved comments: the ones List shows.

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, id string) error

Delete moves the message with id to the trash, with its replies: they leave the website and the admin's lists, and Undelete brings them back. The replies get the same deleted_at, which is how Undelete knows which of them went along.

func (*Service) Latest

func (s *Service) Latest(ctx context.Context, offset, limit int) ([]Comment, bool, error)

Latest returns approved top-level messages of every subject, newest first, with their approved replies, skipping offset; and whether older ones exist. It is the guestbook page. A limit of 0 or less returns all of them.

func (*Service) List

func (s *Service) List(ctx context.Context, subject string) ([]Comment, error)

List returns the approved comments on subject, oldest first, with their approved replies.

func (*Service) Pending

func (s *Service) Pending(ctx context.Context) (int, error)

Pending returns the number of messages waiting for approval.

func (*Service) Purge

func (s *Service) Purge(ctx context.Context, id string) error

Purge removes the message with id from the trash for good, with all its replies. db.ErrNotFound means the message is not in the trash.

func (*Service) Reply

func (s *Service) Reply(ctx context.Context, id, name, body string) (Comment, error)

Reply adds an approved staff reply to the comment with id, signed with name. Replying approves the comment too: the answer would be hidden otherwise.

func (*Service) SaveSettings

func (s *Service) SaveSettings(ctx context.Context, settings Settings) error

SaveSettings replaces the instance's options. Notify must be empty or one e-mail address.

func (*Service) Settings

func (s *Service) Settings(ctx context.Context) (Settings, error)

Settings returns the instance's options; the defaults when none are saved.

func (*Service) Undelete

func (s *Service) Undelete(ctx context.Context, id string) error

Undelete brings the message with id back from the trash, with the replies that were deleted along with it. A reply whose comment is still in the trash cannot come back on its own (ErrParentDeleted); db.ErrNotFound means the message is not in the trash.

type Settings

type Settings struct {
	// Moderated hides new messages until they are approved.
	Moderated bool `db:"moderated"`
	// Closed refuses new messages.
	Closed bool `db:"closed"`
	// Notify receives an e-mail for every new message when set.
	Notify string `db:"notify"`
}

Settings are the options of one instance.

type ThreadView

type ThreadView struct {
	// Key is the instance key, for {{commentForm .Key .Subject}}.
	Key     string
	Subject string
	// Comments are oldest first, each with its approved Replies.
	Comments []Comment
	// Count is the number of approved comments and replies.
	Count int
}

ThreadView is the data of the thread component: the approved comments on a subject, followed by the form (the form component).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL