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

documents

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: 27 Imported by: 0

README

Documents

Files for download, such as minutes, reports, forms and policies, listed by category and year with a download counter.

A document is a file from the media library or a link to another site. Use one instance per collection, for example downloads for public documents and minutes for meeting minutes, each with its own admin section and templates.

Add it to the site

app.Use(
    documents.Module("downloads", "Downloads"),
)

The key (downloads) is the instance's name in URLs and templates and must be unique on the site. The title (Downloads) is what editors see in the admin navigation and the page editor. The module has no options.

Place it on a page

In the admin, open a page and choose the instance under Shows on this page. The page then lists the documents; with the page at /downloads:

Address Template Shows
/downloads downloads/list.gohtml all published documents, newest first
/downloads/category/<slug> downloads/list.gohtml the documents in one category

On a category address .Category is set and .Title is the category name instead of the page title. The slug is made from the category name: "Annual reports" becomes annual-reports, with accents dropped and at most 80 characters. A category name without Latin letters or digits gets no address. Since category names are typed per language, a category address only exists in the languages that use that name.

Templates are looked up by instance key first, then by module type, so documents/list.gohtml serves every Documents instance that has no design of its own. Generate a starting point with every field of the data:

task templates -- downloads

A minimal list, per year:

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

<nav>
    <a href="{{.AllURL}}">All</a>
    {{range .Categories}}
        <a href="{{.URL}}"{{if .Active}} aria-current="page"{{end}}>{{.Name}} ({{.Count}})</a>
    {{end}}
</nav>

{{range .Years}}
    <h2>{{.Year}}</h2>
    {{range .Documents}}
        <article id="{{.ID}}">
            <a href="{{.URL}}">{{.Title}}</a>
            {{if .External}}external link{{else}}{{.FileType}}, {{.SizeText}}{{end}}
            <time datetime="{{.Date.Format "2006-01-02"}}">{{date .Date}}</time>
            {{.Description}}
        </article>
    {{end}}
{{end}}

.Documents holds the same documents as one list, for a design without years. .FileType is the extension in capitals (PDF) and .SizeText the size for people (1.2 MB); both are empty for an external link. .Description is the rendered Markdown and .Downloads the count so far. .Date is a calendar day, so {{date .Date}} shows the same day in every site timezone.

Give each document id="{{.ID}}": site search results link to the document on its list as #<ID>.

A document appears in each language it has a title in.

Show documents anywhere

documents returns up to a number of the newest published documents in the current language, for a home page or a sidebar; 0 or less returns all of them. documentsIn returns the documents of one category, newest first, by the category's name in the current language (ignoring case) or by its slug.

{{range documents "downloads" 5}}
    <a href="{{.URL}}">{{.Title}}</a> {{.FileType}} {{.SizeText}}
{{end}}

{{range documentsIn "downloads" "Minutes"}}
    <a href="{{.URL}}">{{.Title}}</a>
{{end}}

<a href="{{moduleURL "downloads"}}">All downloads</a>

The download links work on every page, also when the instance is not placed on a page; .CategoryURL is then empty.

Visitors

A document's .URL is its download address, /_/downloads/<id>/download. A visit adds one to the count and redirects to the file in the media library or to the external link. HEAD requests, such as those of link checkers, are not counted. Unknown, unpublished and deleted documents get the site's 404 page, 404.gohtml. The redirect and that page are sent with Cache-Control: no-store and X-Robots-Tag: noindex. A download that fails for another reason, such as an unreachable database, gets the site's error page, 500.gohtml.

Every request counts; there is no limit per visitor. Links straight to the media file work too, but are not counted.

Content API

While the site's content API is on (Config.API), /api/v1/<key>/documents and /api/v1/<key>/categories serve the published documents and their categories as JSON, in the language of ?locale=, or the site's default language without it. With the instance downloads:

Endpoint Route name Answers with
/api/v1/downloads/documents api.downloads.documents The published documents, newest first, as a list
/api/v1/downloads/documents?category=<slug> The documents of one category; a category without published documents answers 404 not_found
/api/v1/downloads/documents/{id} api.downloads.document One document
/api/v1/downloads/categories api.downloads.categories The categories of the published documents, in alphabetical order, as a list

Documents have no page of their own, so the list gives each document whole: its id, title and description_html, its category, null without one, its date at midnight UTC, download_url, external, file, url, api_url and updated_at. download_url is the document's download address on the site, which counts the download as the site's links do. external is true for a link to another site, which has no file; file has the filename, mime_type, file_type, such as PDF, and size in bytes. url is the document on the list of its category, or on the whole list, such as https://example.com/en/downloads#<id>, and null while the instance is on no page. A category has its slug, name, document_count, its list as url and its documents in the API as documents_url. The API has no download counts, which change with every download.

/documents/{id} sends no Last-Modified, and neither do the lists: a document's category holds document_count, which another document changes. The ETag answers If-None-Match; see Caching.

Editing

Each instance has its own section in the admin (/admin/downloads; /admin is the default admin path), which shows how often each document was downloaded.

  • A document has a date and either a file or an external http(s) link, not both. Upload the file in the media library first, then choose it under File. The date, file and link are shared by all languages.
  • Per language a document has a title, a category and a Markdown description.
  • Categories are free text: documents with the same category name belong together, and names that differ only in case or punctuation are one category. Type the name in each language to translate it.
  • There are no revisions: saving changes the document at once, also when it is published. Publish and Unpublish show or hide it; publishing needs a title in the site's default language.
  • The site sorts documents by date. The order in the admin (Move up, Move down) only decides between documents of the same date.
  • Saving a document keeps its download count. A file that a document uses cannot be deleted from the media library.
  • Move to trash moves a document to the trash, where it can be restored, with its download count, or deleted for good. Until it is deleted for good, its file stays in use.

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

Once the instance is on a page, published documents appear in the site search, found by title, category and description, and the category addresses appear in sitemap.xml.

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

From Go

documents.Of(app, "downloads") returns the instance once the app has started:

downloads := documents.Of(app, "downloads")

latest, err := downloads.Latest(ctx, "en", 5)
minutes, err := downloads.InCategory(ctx, "en", "Minutes")

While the instance is on no page in that language, both still return the documents with their download address as URL, and an empty CategoryURL.

Download(ctx, id) counts a download and returns the address to send the visitor to, for a download route of your own.

Store() gives write access for imports and seed data, through the same rules the admin uses. New documents are saved unpublished; the date is a day at midnight UTC:

store := downloads.Store()

item, err := store.Save(ctx, content.Draft[documents.Fields, documents.Text]{
    Fields: documents.Fields{
        Link: "https://example.com/annual-report-2025.pdf",
        Date: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC),
    },
    Translations: []content.Translation[documents.Text]{
        {Locale: "en", Text: documents.Text{Title: "Annual report 2025", Category: "Reports"}},
    },
})
if err != nil {
    return err
}

err = store.Publish(ctx, item.ID, "", "en")

For a file, upload it with app.Media.Upload(ctx, "report.pdf", reader) and set Fields.File to the asset's ID.

  • Publishing: a post can link to a document's download address, so those downloads are counted too.
  • Lists keeps short ordered content that is not a file, such as an FAQ.

Documentation

Overview

Package documents offers files for download: minutes, reports, forms and policies. Each document has a date, a file from the media library or a link to an external address, and per language a title, a category and a Markdown description. Downloads are counted. Add an instance with:

app.Use(documents.Module("documents", "Documents"))

Upload files in the media library first and choose them in the document editor. When the module is mounted on a page it serves the list at "/" and the list of one category at "/category/{slug}"; the category slug is made from the category name. The website renders:

documents/list.gohtml  ListView: documents newest first, also per year,
                       and the categories to filter on

Document.URL is the download address /_/<key>/<id>/download. It counts the download and redirects to the file or the external link, so it works on every page, also where the module is not mounted. Templates anywhere on the site can list documents without Go code:

{{range documents "documents" 5}}<a href="{{.URL}}">{{.Title}}</a> {{.FileType}} {{.SizeText}}{{end}}
{{range documentsIn "documents" "Minutes"}}{{.Date.Format "2 January 2006"}} {{.Title}}{{end}}

documentsIn matches the category name in the current language, ignoring case, or the category slug.

The site search finds published documents by title, description and category once the module is on a page. A result links to the document on the list (of its category), at #<ID>: give each document id="{{.ID}}".

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Module

func Module(key, title string) webthings.Module

Module adds a Documents 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 APICategory

type APICategory struct {
	Slug string `json:"slug"`
	Name string `json:"name"`
	// DocumentCount is the number of published documents in the category.
	DocumentCount int `json:"document_count"`
	// URL is the category's list on the site, null while the instance is
	// on no page; DocumentsURL its documents in the API.
	URL          *string `json:"url"`
	DocumentsURL string  `json:"documents_url"`
}

APICategory is a category of published documents in one language, at /api/v1/<key>/categories and with each of its documents. Categories are the documents' category names; the slug is made from the name.

type APIDocument

type APIDocument struct {
	ID              string `json:"id"`
	Title           string `json:"title"`
	DescriptionHTML string `json:"description_html"`
	// Category is null for a document without a category.
	Category *APICategory `json:"category"`
	// Date is the document's own date, such as that of a meeting, at
	// midnight UTC.
	Date time.Time `json:"date"`
	// DownloadURL is the site's download address of the document, which
	// counts the download and leads to the file or the external link, as
	// on the site. External is set for a link, which has no File.
	DownloadURL string   `json:"download_url"`
	External    bool     `json:"external"`
	File        *APIFile `json:"file"`
	// URL is the document on the list of its category, or on the whole
	// list (#<id>), null while the instance is on no page; APIURL the
	// document in the API.
	URL       *string   `json:"url"`
	APIURL    string    `json:"api_url"`
	UpdatedAt time.Time `json:"updated_at"`
}

APIDocument is a published document in the content API (see package api), at /api/v1/<key>/documents/{id} and in the list of /api/v1/<key>/documents. Documents have no page of their own: the list shows them whole, so the list has their description too.

type APIFile

type APIFile struct {
	Filename string `json:"filename"`
	MIMEType string `json:"mime_type"`
	// FileType is the file's extension in capitals, such as "PDF"; Size is
	// in bytes.
	FileType string `json:"file_type"`
	Size     int64  `json:"size"`
}

APIFile is the file of a document from the media library.

type Category

type Category struct {
	Name   string
	Slug   string
	URL    string
	Count  int
	Active bool
}

Category is a category with published documents in one language.

type Document

type Document struct {
	ID          string
	Title       string
	Description template.HTML
	Category    string
	// CategoryURL lists the document's category, empty without a category or
	// when the module is not mounted.
	CategoryURL string
	// Date is a calendar date (see frontend.Civil): {{date .Date}} shows the
	// same day in every site timezone.
	Date time.Time
	// URL counts a download and redirects to the file or the link.
	URL string
	// External is true for a link instead of a file; the file details below
	// are then empty.
	External bool
	Filename string
	MIMEType string
	// FileType is the file's extension in capitals, for example "PDF".
	FileType string
	Size     int64
	// SizeText is Size for people, for example "1.2 MB".
	SizeText  string
	Downloads int
}

Document is a published document as templates see it.

type Fields

type Fields struct {
	File string    `db:"file_id,null"`
	Link string    `db:"link"`
	Date time.Time `db:"document_date"`
}

Fields are the language-independent parts of a document. A document has either a File or a Link. Date is the document's own date, for example the date of a meeting, stored as midnight UTC of that day.

type Item

type Item = content.Item[Fields, Text]

Item is a document with its translations, as the store keeps it.

type ListView

type ListView struct {
	// Title is the title of the page the module is mounted on, or the
	// category name when the list is filtered.
	Title string
	// Documents are newest first, filtered by Category when it is set.
	Documents []Document
	// Years hold the same documents per year, newest first.
	Years []Year
	// Categories are all categories with documents, in alphabetical order.
	Categories []Category
	// Category is the active filter, or nil.
	Category *Category
	// AllURL lists all documents.
	AllURL string
}

ListView is the data of documents/list.gohtml.

type Service

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

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

func Of

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

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

func (*Service) Download

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

Download counts a download of a published document and returns where it is: the file's URL or the external link. It returns content.ErrNotFound for unknown and unpublished documents.

func (*Service) InCategory

func (s *Service) InCategory(ctx context.Context, locale, category string) ([]Document, error)

InCategory returns the published documents in locale whose category name is category, ignoring case, or whose category slug is category.

func (*Service) Latest

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

Latest returns up to limit published documents in locale, newest first. A limit of 0 or less returns all of them.

func (*Service) Store

func (s *Service) Store() content.Store[Fields, Text]

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

type Text

type Text struct {
	Title       string `db:"title"`
	Description string `db:"description"`
	Category    string `db:"category"`
}

Text is a document in one language. Description is Markdown.

type Year

type Year struct {
	Year      int
	Documents []Document
}

Year holds the documents of one year, newest first.

Jump to

Keyboard shortcuts

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