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

webthings

package module
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: 60 Imported by: 0

README

Go WebThings

Go Reference codecov

WebThings is the backend of a website: database, admin, accounts and permissions, pages, media, languages, themes and content modules (news, events, galleries, comments, newsletters, sign-ups and more). The website itself only brings its templates and assets.

Start a site from go-webthings-clone; see go-webthings-demo for a complete, designed example.

app := webthings.New(webthings.Config{
    Name:      "My site",
    Locales:   locales.Config{Default: "nl", Enabled: []string{"nl", "en"}},
    Templates: os.DirFS("templates"),
    Mail:      mail.SMTP{Host: "smtp.example.com", From: "Site <site@example.com>"},
})

app.Use(
    publishing.Module("news", "News"),
    publishing.Module("blog", "Blog"),
    events.Module("agenda", "Agenda"),
    comments.Module("remarks", "Comments"),
    redirects.Module(),
    stats.Module(),
)

if err := app.ListenAndServe(ctx, ":8080"); err != nil {
    log.Fatal(err)
}

The admin is at /admin/, or wherever Config.AdminPath puts it, such as /toolbox. Editors build the page tree there and place module instances on pages; WebThings serves everything below such a page. Code links into the admin by route name, never by its path (Env.AdminURL, App.AdminURL). robots.txt allows everything and names the sitemap, not the admin: admin pages send X-Robots-Tag: noindex, nofollow instead.

Once the app has started, every response is sent with X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin: pages, visitor actions, media, assets, sitemap.xml, robots.txt and the admin, and also redirects to clean paths and /healthz, /readyz and /metrics. Which sites may show the pages and actions in a frame is a site setting (Site settings → Security, site.Settings.Framing), which only administrators change; other editors see it read-only. same-origin, the default, sends X-Frame-Options: SAMEORIGIN and Content-Security-Policy: frame-ancestors 'self'; none sends DENY and frame-ancestors 'none'; any sends neither. The admin always sends X-Frame-Options: DENY and frame-ancestors 'none'. WebThings can also send a strict Content-Security-Policy itself, without 'unsafe-inline' or 'unsafe-eval', for the site and the admin: off (the default), report-only or enforce, on the same card or with Config.ContentSecurityPolicy, which the card then shows, disabled. It allows the import maps and the style of go-partial's error page by their hashes, never by nonces, so the output cache keeps each page with its own policy, and browsers report what it blocks to /_/csp-report, which the site logs. The admin has no inline styles or scripts, and neither do the starting points and the modules' default templates, visitor forms and poll results included. A policy that a handler around the app set keeps its other directives in every mode: core merges frame-ancestors into it, and with enforce the site's directives replace only those the handler also names, so they cannot weaken the site's policy. See A Content-Security-Policy of the site's own.

Templates

The layout gets the page context, every other template only its own data:

Template Data
layout.gohtml frontend.View: site, menu, languages, title
page.gohtml frontend.Page (IsHome, Path such as /about/team, Slug, ParentID)
404.gohtml frontend.NotFound
500.gohtml go-partial's errors.Data, shown on its own when a template or a module fails, with the frontend.View for @model; {{.Title}}, the heading, and {{.Message}} are already in the page's language, and {{styles}} writes the theme's versioned links with their integrity, also when a visitor action fails (optional: without it, go-partial's plain error page, with its heading, message and lang in the page's language, and nothing of what failed)
search.gohtml frontend.SearchView, at /<locale>/search?q= (optional)
<key>/<name>.gohtml the module's view, for example news/post.gohtml gets a publishing.Post

Module templates are found by instance key first (news/post.gohtml), then by module type (publishing/post.gohtml), so instances can share a design or each have their own. App.WriteTemplates(dir, key) writes starting points with the right @dot and every field; the clone runs it as task templates -- <key>. Templates in partials/ can be used everywhere.

A layout that places {{slot "head"}} gets core's tags in <head>: the title, the description, robots for a preview, the canonical and hreflang links with x-default, the og: tags and twitter:card, the icons, {{feedLinks}}, {{apiLink}} and {{structuredData}}. A theme's regions/head.gohtml adds tags after {{template "webthings/head-tags.gohtml" .}}, or replaces core's tags when it does not call it. {{styles "webthings.css" "site.css"}} links go-partial's partial-errors.css, the files given, from the theme or else from core, and the theme's values stylesheet last, each with its version and integrity:

<head>
    <meta charset="utf-8">
    {{slot "head"}}
    {{styles "webthings.css" "site.css"}}
</head>

Start checks every template of every theme with the site's functions and logs each problem as template problem, with the file, the line and a hint: the module a function comes from ("add it to app.Use or remove the call") or the closest name. With Config.Development any problem stops the start; otherwise only one that fails every page of the active theme. A call that finds nothing, such as {{posts "nieuws" 3}}, an asset file the theme does not have or an unknown {{theme}} setting, is logged once with what there is, and fails its page in development. See Templates.

Any template can show any module's content:

{{range posts "news" 3}} … {{end}}
{{range events "agenda" 5}} … {{end}}
{{with album "work" "carousel"}}{{range .Images}} … {{end}}{{end}}
{{range entries "faq"}} … {{end}}
{{range testimonials "reviews" 3}} … {{end}}
<a href="{{moduleURL "news"}}">All news</a>

The site's social media profiles (Site settings → Social media profiles) are {{range .Site.Profiles}} in the layout and {{range profiles}} anywhere; share makes share links for a page (empty for the current one, or a path or URL), labelled through the site's translations:

{{range profiles}}<a href="{{.URL}}" rel="me" class="icon-{{.Network}}">{{.Label}}</a>{{end}}
{{range share "" .Title}}<a href="{{.URL}}" class="share-{{.Network}}">{{.Label}}</a>{{end}}
{{shareBar "" .Title}}

Template functions whose first parameter is a context.Context get the current request; that is how modules know the language. Sites can add their own the same way through Config.Funcs. Of go-partial's interaction helpers, templates get the ones that write attributes (submitAttrs, liveAttrs, clientAttrs, noBoost, …); its generic names, such as poll, are left to modules. Another template engine can be used by implementing frontend.Renderer.

Components

The interactive modules come with components: parts of a page with plain, accessible default markup that any template includes with one function.

{{commentThread "remarks" .ID}}       comments with their replies, then the form
{{commentForm "remarks" .ID}}         only the form
{{pollBox "survey" "colour"}}         a poll by slug or ID ("" for the newest): vote form and results
{{newsletterForm "letters"}}          a signup box
{{registrationForm "signup" .ID}}     places, prices and the sign-up form of an event
{{shareBar "" .Title}}                share links (core)

A theme replaces a component like a module screen, with <key>/<name>.gohtml or <type>/<name>.gohtml (remarks/thread.gohtml, polls/box.gohtml, newsletter/form.gohtml, registrations/form.gohtml; core's are in site/, such as site/share.gohtml), and gets the module's default otherwise. Each name is looked for in the active theme before default/, so a theme's polls/box.gohtml wins over a survey/box.gohtml in default/. App.WriteTemplates(dir, key) writes the defaults to copy and change. Modules declare components in Module.Components and render them with frontend.Component.

The defaults use stable BEM-style classes (wt-comments, wt-comment__body, wt-poll__result, wt-newsletter, wt-registration__facts, wt-share__link, shared wt-form, wt-form__fieldset, wt-form__options, wt-notice, wt-button) and tl for their texts, so the site's translations apply. One optional stylesheet lays them out (spacing, forms, the <meter> bars of poll results) with custom properties a theme sets:

{{styles "webthings.css" "site.css"}}
/* assets/site.css */
:root { --wt-accent: #b5562f; --wt-radius: 0; --wt-gap: 1.25rem; }

Core serves webthings.css at /_/webthings.css; a theme with its own assets/webthings.css serves that instead. Its rules have no specificity (:where), so any rule of the theme wins. styles writes the stylesheet links with the files' Subresource Integrity; see Themes.

Images

Module views give each image as pages show it, never the uploaded original: <X>URL is a WebP copy at most 1600 pixels wide (media.ImageWidth) and never wider than the original, <X>Srcset lists smaller copies, and <X>Width and <X>Height are the size of <X>URL. <X>OriginalURL is the uploaded file, for downloads. A post's featured image is ImageURL, ImageSrcset and so on, a person's photo PhotoURL. Only JPEG and PNG files are resized; other images, such as a GIF, keep their own address and an empty <X>Srcset.

{{if .ImageURL}}<img src="{{.ImageURL}}" srcset="{{.ImageSrcset}}" sizes="100vw" width="{{.ImageWidth}}" height="{{.ImageHeight}}" alt="">{{end}}

{{image .PhotoURL "800x600"}} is a resized copy of an image: it fits inside 800 × 600 and is never made larger. "800" and "x600" bound one side, "800x600 crop" cuts the exact size around the image's focal point, and a format can follow ("800 jpg", "400x400 crop png"); the default is WebP. The function takes the addresses module views give, the original's address or an asset ID, and returns the original for files that are not images.

<img src="{{image .ImageURL "800"}}" srcset="{{srcset .ImageURL 400 800 1600}}" sizes="100vw" alt="">

The logo of the site settings, .Site.LogoURL, is at most 600 pixels wide and never enlarged, in the format it was uploaded in rather than WebP: a PNG stays a PNG and a JPEG a JPEG. A PNG no wider than that, and a GIF, keep their own address. See The site's images.

Copies are served at /media/v/<signature>/<id>_<size>.<format>, kept in the media storage (also S3) and deleted with the image. The copies pages show by default, <X>URL and the smaller ones of <X>Srcset, are made in the background right after the upload, so the first visitor does not wait for them; other sizes, and the copies of uploads that find the queue of 1024 images full, as in a large import, are made on their first request. The URLs are signed with a key in the site settings, so only sizes the templates ask for (at most 4000 pixels) can be made, and they may be cached forever. Resizing uses Catmull-Rom and follows the photo's EXIF orientation; WebP is lossy (quality 80), encoded by gen2brain/webp: libwebp translated to Go, so no cgo. A system libwebp is used when installed, loaded through purego, which makes Linux binaries dynamically linked: build with CGO_ENABLED=0 go build -tags nodynamic for a static binary, as Alpine and scratch images need (a library cannot set build tags for the sites that use it).

/<locale>/search?q= searches the published pages and module content of that language and renders search.gohtml (written by App.WriteTemplates(dir, "site")). {{range search "query" 5}} does the same in any template and {{searchURL}} is the screen's address for a search form. A published page with the path /search takes precedence over the screen; without search.gohtml the screen answers 404. The screen allows each visitor 30 searches a minute and answers more with 429 (Frontend.SetSearchThrottle), rendering search.gohtml with .Throttled set and no results, so the template shows a message; the search function is not limited.

Matching ignores case and accents in every script and requires every word; words in a title rank above words in the text. It runs in Go over the published content, so it behaves the same on every database, which suits sites up to a few thousand items. Modules join through Parts.Search, using frontend.ParseQuery to score and summarise like the core. The bundled publishing, events, gallery, lists, people, directory and documents modules do; a module only shows up in results when it is placed on a page.

Feeds

Publishing serves Atom and RSS feeds of its newest posts below its page, also per category, and Events of its upcoming and running events: /en/news/feed.atom and /en/news/feed.rss, /en/news/category/<slug>/feed.atom and /en/agenda/feed.atom. {{feedLinks}} in the <head> writes a <link rel="alternate"> for each feed of the page shown, Atom and RSS, with absolute addresses, so browsers and feed readers find them; pages without feeds get none. Core's head tags have it, which a layout places in <head> with {{slot "head"}}, as the starting points (App.WriteTemplates(dir, "site")), the demo and the clone do. A feed lists at most 20 items unless Site settings → General → Items in feeds says otherwise, or the instance's own size, publishing.WithFeedSize(n) or events.WithFeedSize(n), and its title carries the site's name, "News from My site", translated with the site's translations. {{feedURL "atom"}} gives the address of the page's feed for a link visitors see. Feeds are cached and refreshed like pages, with an ETag and a Last-Modified that never goes back, also when a post is taken off the site, so a reader that sends only If-Modified-Since gets 304 while a feed is unchanged. Each process keeps these dates itself. Feeds are not in sitemap.xml. Pages send no Last-Modified. Entry IDs are tag: URIs on the host of Config.BaseURL, so set it in production. Any module offers feeds with Parts.Feeds; package feed writes them as Atom 1.0 and RSS 2.0 from one model. See Feeds.

Structured data

{{structuredData}} in the <head> writes the page's structured data for search engines: one <script type="application/ld+json"> with schema.org items for the site, its organisation, the page and its trail, and the content a module shows, such as a post's Article, an event's Event with the Offer of its registration, a Person, a directory entry's LocalBusiness or the ItemList of a list page. Every address in it is absolute. Core's head tags have it, so the starting points, the demo and the clone write it; see Search and SEO. A module describes its content with frontend.Content.StructuredData and adds to other modules' pages with Parts.StructuredData; see Structured data.

Content API

Config.API turns on a read-only JSON API at /api/v1/ for apps and other frontends: the site with its languages and menu, the site search, the page tree, a page by its address or ID, and the modules' content, such as /api/v1/news/posts, /api/v1/agenda/events and the albums, lists, people, testimonials, directory entries and documents. It shows only published content, never drafts or visitors' data, in one language per request (?locale=), with absolute addresses, Markdown as HTML, such as in body_html, and lists paged with limit and offset. Answers go through the output cache with an ETag, and a single page, post or event also with a Last-Modified; each client may make 300 requests a minute unless api.Config.RateLimit says otherwise, and other sites' pages may read it only from AllowedOrigins (CORS). {{apiLink}} in the <head> links each page to its answer in the API. A module adds endpoints with Parts.API. See Content API.

Editing from the site

Editors who are signed in to the admin see a link to the admin screen of the page or module item they look at, when they may edit it:

{{with .Editor}}<a href="{{.EditURL}}">Edit</a>{{end}}
{{with adminURL "admin.dashboard"}}<a href="{{.}}">Admin</a>{{end}}
{{with adminURL "admin.news.edit" "id" .ID}}<a href="{{.}}">Edit</a>{{end}}

adminURL is the address of a named admin route for signed-in editors and "" for visitors, so the admin's address stays off public pages. Module screens set the target with frontend.Content.EditURL; middleware can leave editors out with frontend.Editor(ctx). The session cookie (HttpOnly, SameSite=Lax, Secure unless Config.Insecure) reaches the public site for this; it is checked only when present, and pages for editors are sent with Cache-Control: private, no-store. Pages for visitors get the site's caching from Site settings → Performance: no-cache with an ETag by default.

Themes

Each folder in templates/ is a theme; editors pick one in the admin under Theme, after a preview. A theme only holds what differs from default/: templates and assets it lacks come from there, so a colour variant can be a single stylesheet. A theme's own module templates come first, by key or by type: its publishing/post.gohtml wins over news/post.gohtml in default/. A site with layout.gohtml directly in templates/ has one theme.

templates/default/layout.gohtml
templates/default/assets/site.css     {{styles "site.css"}} or {{assetStyle "site.css"}}
templates/default/assets/site.js      {{assetScript "site.js" "type" "module"}}
templates/default/theme.json          title, description, settings
templates/night/assets/variant.css
templates/night/theme.json

{{asset "site.css"}} is the address of a file in the theme's assets/, /theme/default/site.css?v=…, versioned by its content so browsers may keep it. {{assetStyle "site.css"}} and {{assetScript "site.js" "type" "module"}} write the stylesheet link and the script tag at that address with the file's Subresource Integrity, so browsers refuse a file whose content is not what the page expects; more attributes follow as name and value pairs. While templates reload (Config.ReloadTemplates) the theme's files may change under a page, so their tags carry no integrity then; core's webthings.css keeps it. A file that neither the theme nor core has is logged once with the closest names there are.

Config.Assets or Config.ImportMap serve more files at /assets/, with versioned addresses, and {{importmap}} writes the import map for the layout's head. Core adds go-partial's browser clients to it, such as partial-interactions, so the site needs no copy of them, and the map carries their integrity as it does that of the site's own files, also in an import map the site builds itself. A client file among the import map's own files, such as a partial-flash.js of the site, is used instead, with its own integrity; see Loading htmx.

theme.json declares settings editors change in the admin (colour, text, a choice or a number), read as {{.Theme.Values.accent}} in the layout or {{theme "accent"}} anywhere:

{
  "title": "Atelier",
  "settings": [
    {"name": "accent", "label": {"en": "Accent colour", "nl": "Accentkleur"}, "type": "color", "default": "#b5562f", "css": "--accent"},
    {"name": "headings", "label": "Headings", "type": "select", "options": ["serif", "sans"], "default": "serif"}
  ]
}

A color, select or number setting with "css" becomes a custom property of the theme's values stylesheet, /theme/<theme>/values.css, here :root { --accent: #b5562f; }. {{styles}} links it after the theme's own stylesheets, as {{themeStyle}} does on its own, so a colour reaches the page without a style attribute.

Translations

frontend.Translations(dir) loads the site's catalogues, <dir>/<locale>.po, into a go-translator *translator.Translator, which the site sets as Config.Translations:

translations, err := frontend.Translations("translations")
if err != nil {
    log.Fatal(err)
}

app := webthings.New(webthings.Config{
    Templates:    os.DirFS("templates"),
    Translations: translations,
})

Templates then translate their texts with go-translator's functions: tl, such as {{tl localizer "Read more"}}, tn for plurals and ctl/ctn with a context. Core translates with it its own texts, such as the 404 page's title, the heading and message of 500.gohtml or of the plain error page that answers without it, such as "Internal Server Error", the labels and messages of visitor forms, and the names of months and days and the date patterns (see Dates, times and money). A text without a translation shows as written. A language without a catalogue of its own uses one of the same language, never another: nl_BE uses nl.po, and nl uses nl_NL.po. go-form's own messages, such as its validation errors, that the site's catalogue does not translate show in go-form's catalogue of the page's language; nothing is merged into the site's catalogues.

App.Translations(dir, args) is the site's translations command: it writes every text the site translates to <dir>/translations.pot, and with check lists the texts each .po file misses. App.TranslationCatalog() gives those texts as a catalog of go-translator's extract package, which translations.Run takes. Besides the templates' own texts, the POT file holds those of the default templates that not every theme replaces, referenced as module:<type>/<name>.gohtml, module:<type>/mail/<name>.gohtml and core:site/<name>.gohtml, the texts core translates in Go (core:texts), the names and patterns of the calendar (core:calendar) and, when a template renders forms with go-form's functions, the texts of the modules' visitor forms (module:<type>/forms); see Languages.

The admin brings its own catalogues. <StateDir>/translations/<locale>.po, when present, overrides their entries, and an empty entry keeps the built-in text; the admin writes nothing there.

Dates, times and money

Templates show dates and amounts in the page's language:

{{date .Start "full"}}             Sunday 25 October 2026 · zondag 25 oktober 2026
{{date .Date}}                     25 October 2026 (styles: short, medium, long, full)
{{date .Start "EEE d MMM"}}        Sun 25 Oct (any other style is a CLDR pattern)
{{time .Start}}                    14:00 (2:00 PM in en-US)
{{dateRange .Start .End "long"}}   25–27 October 2026
{{timeRange .Start .End}}          14:00–17:00
{{money .PriceCents "EUR"}}        €45.00 · € 45,00 · 45,00 €

Times stored in UTC (a creation time) are shown in the site's timezone; times already in a zone keep it, such as an event in its own timezone, and calendar dates (a post's or a document's date) show the same day in every timezone. English (day first; en-US month first and 12-hour) and Dutch are built in. The site's translations (Config.Translations) give the names and patterns of other languages, and an entry there replaces a built-in one in every language, English and Dutch included; each msgid has a context (msgctxt): January … December in month, Jan … Dec in month-short, Sunday … Saturday in day, Sun … Sat in day-short, AM and PM in time, and short, medium, long, full and time in date, translated as a pattern such as d. MMMM y (frontend.CalendarTexts lists them; the translations command adds them to the POT file, and its check asks for them only in languages without built-in ones, so English and Dutch catalogues need not contain them). Money uses the currency's decimals and the language's separators. In Go, frontend.FormatDate(locale, t, style), frontend.FormatMoney(locale, cents, currency) and Frontend.Format(ctx, locale) (site timezone and translations) do the same. frontend.When holds a start, an end and whether it lasts all day, for one partial that shows when something takes place: events.Event and registrations.Subject give theirs with .When.

Mail and storage

Modules send e-mail through Config.Mail: mail.SMTP is built in, other providers implement mail.Sender; without one, messages are written to the log. mail.Message.Headers adds header fields such as List-Unsubscribe (checked against header injection). A sender that implements mail.Describer says how it sends, without secrets, in the E-mail step of the admin's setup guide, which also sends a test e-mail. Uploads are kept in StateDir/media unless Config.Storage names another media.Storage, such as an S3 bucket. When the storage gives a file no address of its own (Storage.URL returns "", as the default one does), the site serves it at /media, with the route media.asset. Resized copies are always served there, with the route media.variant, whatever the storage, because a copy that is not made yet is made on its first request.

Mail templates

Modules write their e-mail from templates the site can change and translate. A module declares them like its page templates, with the text it sends by default as Example:

webthings.Module{
    Mail: []frontend.Template{{Name: "confirmation", Data: Confirmation{}, Example: confirmationMail}},
}

message, err := env.Frontend.Mail(ctx, locale, env.Key, "confirmation", data)
message.To = []string{address}
err = env.Mail.Send(ctx, message)

A theme replaces the default with mail/<key>/<name>.gohtml or mail/<type>/<name>.gohtml, looked for in the active theme before default/, as components are; App.WriteTemplates(dir, key) writes the defaults there as a starting point. A mail template defines subject and text (not HTML-escaped) and optionally html; every template function works in the mail's language, including tl, date and money, and mail/partials/*.gohtml can be shared:

{{define "subject"}}{{tl localizer "Registration confirmed"}}: {{.Title}}{{end}}
{{define "text"}}
{{tl localizer "Hello"}} {{.Name}},
{{date .Start "full"}} · {{time .Start}}
{{end}}

Mail for staff uses env.AdminLanguage ("en" unless Config.AdminLanguage says otherwise). Frontend.SendMail(ctx, env.Mail, frontend.Letter{Locale, Key, Name, Data, To, ReplyTo}) writes and sends a mail in one step, without the request's cancellation, and logs a failure. Frontend.Execute(ctx, name, locale, data) renders any template of the active theme on its own, without the layout.

Modules working together

env.Service(key) returns another instance's API once the app runs (resolve it in handlers, not in Setup), for example env.Service("agenda").(*events.Service).Events(ctx, locale) or env.Service("news").(*publishing.Service).Draft(ctx, publishing.DraftInput{…}). env.Secret(ctx, name) is a random 32-byte key of the instance, the same in every process of the site, for example to HMAC visitors' addresses.

Visitor forms

Visitors post to a module's action router, env.Actions(), mounted at /_/<key>/: its routes become part of the site's routes, such as POST /_/remarks/post, named with the key in front (remarks.post, for Frontend.Route), and a wrong method gets 405. The module builds the addresses of its actions from that router by route name, such as env.Actions().RoutePath("post"), also without a request, for example for links in e-mail; Env{}.Actions() gives the same addresses in tests and tools. frontend.Back(w, r, key, result) returns the visitor to the form's page with ?<key>=<result>, and frontend.BackWith adds more query values, such as a code. A post without that page, such as one over the body limit, goes back to the Referer when it is on this site, and to / otherwise. frontend.Limits(key, frontend.ActionLimits{Body, Posts, Window}) is middleware for the action router (frontend.Actions wraps any handler) that limits the request body and the posts per visitor, and returns a visitor over the limit with ?<key>=throttled (frontend.ResultThrottled). Body can only lower the site's limit: 1 MiB, or Parts.ActionBodyLimit. frontend.Result(r, key, allowed...) reads a result for the page, and frontend.BackPath(r) and Frontend.BackLocale(r) give the form's page and its language.

An action reads the post into a struct with go-form's tags with env.Frontend.BindForm(r, &input), which trims text, checks the tags and returns the field errors, also for a value it cannot read, with messages in the language of the page the form was on. A module lists these structs in Module.Forms, so their texts are found for translation, and gives its views a form of one, frontend.VisitorForm(ctx, model, action, key), which a theme renders with {{form_open .Form}}…{{form_close}}. form_open adds the honeypot and the page to return to: the element whose id is the key, so the visitor sees the result next to the form. The fields' ids start with the key, such as remarks-name, so forms of several instances can share a page. {{form_field .Form "name" nil}} renders a field by the key in its name tag, with its label and limits, translated, in the site's go-form theme (Config.FormTheme, by default the classes of webthings.css). See Template functions and Visitor forms.

An action that calls env.Frontend.KeepForm(w, r, key, input, errs) before frontend.Back returns the visitor to a form that shows their values again, with the messages at their fields, with and without HTMX and with no change to the template. The post waits for the form's page in the database (the table wt_kept_form) for at most 5 minutes; the visitor gets a cookie for that page, wt_form_<key> (frontend.KeptFormCookie), that holds only a random token. The first view of the page takes the post, so it shows once. A kept post never holds passwords, files or the spam trap. The comments, newsletter, polls, registrations and submissions modules do this, and still return ?<key>=invalid. See A post with mistakes.

A visitor's post that fails for another reason, such as an unreachable database, is answered by Frontend.Fail: the theme's 500.gohtml with status 500, also for an HTMX post, which go-partial's interactions client shows as the whole page with reloadOnError. The page renders as every page does, with the theme's asset versions and integrity, and the log names the action's module; see 500.gohtml.

Visitors are known by frontend.ClientIP(r). Behind a reverse proxy, list it in Config.TrustedProxies (addresses or CIDR ranges): only then are X-Forwarded-For, X-Real-IP and Forwarded believed, read from the right so a visitor cannot pose as another address. env.VisitorID(ctx, r, parts...) tells visitors apart without storing their address: an HMAC of the parts and the address with the instance's visitors secret.

Scheduled publishing

Pages, posts and events can be published and unpublished later: the publication box in the admin plans the selected revision for a date and time in the site's timezone (stored in UTC), shows the plan and can cancel it. The app checks every Config.ScheduleInterval (30 seconds) and publishes with the same effects as the publish button; App.RunSchedules runs it at once. Publishing or unpublishing by hand cancels the plan. Several servers on one database can all run it: every change is claimed with a conditional update, so one of them makes it. A change that fails, for example because a translation is incomplete, is dropped and logged. Modules with revisioned content join through Parts.Scheduled.

The admin knows which published content links to which page: the text of the published pages and what modules report with Parts.Links, such as the text of a post or the address of a banner's button. The editor of a published page lists that content and asks before the page is unpublished or moved to the trash, and Pages → Broken links lists the links that lead to an address without a page. With the redirects module, the editor also offers to redirect the addresses a page leaves, when it is unpublished and when a new slug or parent changes them, and the report offers a redirect per address. The editors of posts, events, albums, people, directory entries and the categories of publishing and directory do the same for the item's own address, and a module adds this to its editor with editor.Linked. App.Links gives the same links from Go. See Links to a page.

Caching

Site settings, theme values and the published routes are kept in memory, so a page view needs no database queries for them. Changes in the same process apply at once; changes made by another process on the same database show up within Config.CacheTTL (30 seconds; negative disables the cache).

The output cache keeps the pages visitors get in memory, compressed, and sends them again without rendering them: for a minute in at most 32 MB by default, set (or turned off) in Site settings → Performance. Config.OutputCacheTTL overrides the time, and a negative one turns the cache off; without it, Development and ReloadTemplates turn the cache off. The card then shows the time that applies, disabled. It keeps answers to GET requests with status 200 for the site's pages, sitemap.xml and robots.txt, by address and by the request headers the answer varies on, so the whole page and HTMX answers of one address are kept side by side (up to four lists of such headers per address, go-router's MaxVarySets). Never kept are pages for signed-in editors (the admin's session cookie), for visitors with a kept post (a wt_form_<key> cookie), previews, searches with a query, and pages sent as private or no-store or that set a cookie. Changes in the same process show at once: publishing, settings, theme values, the media library, posts to the admin sections of modules and to the trash, visitors' posts and scheduled publication. A visitor's post that stores nothing, such as spam, one with mistakes, one over the throttle or one a module marks with frontend.NothingChanged(r), keeps the pages. Changes made from Go show at once too: the bundled modules' stores and services tell the app once a write that visitors see is committed, such as an import with Store() or a comment added with comments.Service.Add. Writing newsletter subscribers, from Go or by a visitor's signup or unsubscribe, keeps the pages, as no page shows them. The site's own code calls app.Changed() after other changes, such as data its pages show from elsewhere, and a module tells the cache about changes of its own with Env.Changed(). Changes by other processes, and what changes with time alone, such as an event that has passed, show within the cache's time. The modules' feeds are kept and refreshed as the pages are. sitemap.xml follows the same changes, so a page published in this process is in the next sitemap; changes by other processes reach it within 10 seconds plus the cache's time. Env.PersonalCookie(name) names a cookie that changes what pages show to the visitor who has it, such as the polls' wt_poll: the cache keeps a page apart for each value of it, and other cookies count for nothing, so a cookie of the site that changes its pages must be named this way. The metrics count its hits, misses, skips and evictions, and the pages and bytes it holds (webthings_output_cache_*), at /metrics once Config.MetricsToken is set, for requests with Authorization: Bearer <token>; in Development, with OutputCacheTTL set, every answer says in X-Cache whether the cache answered (hit, miss or skip). See Caching and compression and, for modules, The output cache.

Permissions

Administrators may do everything, including managing accounts and groups and downloading a backup of the site under Backup, which no group right opens. Editors get the rights of their groups: a matrix of sections (Pages, Media, Site settings and every module instance) by actions (read, create, edit, publish, delete, review, export), edited under Groups in the admin.

The first administrator comes from Config.AdminEmail and Config.AdminPassword, or from the one-time setup link a start without accounts logs; while no account exists, the sign-in page logs it again, at most once a minute. App.InviteAdministrator creates an administrator and returns a link, valid for an hour, where its owner chooses the password. Administrators then get a setup guide, Getting started: the site's languages and title, its theme, e-mail, first pages and security, with a card on the dashboard until they finish it. See Accounts and permissions and Set up the site.

Every account can turn on two-step sign-in under Your account: after the password, signing in asks for a code from an authenticator app (TOTP) or one of ten recovery codes. The app's secret is stored encrypted with a key of the site, and administrators can turn it off for an account that lost its phone. See Two-step sign-in.

Content and modules

  • content: the shared engine for module content. Revisions keeps drafts, publication, history and restore (Pages, Publishing, Events); Collection keeps ordered items with a published flag (the other modules).
  • admin/editor: the generic admin for content stores; a module supplies a form struct and two mapping functions.
  • modules/*: the bundled modules (below). They use only public packages, exactly like a module from another repository would (a webshop, for instance).

Content modules keep content of their own and are usually placed on a page; modules that attach to something add to other content; site features work for the whole site and need one instance, so their key is optional (redirects.Module(), or redirects.Module(redirects.Named("old", "Old addresses"))).

Module Kind What it does
publishing content News, blogs: posts with categories, revisions, scheduling and feeds
events content Agenda with dates, locations, revisions and a feed
gallery content Albums of images
lists content Ordered entries such as an FAQ, services or courses
testimonials content Quotes from customers
people content Team, staff or board
documents content Downloads by category and year, with a counter
directory content Organisations and places with opening hours, search and CSV import
banners content Hero slides and scheduled notices
forms content Forms built in the admin, with an inbox and notification mail
polls content Questions with options, one vote per visitor; pollBox component
comments attaches to any content Comments on posts, events or pages, and guestbooks; moderation and staff replies; commentThread component
registrations attaches to events (or any subject) Sign-ups: capacity, prices, confirmation mail, lookup and cancel by code; registrationForm component
submissions attaches to publishing Visitors propose news; editors accept it as a draft post
newsletter site feature Double opt-in signup, mailings, CSV export, provider sync; newsletterForm component
redirects site feature Old URLs to new ones, with hits and a list of missed paths; key optional
stats site feature Cookieless page views and "most read" lists; key optional
mailjet (contrib) integration Syncs a newsletter's subscribers with a Mailjet contact list
s3 (contrib) integration Keeps uploaded media in an S3 bucket

Social media profiles and share links are part of core (see Templates). Integrations with outside services are in go-webthings-contrib.

Rules

testsuite/architecture_test.go checks that:

  • core never imports a module (tests may);
  • modules use core's public API, never internal/ packages;
  • modules add no dependencies: only the standard library, core, other modules and the libraries core already uses. Anything else belongs in contrib;
  • nothing imports go-webthings-contrib;
  • sitetest and db/dbtest are used in tests only.

A module is a webthings.Module: a type, an instance key, its schema files, its templates, components, template functions and visitor forms, and a Setup that returns admin sections, a public handler, feeds and a service. The public handler is usually a Frontend.Router(), whose routes are relative to the page the instance is on (/{$} is the page itself). The site serves it at the page's address for each request, so handlers see the whole path, and routes.For(r) builds the addresses of its named routes there (RoutePathArgs); the page itself has no trailing slash, such as /en/guestbook. Outside a request, pages.Links(routes, base) builds them below the page's address (pages.Service.MountURL) with routes.At(base), or gives pages.NoLinks while the instance is on no page (base is ""), so its items are still listed, without links. A request without a route, any path or method, gets the site's 404 page. Every module's Of(app, key) gives site code the instance's API. Modules whose content editors write offer Store() for imports and seeds; comments and newsletter subscribers are imported with comments.Service.Add and newsletter.Service.Import.

Databases

SQLite (default), Postgres and MySQL. Each package keeps its schema in schema/<datetime>_<name>.<flavour>.sql, one file per flavour and version. Before the first release only the _init files exist: change them and recreate the development database. IDs are UUIDv7 (native uuid on Postgres).

Development

task fmt        # gofumpt, grouped imports, line length, blank lines after blocks
task lint       # static checks and whitespace rules
task test       # SQLite
task db:up      # Postgres and MySQL in Docker (task db:down removes them)
task test:all   # all three databases
task race       # the tests with the race detector
task css        # rebuild admin/assets/admin.css after changing admin classes
task vendor     # vendor the admin's JavaScript packages (admin/importmap.json)
task check      # fmt:check, lint, vet, css:check and race, without changing files

Core builds on its own: go.mod requires the released versions of go-partial, go-router, go-form, go-importmap and go-translator v2. To change one of them and core together, check it out next to go-webthings and list it in a go.work, which git ignores, or name a workspace file elsewhere in GOWORK. The demo, the clone and contrib require a released go-webthings; add them to the same workspace to try a change of core in them:

go 1.27.3

use (
	.
	../go-webthings-demo
	../go-partial
)

The tools are pinned and kept in bin/: task tools installs golangci-lint (fmt, fmt:check and lint run it first) and task tools:tailwind downloads the Tailwind CSS 4.3.2 standalone compiler (css and css:check run it first; TAILWIND_BIN names another Tailwind 4 compiler). Tailwind reads only the sources listed in admin/assets/source.css: the admin's templates, Go code and scripts, the admin packages of pages, media and site, and modules/, plus the utilities the admin package docs promise to modules outside core. A new place with admin classes needs an @source line there. task check runs the suite once, with the race detector. task editor rebuilds the admin's Markdown editor bundle, which needs Node.js, and records it in the lock file below.

The admin's JavaScript packages are listed in admin/importmap.json: htmx, and the Markdown editor's ProseMirror bundle as a local package that the admin builds itself (admin/assets/editor/prosemirror.js, see tools/editor). task vendor downloads htmx with go-importmap's importmap command into admin/assets/packages/, with its licence and importmap.lock.json, which records the integrity of each file, the editor bundle and its licences included; task vendor:check checks the files against it without network (importmap verify with -root admin/assets, as the bundle is outside the packages folder). The folder is not named vendor: a Go module leaves out the folders below a vendor folder, so sites that require go-webthings would miss htmx. They are embedded with the other admin assets, and the admin does not start when a file differs from the lock. Admin assets, go-partial's browser clients among them, are served with versioned addresses and strong ETags. The admin's import map, and the script and stylesheet tags its templates write with importmap_script and importmap_style, carry the files' integrity; source.css, the Tailwind source of admin.css, is not in the map.

sitetest starts a whole site in a test and drives the admin like a browser. Its sites have the output cache off, so a test sees a change on the next page; sitetest.Options{OutputCache: true} keeps the site's setting, and Configure can set Config.OutputCacheTTL. catalogtest.Complete, of sitetest/catalogtest, checks a section's form texts in the admin's catalogues of every language but en_US (catalogtest.Source), the language they are written in. db/dbtest runs a test on every configured database. The tests of the whole app are in testsuite/ (package testsuite, with their data in testsuite/testdata).

The user guides are in docs, also published at https://docs.gowebthings.com/go-webthings; the bundled modules are documented at https://modules.gowebthings.com.

Documentation

Overview

Package webthings runs a website's backend: database, accounts, pages, media, site settings, the admin and the selected modules. The website itself only supplies templates (or its own Renderer) and assets.

app := webthings.New(webthings.Config{Name: "My site", Templates: os.DirFS("templates")})
app.Use(publishing.Module("news", "News"))

if err := app.ListenAndServe(ctx, ":8080"); err != nil {
	log.Fatal(err)
}

ListenAndServe returns nil once ctx is cancelled and the app has shut down, so a clean stop, for example on SIGTERM, exits without an error.

Routing

Once started, every request passes one go-router, the app's root router (App.ServeHTTP):

  • Around it, with UseAll, the request-wide middleware: the visitor's address (Config.TrustedProxies), the request ID (RequestID), the security headers X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin, which every response gets, also go-router's own redirects to clean paths, telemetry, the access log and metrics for every request but those to the endpoints for operations, and recovery from panics, which they see as a 500.
  • The endpoints for operations: /healthz, /readyz and /metrics.
  • Behind compression, the core routes: the admin, whose router is mounted at Config.AdminPath, so its routes and their names are the root router's, the media library's routes at /media, named media.asset and media.variant, sitemap.xml, robots.txt, /assets, /theme and /_/webthings.css.
  • The modules' visitor actions, each module's action router (Env.Actions) mounted at /_/<key>/ behind the protection against cross-site posts, the framing headers of the site setting (site.Settings.Framing) and the body limit (Parts.ActionBodyLimit); its routes become part of the root router.
  • With Config.API, the content API at /api/v1/ (package api), with each module's endpoints (Parts.API) mounted at /api/v1/<key>/, behind its rate limit and CORS, the output cache, compression and an ETag.
  • The site, as the catch-all "/", behind the framing headers of the site setting. Pages live in the database and can move, so the site resolves the page itself and serves the module on it there: each module's Parts.Public, a go-router of its own from frontend.Router, whose routes are relative to the page, served at the page's address for that request (go-router's ServeAt), with the routes of its Atom and RSS feeds (Parts.Feeds). The site's requests pass the editors' recognition and the modules' middleware (Parts.Middleware) first, then the output cache, which keeps pages for visitors in memory, compressed (see Config.OutputCacheTTL), then compression.

Before Start a small router answers instead: the endpoints for operations, with request IDs and recovery, and 503 for everything else. Every registration of the root router but the site's is labelled with its route class, such as admin, media or asset: they are CorePaths, known from New on, and the metrics count requests by them.

Index

Constants

View Source
const DefaultActionBodyLimit = 1 << 20

DefaultActionBodyLimit is the largest request body modules' visitor actions (Env.Actions) accept unless Parts.ActionBodyLimit says otherwise: 1 MiB, ample for a form.

Variables

View Source
var ErrNotStarted = errors.New("the app has not started")

ErrNotStarted is returned by what needs a started app (see Start), such as InviteAdministrator.

View Source
var ErrTemplates = errors.New("templates have problems")

ErrTemplates is the error Start returns for templates that fail their renders: in development for every problem of every theme, otherwise for a template of the active theme's layout, regions or partials, which every page parses, that does not parse or calls a function the site does not have; see Config.Development.

Functions

func LogHandler

func LogHandler(handler slog.Handler) slog.Handler

LogHandler wraps a log handler so every record logged with a request's context, as slog.InfoContext(r.Context(), …) does, carries the request's ID as request_id. Start installs it itself with Config.LogFormat; a site with a logger of its own wraps that one:

slog.SetDefault(slog.New(webthings.LogHandler(handler)))

It is go-router's middleware.LogHandler.

func RequestID

func RequestID(ctx context.Context) string

RequestID returns the ID of the request ctx belongs to: the X-Request-ID header a proxy sent, when it is a sensible one (at most 64 letters, digits and -_.:), or one WebThings made. Every response names it in X-Request-ID; empty outside a request. It is go-router's middleware.GetRequestID.

Types

type App

type App struct {

	// DB is the database, for a site's own tables and queries.
	DB *db.DB
	// Auth holds the accounts, sessions and groups of editors.
	Auth *auth.Service
	// Media is the media library of uploaded files.
	Media *media.Service
	// Site holds the Site settings, such as the title, languages and theme.
	Site *site.Service
	// Pages is the page tree with its revisions.
	Pages *pages.Service
	// Frontend renders the public site with the active theme.
	Frontend *frontend.Frontend
	// Links knows which published content links to which address on the
	// site, the pages' and the modules' (Parts.Links).
	Links *links.Check
	// contains filtered or unexported fields
}

App is a running site: the core services, the modules chosen with Use, the admin and the public site, served as one http.Handler. Create it with New, choose modules with Use, then Start it, or let ListenAndServe do both. The services in its exported fields are ready once Start has returned.

func New

func New(config Config) *App

New returns an App for config, with the defaults filled in: the name "WebThings", state in "var", SQLite in var/site.sqlite, English as the only language and the admin at "/admin". It opens nothing yet; Start does, so New cannot fail.

func (*App) AdminPath

func (a *App) AdminPath() string

AdminPath is where the admin is served, Config.AdminPath, such as "/admin". Code links into the admin by route name (Env.AdminURL), not from this path; tests and tools use it to reach the admin.

func (*App) AdminURL

func (a *App) AdminURL(name string, args ...any) string

AdminURL is the address of a named admin route, as Env.AdminURL builds it, for the site's own code, tests and tools: AdminURL("admin.login"), AdminURL("admin.pages.edit", "id", id). It works once the app has started; before, it returns "".

func (*App) Changed

func (a *App) Changed()

Changed tells the app that what visitors see changed outside a request that tells it already, such as content the site's own code wrote to a module's store or a module's service from Go, or data the site's pages show from elsewhere: the output cache, which keeps public pages in memory, renders them again. The app tells it itself after every successful post to the admin and to the site and its actions that does not say it changed nothing (frontend.NothingChanged), after scheduled publication (see RunSchedules) and after the writes of the bundled modules' stores and services, also those made from Go. A change it misses shows when the output cache's time has passed (site.Settings.OutputCache).

func (*App) Close

func (a *App) Close() error

Close stops the background work (scheduler, cleanup and the variants of uploaded images, see media.Service.Prepare) and the modules, last added first, then closes storage and the database.

func (*App) CorePaths

func (a *App) CorePaths() []string

CorePaths are the addresses the app serves itself, next to the site's pages: the admin (Config.AdminPath), visitor actions (/_/), media, the site's assets, theme files, sitemap.xml, robots.txt, the endpoints for operations (/healthz, /readyz and /metrics) and, while Config.API turns it on, the content API (/api/v1). Requests below most of them never reach module middleware, so modules use Env.IsCorePath to keep away from them, for example a redirect that could never fire or a statistic of a file. They are the root router's own routes, labelled with the class the metrics count their requests under, so they and the routes cannot drift apart; they are known from New on. The admin comes first.

func (*App) InviteAdministrator

func (a *App) InviteAdministrator(ctx context.Context, email string) (string, error)

InviteAdministrator creates an administrator account for email and returns a link with which its owner chooses a password: the admin's password reset page with a new reset token, which works for an hour (auth.ResetLifetime). The account gets no password anyone knows, so it cannot be signed in to before then. The link is absolute on Config.BaseURL, or the path below the site's address without one, such as /admin/reset-password?token=….

It is for whoever runs the site, such as a command line that adds the first administrator without the setup link (see SetupToken), and works once the app has started, also on a site with accounts. It returns auth.ErrEmail for an address that is no address, auth.ErrDuplicate for one that has an account, and ErrNotStarted before Start. Give the link only to the owner of the address: it opens the account.

func (*App) IsCorePath

func (a *App) IsCorePath(path string) bool

IsCorePath reports whether path is one of CorePaths or below one of them: "/theme" and "/theme/site.css" are, "/themes" is not. It asks the root router which registration receives path.

func (*App) ListenAndServe

func (a *App) ListenAndServe(ctx context.Context, addr string) error

ListenAndServe starts the app (unless Start was already called, for example to import content first) and serves on addr until ctx is cancelled; see Serve.

func (*App) Metrics

func (a *App) Metrics() http.Handler

Metrics serves the app's metrics in the Prometheus text format: requests by route class, method and status with their durations, renders by kind with theirs, the database's connection pool, the output cache (hits, misses, skips, evictions, entries and bytes) and a few of the process. The site serves it at /metrics with Config.MetricsToken; without a token a site can serve it itself, for example on an address only its monitoring reaches.

func (*App) PurgeTrash

func (a *App) PurgeTrash(ctx context.Context) (int, error)

PurgeTrash deletes for good the items that have been in the admin's trash longer than Site settings › Trash allows (site.Settings.TrashDays), in every section, as the background cleanup does every hour, and returns how many it deleted. Every item goes the way Delete selected for good takes it, with its revisions and what else belongs to it (see admin.Admin.PurgeTrash). While the setting is off, the default, it deletes nothing. Several processes may run it at once: each item is deleted by one of them.

func (*App) Ready

func (a *App) Ready(ctx context.Context) error

Ready reports whether the app can serve: it has started, the database answers, every schema it applied is still applied (another version of the site may have moved the database on, or it was restored from an old backup) and the media storage can be reached, when it can tell (see media.Pinger). /readyz answers 200 when it returns nil, 503 otherwise.

func (*App) RunSchedules

func (a *App) RunSchedules(ctx context.Context) (int, error)

RunSchedules publishes and unpublishes the pages and module content whose scheduled time has come, as the background scheduler does every ScheduleInterval, and returns the number of changes. Every change has the same effects as publishing by hand. Several processes may run it at once: each change is made by one of them.

func (*App) Serve

func (a *App) Serve(ctx context.Context, listener net.Listener) error

Serve is ListenAndServe on a listener the caller opened: for example one handed over by systemd socket activation, or one on port 0, whose address a test knows before the app serves. It starts the app unless Start was already called, serves until ctx is cancelled, lets running requests finish, and then closes the listener and the app.

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves the site through the root router once the app has started; before that only the endpoints for operations answer, and every other address with 503 (see idleRoutes).

func (*App) Service

func (a *App) Service(key string) any

Service returns the API of a started module instance, for the website's own code. Module packages offer typed helpers, for example publishing.Of(app, "news").

func (*App) SetupToken

func (a *App) SetupToken() string

SetupToken is the secret of this start's first-run setup link, <AdminPath>/setup?token=<SetupToken>, which Start makes when the site has no accounts and logs; empty when accounts existed. A new one is made at every start, and the setup answers 404 once any account exists. Tests and tools that create the first account through the admin use it.

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start opens the database, applies schemas and builds the site and admin. Without accounts it logs the link to the first-run setup (see SetupToken).

func (*App) TemplateKeys

func (a *App) TemplateKeys() []string

TemplateKeys lists the keys WriteTemplates accepts: "site", then the keys of the modules with templates, in the order they were added. A site's command line can show them when no key is given.

func (*App) TranslationCatalog

func (a *App) TranslationCatalog() (*extract.Catalog, error)

TranslationCatalog is every text the site translates, found with go-translator's extract package. First come the texts of every template in Config.Templates. Then, in the order of their references, come those of the default templates of the modules (Module.Templates, Module.Components and Module.Mail) and of the core components that not every theme replaces, referenced as module:<type>/<name>.gohtml, module:<type>/mail/<name>.gohtml and core:site/<name>.gohtml, and the texts Go code translates: core's, such as the 404 page's title "Not found" and the networks' names of share links and profiles (frontend.CoreTexts), as core:texts, the names of months and days and the date patterns, with their contexts (frontend.CalendarTexts), as core:calendar, which a catalogue may translate to replace the built-in ones and must for a language without built-in ones, and the texts of the modules' visitor forms (Module.Forms, go-form's Texts) as module:<type>/forms. It needs no Start.

func (*App) Translations

func (a *App) Translations(dir string, args []string) error

Translations is the "translations" command of a site's main, with dir its translations folder (where the .po files are):

go run . translations              writes dir/translations.pot
go run . translations check        lists the texts each .po file misses
go run . translations check --csv  the same as CSV

The check returns translations.ErrMissing when a language misses texts, so the command fails. It reads TranslationCatalog and needs no Start.

func (*App) Use

func (a *App) Use(modules ...Module) error

Use selects modules. Call it before Start.

func (*App) WriteTemplates

func (a *App) WriteTemplates(dir, key string) ([]string, error)

WriteTemplates writes starting points for templates into dir: for a module instance key the module's templates and components in dir/<key>/ and its mail templates in dir/mail/<key>/, for "site" the layout with its head region (regions/head.gohtml, core's tags and room for the theme's own; see frontend.HeadScaffold), the stylesheet the layout links (assets/site.css), the page, 404 and search templates and core's components in dir/site/. Existing files are never overwritten. It returns the files it wrote and needs no Start.

type Config

type Config struct {
	// Name is the initial site title and the admin title.
	Name string
	// StateDir holds the SQLite database and uploaded media, and may hold
	// translations/<locale>.po files that override the admin's own texts.
	StateDir string
	// Driver is sqlite (default; "sqlite3" is accepted too), pgx or mysql.
	// SQLite is pure Go, so no build needs cgo. An empty DSN with SQLite uses
	// site.sqlite in StateDir; db.Open adds foreign keys, a busy timeout and
	// the other settings WebThings needs to any SQLite DSN that lacks them.
	Driver string
	// DSN is the data source name the driver connects with, such as
	// "postgres://user:pass@host/site"; see Driver for SQLite's default.
	DSN string
	// Locales are the initial content languages, used on first start only.
	// Later changes happen in the admin's site settings.
	Locales locales.Config
	// AdminLanguage is the admin UI language, for example "en" or "nl";
	// "en" when empty.
	AdminLanguage string
	// AdminPath is where the admin is served, such as "/admin" (the default,
	// when empty) or "/toolbox": a leading slash, no trailing slash,
	// letters, digits and - . _ ~ only, and no "." or ".." segment. The
	// admin, the core sections and the modules link to its pages by route
	// name (see Env.AdminURL), so
	// nothing depends on it, and robots.txt does not name it: admin pages
	// answer with X-Robots-Tag: noindex instead. It cannot be one of the
	// other core paths (see App.CorePaths), such as /media.
	AdminPath string
	// AdminEmail and AdminPassword create the first admin account on an empty
	// database. They are ignored once any account exists. Without them, a
	// start without accounts logs a one-time link to the first-run setup,
	// <AdminPath>/setup?token=… on BaseURL or the address ListenAndServe listens
	// on, where the first administrator account is created (see
	// App.SetupToken). The setup answers 404 without that link's token and
	// once any account exists.
	AdminEmail    string
	AdminPassword string
	// Insecure allows session cookies over plain HTTP, for local development.
	Insecure bool
	// Authorize decides admin permissions. Nil applies the built-in rules:
	// administrators may do everything, editors what their groups' matrix
	// allows (see auth.Group). When set, it decides every permission check,
	// for administrators too. The Accounts and Groups screens always need the
	// administrator role as well, and Authorize is asked about them too, so
	// it can keep them from administrators but never open them to editors.
	// It must be safe for concurrent use: requests and background work ask
	// it at the same time.
	Authorize func(ctx context.Context, user auth.User, permission admin.Permission) bool

	// Templates are the public site's templates: one folder per theme, such as
	// templates/default/, or a single theme at the root (see frontend.Themes).
	Templates fs.FS
	// Translations are the site's own translations of the texts of its
	// templates and of the modules' default templates and visitor forms,
	// usually frontend.Translations("translations"): templates get its tl,
	// tn, ctl and ctn, and core translates with it its own texts (such as
	// the 404 page's title), go-form's labels and messages and the names of
	// months and days and the date patterns. Those replace the built-in ones
	// in every language, English and Dutch included; what it leaves out is
	// built in, or English for a language without built-in ones (see
	// frontend.BuiltInCalendar). Nil shows every text as written.
	// App.Translations extracts the texts to translate.
	Translations *translator.Translator
	// Funcs are added to the templates.
	Funcs []template.FuncMap
	// Renderer replaces Templates with another template engine; see
	// frontend.Renderer for what it renders and frontend.TemplateFuncs for
	// the template functions. Funcs and the modules' functions still serve
	// components, mail and the site's translations.
	Renderer frontend.Renderer
	// ReloadTemplates reads Templates again for every request, so edits to
	// template files show without a restart; for development. Otherwise
	// every template is parsed once.
	ReloadTemplates bool
	// Assets are served at /assets/ (the route "assets") like the files of
	// an ImportMap of their own: templates get {{importmap_url "file"}}
	// for a file's versioned URL, which may be cached for a year, and a
	// plain /assets/<file> revalidates with a strong ETag. They are
	// fingerprinted at Start, and again after a change while templates
	// reload. Set Assets or ImportMap, not both.
	Assets fs.FS
	// ImportMap is the site's import map of go-importmap, such as one of
	// packages vendored with its importmap command and loaded with
	// importmap.FromFS, which may hold the site's own scripts and
	// stylesheets too. Its files are served at /assets/ (the route
	// "assets") with versioned URLs and strong ETags, and templates get
	// {{importmap}} for the head and {{importmap_url "file"}} for the
	// versioned URL of one of its files.
	//
	// Core adds go-partial's browser clients to either import map, under
	// the names scripts import them by: partial-flash, partial-sse and
	// partial-interactions, unless the site maps such a name itself. The
	// site needs no copy of them.
	ImportMap *importmap.ImportMap
	// FormTheme is the go-form theme the visitor forms of templates render
	// with ({{form_field .Form "name" nil}} and the other form functions),
	// such as one the site registers with go-form's
	// templates.RegisterClassTheme with the classes of its CSS. Empty uses
	// frontend.FormTheme, the classes of webthings.css.
	FormTheme string
	// BaseURL is the public address, for example "https://example.com", used in
	// sitemap.xml, share links, {{absURL}} and links sent by e-mail. Empty
	// uses the address of each request: its Host header, which the client
	// chooses, and X-Forwarded-Proto and X-Forwarded-Host (or Forwarded)
	// from TrustedProxies only. Set it in production; Start warns without it
	// unless Insecure.
	BaseURL string
	// TrustedProxies are the reverse proxies (IP addresses or CIDR ranges,
	// for example "127.0.0.1" or "10.0.0.0/8") whose X-Forwarded-For,
	// X-Real-IP and RFC 7239 Forwarded (for=) headers name the visitor (see
	// frontend.ClientIP) and whose X-Forwarded-Proto and X-Forwarded-Host
	// headers, with Forwarded's proto= and host= as a fallback, name the
	// address visitors used (see frontend.AbsoluteURL). Empty trusts none,
	// so visitors are known by the address they connect from.
	TrustedProxies []string
	// Mail sends modules' e-mail; nil logs messages instead (mail.Log).
	Mail mail.Sender
	// Storage keeps uploaded media; nil stores files in StateDir/media.
	Storage media.Storage
	// CacheTTL is how long site settings and published routes are kept in
	// memory: changes in this process apply at once, changes by other
	// processes (several servers on one database) within this time. Zero
	// means 30 seconds; negative disables the cache.
	CacheTTL time.Duration
	// OutputCacheTTL overrides the site setting of the output cache, which
	// keeps the public pages visitors get in memory and serves them again
	// without rendering them (see site.Settings.OutputCache): a positive
	// time keeps them that long, also in Development, where responses say
	// in X-Cache whether the cache answered (hit, miss or skip); negative
	// turns the cache off. Zero follows the site setting, except that
	// Development and ReloadTemplates turn the cache off. Site settings ›
	// Performance then shows the time that applies, which editors cannot
	// change there.
	OutputCacheTTL time.Duration
	// ScheduleInterval is how often scheduled publication changes are carried
	// out. Zero means 30 seconds; negative stops the background scheduler, for
	// example in tests that call RunSchedules themselves.
	ScheduleInterval time.Duration

	// Development shows failing templates in detail, with the template and
	// line, on the site and in the admin, and makes {{debug .}} show a
	// value; for local development only. Otherwise a failing template is
	// logged and answered with the theme's 500.gohtml (or go-partial's
	// plain error page when the theme has none), or in the admin with its own
	// error page, and {{debug .}} shows nothing.
	//
	// Start checks every template of every theme (see ErrTemplates): in
	// development it fails with every problem the check finds, and a
	// template call that would show nothing, such as {{posts "nieuws" 3}}
	// for an instance key the site does not have, an asset file the theme
	// does not have or an unknown theme setting, fails the page. Otherwise
	// the problems and such calls are logged at WARN, and Start fails only
	// when every page of the active theme would fail.
	Development bool
	// LogFormat, "text" or "json", makes Start log to stderr in that format,
	// at LogLevel and up, with the request ID (see RequestID) on every line
	// logged with a request's context; Start makes it slog's default. Empty
	// leaves logging to the site (see LogHandler).
	LogFormat string
	// LogLevel is the lowest level LogFormat logs; the zero value is info.
	LogLevel slog.Level
	// AccessLog logs a line for every request: method, path without the
	// query, status, size, duration and request ID.
	AccessLog bool
	// MetricsToken serves the metrics (see App.Metrics) at /metrics to
	// requests that carry it as "Authorization: Bearer <token>", such as
	// the output cache's counters:
	//
	//	curl -H "Authorization: Bearer $TOKEN" https://example.com/metrics | grep output_cache
	//
	// Empty serves no metrics at /metrics: it answers 404. /healthz and
	// /readyz are always served (see App.Ready).
	MetricsToken string
	// Telemetry plugs in tracing and other instrumentation.
	Telemetry Telemetry

	// ContentSecurityPolicy overrides the site setting of the
	// Content-Security-Policy (site.Settings.ContentSecurity), for tests
	// and deployments that decide it in code: "off", "report-only" or
	// "enforce" (site.CSPOff, site.CSPReportOnly, site.CSPEnforce). Empty
	// follows the site setting, which is off until an administrator
	// changes it; otherwise Site settings › Security shows this value,
	// which cannot be changed there. The policy lets pages run only the
	// scripts and styles the site serves, by go-router's
	// middleware.StrictCSP and the hashes of the import maps; see the
	// package documentation.
	ContentSecurityPolicy string
	// ContentSecuritySources are sources the policy allows by directive,
	// besides the site's own, such as {"font-src":
	// {"https://fonts.example.com"}}, added to those of the site setting
	// and the theme. Start refuses what site.CheckCSPSources does not
	// allow, such as 'unsafe-inline' or *.
	ContentSecuritySources map[string][]string

	// API serves the published content as JSON at /api/v1/ (package api),
	// with the modules' endpoints (Parts.API), for apps and decoupled
	// frontends: its rate limit and the sites that may read it in the
	// browser (CORS). Off by default.
	API api.Config
}

Config is everything a site decides in code when it creates its App: its name, where it keeps its state, which database it uses, its templates and languages, and the limits it runs with. Everything an editor may change later, such as the site title or the theme, lives in the Site settings instead. The zero value of every field is a working default.

type Env

type Env struct {
	// Key and Title are those of the Module being set up.
	Key   string
	Title string
	// DB is the site's database.
	DB *db.DB
	// Locales gives the content languages, as the Site settings have them
	// at the time of asking.
	Locales locales.Provider
	// Site holds the Site settings.
	Site *site.Service
	// Auth holds the editors' accounts, for names in the admin and for
	// notifying editors.
	Auth *auth.Service
	// Media is the media library.
	Media *media.Service
	// Pages is the page tree, for links to the pages a module is placed on.
	Pages *pages.Service
	// Frontend renders templates, components and e-mail with the theme.
	Frontend *frontend.Frontend
	// Mail sends e-mail; it logs messages when the site has no Config.Mail.
	Mail mail.Sender
	// BaseURL is Config.BaseURL, for absolute links in e-mail.
	BaseURL string
	// AdminLanguage is Config.AdminLanguage, the language of the admin, for
	// text staff read, such as notification e-mail; "en" when not set.
	AdminLanguage string
	// contains filtered or unexported fields
}

Env gives a module the shared services.

func (Env) AbsoluteURL

func (e Env) AbsoluteURL(r *http.Request, path string) string

AbsoluteURL makes path absolute with BaseURL, or with the address of r when BaseURL is empty; for links in e-mail.

func (Env) Actions

func (e Env) Actions() *router.Router

Actions is the go-router of the module's visitor actions: the forms visitors post to and links they follow, wherever the site shows them. It is mounted at /_/<key>/ behind the protection against cross-site posts and the body limit (Parts.ActionBodyLimit), so its routes are relative to that and become part of the app's root router, their names with the key in front, such as <key>.post for a route named post. A module that runs more than once names its routes alike.

actions := env.Actions()
actions.Use(frontend.Limits(env.Key, frontend.ActionLimits{Body: 64 << 10, Posts: 5, Window: time.Minute}))
actions.Post("/post", post).As("post")

Modules keep the router and build the addresses of their actions from it by route name, also without a request, such as for links in e-mail: actions.RoutePath("post") is "/_/<key>/post", and actions.RoutePathArgs("download", "id", id) fills in a wildcard. Handlers see the whole request path, and PathValue gives the route's wildcards; a request without a route gets a plain 404, or 405 for a path that has routes for other methods.

On an Env the app made, every call returns the same router; the app mounts it on the first. An Env made without an app, as in tests and tools, gets a new router on every call, mounted on a private root router with the app's own routes, so the addresses are the same.

func (Env) AdminURL

func (e Env) AdminURL(name string, args ...any) string

AdminURL is the address of a named admin route, as admin.Admin.URL builds it, for links into the admin from outside it, such as an edit link on the site or a link in an e-mail to staff:

env.AdminURL(admin.RouteName(env.Key, "edit"), "id", id)

Modules never build admin addresses from paths: the admin may be anywhere (Config.AdminPath). Section routes are named "admin.<section ID>.<name>" (see admin.RouteName and admin.Route.As); sections built with admin/editor name theirs, such as index, new and edit, and a module names the routes it adds with Route.As. Inside the admin, handlers use admin.Routes.URL or admin.Admin.URL and templates the route function. It works once the app has started; before, it returns "". Like admin.Admin.URL, it panics on an unknown route or a missing or unknown parameter.

func (Env) Changed

func (e Env) Changed()

Changed tells the app that what visitors see of the module changed outside a request that tells it already (see App.Changed). Modules call it after the writes their stores and services make, so a write made from Go, such as an import or a background job, shows at once; a store made with a content.Spec whose Changed is set to it tells it by itself. Call it once the write is committed (see db.AfterCommit).

func (Env) IsCorePath

func (e Env) IsCorePath(path string) bool

IsCorePath reports whether path is one of the app's CorePaths or below one of them, such as the admin's pages wherever it is (Config.AdminPath).

func (e Env) Links() *links.Check

Links returns the site's link check (App.Links), which knows the published content that links to an address, for the editor of a module whose items have an address of their own (see editor.Linked). It is nil on an Env made without an app.

func (Env) PersonalCookie

func (e Env) PersonalCookie(name string)

PersonalCookie names a cookie the module reads to show public pages differently to the visitor who has it, such as the polls' visitor ID, which marks the polls they voted on. The output cache, which keeps public pages in memory, keeps a page apart for every value of such cookies, also when a response varies on the Cookie header, where other cookies, such as a site's analytics, count for nothing. Call it in Setup. Pages that are a visitor's own altogether, such as a registration looked up by its code, say so instead with Cache-Control: private or no-store, which the output cache never keeps.

func (Env) Secret

func (e Env) Secret(ctx context.Context, name string) ([]byte, error)

Secret returns a random 32-byte key of this module instance, the same in every process of the site, for example to hash visitors' addresses with HMAC. name tells a module's keys apart: lowercase letters, digits, - and _, at most 35.

func (Env) Service

func (e Env) Service(key string) any

Service returns the API of another module instance, for example env.Service("agenda").(*events.Service). It works once the app has started, so modules resolve it when they need it (in a handler) rather than in Setup; before, and for unknown keys, it returns nil.

func (Env) VisitorID

func (e Env) VisitorID(ctx context.Context, r *http.Request, parts ...string) (string, error)

VisitorID identifies the visitor of r without storing their address: an HMAC-SHA256 of the address (frontend.ClientIP), with the instance's "visitors" Secret as key, as 64 hexadecimal digits. Nobody can trace it back to the address by trying all addresses, and it differs between module instances.

parts are hashed before the address, joined with "|", to narrow the identifier: with the date (time.DateOnly) it changes every day, so messages of one day can be grouped by sender without following anyone for longer. Modules that keep the identifier, such as a poll counting one vote per visitor, may store a prefix of it.

type Module

type Module struct {
	// Type is the kind of module, such as "publishing"; it is the same for
	// every instance.
	Type string
	// Key names this instance in addresses, template folders and the
	// admin, such as "news": lowercase letters, digits and dashes.
	Key string
	// Title is the instance's name in the admin, such as "News".
	Title string
	// Schema holds the module's schema files, applied at Start once for
	// all instances of the Type (see db.ApplySchema); nil for modules
	// without tables.
	Schema fs.FS
	// Setup builds the instance once the core services are ready, and
	// returns what it adds to the site and the admin.
	Setup func(Env) (Parts, error)
	// Templates are the public templates the module renders, looked up as
	// <key>/<name>.gohtml, then <type>/<name>.gohtml.
	Templates []frontend.Template
	// Funcs are template functions the module offers to every template, such
	// as {{range posts "news" 3}}. A first context.Context parameter receives
	// the request; frontend.Service gives the instance for a key.
	Funcs template.FuncMap
	// Mail are the e-mail templates the module writes with Frontend.Mail, with
	// the default text as Example; themes replace them with
	// mail/<key>/<name>.gohtml or mail/<type>/<name>.gohtml.
	Mail []frontend.Template
	// Components are parts of a page the module offers to every template,
	// such as a comment thread or a signup form, with the default markup as
	// Example. The module's template functions render them with
	// frontend.Component; themes replace them with <key>/<name>.gohtml or
	// <type>/<name>.gohtml, so their names differ from those of Templates.
	Components []frontend.Template
	// Forms are the visitor forms the module offers to templates, values of
	// structs with go-form's tags such as comments.PostInput{}, which
	// themes render with form_open and form_field. Their texts (labels,
	// descriptions, placeholders, option labels, submit texts and messages,
	// go-form's Texts) are translated with the site's translations, so
	// extraction finds them (see TranslationCatalog).
	Forms []any
	// contains filtered or unexported fields
}

Module is one capability instance. Type names the module's schema and its template folder; Key identifies this instance, for example "news" and "blog" for two Publishing instances.

func (Module) Group

func (m Module) Group(name string) Module

Group lists the module's admin sections under a heading of the navigation, for example:

app.Use(banners.Module("notice", "Notices").Group("Website"))

All sections of the module get it, also the ones it adds next to its main section, such as categories. The name is translated by the admin when its catalogue has it: Agenda, Content, Visitors, Website, Communication, Shop and the admin's own groups are; other names are shown as written. Modules without a group are listed under "Modules" (admin.GroupModules), unless they set Section.Group themselves.

type Parts

type Parts struct {
	// Admin sections, usually one with the module's key as ID, at
	// <AdminPath>/<key> (see admin.Section.Path).
	Admin []admin.Section
	// Public serves the module at and below the page it is mounted on. Nil
	// means the module cannot be mounted. A go-router of frontend.Router is
	// served at the page's address (see frontend.Router); any other handler
	// sees the request's whole path, its page in frontend.MountFrom.
	Public http.Handler
	// Service is the module's API for the website's own code; see App.Service.
	Service any
	// Sitemap lists the module's public paths below its page, for sitemap.xml.
	Sitemap frontend.SitemapFunc
	// Feeds are the module's Atom and RSS feeds, served below its page on
	// Public, which is then a router of frontend.Router, as routes of
	// their own, such as <page>/feed.atom named feed.atom (see
	// frontend.Feed). The layout links to those of the page shown with
	// {{feedLinks}}.
	Feeds []frontend.Feed
	// ActionBodyLimit is the largest request body the module's visitor
	// actions (Env.Actions) accept in bytes, for example more for an upload;
	// zero means DefaultActionBodyLimit.
	ActionBodyLimit int64
	// Middleware wraps the public site, for example to redirect old URLs.
	// It sees every request for the site's pages, also those the output
	// cache answers, outside compression: a response it reads may be
	// gzip-compressed (Content-Encoding).
	Middleware func(http.Handler) http.Handler
	// Close runs when the app closes, before the database does, for example
	// to write buffered data.
	Close func(ctx context.Context) error
	// Search finds the module's published content for the site search (see
	// frontend.Search); results need a URL.
	Search frontend.SearchFunc
	// Scheduled is content whose publication editors can plan, carried out by
	// the app's scheduler; see content.Revisions.Scheduler.
	Scheduled []content.Scheduler
	// Links lists the module's published content in a language with the
	// links in it, such as Markdown texts and link fields (see
	// links.Source), so the admin knows which content links to a page: it
	// warns editors before the page leaves its address and lists the
	// links that lead nowhere (App.Links).
	Links links.Func
	// Redirects is the module's service when it redirects old addresses,
	// such as the redirects module's: the admin then offers to redirect
	// the addresses links lead to when a page leaves them. The first
	// module that sets it is used.
	Redirects links.Redirector
	// API serves the module's published content as JSON below
	// /api/v1/<key>/ while the site's content API is on (Config.API): a
	// router of api.NewRouter, whose routes are relative to that and named
	// api.<key>.<name> for the app's other routers. Its handlers answer in
	// the request's language (api.Locale) with the helpers of package api,
	// and never show drafts or personal data, such as entries of visitors.
	// It is not mounted while the API is off.
	API *router.Router
	// StructuredData adds to the structured data of the site's pages, or
	// changes it, such as the offers of an event visitors register for.
	// A module's own pages give theirs with frontend.Content's
	// StructuredData instead.
	StructuredData frontend.StructuredDataFunc
}

Parts is what a module adds to the site.

type Telemetry

type Telemetry struct {
	// Middleware wraps every request below the request ID, outside the
	// access log, compression and the routes, for example to start a trace
	// span per request; the health and metrics endpoints are left out.
	Middleware func(http.Handler) http.Handler
	// RenderStages run in every render of the site's templates and the
	// admin's, for example go-partial's exp/metrics with a sink of its own.
	RenderStages []partial.RenderStage
	// RenderEvents receive go-partial's diagnostic events, for example
	// go-partial's exp/otel Sink, which adds them to the request's span.
	RenderEvents []partial.EventSink
}

Telemetry plugs instrumentation into the app, such as tracing with OpenTelemetry. Core brings no tracing library of its own: integrations such as go-webthings-contrib's provide these parts.

Directories

Path Synopsis
Package admin is the shared back office: one shell (layout, navigation, notices), sign-in with optional two-step sign-in, password reset and first-run setup, translations and a permission check on every route.
Package admin is the shared back office: one shell (layout, navigation, notices), sign-in with optional two-step sign-in, password reset and first-run setup, translations and a permission check on every route.
editor
Package editor is the ready-made admin for content stores (see the content package): an overview with language tabs, a go-form editor per language, publication and, for revisioned content, history, restore and preview.
Package editor is the ready-made admin for content stores (see the content package): an overview with language tabs, a go-form editor per language, publication and, for revisioned content, history, restore and preview.
presence
Package presence keeps track of which admin page every signed-in user is looking at, so the admin can show "Ann is also on this page".
Package presence keeps track of which admin page every signed-in user is looking at, so the admin can show "Ann is also on this page".
Package api is the site's content API: its published content as JSON, for apps and decoupled frontends, at /api/v1/ (Path).
Package api is the site's content API: its published content as JSON, for apps and decoupled frontends, at /api/v1/ (Path).
Package auth keeps the accounts of the people who edit the site, their sessions and their groups of rights.
Package auth keeps the accounts of the people who edit the site, their sessions and their groups of rights.
Package content stores multilingual content for modules.
Package content stores multilingual content for modules.
db
Package db is WebThings' small layer over SQL.
Package db is WebThings' small layer over SQL.
dbtest
Package dbtest opens a fresh database per test for every configured dialect.
Package dbtest opens a fresh database per test for every configured dialect.
Package feed writes web feeds, Atom 1.0 (RFC 4287) and RSS 2.0, from one model: a Feed with its Entries.
Package feed writes web feeds, Atom 1.0 (RFC 4287) and RSS 2.0, from one model: a Feed with its Entries.
Package frontend serves the public website.
Package frontend serves the public website.
internal
bundled
Package bundled knows the template functions of the modules bundled with WebThings, also of those a site does not use, so the template check at Start can say where an unknown function comes from.
Package bundled knows the template functions of the modules bundled with WebThings, also of those a site does not use, so the template check at Start can say where an unknown function comes from.
cache
Package cache keeps one value read from the database in memory for a short time.
Package cache keeps one value read from the database in memory for a short time.
ops
Package ops holds the operational parts of serving a site that are WebThings' own: its metrics in the Prometheus text format, with the access log, and the caching policy of public pages.
Package ops holds the operational parts of serving a site that are WebThings' own: its metrics in the Prometheus text format, with the access log, and the caching policy of public pages.
testhooks
Package testhooks holds switches that only test helpers flip, to make expensive work cheap in tests.
Package testhooks holds switches that only test helpers flip, to make expensive work cheap in tests.
Package links knows which published content of a site links to which address on it: the Markdown of the published pages, and what modules report of their published content (Func), such as the text of a post or the address of a banner's button.
Package links knows which published content of a site links to which address on it: the Markdown of the published pages, and what modules report of their published content (Func), such as the text of a post or the address of a banner's button.
Package locales checks the content languages a site offers, such as "en" and "nl-BE", and writes their codes the same way everywhere.
Package locales checks the content languages a site offers, such as "en" and "nl-BE", and writes their codes the same way everywhere.
Package mail sends e-mail for modules: confirmations, notifications and newsletters.
Package mail sends e-mail for modules: confirmations, notifications and newsletters.
Package markdown renders editor content.
Package markdown renders editor content.
Package media stores shared assets (images and documents) with translated titles and alt texts.
Package media stores shared assets (images and documents) with translated titles and alt texts.
mediaadmin
Package mediaadmin is the Media section of the admin: the media library, where editors upload files, write their titles and alternative texts per language and choose the focal point of images.
Package mediaadmin is the Media section of the admin: the media library, where editors upload files, write their titles and alternative texts per language and choose the focal point of images.
modules
banners
Package banners shows editor-managed banners anywhere on the site: hero slides on the home page, a carousel, or a scheduled announcement bar at the top of every page.
Package banners shows editor-managed banners anywhere on the site: hero slides on the home page, a carousel, or a scheduled announcement bar at the top of every page.
comments
Package comments lets visitors leave messages on the website: comments below news posts or pages, and guestbooks.
Package comments lets visitors leave messages on the website: comments below news posts or pages, and guestbooks.
directory
Package directory is a categorised directory: organisations, shops, venues or a neighbourhood guide.
Package directory is a categorised directory: organisations, shops, venues or a neighbourhood guide.
documents
Package documents offers files for download: minutes, reports, forms and policies.
Package documents offers files for download: minutes, reports, forms and policies.
events
Package events is an agenda module: dated events with revisions, a timezone, an optional end, all-day events, a location, an image and Markdown content.
Package events is an agenda module: dated events with revisions, a timezone, an optional end, all-day events, a location, an image and Markdown content.
forms
Package forms adds a public form, such as a contact form or a quote request, with an inbox for its submissions.
Package forms adds a public form, such as a contact form or a quote request, with an inbox for its submissions.
gallery
Package gallery shows photo albums.
Package gallery shows photo albums.
lists
Package lists keeps short ordered lists such as FAQs, services, features or steps.
Package lists keeps short ordered lists such as FAQs, services, features or steps.
newsletter
Package newsletter keeps a subscriber list with double opt-in and sends mailings to it.
Package newsletter keeps a subscriber list with double opt-in and sends mailings to it.
people
Package people presents the people behind an organisation: staff, a team, a board or volunteers.
Package people presents the people behind an organisation: staff, a team, a board or volunteers.
polls
Package polls asks visitors a question with a few options and shows the results.
Package polls asks visitors a question with a few options and shows the results.
publishing
Package publishing is a news or blog module: dated posts with revisions, categories, a featured image and Markdown content.
Package publishing is a news or blog module: dated posts with revisions, categories, a featured image and Markdown content.
redirects
Package redirects sends visitors from old URLs to new ones, for example after moving a site to WebThings.
Package redirects sends visitors from old URLs to new ones, for example after moving a site to WebThings.
registrations
Package registrations lets visitors sign up for events, or for any other subject: places with an optional capacity, adults and children with prices, a deadline, a confirmation e-mail with a personal code, and a page where visitors look up and cancel their registration.
Package registrations lets visitors sign up for events, or for any other subject: places with an optional capacity, adults and children with prices, a deadline, a confirmation e-mail with a personal code, and a page where visitors look up and cancel their registration.
stats
Package stats counts the public site's page views without cookies and without storing anything about visitors.
Package stats counts the public site's page views without cookies and without storing anything about visitors.
submissions
Package submissions lets visitors propose a news item: a title, a text and an optional image, with their name and e-mail address.
Package submissions lets visitors propose a news item: a title, a text and an optional image, with their name and e-mail address.
testimonials
Package testimonials shows quotes from customers or partners: a quote, the author's name, role and organisation, and an optional photo.
Package testimonials shows quotes from customers or partners: a quote, the author's name, role and organisation, and an optional photo.
Package pages owns the public page tree.
Package pages owns the public page tree.
pageadmin
Package pageadmin is the Pages section of the admin: the page tree, where editors add, move and publish pages and place modules on them, and the editor of each page, built with package editor.
Package pageadmin is the Pages section of the admin: the page tree, where editors add, move and publish pages and place modules on them, and the editor of each page, built with package editor.
Package render wraps go-partial: a layout with a content slot and optional out-of-band regions, served as a full page or as an HTMX fragment.
Package render wraps go-partial: a layout with a content slot and optional out-of-band regions, served as a full page or as an HTMX fragment.
Package site holds the website's identity and settings: languages, timezone, branding, contact details, social media profiles and per-language title, tagline, SEO and footer.
Package site holds the website's identity and settings: languages, timezone, branding, contact details, social media profiles and per-language title, tagline, SEO and footer.
siteadmin
Package siteadmin is the Site settings section of the admin: the site's languages, time zone, texts, social media profiles, performance, security, review and trash settings, the Theme screen where editors choose the theme and set its values, and the Backup screen where administrators download the site as a zip file.
Package siteadmin is the Site settings section of the admin: the site's languages, time zone, texts, social media profiles, performance, security, review and trash settings, the Theme screen where editors choose the theme and set its values, and the Backup screen where administrators download the site as a zip file.
Package sitetest starts a complete WebThings app for end-to-end tests and drives its admin like a browser: sign in, submit forms, publish.
Package sitetest starts a complete WebThings app for end-to-end tests and drives its admin like a browser: sign in, submit forms, publish.
catalogtest
Package catalogtest checks in tests that the admin's catalogues translate every text of a section's forms in each language the admin comes with, so a label or description added to a form cannot go untranslated:
Package catalogtest checks in tests that the admin's catalogues translate every text of a section's forms in each language the admin comes with, so a label or description added to a form cannot go untranslated:
Package translations is the "translations" command of a site: it writes the texts a site translates with tl, tn, ctl and ctn, found with go-translator's extract package, to a gettext template (translations.pot) and checks which texts each language's .po file still misses.
Package translations is the "translations" command of a site: it writes the texts a site translates with tl, tn, ctl and ctn, found with go-translator's extract package, to a gettext template (translations.pot) and checks which texts each language's .po file still misses.

Jump to

Keyboard shortcuts

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