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

events

package
v0.1.2 Latest Latest
Warning

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

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

README

Events

An agenda of dated events with a location, an image and Markdown text, each in its own timezone, with drafts, previews and scheduled publishing.

Use one instance per agenda. Upcoming and past events are listed separately, and other modules, such as Registrations and Comments, attach to an event by its ID.

Add it to the site

app.Use(
    events.Module("agenda", "Agenda"),
)

The key (agenda) is the instance's name in URLs and templates and must be unique on the site. The title (Agenda) is what editors see in the admin navigation and the page editor.

Place it on a page

In the admin, open a page and choose the instance under Shows on this page. The page then serves the events below its own address; with the page at /agenda:

Address Template Shows
/agenda agenda/list.gohtml the upcoming and running events, soonest first
/agenda/past agenda/list.gohtml the past events, newest first
/agenda/<slug> agenda/event.gohtml one event

An event is past once its end has passed, or its start when it has no end. All-day events last until the end of their last day. Both lists use the same template: .Past tells them apart, and .UpcomingURL and .PastURL link between them. The lists are not paged.

Templates are looked up by instance key first, then by module type, so events/event.gohtml serves every Events instance that has no design of its own. Generate starting points with every field of the data:

task templates -- agenda

A minimal list:

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

{{if .Past}}
    <a href="{{.UpcomingURL}}">Upcoming events</a>
{{else}}
    <a href="{{.PastURL}}">Past events</a>
{{end}}

{{range .Events}}
    <article>
        <h2><a href="{{.URL}}">{{.Title}}</a></h2>
        <p>
            {{with .When}}
                {{if .AllDay}}{{dateRange .Start .End "full"}}
                {{else if .SameDay}}{{date .Start "full"}} · {{timeRange .Start .End}}
                {{else}}{{date .Start "full"}}, {{time .Start}} – {{date .End "full"}}, {{time .End}}{{end}}
            {{end}}
            {{with .Location}} · {{.}}{{end}}
        </p>
        {{with .Summary}}<p>{{.}}</p>{{end}}
    </article>
{{else}}
    <p>No events planned.</p>
{{end}}

event.gohtml gets a single events.Event; its Body is the rendered Markdown, with media from the library already resolved. Start and End are in the event's own timezone (.Timezone), and the date functions show them in that zone, also for events in UTC. End is zero when the event has none; for all-day events it is the last day, so {{dateRange .Start .End}} reads "25–27 October 2026".

.When gives the start, end and all-day switch as a frontend.When, which a registration's subject gives too (registrations.Subject.When), so one partial shows both:

{{/* @dot github.com/donseba/go-webthings/frontend.When */}}
{{if .AllDay}}{{dateRange .Start .End "full"}}
{{else if .SameDay}}{{date .Start "full"}} · {{timeRange .Start .End}}
{{else}}{{date .Start "full"}}, {{time .Start}} – {{date .End "full"}}, {{time .End}}{{end}}

Saved as partials/when.gohtml, it is {{template "partials/when.gohtml" .When}} in event.gohtml. .SameDay is set when the event ends on the day it starts, or has no end. The starting points from task templates -- agenda show events the same way, in one line, in the list and on the event's page; there an event with times is followed by its timezone, such as "Monday 16 November 2026, 09:00 – Tuesday 17 November 2026, 18:00 (Europe/Amsterdam)".

ImageURL is the event's image as pages show it, at most 1600 pixels wide, with ImageSrcset, ImageWidth and ImageHeight for the img tag, and ImageOriginalURL is the uploaded file; all are empty for an event without one. Events in a list have the same fields:

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

An event appears in each language it has a title and slug for. Events are only reachable while the instance is placed on a page.

Show events anywhere

The events template function returns up to a number of the next upcoming and running events in the current language, for a home page or a sidebar; 0 or less returns all of them. Links point to the page the instance is placed on; while it is on no page, .URL is empty.

{{range events "agenda" 3}}
    <a href="{{.URL}}">{{.Title}}</a>
    {{date .Start "EEE d MMM"}}{{if not .AllDay}}, {{time .Start}}{{end}}
{{end}}

<a href="{{moduleURL "agenda"}}">Full agenda</a>

Feeds

Each instance serves Atom and RSS feeds of its upcoming and running events, soonest first, in the page's language: with the page at /agenda, /agenda/feed.atom and /agenda/feed.rss (routes feed.atom and feed.rss). Core's head tags link to them with {{feedLinks}} from every page of the instance; see Themes. While the instance is on no page, the feeds get the site's 404 page.

  • The feeds list as many events as the site setting Items in feeds says (Site settings › General, 20 by default). An event leaves them once it is past.
  • events.Module("agenda", "Agenda", events.WithFeedSize(50)) makes the instance's feeds list 50 events instead, whatever the setting says. The size is at most 100 (site.MaxFeedSize) and 0 follows the setting; the app does not start with another size.
  • An entry's summary says when and where the event takes place, in the page's language and the event's timezone, such as "14 November 2026 · 20:00–22:00 · De Werf", then the event's own summary after a blank line. All-day events have no times.
  • Its content is the event's Markdown with absolute addresses, its images shown as in e-mail, and the event's image is an enclosure.
  • It is published when the event was created and updated when its published revision was saved. Its ID is tag:<host>,<date created>:<key>/<event ID>, so it stays the same when the event's slug or page changes. Set Config.BaseURL, so the host does not depend on the address a reader used.

Feeds are cached like pages and are not in sitemap.xml; a past event leaves a cached feed when the cache's time has passed. Feeds for modules explains what core adds to them.

Structured data

An event's page describes the event to search engines as a schema.org Event. Core's head tags write it, with the site's own items, with {{structuredData}}; see Search and SEO. The Event of an event at /en/agenda/harbour-fair that visitors can register for, one item of the page's @graph:

{
  "@type": "Event",
  "description": "Games and food",
  "endDate": "2027-06-12T17:00:00+02:00",
  "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
  "eventStatus": "https://schema.org/EventScheduled",
  "inLanguage": "en",
  "location": {"@type": "Place", "address": "Quay 1, Harbourtown", "name": "Town hall"},
  "mainEntityOfPage": {"@id": "https://example.com/en/agenda/harbour-fair"},
  "name": "Harbour fair",
  "offers": {
    "@type": "Offer",
    "availability": "https://schema.org/InStock",
    "price": "12.50",
    "priceCurrency": "EUR",
    "url": "https://example.com/en/agenda/harbour-fair",
    "validThrough": "2027-06-12T12:30:00Z"
  },
  "organizer": {"@id": "https://example.com/#organization"},
  "startDate": "2027-06-12T14:30:00+02:00",
  "url": "https://example.com/en/agenda/harbour-fair"
}
  • name is the event's title and description its summary; image, for an event with an image, is that image as it is shown when the page is shared, a 1200 × 630 JPEG cut around its focal point.
  • startDate and endDate are in the event's timezone, such as 2027-06-12T14:30:00+02:00, or the day alone for an all-day event, such as 2027-06-12. An event without an end has no endDate.
  • eventStatus is always EventScheduled: the module has no cancelled or moved events.
  • location is a Place with the event's location as name and its address as address, and eventAttendanceMode says it takes place there; an event without either has neither.
  • inLanguage is the page's language, and organizer the site's organisation.

A Registrations instance linked with registrations.ForEvents("agenda") adds offers, an Offer, while visitors can register for the event (InStock) and once every place is taken (SoldOut), also after registration is switched off: price is the adult price, priceCurrency the instance's currency, url the event's page and validThrough when registration closes, the deadline or else the event's start. An event without registration gets none, and so does one whose registration closed with places left.

The lists of upcoming and past events are an ItemList of the events they show, in their order.

Content API

While the site's content API is on (Config.API), each instance serves its published events as JSON below /api/v1/<key>/, in the language of ?locale=, or the site's default language without it. With the instance agenda:

Endpoint Route name Answers with
/api/v1/agenda/events api.agenda.events The upcoming and running events, soonest first, as a list
/api/v1/agenda/events?past=true The past events, newest first
/api/v1/agenda/events?slug=<slug> The event with that slug: the module.path that /api/v1/pages/resolve gives for an event's address, without its leading slash; add past=true for a past event
/api/v1/agenda/events/{id} api.agenda.event One event, past or not, with its text

An event appears in each language it has a title and slug for, as on the site, and never as a draft. GET /api/v1/agenda/events/{id}:

{
  "id": "01a115cb-b189-7cb8-9adc-82a8952a89ef",
  "slug": "harbour-fair",
  "title": "Harbour fair",
  "summary": "Games and food",
  "body_html": "\u003cp\u003eBring \u003cem\u003efriends\u003c/em\u003e.\u003c/p\u003e\n",
  "location": "Town hall",
  "address": "Quay 1, Harbourtown",
  "start": "2027-06-12T14:30:00+02:00",
  "end": "2027-06-12T17:00:00+02:00",
  "all_day": false,
  "timezone": "Europe/Amsterdam",
  "past": false,
  "url": "https://example.com/en/agenda/harbour-fair",
  "api_url": "https://example.com/api/v1/agenda/events/01a115cb-b189-7cb8-9adc-82a8952a89ef?locale=en",
  "image": null,
  "updated_at": "2026-10-07T09:57:13.225795Z"
}
Field
body_html The event's text as HTML, with absolute addresses and images with their srcset; only /events/{id} gives it
location, address The location's name and its address, as editors wrote them
start, end The start and the end in the event's timezone, with its offset; end is null for an event without one. An all-day event starts and ends at midnight of its first and its last day.
all_day, timezone The All day switch and the event's timezone, an IANA name
past Set once the event is over, as for the list of past events on the site
url The event's page on the site; null while the instance is on no page in that language
api_url The event in the API
image The event's image as an image object, null without one
updated_at When the published revision was saved

The API leaves out registrations and their places: visitors' sign-ups are personal data.

/events/{id} sends a Last-Modified: the latest change of the event, of the page the instance is on and of the event's media, and, once the event is past, when it ended, since past changes then. The lists send none; see Caching.

Editing

Each instance has its own section in the admin (/admin/agenda; /admin is the default admin path) with the list of events.

  • An event has a start, an optional end, an All day switch, a timezone and an optional image from the media library; per language a title, web address, summary, Markdown text, location name and address. An empty Web address is made from the title when the event is saved.
  • Start and end are the local time in the event's timezone, an IANA name such as Europe/Amsterdam: the editor shows and reads them as typed in that timezone, not in the site's timezone as other dates in the admin. A value that is not a date and time gets an error at its field. When the clocks go forward, a time they skip moves forward by the change (02:30 becomes 03:30); when they go back, a time that occurs twice is the first of the two. New events get the site's timezone from the site settings and start at the next full hour on the clocks of that timezone, also in a timezone whose offset is not whole hours, such as Asia/Kolkata. For all-day events only the dates count; an end on the first day makes a one-day event.
  • Save draft creates a new revision as a draft: "Saved as a draft. Publish it to show it on the website." The published version stays online until a revision is published. Editors can preview a draft at the event's own address on the website, while the instance is placed on a page, and restore an earlier revision from the history.
  • The publication box plans publishing or unpublishing for a date and time in the site's timezone. Publishing by hand cancels the plan. Events that are over stay published and move to the past list.
  • A site can require a review per instance in Site settings › Review. An event's revision then needs a reviewer's approval before it is published or planned; see Review before publishing.
  • Publishing needs a title and a web address in the site's default language. The web address past is reserved for the list of past events.
  • Move to trash moves an event to the trash, where it can be restored with its revisions or deleted for good. It cancels a planned publication. A published event cannot be restored while another published event uses its web address.

Editors' rights are set per instance in the groups' permission matrix.

Published events, upcoming and past, appear in the site search by title, summary, text, location and address. They are also listed in sitemap.xml, with /agenda/past when there are past events.

Links to addresses on the site in the text of published events count for Links to a page: the editor of a page lists the events that link to it, and Pages › Broken links lists their links that lead nowhere.

The editor of a published event lists the published content that links to the event and asks before it is unpublished or moved to the trash. A revision with a new Web address shows the old and the new address before it is published. With the redirects module, editors who may create redirects also get Unpublish and redirect, and Publish this revision comes with Redirect the old addresses to the new ones, ticked by default; see Links to an item.

From Go

events.Of(app, "agenda") returns the instance once the app has started:

agenda := events.Of(app, "agenda")

next, err := agenda.Upcoming(ctx, "en", 3)

Event(ctx, id, locale) returns one event and Events(ctx, locale) all of them, published or not, as events.Info with Published, Past and the public URL. While the instance is on no page in that language, Upcoming, Event and Events still return the events, with an empty URL. They are meant for modules that refer to events, which reach the instance with env.Service("agenda").(*events.Service) once the app runs.

Store() gives write access for imports and seed data, through the same rules the admin uses. Times are stored in UTC next to the event's timezone, and publishing takes the revision to publish:

amsterdam, err := time.LoadLocation("Europe/Amsterdam")
if err != nil {
    return err
}

store := agenda.Store()

item, err := store.Save(ctx, content.Draft[events.EventFields, events.EventText]{
    Fields: events.EventFields{
        Start:    time.Date(2026, 11, 14, 20, 0, 0, 0, amsterdam).UTC(),
        Timezone: "Europe/Amsterdam",
    },
    Translations: []content.Translation[events.EventText]{
        {
            Locale: "en",
            Text: events.EventText{
                Slug:  "autumn-concert",
                Title: "Autumn concert",
            },
        },
    },
})
if err != nil {
    return err
}

err = store.Publish(ctx, item.ID, item.Revision.ID, "en")
  • Registrations takes sign-ups for events: link an instance with registrations.ForEvents("agenda") and show the form with {{registrationForm "signup" .ID}} in event.gohtml.
  • Comments adds reactions below an event with {{commentThread "remarks" .ID}}.
  • Publishing keeps news and other dated posts.

Documentation

Overview

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. Add one instance per agenda, for example:

app.Use(events.Module("agenda", "Agenda"))

Below its page the module serves the upcoming events at "/", past events at "/past" and one event at "/{slug}", and Atom and RSS feeds of the upcoming events at "/feed.atom" and "/feed.rss" (routes feed.atom and feed.rss), which the layout links to with {{feedLinks}}. The website renders these templates:

events/list.gohtml   ListView: upcoming or past events
events/event.gohtml  Event: one event

Times in Event are in the event's own timezone (events in "UTC" as civil times, see frontend.Civil, so they are not moved to the site's timezone); the template functions show them in the page's language, for example {{dateRange .Start .End "full"}} · {{timeRange .Start .End}}. Other modules read events with Service.Event and Service.Events.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Module

func Module(key, title string, opts ...Option) webthings.Module

Module adds an Events instance. key is used in URLs of the admin and must be unique on the site; title is shown in the admin and the page editor.

Types

type APIEvent

type APIEvent struct {
	ID       string `json:"id"`
	Slug     string `json:"slug"`
	Title    string `json:"title"`
	Summary  string `json:"summary"`
	BodyHTML string `json:"body_html,omitempty"`
	Location string `json:"location"`
	Address  string `json:"address"`
	// Start and End are in Timezone, with its offset; End is null for an
	// event without an end. For an all-day event they are the first and
	// the last day at midnight.
	Start    time.Time  `json:"start"`
	End      *time.Time `json:"end"`
	AllDay   bool       `json:"all_day"`
	Timezone string     `json:"timezone"`
	// Past is set once the event is over.
	Past bool `json:"past"`
	// URL is the event's page, null while the agenda is on no page; APIURL
	// the event in the API.
	URL       *string    `json:"url"`
	APIURL    string     `json:"api_url"`
	Image     *api.Image `json:"image"`
	UpdatedAt time.Time  `json:"updated_at"`
}

APIEvent is a published event in the content API (see package api), at /api/v1/<key>/events/{id} and in the list of /api/v1/<key>/events, which leaves out BodyHTML.

type Event

type Event struct {
	ID       string
	Title    string
	Summary  string
	Body     template.HTML
	Location string
	Address  string
	// Start and End are in Timezone; for "UTC" they are civil times (see
	// frontend.Civil). End is zero when the event has no end; for all-day
	// events it is the last day.
	Start    time.Time
	End      time.Time
	AllDay   bool
	Timezone string
	// URL is the event's page, empty when the agenda is on no page.
	URL string
	// ImageURL is the event's image as pages show it, at most media.ImageWidth
	// wide (see frontend.Frontend.Image); ImageSrcset lists smaller
	// copies for a srcset attribute, ImageWidth and ImageHeight are the
	// size of ImageURL and ImageOriginalURL is the uploaded file, for
	// downloads. All are empty when the event has no image.
	ImageURL         string
	ImageSrcset      string
	ImageOriginalURL string
	ImageWidth       int
	ImageHeight      int
}

Event is a published event as templates see it.

func (Event) When

func (e Event) When() frontend.When

When is when the event takes place, for a template or partial that shows it in one line, also for a registration's Subject.

type EventFields

type EventFields struct {
	Start    time.Time  `db:"starts_at"`
	End      *time.Time `db:"ends_at"`
	AllDay   bool       `db:"all_day"`
	Timezone string     `db:"timezone"`
	Image    string     `db:"image_id,null"`
}

EventFields are the language-independent parts of an event. Start and End are stored in UTC. All-day events start at midnight in Timezone and End is midnight of their last day.

type EventItem

type EventItem = content.Item[EventFields, EventText]

EventItem is an event with its translations, as the store keeps it.

type EventText

type EventText struct {
	Slug     string `db:"slug"`
	Title    string `db:"title"`
	Summary  string `db:"summary"`
	Body     string `db:"body"`
	Location string `db:"location_name"`
	Address  string `db:"location_address"`
}

EventText is an event in one language. Body is Markdown.

type Info

type Info struct {
	ID string
	// Title is in the requested language, or in the first language the
	// event has when it is not translated.
	Title string
	// Start and End are in Timezone, for "UTC" as civil times (see
	// frontend.Civil); End is zero when the event has none and the last day
	// for all-day events.
	Start    time.Time
	End      time.Time
	AllDay   bool
	Timezone string
	// Published is true when visitors can see the event in the requested
	// language; URL is then its address, when the agenda is on a page.
	Published bool
	URL       string
	// Past is true once the event is over.
	Past bool
}

Info is an event as other modules see it, published or not, for example a registrations module that lists events to sign up for and names them in e-mail. Get it with Service.Event and Service.Events, through webthings.Env.Service.

type ListView

type ListView struct {
	Title  string
	Events []Event
	// Past is true on the list of past events, newest first. Otherwise the
	// list holds upcoming and running events, soonest first.
	Past        bool
	UpcomingURL string
	PastURL     string
}

ListView is the data of events/list.gohtml.

type Option

type Option func(*options)

Option changes how an instance works.

func WithFeedSize

func WithFeedSize(size int) Option

WithFeedSize makes the instance's feed list the size soonest events, rather than as many as the site setting says (site.Settings.FeedLimit); at most site.MaxFeedSize.

type Service

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

Service is one Events instance, for the website's own code (for example "upcoming events" on the home page).

func Of

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

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

func (*Service) Event

func (s *Service) Event(ctx context.Context, id, locale string) (Info, error)

Event returns one event in locale: as published when visitors can see it in locale, otherwise its latest revision.

func (*Service) Events

func (s *Service) Events(ctx context.Context, locale string) ([]Info, error)

Events returns every event of the instance in locale, latest start first, with the latest revision of each, published or not.

func (*Service) Store

func (s *Service) Store() content.Store[EventFields, EventText]

Store gives write access to this instance's events, for imports and seeds. Admin edits go through the same store.

func (*Service) Upcoming

func (s *Service) Upcoming(ctx context.Context, locale string, limit int) ([]Event, error)

Upcoming returns at most limit of the next published events in locale, all of them when limit is 0 or less, for use outside the module's own pages. Links point to the page the module is mounted on; they are empty when it is on no page.

Jump to

Keyboard shortcuts

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