SEO configuration

You supply piko.SEOConfig through the piko.WithSEO(...) functional option, not through a YAML seo: block. It controls sitemap.xml, robots.txt, hreflang alternates, and per-page and per-item overrides. SEO generation is active when a caller passes WithSEO with a non-empty Sitemap.Hostname. Passing the option is itself the enable signal, so you can leave Enabled alone. This page enumerates every field, type, template attribute, environment variable, and flag. For task recipes see the how-to guides on sitemap and robots.txt and a multilingual sitemap.

Enabling SEO

Three functional options configure the SEO surface. WithSitemapURLProvider and WithRouteSource both require WithSEO.

func WithSEO(seoConfig config.SEOConfig) Option
func WithSitemapURLProvider(provider func(ctx context.Context) ([]seo_dto.SitemapURLInput, error)) Option
func WithRouteSource(source seo_domain.RouteSource) Option
OptionMeaning
piko.WithSEOProvides the SEO configuration for sitemap and robots.txt generation, and enables SEO. Requires a non-empty sitemap hostname. Omit the option to disable SEO.
piko.WithSitemapURLProviderRegisters a build-time, in-process provider of additional sitemap URLs for dynamic routes whose slugs come from application data instead of content collections. Piko ignores a nil provider. Requires WithSEO.
piko.WithRouteSourceRegisters a composable build-time RouteSource for a page bound to it with the p-route-source directive, expanding param values against the page's real route pattern so localised paths and hreflang are correct. Preferred over WithSitemapURLProvider for dynamic pages templated over a Go registry. Piko ignores a nil source. Requires WithSEO.

SEOConfig

type SEOConfig struct {
    Robots  RobotsConfig
    Sitemap SitemapConfig
    Enabled bool
}
FieldTypeMeaning / default
RobotsRobotsConfigrobots.txt settings. See RobotsConfig.
SitemapSitemapConfigsitemap.xml settings. See SitemapConfig.
EnabledboolPiko sets this to true when it stores the configuration, because supplying WithSEO is the enable signal. A bool cannot distinguish an omitted field from an explicit false, so an explicit false here does not disable SEO. Omit WithSEO instead. Piko logs a notice when it enables SEO for a configuration that did not set this.

SitemapConfig

type SitemapConfig struct {
    Sitemaps              map[string]SitemapChunkConfig
    CacheMaxAgeSeconds    *int
    DiscoverImages        *bool
    Hostname              string
    Exclude               []string
    Sources               []string
    Defaults              SitemapEntryDefaults
    RouteRules            []SitemapRouteRule
    MaxURLsPerSitemap     int
    IncludeAuthGatedPages bool
    GitLastMod            bool
}
FieldTypeMeaning / default
Sitemapsmap[string]SitemapChunkConfigNamed sitemap chunks for large sites. Each chunk gets its own sources, and the builder lists it in a sitemap index.
CacheMaxAgeSeconds*intCache-Control max-age in seconds for the served sitemap.xml, its chunks, and robots.txt. nil uses 600; 0 disables caching.
HostnamestringCanonical base URL (for example https://example.com). Required to build full URLs; SEO is inactive when empty.
Exclude[]stringGlob patterns for routes to leave out of the sitemap.
Sources[]stringRuntime API endpoints, each returning a JSON array of SitemapURLInput.
DefaultsSitemapEntryDefaultsDefault values for sitemap entry fields. See SitemapEntryDefaults.
RouteRules[]SitemapRouteRulePer-route SEO metadata matched by glob, without editing pages. See SitemapRouteRule.
MaxURLsPerSitemapintWhen the URL count exceeds this value, the builder splits the sitemap and generates an index. Default 5000; validated min=1,max=50000.
DiscoverImages*boolAutomatically discover and include images in the sitemap. nil takes the default of true; pass new(false) to omit them. It is a pointer because the defaults merge cannot tell a plain false apart from an unset field.
IncludeAuthGatedPagesboolInclude AuthPolicy-gated pages in the sitemap. Default false (excluded).
GitLastModboolDerive a static page's <lastmod> from its last git commit date instead of file mtime. Default false.

SitemapEntryDefaults

type SitemapEntryDefaults struct {
    ChangeFreq string
    Priority   float32
}
FieldTypeMeaning / default
ChangeFreqstringDefault changefreq. Default weekly; validated oneof=always hourly daily weekly monthly yearly never.
Priorityfloat32Default priority for entries. Default 0.5; validated min=0,max=1.

SitemapChunkConfig

type SitemapChunkConfig struct {
    Sources []string
}
FieldTypeMeaning / default
Sources[]stringRuntime API endpoints for this named sitemap chunk.

RobotsConfig

type RobotsConfig struct {
    CustomRules                []RobotsRuleGroup
    NeverIndex                 bool
    PreviewDeployment          bool
    BlockAiBots                bool
    BlockNonSeoBots            bool
}
FieldTypeMeaning / default
CustomRules[]RobotsRuleGroupCustom robots.txt rules per user agent. Appended after the base and bot-blocking groups.
NeverIndexboolThis project must never appear in a search index, in any environment. A property of the project. Blocks the base group at generation and at serve time, and no deploy setting, environment variable or run mode lifts it. A CustomRules group that grants a named user agent Allow: / still applies to that agent, because a crawler obeys the most specific group that matches it. Default false. See Indexing behaviour.
PreviewDeploymentboolThis deploy is a copy of the site, not the live one. A property of the deploy. Piko reads it only when serving robots.txt and never writes it into an artefact. Default false. See Indexing behaviour.
BlockAiBotsboolBlock known AI crawler bots (the seo_dto.AIBots list). Default false. See AI and non-SEO bot lists.
BlockNonSeoBotsboolBlock known non-SEO web scrapers (the seo_dto.NonSEOBots list). Default false. See AI and non-SEO bot lists.

RobotsRuleGroup

type RobotsRuleGroup struct {
    UserAgents []string
    Disallow   []string
    Allow      []string
}
FieldTypeMeaning / default
UserAgents[]stringUser agent names this group applies to. Use "*" for all bots. Validated required,min=1.
Disallow[]stringURL path patterns these user agents cannot crawl.
Allow[]stringURL path patterns these user agents may crawl, overriding more general Disallow rules.

Indexing behaviour

Indexability is two independent questions. NeverIndex answers "is this project ever meant to be public?", a fixed property of the software. PreviewDeployment answers "is this deploy the live one?", a property that varies between deploys of the same build.

NeverIndexPreviewDeploymentrun signalGenerated artefactServed
trueanyanyDisallow: /Disallow: /
falsetruenone (generate mode) or prodAllow: /Disallow: /
falsefalsenone (generate mode) or prodAllow: /as generated
falseanydev / dev-iDisallow: /Disallow: /

Serving only ever tightens. It can add a block but never remove one, so a build that already baked a block stays blocked whatever the deploy declares.

Piko logs every case, including the permissive one, so the build output states the indexing posture instead of leaving you to assume it.

PreviewDeployment is deliberately absent from the generated artefact. Baking it in would produce a block that no later deploy of that artefact could lift, which defeats the purpose of promoting one build through environments.

Only a run mode supplies a production signal: WithProductionMode(true) for prod, false for dev and dev-i. The generate modes (all, manifest, assets, sql) supply none, because a generate mode says what to build, not where the result lands. With no signal the SEO service assumes production, so an unwired path fails open instead of de-indexing a live site.

The build writes robots.txt, and in production the server then serves it unchanged from the registry, because the production daemon wires no coordinator. The dev and dev-i daemons do regenerate it on every rebuild, and those rebuilds always write a blocking file so a development run cannot leave a permissive one behind.

A Disallow asks a crawler not to fetch a URL. It does not remove a URL already in the index, and a crawler you have blocked never reads a noindex directive in the body. Both NeverIndex and PreviewDeployment therefore also set X-Robots-Tag: noindex, nofollow on every response, which is what actually keeps a site out of an index.

BlockAiBots and BlockNonSeoBots are independent of all the above. Each, when true, appends an extra group with Disallow: / targeting its fixed user-agent list.

AI and non-SEO bot lists

BlockAiBots blocks the seo_dto.AIBots list: GPTBot, ChatGPT-User, ClaudeBot, anthropic-ai, Applebot-Extended, Bytespider, CCBot, cohere-ai, Diffbot, FacebookBot, Google-Extended, ImagesiftBot, PerplexityBot, OmigiliBot, Omigili.

BlockNonSeoBots blocks the seo_dto.NonSEOBots list: AhrefsBot, SemrushBot, DotBot, Baiduspider, Nuclei, WikiDo, Riddler, PetalBot, Zoominfobot, Go-http-client, Node/simplecrawler, CazoodleBot, dotbot/1.0, Gigabot, Barkrowler, BLEXBot, magpie-crawler.

SitemapRouteRule

type SitemapRouteRule struct {
    Pattern    string
    Priority   *float32
    ChangeFreq string
    Robots     string
    Exclude    bool
}
FieldTypeMeaning / default
PatternstringGlob matched against the route pattern. Uses the same matcher as Sitemap.Exclude.
Priority*float32Overrides priority for matching routes; nil inherits. Validated min=0,max=1.
ChangeFreqstringOverrides changefreq for matching routes; empty inherits. Validated oneof=always hourly daily weekly monthly yearly never.
RobotsstringRobots rule for matching routes. When it contains "noindex", the builder drops the routes from the sitemap.
ExcludeboolWhen true, removes matching routes from the sitemap entirely.

The builder evaluates rules first-match-wins against the route pattern. Glob semantics use filepath.Match, plus a trailing ** that matches every route sharing the preceding prefix, so Pattern values such as /, /blog/**, and /*/search/* match as expected. The overall precedence for priority and changefreq is:

  1. Page declaration (a per-page override on PageSEOMetadata, from a p-* template attribute or collection-item frontmatter).
  2. Matching route rule.
  3. Configured Sitemap.Defaults.

The resolved order runs page declaration, then route rule, then defaults.

RouteSource is a build-time enumerator of the concrete URLs for a page whose dynamic segment is not a content collection. Register one with WithRouteSource and bind a page to it with the p-route-source directive.

type RouteSource interface {
    Name() string
    Enumerate(ctx context.Context, rc RouteContext) ([]RouteURL, error)
}

type RouteSourceFunc struct {
    Fn         func(ctx context.Context, rc RouteContext) ([]RouteURL, error)
    SourceName string
}

type RouteContext struct {
    SourceName    string
    RoutePattern  string
    ParamName     string
    DefaultLocale string
    Strategy      string
    Locales       []string
}

type RouteURL struct {
    ParamValue string
    Locales    []string
    Alternates []AlternateLink
    SEO        SitemapURLInput
}

RouteContext fields work as follows. SourceName is the p-route-source value that bound the source to the page. RoutePattern is the brace-retaining route pattern (for example /services{locationslug}/kubernetes). ParamName is the dynamic segment the source enumerates (for example locationslug). DefaultLocale, Strategy, and Locales carry the i18n configuration. RouteContext exposes the method Expand(paramValue, locale string) string. Expand applies the strategy, substitutes the param value, and percent-encodes each path segment so the emitted URL matches the served route.

RouteURL fields work as follows. ParamValue substitutes into the pattern's {param} (for example -jersey). An empty Locales falls back to RouteContext.Locales. When a source populates Alternates, the builder uses it verbatim instead of automatic cross-linking. SEO carries the per-URL lastmod, changefreq, priority, images, videos, and news.

type SitemapURLInput struct {
    News         *NewsInputEntry   `json:"news,omitempty"`
    Location     string            `json:"loc"`
    LastMod      string            `json:"lastmod,omitempty"`
    ChangeFreq   string            `json:"changefreq,omitempty"`
    Images       []string          `json:"images,omitempty"`
    Videos       []VideoInputEntry `json:"videos,omitempty"`
    ImageEntries []ImageInputEntry `json:"imageEntries,omitempty"`
    Alternates   []AlternateLink   `json:"alternates,omitempty"`
    Priority     float32           `json:"priority,omitempty"`
}

SitemapURLInput is the JSON input shape for both Sources endpoints and WithSitemapURLProvider. The builder normalises LastMod to YYYY-MM-DD on ingest. ImageEntries takes precedence over Images, and Priority is 0.0 to 1.0.

type AlternateLink struct {
    Rel      string `xml:"rel,attr"`
    Hreflang string `xml:"hreflang,attr"`
    Href     string `xml:"href,attr"`
}

AlternateLink is one hreflang alternate link for a sitemap URL. Rel must be "alternate". Hreflang is a language code (for example en, en-GB, fr, es-MX), and Href is the absolute alternate URL.

A representative RouteSource contribution using the RouteSourceFunc adapter:

piko.WithRouteSource(piko.RouteSourceFunc{
    SourceName: "office-locations",
    Fn: func(ctx context.Context, rc piko.RouteContext) ([]piko.RouteURL, error) {
        return []piko.RouteURL{
            {
                ParamValue: "-jersey",
                Locales:    []string{"en", "fr"},
                SEO: piko.SitemapURLInput{
                    LastMod:    "2026-07-09",
                    ChangeFreq: "monthly",
                    Priority:   0.8,
                },
            },
        }, nil
    },
})

Per-source caps and skip conditions

  • The builder caps a build-time WithSitemapURLProvider contribution at 100000 URLs, then truncates and logs the excess.
  • The builder caps all build-time RouteSources combined at 100000 URLs, counted after per-locale fan-out. On reaching the cap, enumeration stops, and the builder truncates to the cap and logs the event.
  • The builder skips a RouteURL whose ParamValue is "", ., or ...
  • The builder skips and logs a page declaring p-route-source without a p-param (empty param name).
  • The builder skips and logs a page naming an unregistered source.
  • The builder excludes a page from the sitemap when it matches Sitemap.Exclude, is auth-gated without IncludeAuthGatedPages, has a Robots value containing "noindex" (from PageSEOMetadata or a matching route rule), or matches a route rule with Exclude: true.
  • The builder additionally filters discovered pages by an indexability check. It drops any route still containing a { placeholder, or any segment starting with ! (such as /!404 or /!error). The builder deliberately does not apply this brace and bang filter to route-source pages.
  • The builder deduplicates URLs by Location, with discovered URLs winning over provider, dynamic, and route-source URLs, then sorts by Location.

Per-page and per-item overrides

Two override surfaces feed seo_dto.PageSEOMetadata: <template>-tag attributes on a page component, and reserved frontmatter keys on a collection item. For recipes see the sitemap and robots.txt and multilingual sitemap how-tos. This section is the field-list home.

Template attributes

These are attributes on the page's <template> tag.

AttributeValueEffect
p-route-sourcesource nameBinds the page to a build-time RouteSource that enumerates its dynamic URLs.
p-paramsegment nameNames the dynamic route segment the bound RouteSource enumerates (for example locationslug). No default for route-source binding.
p-noindexpresence-onlyKeeps the page out of the sitemap and marks it noindex.
p-sitemap-priority0.0-1.0 stringOverrides the sitemap priority.
p-sitemap-changefreqchangefreq stringOverrides the sitemap changefreq.
p-canonicalURLSets an explicit canonical URL for the page.

A page binds to a RouteSource by declaring p-route-source and p-param on its <template> tag, with a matching {param} segment in its file path:

<template p-route-source="stores" p-param="locationslug">
  <main>
    <h1>{{ props.Store.Name }}</h1>
  </main>
</template>

PageSEOMetadata

type PageSEOMetadata struct {
    LastModified     *time.Time
    Priority         *float32
    News             *NewsInputEntry
    RobotsRule       string
    ChangeFrequency  string
    Canonical        string
    SupportedLocales []string
    ImageURLs        []string
    Videos           []VideoInputEntry
}
FieldTypeMeaning / default
LastModified*time.TimeLast-changed date; nil falls back to file modification time.
Priority*float32Sitemap priority override; nil inherits route rule or default.
News*NewsInputEntryOptional news sitemap entry.
RobotsRulestringRobots meta value; a value containing "noindex" drops the page from the sitemap.
ChangeFrequencystringChangefreq override; empty inherits route rule or default.
CanonicalstringExplicit canonical URL; empty lets the framework derive one.
SupportedLocales[]stringLanguage codes for hreflang alternates.
ImageURLs[]stringImage URLs for the image sitemap extension.
Videos[]VideoInputEntryVideo sitemap entries.

NewsInputEntry fields (all string): PublicationName, PublicationLanguage, PublicationDate, Title.

Collection-item frontmatter keys

For a collection item (one markdown file), the following keys under the item's page frontmatter map onto PageSEOMetadata. Basic overrides:

KeyTypeEffect
noindexboolWhen true, sets RobotsRule to "noindex" (drops the item from the sitemap).
changefreqstringSets ChangeFrequency.
canonicalstringSets Canonical.
prioritynumber or numeric stringSets Priority. The builder rejects non-finite values.

Rich-media overrides (flat scalar keys). The builder emits a video entry only when both sitemapVideoTitle and sitemapVideoThumbnail are present. It emits a news entry only when both sitemapNewsPublication and sitemapNewsDate are present:

KeyTypeEffect
sitemapImagestringAppended to ImageURLs (renders <image:image>).
sitemapVideoTitlestringVideo title (required to emit a video entry).
sitemapVideoThumbnailstringVideo thumbnail (required to emit a video entry).
sitemapVideoDescriptionstringVideo description.
sitemapVideoPlayerstringPlayer location.
sitemapVideoContentstringContent location.
sitemapVideoDatestringPublication date.
sitemapVideoDurationintDuration in seconds, clamped to 0-28800.
sitemapNewsPublicationstringNews publication name (required to emit a news entry).
sitemapNewsDatestringNews date (required to emit a news entry).
sitemapNewsLanguagestringNews publication language.
sitemapNewsTitlestringNews article title.

The builder derives an item's last-modified from the standard UpdatedAt then PublishedAt metadata keys.

Multi-locale opt-in

A page opts into locale routing by declaring a SupportedLocales() function in its <script type="application/x-go"> block. The sitemap builder then enrols the page in the full configured locale set (from piko.WithWebsiteConfig(...) I18n.Locales), not the literal list it returns. For a page with more than one locale, the sitemap builder emits one self-referential <xhtml:link rel="alternate"> per locale. It also emits one hreflang="x-default" pointing at the default-locale variant. A single-locale page emits no alternates. i18n strategies are prefix, prefix_except_default, query-only (default), and disabled.

Environment variables and flags

Piko itself reads no environment variables or flags for SEOConfig. The configuration reaches the framework only through piko.WithSEO(...), which never passes through the configuration loader, so Piko consults none of the names below at runtime. They are metadata for a host application that chooses to run its own environment or flag pass over the struct. To vary SEO settings per environment, read the variable in your own code and pass the result to piko.WithSEO.

The default column is accurate. Piko applies those defaults when it stores the configuration, so a field you omit gets the documented value.

Fields not listed below (Sitemaps, Defaults as a whole, RouteRules, CustomRules, and every SitemapRouteRule and RobotsRuleGroup field) are YAML/JSON only.

Environment variableFlagDefaultField
PIKO_SEO_ENABLEDseoEnabledtrueSEOConfig.Enabled
PIKO_SEO_SITEMAP_CACHE_MAX_AGEsitemapCacheMaxAge600SitemapConfig.CacheMaxAgeSeconds
PIKO_SEO_SITEMAP_HOSTNAMEsitemapHostname(none)SitemapConfig.Hostname
PIKO_SEO_SITEMAP_EXCLUDEsitemapExclude(none)SitemapConfig.Exclude
PIKO_SEO_SITEMAP_SOURCESsitemapSources(none)SitemapConfig.Sources
PIKO_SEO_SITEMAP_MAX_URLSsitemapMaxUrls5000SitemapConfig.MaxURLsPerSitemap
PIKO_SEO_SITEMAP_DISCOVER_IMAGESsitemapDiscoverImagestrueSitemapConfig.DiscoverImages
PIKO_SEO_SITEMAP_INCLUDE_AUTH_GATEDsitemapIncludeAuthGatedfalseSitemapConfig.IncludeAuthGatedPages
PIKO_SEO_SITEMAP_GIT_LASTMODsitemapGitLastModfalseSitemapConfig.GitLastMod
PIKO_SEO_SITEMAP_DEFAULT_CHANGEFREQsitemapDefaultChangeFreqweeklySitemapEntryDefaults.ChangeFreq
PIKO_SEO_SITEMAP_DEFAULT_PRIORITYsitemapDefaultPriority0.5SitemapEntryDefaults.Priority
PIKO_SEO_ROBOTS_BLOCK_AI_BOTSrobotsBlockAiBotsfalseRobotsConfig.BlockAiBots
PIKO_SEO_ROBOTS_BLOCK_NON_SEO_BOTSrobotsBlockNonSeoBotsfalseRobotsConfig.BlockNonSeoBots
(none)robotsNeverIndexfalseRobotsConfig.NeverIndex
(none)robotsPreviewDeploymentfalseRobotsConfig.PreviewDeployment

See also

Used in. Any project supplying piko.WithSEO(...) to generate sitemap.xml and robots.txt.