# How Hugo’s Permalink Patterns Are Defined and Resolved at Build Time

> Learn how Hugo's permalink patterns work. Hugo expands tokens like :year and :slug at build time to create dynamic page URLs.

- Repository: [GoHugo.io/hugo](https://github.com/gohugoio/hugo)
- Tags: internals
- Published: 2026-02-28

---

**Hugo resolves permalink patterns at build time by parsing configuration into a `PermalinkExpander`, which substitutes tokens like `:year`, `:slug`, or `:sections[2]` with page-specific values to generate the final URL path.**

Hugo’s permalink system allows you to define URL structures via configuration in [`hugo.toml`](https://github.com/gohugoio/hugo/blob/main/hugo.toml) (or [`config.toml`](https://github.com/gohugoio/hugo/blob/main/config.toml)). During the build, the `gohugoio/hugo` engine compiles these patterns into executable functions that extract attributes from each page—such as dates, titles, or section paths—and assemble the destination URL. This article traces the full resolution pipeline from configuration parsing to final path generation.

---

## 1. Loading the Permalink Configuration

Hugo reads the `permalinks` map from site configuration into a generic `map[string]any`. The function `DecodePermalinksConfig` in [`resources/page/permalinks.go`](https://github.com/gohugoio/hugo/blob/main/resources/page/permalinks.go) normalizes this into a structured `map[string]map[string]string` where the outer key is the content *kind* (`pages`, `sections`, `taxonomies`, `terms`) and the inner key is the section name.

```go
// resources/page/permalinks.go
func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, error) {
    // …initialize per‑kind maps…
    // Handle entries like:
    //   [permalinks]          → default for pages & terms
    //   [permalinks.blog]     → per‑section overrides
    //   [permalinks.section]  → kind‑specific map
}

```

This ensures that a pattern defined under `[permalinks.posts]` applies only to the `posts` section, while a top‑level `[permalinks]` entry acts as a fallback.

---

## 2. Building the Permalink Expander

Once the configuration is decoded, Hugo constructs a `PermalinkExpander` via `NewPermalinkExpander`. This object stores:

* A reference to the `urlize` function (used to sanitize strings for URLs).
* A cache of compiled patterns.
* A map of **known permalink attributes**—callbacks that extract data from a `Page` object.

```go
// resources/page/permalinks.go
func NewPermalinkExpander(urlize func(string) string,
    patterns map[string]map[string]string) (PermalinkExpander, error) {

    p := PermalinkExpander{
        urlize:       urlize,
        patternCache: hmaps.NewCache[string, func(Page) (string, error)](),
    }

    // Register the built‑in attribute callbacks (date, slug, etc.)
    p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
        "year":    p.pageToPermalinkDate,
        "slug":    p.pageToPermalinkSlugElseTitle,
        // …
    }

    // Parse each kind‑specific pattern set
    for kind, patterns := range patterns {
        e, err := p.parse(patterns)
        if err != nil { return p, err }
        p.expanders[kind] = e
    }
    return p, nil
}

```

The `knownPermalinkAttributes` map is the heart of the system: it associates tokens like `:year` or `:slug` with Go functions that fetch the relevant value from the page.

---

## 3. Parsing a Pattern into an Executable Function

When `Expand` is called for a specific section, the expander looks up the raw pattern string (e.g., `/:section/:year/:slug/`) and compiles it via `getOrParsePattern`.

The compilation process:

1. **Escape handling** – literal colons are escaped as `\:` and replaced with a placeholder to avoid mis‑interpretation.
2. **Token detection** – a regex `: \w+ (\[.+?\])?` identifies tags like `:year` or `:sections[2:4]`.
3. **Callback resolution** – each token is mapped to its callback in `knownPermalinkAttributes`. Slice syntax (`[n]`, `[n:m]`, `[last]`) is parsed by `toSliceFunc`.
4. **Function generation** – the final result is a closure that, given a `Page`, iterates over the callbacks and concatenates their outputs with the static parts of the pattern.

```go
// resources/page/permalinks.go
func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string, error), error) {
    // Normalise escape sequences
    pattern, normalized := l.normalizeEscapeSequencesIn(pattern)

    // Find all :tags
    matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
    // … build callbacks slice …
    // Return a closure that replaces each tag with its resolved value.
}

```

---

### 3.1 Built‑in Attribute Callbacks

The following tokens are available in any permalink pattern:

| Token | Callback | Description |
|-------|----------|-------------|
| `:year` | `pageToPermalinkDate` | 4‑digit year from `Page.Date` |
| `:month` | `pageToPermalinkDate` | 2‑digit month |
| `:day` | `pageToPermalinkDate` | 2‑digit day |
| `:title` | `pageToPermalinkTitle` | URL‑safe version of `Page.Title` |
| `:slug` | `pageToPermalinkSlugElseTitle` | `Page.Slug` if defined, otherwise title |
| `:sections` | `pageToPermalinkSections` | Full section path (e.g., `blog/2024`) |
| `:sectionslugs` | `pageToPermalinkSectionSlugs` | Section slugs only |
| `:sections[n]` | `toSliceFunc` | Slice syntax to pick specific section levels |
| `\: ` | *escape* | Literal colon character |

**Slice syntax** allows fine‑grained control over deep section hierarchies. For example, `:sections[2:4]` selects the 2nd through 4th path segments, while `:sections[last]` grabs the deepest level.

Source: <https://github.com/gohugoio/hugo/blob/master/resources/page/permalinks.go#L998-L1048>

---

## 4. Computing the Expanded Permalink for a Page

During the site build, Hugo’s page assembler invokes the expander. In [`hugolib/page__paths.go`](https://github.com/gohugoio/hugo/blob/main/hugolib/page__paths.go), the `Expand` method is called with the page’s section and the page itself:

```go
// hugolib/page__paths.go (excerpt)
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
if err != nil { return desc, err }
if opath != "" {
    opath, _ = url.QueryUnescape(opath)
    opath = path.Clean(opath)
    desc.ExpandedPermalink = opath
}

```

The returned string (`opath`) is URL‑decoded and cleaned (removing redundant slashes or dots) before being stored in `desc.ExpandedPermalink`.

Source: <https://github.com/gohugoio/hugo/blob/master/hugolib/page__paths.go#L90-L100>

---

## 5. Building the Final URL from the Expanded Permalink

The `ExpandedPermalink` is consumed by `CreateTargetPaths` in [`resources/page/page_paths.go`](https://github.com/gohugoio/hugo/blob/main/resources/page/page_paths.go). This function assembles the final relative and absolute URLs for the page and its associated resources (e.g., [`index.html`](https://github.com/gohugoio/hugo/blob/main/index.html), page bundles).

```go
// resources/page/page_paths.go (excerpt)
if d.Kind != kinds.KindHome && d.URL == "" && d.Section.Base() != "/" {
    if d.ExpandedPermalink != "" {
        pb.Add(d.ExpandedPermalink)   // <-- use the expanded pattern
    } else {
        pb.Add(d.Section.Base())
    }
    // …
}

```

If `ExpandedPermalink` is present, it overrides the default section‑based path, ensuring the custom pattern appears in both the relative link (`tp.Link`) and the absolute permalink (`tp.Permalink`).

Source: <https://github.com/gohugoio/hugo/blob/master/resources/page/page_paths.go#L155-L170>

---

## 6. Configuration Example  

The following [`hugo.toml`](https://github.com/gohugoio/hugo/blob/main/hugo.toml) demonstrates a common setup:

```toml
[permalinks]
  # Default for all pages

  pages = "/:section/:year/:month/:slug/"
  
  # Override for the 'blog' section only

  [permalinks.blog]
    posts = "/:year/:month/:day/:title/"

```

Given a page with `date: 2024-03-15`, `slug: "hugo-guide"`, and section `blog`, the final URL becomes:

```

https://example.org/2024/03/15/hugo-guide/

```

If the slug were omitted, the `:slug` token would fall back to the URL‑safe title.

---

## Summary  

- **Configuration parsing** – `DecodePermalinksConfig` turns the TOML map into a structured `map[kind][section]pattern`.  
- **Expander construction** – `NewPermalinkExpander` registers attribute callbacks for tokens like `:year`, `:slug`, and `:sections[n]`.  
- **Pattern compilation** – `getOrParsePattern` compiles regex matches into a closure that replaces tokens with page data, supporting slice syntax and escaped colons.  
- **Per‑page expansion** – [`page__paths.go`](https://github.com/gohugoio/hugo/blob/main/page__paths.go) calls `Expand(section, page)` to generate the `ExpandedPermalink` string.  
- **URL assembly** – `CreateTargetPaths` in [`page_paths.go`](https://github.com/gohugoio/hugo/blob/main/page_paths.go) injects the expanded value into the final relative and absolute URLs.  

---

## Frequently Asked Questions  

### How do I select only specific section levels in a permalink pattern?  

Use **slice syntax** inside the `:sections` token. For example, `:sections[2]` returns only the second path segment, while `:sections[1:3]` returns the first through third segments. The special index `last` grabs the deepest section level. This logic is handled by `toSliceFunc` in [`resources/page/permalinks.go`](https://github.com/gohugoio/hugo/blob/main/resources/page/permalinks.go).

### What happens if a page lacks a slug but my pattern uses `:slug`?  

The `:slug` token maps to `pageToPermalinkSlugElseTitle`, which checks `Page.Slug`. If the slug is empty, it falls back to the URL‑safe version of the page title. This ensures every page receives a valid path component even when no explicit slug is defined in front matter.

### Can I include literal colons in my permalink pattern?  

Yes. To prevent Hugo from interpreting a colon as a token prefix, escape it with a backslash: `\:`. During pattern parsing, `normalizeEscapeSequencesIn` temporarily replaces `\:` with a placeholder, performs token substitution, then restores the literal colon in the final string.

### How does Hugo prioritize permalink settings when multiple patterns exist?  

Hugo evaluates the `permalinks` map using a **kind → section** hierarchy. First, it looks for a pattern matching the page’s specific kind (`pages`, `sections`, `taxonomies`, or `terms`). Within that kind, it matches the exact section name (e.g., `blog`). If no section-specific rule exists, it falls back to the default pattern for that kind, or the global default if none is defined.