Documentation
¶
Overview ¶
Package content stores the CMS's pages and their editable content (blocks) in Postgres. Pages carry per-locale metadata; blocks carry the text and HTML that fill the editable regions declared in the host application's templates.
Index ¶
- Constants
- Variables
- func NormalizeSlug(s string) string
- func SearchText(htmlText string) string
- func Slugify(title string) string
- func Snippet(body string, terms []SearchTerm, words int) string
- func ValidEditorTheme(t string) bool
- func ValidFeed(s string) bool
- func ValidMode(m string) bool
- func ValidSlug(s string) bool
- func ValidVisibility(s string) bool
- type Block
- type Feed
- type Kind
- type MenuItem
- type MenuItemInput
- type Page
- type PageMeta
- type PathViews
- type Post
- type RestoreResult
- type SearchResult
- type SearchTerm
- type SectionInput
- type SiteSettings
- type SitemapEntry
- type Snapshot
- type SnapshotBlock
- type SnapshotCode
- type SnapshotMeta
- type SnapshotPage
- type Status
- type Store
- func (s *Store) All(ctx context.Context, locale string) ([]Page, error)
- func (s *Store) AllNonPost(ctx context.Context, locale string) ([]Page, error)
- func (s *Store) AllNonPostPage(ctx context.Context, locale string, limit, offset int) ([]Page, error)
- func (s *Store) BlocksFor(ctx context.Context, pageID int64, locale string, status Status) ([]Block, error)
- func (s *Store) CountNonPost(ctx context.Context) (int, error)
- func (s *Store) CountPosts(ctx context.Context, feed Feed, publishedOnly bool) (int, error)
- func (s *Store) CountSearch(ctx context.Context, terms []SearchTerm, locale string) (int, error)
- func (s *Store) Counts(ctx context.Context) (pages, posts int, err error)
- func (s *Store) Delete(ctx context.Context, id int64) error
- func (s *Store) DeleteLocaleContent(ctx context.Context, pageID int64, locale string) error
- func (s *Store) DiscardDraft(ctx context.Context, pageID int64) error
- func (s *Store) Duplicate(ctx context.Context, srcID int64, slug, title, locale string) (int64, error)
- func (s *Store) EffectiveBlocks(ctx context.Context, pageID int64, locale string, status Status) ([]Block, error)
- func (s *Store) EffectiveBlocksWithShared(ctx context.Context, pageID int64, locale string, status Status) (page, shared []Block, err error)
- func (s *Store) GetByID(ctx context.Context, id int64, locale string) (*Page, error)
- func (s *Store) GetBySlug(ctx context.Context, slug, locale string, publishedOnly bool) (*Page, error)
- func (s *Store) HasSharedUnpublishedChanges(ctx context.Context) (bool, error)
- func (s *Store) HasUnpublishedChanges(ctx context.Context, pageID int64) (bool, error)
- func (s *Store) Insert(ctx context.Context, p *Page, locale string) (int64, error)
- func (s *Store) InsertPost(ctx context.Context, p *Post, locale string) (int64, error)
- func (s *Store) MenuItems(ctx context.Context, menu string) ([]MenuItem, error)
- func (s *Store) MetaFor(ctx context.Context, pageID int64, locale string) (PageMeta, error)
- func (s *Store) PageViewsByDay(ctx context.Context, from, to time.Time) (map[string]int, error)
- func (s *Store) PostByID(ctx context.Context, id int64, locale string) (*Post, error)
- func (s *Store) PostByPageID(ctx context.Context, pageID int64, locale string, draft bool) (*Post, error)
- func (s *Store) Posts(ctx context.Context, feed Feed, locale string, publishedOnly bool, limit int) ([]Post, error)
- func (s *Store) PostsPage(ctx context.Context, feed Feed, locale string, publishedOnly bool, ...) ([]Post, error)
- func (s *Store) PrunePageViews(ctx context.Context, before time.Time) error
- func (s *Store) PruneVersions(ctx context.Context, pageID int64, keep int) error
- func (s *Store) Publish(ctx context.Context, pageID int64) error
- func (s *Store) PublishAs(ctx context.Context, pageID int64, by *int64) error
- func (s *Store) PublishShared(ctx context.Context) error
- func (s *Store) PublishSharedAs(ctx context.Context, by *int64) error
- func (s *Store) PublishedSnapshot(ctx context.Context, pageID int64) (*Snapshot, error)
- func (s *Store) RecordPageView(ctx context.Context, day time.Time, path string) error
- func (s *Store) ReindexAll(ctx context.Context) (int, error)
- func (s *Store) ReindexPage(ctx context.Context, pageID int64) error
- func (s *Store) ReplaceDraftSections(ctx context.Context, pageID int64, region, locale string, ...) error
- func (s *Store) ReplaceMenu(ctx context.Context, menu string, items []MenuItemInput) error
- func (s *Store) RestoreVersion(ctx context.Context, pageID, versionID int64) (*RestoreResult, error)
- func (s *Store) SaveSiteSettings(ctx context.Context, in SiteSettings) error
- func (s *Store) Search(ctx context.Context, terms []SearchTerm, locale string, limit, offset int) ([]SearchResult, error)
- func (s *Store) SearchIndexEmpty(ctx context.Context) (bool, error)
- func (s *Store) SetLocales(locales []string)
- func (s *Store) SetSiteLocked(ctx context.Context, on bool) error
- func (s *Store) SetSiteMode(ctx context.Context, mode string) error
- func (s *Store) SetSitemap(ctx context.Context, on bool) error
- func (s *Store) SetVersionsKept(n int)
- func (s *Store) SetVisibility(ctx context.Context, pageID int64, v Visibility) error
- func (s *Store) SharedBlocks(ctx context.Context, locale string, status Status) ([]Block, error)
- func (s *Store) SitePageID(ctx context.Context) (int64, error)
- func (s *Store) SiteSettings(ctx context.Context) (SiteSettings, error)
- func (s *Store) SitemapPages(ctx context.Context, limit int) ([]SitemapEntry, error)
- func (s *Store) TopPages(ctx context.Context, from, to time.Time, limit int) ([]PathViews, error)
- func (s *Store) Unpublish(ctx context.Context, pageID int64) error
- func (s *Store) Update(ctx context.Context, p *Page, locale string) error
- func (s *Store) UpdateMeta(ctx context.Context, pageID int64, locale string, m PageMeta) error
- func (s *Store) UpdatePost(ctx context.Context, p *Post, locale string) error
- func (s *Store) UpsertDraftBlock(ctx context.Context, pageID int64, region, locale string, kind Kind, ...) error
- func (s *Store) UpsertSharedBlock(ctx context.Context, region, locale string, kind Kind, content string) error
- func (s *Store) VersionSnapshot(ctx context.Context, pageID, versionID int64) (*Version, *Snapshot, error)
- func (s *Store) Versions(ctx context.Context, pageID int64) ([]Version, error)
- type Version
- type VersionKind
- type Visibility
Constants ¶
const ( ModeProduction = "production" ModeDevelopment = "development" )
The two site modes. A site under construction sits in development, where the CMS asks search engines to keep it out of their indexes; the switch to production is what makes it findable.
The empty value reads as production, so a site that predates this setting — or one whose settings were never saved — keeps behaving exactly as it did. New installs are seeded into development instead, which is the safe end to start from.
const ( EditorThemeDark = "dark" EditorThemeLight = "light" )
The in-place editor's two colour schemes. The empty value reads as dark, so a site that predates this setting keeps the chrome it has always had.
const DefaultSnippetWords = 30
DefaultSnippetWords is how many words of context a result shows around the words that matched. Enough for a sentence and a bit, which is what a reader needs to tell whether this is the page they meant.
const DefaultVersionsKept = 50
DefaultVersionsKept is how many editions of a page PruneVersions keeps when the caller names no limit. Payloads hold a page's entire content, so history is not free; fifty editions is far more than anyone scrolls through and still bounds a busy page's share of the table.
const SiteSlug = "__site"
SiteSlug is the reserved slug of the site page — the system row that owns shared-region content ({{cmsShared "footer"}}). It holds an underscore, so ValidSlug rejects it and no editor-created page can ever take it.
Variables ¶
var ( // ErrNotFound is returned when no page matches the query. ErrNotFound = errors.New("content: page not found") // ErrDuplicateSlug is returned by Insert/Update when the slug is taken. ErrDuplicateSlug = errors.New("content: slug already in use") )
Functions ¶
func NormalizeSlug ¶
NormalizeSlug lowercases s and trims whitespace and surrounding slashes, so "/About-Us/" becomes "about-us".
func SearchText ¶ added in v1.2.1
SearchText renders a block's stored HTML as the plain text the search index holds: no markup, no entities, no runs of whitespace.
Three things happen in an order that matters.
Every "<" is padded with a space first. bluemonday removes tags without putting anything in their place, so "<p>one</p><p>two</p>" would come out as "onetwo" — one word that appears on no page, and two words that can no longer be found. Padding is cruder than walking the tree and asking which elements are block-level, and it is also right in every case: an extra space between two words is collapsed away below, and a missing one is a wrong index. It is safe because it only ever inserts whitespace ahead of a "<" that bluemonday is about to consume anyway.
Then the sanitize, which drops the markup and the invisible elements' contents.
Then the entities are decoded, because bluemonday escapes what it emits — it is built to produce HTML, and this is the one caller that wants text. Without it the index would hold "&" where the page says "&", and a search for "R&D" would find nothing.
func Slugify ¶
Slugify derives a URL slug from a human title: lowercased, diacritics stripped, everything else hyphenated — "Café & Bar!" becomes "cafe-bar". Returns "" when nothing usable remains.
func Snippet ¶ added in v1.2.1
func Snippet(body string, terms []SearchTerm, words int) string
Snippet cuts the part of an indexed body worth showing under a search result: a window of words around the first term that matched, with an ellipsis on whichever side was cut.
It works on words rather than characters because the body is already space-normalized (SearchText leaves exactly one space between words), so a window of words is a window of whole words, and a snippet never ends mid-syllable.
Matching is deliberately looser here than in the index: a substring test, case-folded, with no stemming. The index decides *which* pages come back; this only decides where to point in one of them, and a stemmed match ("returning" for "return") should still be pointed at rather than sending the reader to the top of the page.
With no term found — the match was on the title, or on a stemmed form no substring test finds — the opening words are returned, which is the same thing a page's own summary would have said.
func ValidEditorTheme ¶ added in v1.1.16
ValidEditorTheme reports whether t is a theme that may be stored: one of the two named schemes, or "" for the dark default.
func ValidMode ¶ added in v0.9.2
ValidMode reports whether m is a mode that may be stored: one of the two named modes, or "" for the production default.
func ValidSlug ¶
ValidSlug reports whether s is empty (the homepage) or made of slash-separated segments of lowercase letters, digits, and hyphens.
func ValidVisibility ¶
ValidVisibility reports whether s names a known visibility.
Types ¶
type Block ¶
type Block struct {
ID int64
PageID int64
Region string
Locale string
Status Status
Sort int
Kind Kind
SnippetKey *string
Content string
Settings map[string]string // section presentation settings (e.g. bg, width)
}
Block is one unit of editable content inside a page region. Simple regions (cmsText/cmsRegion/cmsImage) use a single block at sort 0; sections regions (cmsSections) hold an ordered list of blocks, one per section, with presentation settings.
type Feed ¶
type Feed string
Feed is which of the two post feeds a post belongs to. Blog and news are the same engine — a post's feed decides which listings and RSS feed it appears in, and the slug prefix its page lives under.
type Kind ¶
type Kind string
Kind distinguishes short plain text ({{cmsText}}) from rich HTML ({{cmsRegion}}) block content.
type MenuItem ¶
type MenuItem struct {
ID int64
Menu string
Sort int
Label string // default-locale label
Labels map[string]string // per-locale overrides, e.g. {"fr": "À propos"}
PageID *int64
URL string
NewTab bool
ParentID *int64
PageSlug *string // nil when the item is a literal URL
PageStatus *Status
PageVisibility *Visibility
}
MenuItem is one entry in a navigation menu, with the linked page's slug and status joined in for URL resolution and visibility filtering.
type MenuItemInput ¶
type MenuItemInput struct {
Label string // default-locale label
Labels map[string]string // per-locale overrides; nil stores {}
PageID *int64
URL string
NewTab bool
Children []MenuItemInput
}
MenuItemInput is one entry supplied to ReplaceMenu. A label-only entry (no page, no URL) is a dropdown parent; Children go one level deep and may not have children of their own.
type Page ¶
type Page struct {
ID int64
Slug string // "" is the homepage; otherwise e.g. "about" or "about/team"
TemplateName string // template file within the host's TemplateFS
Status Status
Visibility Visibility
HeadCSS string // extra per-page CSS (or raw head markup), injected by cmsHead
BodyJS string // extra per-page JS (or raw markup), injected by cmsScripts
Title string
Description string
// MetaDescription is what search engines are told the page is about,
// when that should differ from Description. It exists for posts,
// whose Description is the summary shown in listings and feeds — a
// blurb written for readers browsing a list, which is not always the
// line worth showing under a search result. Empty means "use
// Description", so an ordinary page, whose Description is already its
// meta description, never sets it. Read MetaTag rather than this
// field to get the words a page actually publishes.
MetaDescription string
CreatedAt time.Time
UpdatedAt time.Time
}
Page is a site page. Title and Description are the metadata for the locale the page was loaded with.
type PageMeta ¶
type PageMeta struct {
Title string
Description string
MetaDescription string
// The default locale's stored values. All are empty when the
// metadata read is the default locale's own.
InheritedTitle string
InheritedDescription string
InheritedMetaDescription string
}
PageMeta is one locale's page metadata exactly as stored, without the default-locale fallback Page.Title and Page.Description carry. Inherited holds what that fallback would supply, so a caller editing a translation can offer the inherited words as a placeholder rather than prefilling the field with them — prefilling would copy the default language into the translation's own row on the next save, and the page would stop tracking the original for good.
It is also what UpdateMeta writes, so a save states every stored field rather than a list of strings whose order is the only thing keeping the title out of the description.
type Post ¶
type Post struct {
Page
PostID int64
Feed Feed
PublishedAt time.Time // display and ordering date, not a schedule
AuthorID *int64
AuthorName string // resolved from cms_users; "" when the author is gone
// HideAuthor drops the byline from the post: a site that publishes as
// itself — a notice, a release, a listing — wants the date without
// the name of whoever typed it. It hides the byline only; the author
// stays recorded, so clearing it brings the same name back. Stated
// as "hide" so the zero value is the ordinary bylined post.
HideAuthor bool
// A post's thumbnail is either a library image or a bare URL. The
// library is the normal case: the id is stored and ThumbnailMediaID
// and Thumbnail are set, which lets the renderer pick the rendition
// that fits the slot and build a srcset from the rest. The URL field
// carries an image the library does not hold — an absolute URL
// elsewhere, or a path the host site serves itself — and is empty
// whenever an id is set.
//
// The banner at the top of a post is not here: it is a section in the
// post template's header region, so it comes with the settings every
// section has and follows the draft/publish flow.
ThumbnailMediaID *int64
ThumbnailURL string // optional listing thumbnail, when not from the library
Thumbnail *media.Media // joined library record, nil when there is none
}
Post is one blog or news entry. The embedded Page is its backing page — the post's body lives in that page's blocks and is edited exactly like any page (in place, with sections and snippets); the page's Title and Description double as the post's title and summary. Post slugs are always prefixed with the feed name, e.g. "blog/launch-day".
func (*Post) ThumbnailMediaIDValue ¶
ThumbnailMediaIDValue returns the thumbnail's media id, or 0 when the post has no library thumbnail — convenient in templates, where pointers are awkward to compare.
type RestoreResult ¶ added in v1.2.0
type RestoreResult struct {
// CodeRecreated names the blocks the edition carried that the library
// no longer held, and which were put back from it. Recreating one is
// safe precisely because nothing else can be using it: it was gone.
CodeRecreated []string
// CodeChanged names the blocks the library still holds under a
// different body. They are left alone — the library is shared, and a
// button labelled "restore this page" must not rewrite a widget that
// other pages show — so this is the list to tell someone about rather
// than to act on.
CodeChanged []string
}
RestoreResult reports what a restore did beyond the page's own content: what became of the custom-code blocks the edition depended on. Both lists are ordered by key, and both are empty on the ordinary restore of a page whose blocks are all still exactly as they were.
type SearchResult ¶ added in v1.2.1
type SearchResult struct {
PageID int64
Locale string
// Kind is "page", "blog" or "news" — what the result is, so a results
// template can badge or group them.
Kind string
Slug string
// Title and Summary are the page's published metadata for this locale.
// Summary is a post's summary or an ordinary page's description, and
// is empty on a page that has neither.
Title string
Summary string
// Body is the page's whole indexed text, which the caller cuts a
// snippet out of (see Snippet). It is carried rather than snipped in
// SQL because the only portable way to snip around a match is to have
// the text — Postgres has ts_headline for it and MySQL has nothing.
Body string
// PublishedAt is a post's display date, nil on an ordinary page.
PublishedAt *time.Time
// Rank is the engine's score for this hit. Comparable within one set
// of results and meaningless between two — it exists for ordering and
// for looking at when the ordering seems wrong.
Rank float64
}
SearchResult is one hit from the site search: enough to draw a result without going back for the page.
type SearchTerm ¶ added in v1.2.1
type SearchTerm = dialect.SearchTerm
SearchTerm is one unit of a parsed query; see dialect.SearchTerm.
func ParseSearchQuery ¶ added in v1.2.1
func ParseSearchQuery(q string) []SearchTerm
ParseSearchQuery turns what a visitor typed into terms the engines can both be asked about.
The grammar is the one every search box has taught people to expect, and nothing more: words are AND-ed, "a quoted phrase" must appear intact, and a leading - excludes. Everything else a visitor might type — the punctuation that means something to one engine's query language and something else to the other's, or nothing at all — is dropped here rather than passed on. That is what keeps a stray bracket from being a syntax error on MySQL and a stray & from being an AND on Postgres.
Characters are kept when they are letters, digits, or marks: the test is Unicode's, not ASCII's, so a search in Greek or Japanese survives it. Apostrophes and hyphens inside a word are kept too — "l'entreprise" and "e-mail" are one word to a reader and should be one term here.
A query with no positive term ("-cat" alone, or nothing but punctuation) yields nil: there is nothing to look for, only things to avoid, and neither engine agrees with the other about what that means.
type SectionInput ¶
SectionInput is one section supplied to ReplaceDraftSections.
type SiteSettings ¶
type SiteSettings struct {
MenuAlign string // "left", "center", "right", or "" (host default)
SiteName string
LogoURL string // "" = no logo
// FaviconURL is the site's browser-tab icon, emitted by cmsHead as
// <link rel="icon">. "" leaves the host template's own icon (or the
// browser's /favicon.ico guess) alone.
FaviconURL string
// aren't logged in, pointing at the admin login page.
LoginInNav bool
// opens a search box. It shows only where there is somewhere for it
// to lead: a host that configured no Config.SearchTemplate has no
// results page, and the icon stays off however this is set.
SearchInNav bool
// SiteCSS and SiteJS are injected raw into every public page (via
// cmsHead/cmsScripts). Each holds plain code or full markup — <style>,
// <link>, and <script> tags pass through as-is. Editing them is
// admin-only, like per-page code.
SiteCSS string
SiteJS string
// SiteMeta is written raw into the <head> of every public page,
// ahead of everything else the CMS emits: the verification tags a
// search console or an analytics service asks to be pasted in, and
// any other site-wide <meta> or <link> the host template does not
// already carry. Admin-only for the same reason SiteCSS is — it is
// markup nobody sanitizes.
SiteMeta string
// Mode is ModeDevelopment or ModeProduction (or "", read as
// production). Development asks search engines to leave the site
// alone; see Development. Changing it is superadmin-only.
Mode string
// RobotsTxt is the site's own /robots.txt, served verbatim once the
// site is in production. "" — the default — leaves the path to the
// host, which is what a site that predates this setting keeps
// getting. Development ignores it and serves its own Disallow; see
// Development. Editing it is superadmin-only, like Mode.
RobotsTxt string
// Sitemap makes the CMS serve a sitemap of every published, public
// page at /sitemap.xml. Off — the default — leaves that address to
// the host app, so an upgrade never shadows a sitemap it already
// serves; SeedAdmin turns it on for brand-new sites. A site in
// development serves none regardless: it has nothing it wants found.
// Switching it is superadmin-only, like Mode.
Sitemap bool
// Locked closes the site to everyone but superadmins: every public
// address answers 503 while it is on, and only the login page, the
// admin behind it, and whatever the host named exempt stay
// reachable. It is the switch for the
// hours a site has to be off — a botched import, a price list that
// went out wrong, a rebuild mid-flight — where the alternative is
// stopping the container and losing the admin with it.
//
// Unlike Mode this is access control, not a request to crawlers:
// nothing is served to anyone who is not signed in as a superadmin.
// It is deliberately separate from Mode, because the two answer
// different questions — a site can be live and findable and still
// need to be shut for an afternoon, and coming back must not mean
// remembering to switch indexing on again.
//
// Enforcement is the host's to mount: see (*cms.CMS).Lockdown.
// Switching it is superadmin-only, like Mode.
Locked bool
// NoticeBar shows the site-wide notice bar — a thin strip above
// everything else on every page, for the message the whole site has
// to carry at once: a holiday closure, a delivery delay, a service
// interruption. Its words are not here: they live in the shared
// region render.NoticeRegion, so they translate, sanitize, and
// publish exactly like a footer does. These three settings are the
// bar itself.
NoticeBar bool
// NoticeStyle names the bar's colour scheme, one of the curated keys
// in render.NoticeStyles. "" is the first of them.
NoticeStyle string
// NoticeDismissible gives the bar a close button, and remembers the
// dismissal in the visitor's browser until the notice's words
// change. Off, the bar stays until it is switched off here.
NoticeDismissible bool
// EditorTheme is the colour scheme of the in-place editor's own
// chrome — the edit bar, the tool rail, the floating block and
// section toolbars, and TinyMCE's formatting toolbar. "" and
// EditorThemeDark are the dark chrome the editor has always worn;
// EditorThemeLight swaps it for a pale one, which is what a site
// with a dark design of its own needs: dark chrome on a dark page
// stops reading as chrome at all.
EditorTheme string
}
SiteSettings are the site-wide presentation settings the in-place editor's "Site settings" dialog manages. Zero values mean "not set" — templates fall back to their own defaults.
func (SiteSettings) Development ¶ added in v0.9.2
func (s SiteSettings) Development() bool
Development reports whether the site is in development mode, and so should be kept out of search results.
This is a request to well-behaved crawlers, not access control: the site is still served to anyone who asks for it. Keeping an unfinished site genuinely private is the host's job — HTTP auth, an IP allowlist, or simply not pointing a public name at it.
type SitemapEntry ¶ added in v1.1.3
SitemapEntry is one page as a sitemap sees it: where it lives and when it last changed. No metadata — a sitemap lists addresses, and the titles and descriptions belong to the pages themselves.
type Snapshot ¶ added in v1.2.0
type Snapshot struct {
V int `json:"v"`
Page SnapshotPage `json:"page"`
Meta []SnapshotMeta `json:"meta"`
Blocks []SnapshotBlock `json:"blocks"`
// Code is the custom-code library entries the blocks above refer to,
// frozen with them and ordered by key. A page's markup holds only an
// inert placeholder naming a key (see snippets.CodeSnippet), so
// without this an edition would restore a page whose widgets had
// since been rewritten — or, where the key was deleted, whose widgets
// were gone. Absent in format 1, and on a page that names none.
Code []SnapshotCode `json:"code,omitempty"`
}
Snapshot is a page's published content frozen at one moment: the JSON document stored in cms_page_versions.payload.
The field names are the payload's wire format. They are stable data on disk, not an internal struct — renaming one silently orphans every version already written — so they are spelled out rather than defaulted.
func (*Snapshot) BlocksFor ¶ added in v1.2.0
BlocksFor returns an edition's blocks for one locale, shaped as the live blocks a renderer takes, with the same region-level fallback to defaultLocale that EffectiveBlocks applies: a region with no rows in the requested locale reads as the default locale's wholesale. It is how a stored edition is previewed through the real site templates.
pageID is stamped on each block because a snapshot does not record one — it is content, and the page it belongs to is the row that holds it.
func (*Snapshot) MetaFor ¶ added in v1.2.0
func (snap *Snapshot) MetaFor(locale, defaultLocale string) SnapshotMeta
MetaFor returns an edition's metadata for one locale with the same field-level fallback the live read applies: an absent row, or an empty field within one, falls back to defaultLocale, so an edition of a French page that only ever had a title still previews with the English description.
type SnapshotBlock ¶ added in v1.2.0
type SnapshotBlock struct {
Region string `json:"region"`
Locale string `json:"locale"`
Sort int `json:"sort"`
Kind Kind `json:"kind"`
SnippetKey *string `json:"snippet_key,omitempty"`
Content string `json:"content"`
Settings map[string]string `json:"settings,omitempty"`
}
SnapshotBlock is one published block. It carries no id: block ids identify live rows, and a restore writes new ones, so recording them would freeze a number that means nothing by the time anyone reads it.
type SnapshotCode ¶ added in v1.2.0
type SnapshotCode struct {
Key string `json:"key"`
Name string `json:"name"`
HTML string `json:"html"`
}
SnapshotCode is one custom-code library entry as a page depended on it.
It is a copy of something the page does not own: the library is shared, and two pages can name the same key. That is why restoring puts one back only when the library no longer holds it — see RestoreVersion.
type SnapshotMeta ¶ added in v1.2.0
type SnapshotMeta struct {
Locale string `json:"locale"`
Title string `json:"title"`
Description string `json:"description"`
MetaDescription string `json:"meta_description"`
}
SnapshotMeta is one locale's published page metadata.
type SnapshotPage ¶ added in v1.2.0
type SnapshotPage struct {
TemplateName string `json:"template_name"`
HeadCSS string `json:"head_css"`
BodyJS string `json:"body_js"`
}
SnapshotPage holds the page-level fields that take part in the draft/publish workflow — the three cms_page_drafts stages and Publish copies onto cms_pages. Slug and visibility are not among them: 0021 kept those immediate because they are addressing rather than content.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes pages and blocks in Postgres. Reads for a non-default locale fall back to the default locale's values where the requested locale has none.
func NewStore ¶
NewStore returns a Store backed by db. defaultLocale is the fallback for per-locale reads (page metadata, blocks); pass the site's first configured locale. Empty defaults to "en".
func (*Store) All ¶
All returns every page with metadata for locale, ordered by slug, as the working copy — it backs admin listings.
func (*Store) AllNonPost ¶
AllNonPost returns every page that is not a post's backing page, ordered by slug — the admin Pages list, where posts appear under Blog & News instead.
func (*Store) AllNonPostPage ¶
func (s *Store) AllNonPostPage(ctx context.Context, locale string, limit, offset int) ([]Page, error)
AllNonPostPage is AllNonPost windowed: the limit pages starting offset in from the first. A non-positive limit returns everything, offset and all — there is no window to slide without one.
Slugs are unique, so ordering by slug is a total order: no page can straddle two pages of the list or be skipped between them.
func (*Store) BlocksFor ¶
func (s *Store) BlocksFor(ctx context.Context, pageID int64, locale string, status Status) ([]Block, error)
BlocksFor returns a page's blocks for one locale and publication state, ordered by region and sort.
func (*Store) CountNonPost ¶
CountNonPost is how many pages AllNonPost would return — what the admin's paginated Pages list needs to size its page links. It takes no locale: the metadata joins only decide which title a page is listed under, never whether it is listed.
func (*Store) CountPosts ¶
CountPosts is how many posts Posts would return for the same feed and publishedOnly with no limit — what a paginated listing needs to know how many pages it has. It takes no locale: the locale joins only decide which title a post is listed under, never whether it is listed.
func (*Store) CountSearch ¶ added in v1.2.1
CountSearch is how many documents match, for sizing a results pager. It runs the same filter as Search, so the page count always describes the results being shown.
func (*Store) Counts ¶
Counts returns how many non-post pages and how many posts exist — the numbers the admin shows beside its Pages and Blog & News nav entries.
func (*Store) Delete ¶
Delete removes a page and (via cascade) its metadata and blocks. The site page is not deletable — losing it would take every shared region with it — so it reads as not found.
func (*Store) DeleteLocaleContent ¶
DeleteLocaleContent removes a page's draft blocks and draft metadata row for one (non-default) locale, so the page reverts to default-locale fallback. Draft-side only: like any edit it goes live on the next Publish.
func (*Store) DiscardDraft ¶
DiscardDraft throws away a page's unpublished edits: the draft blocks and metadata are replaced by copies of the currently published ones and the staged page-level fields revert to the page row, so the editor returns to exactly what is live. The page's publication status is left unchanged. It is the inverse of Publish.
func (*Store) Duplicate ¶
func (s *Store) Duplicate(ctx context.Context, srcID int64, slug, title, locale string) (int64, error)
Duplicate copies the page srcID under a new slug: the page row itself (template, per-page CSS/JS), its metadata for every locale, and its draft blocks. title becomes the copy's title for locale; other locales keep the source's titles. The copy always starts as a draft, so the source's published blocks are not copied — the copy's first Publish snapshots the duplicated draft. Returns the new page's id, ErrDuplicateSlug when slug is taken, or ErrNotFound when the source page doesn't exist.
func (*Store) EffectiveBlocks ¶
func (s *Store) EffectiveBlocks(ctx context.Context, pageID int64, locale string, status Status) ([]Block, error)
EffectiveBlocks returns a page's blocks for locale with region-level fallback to the store's default locale: regions with no rows in the requested locale use the default locale's rows wholesale. Region-level (not per-block) because a sections region is one ordered document — interleaving two locales' section lists would be nonsense. Callers can tell fallback content apart by the blocks' Locale field.
func (*Store) EffectiveBlocksWithShared ¶
func (s *Store) EffectiveBlocksWithShared(ctx context.Context, pageID int64, locale string, status Status) (page, shared []Block, err error)
EffectiveBlocksWithShared returns a page's blocks and the site's shared blocks for one locale and publication state, in a single query. Both sets get the same region-level locale fallback EffectiveBlocks applies.
The two travel together because every page render needs both: shared regions are the site's chrome, so a separate round trip would be one more query on every request, for content that is the same on all of them.
func (*Store) GetByID ¶
GetByID returns the page with the given id, with metadata for locale. It reads the draft working copy: every caller is an admin screen.
func (*Store) GetBySlug ¶
func (s *Store) GetBySlug(ctx context.Context, slug, locale string, publishedOnly bool) (*Page, error)
GetBySlug returns the page with the given slug, with metadata for locale. With publishedOnly, draft pages are treated as not found and the page reads as the site serves it; without, it reads as the working copy, which is what the editor and preview want.
func (*Store) HasSharedUnpublishedChanges ¶
HasSharedUnpublishedChanges reports whether shared regions hold saved edits the site is not showing yet — the same probe HasUnpublishedChanges runs for a page, so the editor's status chip can count shared content as what it is: an unpublished edit visible on the page in front of you.
A site page that does not exist has no edits, rather than being an error: this runs on every editor render and must not take pages down.
func (*Store) HasUnpublishedChanges ¶
HasUnpublishedChanges reports whether a page has edits that the site is not yet showing: draft blocks or metadata differing from the published ones in any way (content, order, settings, or rows added/removed) in any locale, or staged page-level fields differing from the page row. Locale- blind because Publish snapshots every locale at once. The set-difference probes rely on EXCEPT, which MySQL only gained in 8.0.31 and MariaDB in 10.3 — that is where the CMS's MySQL floor comes from. The JSON cast and the NULL-safe comparison have no shared spelling, so both come from the dialect.
func (*Store) Insert ¶
Insert stores a new page and its metadata for locale, returning its id. New pages always start as drafts.
func (*Store) InsertPost ¶
InsertPost stores a new post and its backing page (always a draft) in one transaction, returning the post's id. The caller sets the page fields (Slug already feed-prefixed, TemplateName, Title, Description) and the post fields; a zero PublishedAt becomes now.
func (*Store) MenuItems ¶
MenuItems returns menu items ordered by menu and sort. An empty menu returns items for every menu (for rendering, which may need several).
func (*Store) MetaFor ¶
MetaFor returns the page's draft metadata for locale as stored: no fallback applied, so an empty field means this locale has none of its own and reads as the default locale's. It is the read behind an editing form, where Page's already-resolved Title and Description cannot tell an inherited value from an authored one.
func (*Store) PageViewsByDay ¶ added in v0.9.0
PageViewsByDay sums the recorded views per day over [from, to], both taken as UTC dates. The result maps "2006-01-02" keys to totals; days with no traffic are simply absent, so callers fill their own zeroes. The summing happens here rather than in SQL because SUM's result type is engine-flavoured (numeric, DECIMAL) while the per-row counters scan as plain integers everywhere — and a week holds few rows.
func (*Store) PostByID ¶
PostByID returns the post with the given post id, with page metadata for locale. It reads the working copy: every caller is an admin screen.
func (*Store) PostByPageID ¶
func (s *Store) PostByPageID(ctx context.Context, pageID int64, locale string, draft bool) (*Post, error)
PostByPageID returns the post backed by the given page, or ErrNotFound when the page is not a post. With draft it reads the working copy, which is what the in-place editor shows; without, what the site serves.
func (*Store) Posts ¶
func (s *Store) Posts(ctx context.Context, feed Feed, locale string, publishedOnly bool, limit int) ([]Post, error)
Posts returns a feed's posts newest first, with page metadata for locale. An empty feed returns both feeds (the admin's combined list). With publishedOnly, draft and private posts are omitted (the public view); without, editors see everything. A non-positive limit returns everything.
func (*Store) PostsPage ¶
func (s *Store) PostsPage(ctx context.Context, feed Feed, locale string, publishedOnly bool, limit, offset int) ([]Post, error)
PostsPage is Posts with an offset: the window of limit posts starting offset in from the newest, which is what a paginated listing asks for. A non-positive limit still returns everything, offset and all — there is no window to slide without one.
The ordering is total (published_at, then id), so no post can straddle two pages or be skipped between them the way an ordering with ties can.
func (*Store) PrunePageViews ¶ added in v0.9.0
PrunePageViews deletes counters for days before the given UTC date. The dashboard charts a week; keeping a season of history costs almost nothing and leaves room for a longer chart later, but the table should not grow forever, so Migrate calls this on every startup.
func (*Store) PruneVersions ¶ added in v1.2.0
PruneVersions deletes all but the newest keep editions of a page. A keep of zero or less means DefaultVersionsKept. Deleting nothing — a page with fewer editions than that — is not an error.
func (*Store) Publish ¶
Publish makes the page's draft content live: the published block set and metadata are replaced by copies of the draft ones, the staged page-level fields are copied onto the page row, and the page is marked published.
It attributes the edition it records to nobody. Use PublishAs when a person is behind the publish, so the page's history says who.
func (*Store) PublishAs ¶ added in v1.2.0
PublishAs is Publish, recording the edition it creates against the user with id by (nil when no account is responsible — a seed, a migration, the host application publishing on its own).
The snapshot is taken inside the publishing transaction, after the page has been made live and before the commit, so it captures exactly what the site is about to serve and a publish that fails leaves no edition behind claiming to have gone live. History that repeats itself is not stored: see SaveVersion.
func (*Store) PublishShared ¶
PublishShared makes the shared regions' draft content live. Every page shows shared content, so there is no page to publish it "on": it goes live alongside whichever page the editor published from.
func (*Store) PublishSharedAs ¶ added in v1.2.0
PublishSharedAs is PublishShared, attributing the edition to the user with id by.
The site page keeps its own history, like any page — which is what makes a footer recoverable at all, since shared content belongs to no page that could hold a version of it. It is also why an unchanged snapshot is never stored: this runs on every publish anywhere on the site, and almost none of those touched the footer.
func (*Store) PublishedSnapshot ¶ added in v1.2.0
PublishedSnapshot freezes a page's currently published content — what the site is serving — without storing anything. It is the read behind SaveVersion, and on its own it is how a caller compares an old edition against what is live.
func (*Store) RecordPageView ¶ added in v0.9.0
RecordPageView adds one to the counter for path on the given day (taken as a UTC date). Concurrent instances land on the same row; the upsert makes the increment atomic.
func (*Store) ReindexAll ¶ added in v1.2.1
ReindexAll rebuilds the entire search index from what is published, and reports how many pages it visited.
It exists for the two moments the incremental path cannot cover: an install that had content before it had a search index, and a change to how text is extracted, which makes every stored document a little wrong in the same way. Ordinary operation never needs it — Publish keeps the index current on its own.
Each page is its own transaction rather than the whole rebuild being one. A site's entire published content in a single transaction is a long lock over a table the public is reading from, and the alternative failure mode — an interrupted rebuild leaving some pages reindexed and some not — costs nothing, since running it again is the fix and running it twice is harmless.
func (*Store) ReindexPage ¶ added in v1.2.1
ReindexPage rebuilds one page's search documents on its own transaction. It is what every change outside the publish flow calls — a rename, a visibility switch, an unpublish, a post's date being edited — since all of those change what a result would say without touching a block.
func (*Store) ReplaceDraftSections ¶
func (s *Store) ReplaceDraftSections(ctx context.Context, pageID int64, region, locale string, sections []SectionInput) error
ReplaceDraftSections replaces a sections region's draft blocks with the given ordered list, atomically. An empty list clears the region.
func (*Store) ReplaceMenu ¶
ReplaceMenu replaces a menu's items with the given ordered tree, atomically. Menus have no draft state — changes are live on commit.
func (*Store) RestoreVersion ¶ added in v1.2.0
func (s *Store) RestoreVersion(ctx context.Context, pageID, versionID int64) (*RestoreResult, error)
RestoreVersion replaces a page's draft content with the edition versionID: its blocks, its metadata for every locale, and the page-level fields it staged. ErrNotFound when the version does not exist or belongs to another page.
It writes the working copy and nothing else, which is exactly what DiscardDraft does with the published rows as its source — the page's publication status, and what the site is serving, are untouched until someone publishes. That is the point: a rollback is reviewable, the editor and the preview show it first, and nothing reaches the public site by way of a button labelled "restore".
The current draft is overwritten. A caller with unpublished edits in hand should say so before calling this; HasUnpublishedChanges is the question to ask.
The custom-code blocks the edition depended on are handled as described on RestoreResult: put back when the library lost them, left alone when it still has them under some other body. Everything else the page points at — media, palette snippets, the site's shared regions — belongs to somebody else and is not touched at all.
func (*Store) SaveSiteSettings ¶
func (s *Store) SaveSiteSettings(ctx context.Context, in SiteSettings) error
SaveSiteSettings stores the settings, atomically. Like menus they have no draft state — a save is live on commit.
func (*Store) Search ¶ added in v1.2.1
func (s *Store) Search(ctx context.Context, terms []SearchTerm, locale string, limit, offset int) ([]SearchResult, error)
Search returns one window of results for a parsed query, best match first. An empty term list matches nothing — there is no query to run.
The ordering is total: page_id breaks a tie in rank, and it is unique within a locale, so no result can straddle two pages of a paginated listing or be skipped between them. That is the same property the post listings are careful to have, and for the same reason.
func (*Store) SearchIndexEmpty ¶ added in v1.2.1
SearchIndexEmpty reports whether the index holds no documents at all. It is how a host decides to run ReindexAll once at startup: an install that predates the search table has content and no index, and nothing else distinguishes it from a site with nothing published yet — for which the rebuild is instant anyway.
func (*Store) SetLocales ¶ added in v1.2.1
SetLocales tells the store which locales the site serves, so that publishing a page indexes it once per locale. Empty or nil leaves the store indexing the default locale alone, which is what a single-language site wants and what a Store built without this keeps doing.
It mirrors the site's Config.Locales rather than being read from the database because it is a fact about the host application, not about the content: a locale the site does not serve has no URL to put in a result.
func (*Store) SetSiteLocked ¶ added in v1.1.20
SetSiteLocked closes or opens the site on its own, leaving every other setting alone. It exists for the same reason SetSiteMode does, and for one more: a site locked by mistake has to be openable from a shell — one UPDATE against cms_settings, or a call to this — without a working admin to click through.
func (*Store) SetSiteMode ¶ added in v0.9.2
SetSiteMode stores the site mode on its own, leaving every other setting alone — what a fresh install's seeding wants, where writing a whole SiteSettings would mean inventing values for keys nobody has set yet.
func (*Store) SetSitemap ¶ added in v1.1.3
SetSitemap turns the generated sitemap on or off on its own, for the same reason SetSiteMode exists: seeding a new site sets this one key and has no opinion about the others.
func (*Store) SetVersionsKept ¶ added in v1.2.0
SetVersionsKept bounds how many editions of each page Publish keeps, oldest dropped first. Zero or less restores DefaultVersionsKept.
It is a knob for the operator rather than a site setting: payloads hold whole pages, so the right number is a question about the database, not about the site's content. The CMS points it at Config.PageVersionsKept.
func (*Store) SetVisibility ¶
SetVisibility changes who may view the page on the public site. It does not touch publication status or content.
func (*Store) SharedBlocks ¶
SharedBlocks returns just the site's shared blocks, with the same locale fallback.
func (*Store) SitePageID ¶
SitePageID returns the id of the site page, the row shared blocks hang off, creating it if it has gone missing. The migration writes it, so the insert is only reached by a database that has been emptied — a test harness truncating between cases, or a hand-cleaned install — and recreating it there is better than leaving shared content unsavable.
Only write paths need the id: reads reach the site page through a subquery, so rendering never pays for this.
func (*Store) SiteSettings ¶
func (s *Store) SiteSettings(ctx context.Context) (SiteSettings, error)
SiteSettings returns the stored site settings. Keys never saved come back as zero values, so a fresh install reads as "all defaults".
func (*Store) SitemapPages ¶ added in v1.1.3
SitemapPages returns every page a search engine may be pointed at: published, publicly visible, and not a system page. Posts come back alongside ordinary pages — a post is a page, so one pass covers both — ordered by slug.
UpdatedAt is the page row's, which moves when a page is published, unpublished, renamed, or has its visibility changed, and stays put while a draft is edited (those writes land on cms_blocks). That makes it the date the live page last changed, which is what a sitemap's lastmod means.
A non-positive limit returns everything; callers that serve the result in one document pass the protocol's ceiling.
func (*Store) TopPages ¶ added in v0.9.0
TopPages returns the most-viewed paths over [from, to] (UTC dates, inclusive), busiest first, at most limit of them. Ties break on path so the order is stable across renders. Summed in Go for the same reason PageViewsByDay is: the per-row counters scan as plain integers on every engine, and a season of a real site's counters is small.
func (*Store) Unpublish ¶
Unpublish takes a page off the public site. Draft and published content are left as they are.
func (*Store) Update ¶
Update saves a page's fields and its metadata for locale. It does not change publication status; use Publish and Unpublish for that.
Title, description, template and per-page code are staged: they land in the working copy and reach the site on the next Publish. Slug and visibility are not staged and take effect immediately.
func (*Store) UpdateMeta ¶
UpdateMeta saves only a page's per-locale metadata — how non-default- locale admin tabs save, since every other page field is locale- independent, and how the in-place editor's settings dialogs save. Like Update it writes the working copy, so the change reaches the site on the next Publish.
func (*Store) UpdatePost ¶
UpdatePost saves a post's fields and its backing page's fields and metadata for locale, in one transaction. Like Page updates it does not change publication status, and the author is fixed at creation.
The backing page's staged fields (title, description, template, per-page code) go to the working copy and reach the site on the next Publish; the slug and the cms_posts fields — feed, date, images — apply immediately.
func (*Store) UpsertDraftBlock ¶
func (s *Store) UpsertDraftBlock(ctx context.Context, pageID int64, region, locale string, kind Kind, content string) error
UpsertDraftBlock creates or updates the draft block at sort position 0 of a region — the single-block-per-region model used until snippets arrive.
func (*Store) UpsertSharedBlock ¶
func (s *Store) UpsertSharedBlock(ctx context.Context, region, locale string, kind Kind, content string) error
UpsertSharedBlock stores one shared region's draft content. It is UpsertDraftBlock aimed at the site page, so shared edits ride the same draft/publish workflow as page content.
func (*Store) VersionSnapshot ¶ added in v1.2.0
func (s *Store) VersionSnapshot(ctx context.Context, pageID, versionID int64) (*Version, *Snapshot, error)
VersionSnapshot returns one edition of a page: its metadata and the content frozen in it. ErrNotFound when no such version exists.
It takes the page id as well as the version id, and matches on both, so that a version id belonging to another page cannot be read — or, later, restored — through a route scoped to this one.
type Version ¶ added in v1.2.0
type Version struct {
ID int64
PageID int64
Kind VersionKind
Note string
// SavedBy is the account that published this edition, nil when it is
// unknown (published by something other than a person) or when the
// account has since been deleted.
SavedBy *int64
// SavedByName is that account's name, resolved from cms_users. Empty
// when SavedBy is nil, and also when the user is gone — history
// survives the account, so a version whose author was deleted still
// lists, just without a name.
SavedByName string
SavedAt time.Time
}
Version is one entry in a page's history without its content: what the list screen shows. The payload is deliberately absent, so listing a page with fifty editions does not drag fifty page-sized documents out of the database to render fifty dates.
type VersionKind ¶ added in v1.2.0
type VersionKind string
VersionKind is why a version was taken.
const ( // VersionPublish is an edition captured as the page went live. Every // version is one of these until manual snapshots exist. VersionPublish VersionKind = "publish" // VersionManual is a snapshot someone asked for by hand. VersionManual VersionKind = "manual" )
type Visibility ¶
type Visibility string
Visibility is who may view a page on the public site, independent of its publication status: a private page goes through the same draft/publish workflow but is only served to logged-in users once published.
const ( VisibilityPublic Visibility = "public" VisibilityPrivate Visibility = "private" )