Repository: gohugoio/hugo (language: go)

Issue:
Add segments config + --renderSegments flag
I'm doing some ground work now making Hugo much more effective for big data/page sets. One of the nice effects I see is that it takes Hugo much less time getting ready to render. A Hugo build basically looks like this:

1. Read and index data.
2. Render pages.

The flexibility of the Hugo API means that the second step always needs a full data set, even in a "partial render" situation. But the "render everything" comes at a cost (writing lots of file to disk etc.), which is especially true when we now may talk about "a million pages" and more.

There are a couple of situations where rendering only one or more segments of a site might come in handy:

1. When rendering everything in one go is too heavy for the server (but note that the upcoming Hugo should be much more efficient in this department).
2. When building at intervals and you have a segmented site with very different change frequency (e.g. "render the home page and the news section only").

You could argue that Hugo should be smart about "what's changed" and render just that (which is what we're doing in server mode), and you're right, but even then it's much simpler to determine _which segments have changed_.

I thought we could introduce a new concept of _named segments_ in the Site config (which we may also consider using for other things):

```toml
[segments]
[segments.hot]
[[segments.hot.matchers]]
# Need to also render the home page etc.
kind = '{home,term,taxonomy}'
[[segments.hot.matchers]]
path = '/news/**'
[segments.docs]
[[segments.docs.matchers]]
path = '/docs/**'
[segments.old]
[[segments.old.matchers]]
path = '/blog/**'
```

And then when building:

```
hugo --renderSegments hot,docs
```






Discussion / hints:
@bep I love this idea! I currently have to wait about 1.5mins for `hugo server` to run (and it crashes often, so I have to keep re-running it), and would love to specify a segment so that the build is a lot quicker. 
I like the general idea of building only parts of a site!

But in a most extreme case, if I only want to rebuild one specific arbitrary page of a project, it means I'ld have to define a segment for each page in the config.

Would be great to have something more "on the fly" like:

````
hugo --renderSegments path=/news/**,path=/docs/**
````

@McShelby yea, we can certainly do that ... but if we get "named segments" working, it should be straight forward to also do "ad hoc rendering".
In addition to this I'd find it useful to have 

- The new renderSegments work with languages too. We would love to skip rendering a whole site in another language in controlled scenarios. `HUGO_DISABLELANGUAGES` partially works in that regard but isn't quite its purpose. You end up loosing the context/knowledge of the disabled languages which can change the render output depending on your logic.
- The ability to give an explicit list of files to render from. The use case would be for a CI build being able to pass the git diff list of files that actually changed and hugo render and later deploy only those output files.

Something like `hugo --renderGitDiff  9b8eda3...0e87bfd` could work by emulating [git diff](https://git-scm.com/docs/git-diff). However we quite often clone a repo with almost no history to speed up the build time so passing an explicit list or file we generate elsewhere would still be valuable. 
>The new renderSegments work with languages too.

I agree. We kind of already have that implicit in the first comment, I think, as it uses the same "path matching" used elsewhere in Hugo, e.g{

```
[segments]
[segments.exotic_languages]
[[segments.exotic_languages.matchers]]
language = '{jp,nb}'
```

Etc.
Hi @bep,

I noticed that the milestone for the --renderSegments flag has been shifted multiple times. Is there a new estimated release date for this feature? It would be really beneficial for my projects and I am keen to know when it might be available.

Thanks!
How would data persist to the incremental build.

Take a complete build, then a incremental build of a named segment.

Given a file is added to a segment, how does the hot pages get to know to be re-rendered? Is is that just something to be abstracted above named segments.

Always rebuild hot pages. Meaning all data will have to be traversed for a changed segment only when a file has been added or removed. Or link changes.. idk

Does a file have a dependency graph?

Fix patch:
diff --git a/commands/commandeer.go b/commands/commandeer.go
index 1e5928ff97b..a0e4e383696 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -521,6 +521,7 @@ func applyLocalFlagsBuildConfig(cmd *cobra.Command, r *rootCommand) {
 	cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory")
 	_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
 	cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
+	cmd.Flags().StringSliceP("renderSegments", "", []string{}, "named segments to render (configured in the segments config)")
 	_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
 }
 
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index 9cafc876e15..2a1aefc931b 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -39,6 +39,7 @@ import (
 	"github.com/gohugoio/hugo/config/services"
 	"github.com/gohugoio/hugo/deploy/deployconfig"
 	"github.com/gohugoio/hugo/helpers"
+	"github.com/gohugoio/hugo/hugolib/segments"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/markup/markup_config"
 	"github.com/gohugoio/hugo/media"
@@ -137,6 +138,9 @@ type Config struct {
 	// a slice of page matcher and params to apply to those pages.
 	Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"`
 
+	// The segments defines segments for the site. Used for partial/segmented builds.
+	Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
+
 	// Menu configuration.
 	// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
 	Menus *config.ConfigNamespace[map[string]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"`
@@ -364,6 +368,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
 		CreateTitle:       helpers.GetTitleFunc(c.TitleCaseStyle),
 		IsUglyURLSection:  isUglyURL,
 		IgnoreFile:        ignoreFile,
+		SegmentFilter:     c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
 		MainSections:      c.MainSections,
 		Clock:             clock,
 		transientErr:      transientErr,
@@ -400,6 +405,7 @@ type ConfigCompiled struct {
 	CreateTitle       func(s string) string
 	IsUglyURLSection  func(section string) bool
 	IgnoreFile        func(filename string) bool
+	SegmentFilter     segments.SegmentFilter
 	MainSections      []string
 	Clock             time.Time
 
@@ -472,6 +478,10 @@ type RootConfig struct {
 	// A list of languages to disable.
 	DisableLanguages []string
 
+	// The named segments to render.
+	// This needs to match the name of the segment in the segments configuration.
+	RenderSegments []string
+
 	// Disable the injection of the Hugo generator tag on the home page.
 	DisableHugoGeneratorInject bool
 
diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go
index 5d31d5d35ad..7d968e4adab 100644
--- a/config/allconfig/alldecoders.go
+++ b/config/allconfig/alldecoders.go
@@ -25,11 +25,13 @@ import (
 	"github.com/gohugoio/hugo/config/security"
 	"github.com/gohugoio/hugo/config/services"
 	"github.com/gohugoio/hugo/deploy/deployconfig"
+	"github.com/gohugoio/hugo/hugolib/segments"
 	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/markup/markup_config"
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/minifiers"
 	"github.com/gohugoio/hugo/modules"
+
 	"github.com/gohugoio/hugo/navigation"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/related"
@@ -120,6 +122,14 @@ var allDecoderSetups = map[string]decodeWeight{
 			return err
 		},
 	},
+	"segments": {
+		key: "segments",
+		decode: func(d decodeWeight, p decodeConfig) error {
+			var err error
+			p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key))
+			return err
+		},
+	},
 	"server": {
 		key: "server",
 		decode: func(d decodeWeight, p decodeConfig) error {
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index 99dc88b10a1..8d7fe83e3f7 100644
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -410,6 +410,10 @@ type BuildCfg struct {
 
 // shouldRender returns whether this output format should be rendered or not.
 func (cfg *BuildCfg) shouldRender(p *pageState) bool {
+	if p.skipRender() {
+		return false
+	}
+
 	if !p.renderOnce {
 		return true
 	}
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 382b1eed02b..24d5bb4539a 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -28,16 +28,16 @@ import (
 	"github.com/bep/logg"
 	"github.com/gohugoio/hugo/cache/dynacache"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/hugofs/files"
 	"github.com/gohugoio/hugo/hugofs/glob"
+	"github.com/gohugoio/hugo/hugolib/segments"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/publisher"
 	"github.com/gohugoio/hugo/source"
 	"github.com/gohugoio/hugo/tpl"
 
-	"github.com/gohugoio/hugo/hugofs"
-
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/loggers"
 	"github.com/gohugoio/hugo/common/para"
@@ -318,9 +318,20 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
 
 	i := 0
 	for _, s := range h.Sites {
+		segmentFilter := s.conf.C.SegmentFilter
+		if segmentFilter.ShouldExcludeCoarse(segments.SegmentMatcherFields{Lang: s.language.Lang}) {
+			l.Logf("skip language %q not matching segments set in --renderSegments", s.language.Lang)
+			continue
+		}
+
 		siteRenderContext.languageIdx = s.languagei
 		h.currentSite = s
 		for siteOutIdx, renderFormat := range s.renderFormats {
+			if segmentFilter.ShouldExcludeCoarse(segments.SegmentMatcherFields{Output: renderFormat.Name, Lang: s.language.Lang}) {
+				l.Logf("skip output format %q for language %q not matching segments set in --renderSegments", renderFormat.Name, s.language.Lang)
+				continue
+			}
+
 			siteRenderContext.outIdx = siteOutIdx
 			siteRenderContext.sitesOutIdx = i
 			i++
diff --git a/hugolib/page.go b/hugolib/page.go
index c743d42823d..400b294eb0f 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/hugolib/doctree"
+	"github.com/gohugoio/hugo/hugolib/segments"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/output"
@@ -151,6 +152,19 @@ func (p *pageState) reusePageOutputContent() bool {
 	return p.pageOutputTemplateVariationsState.Load() == 1
 }
 
+func (p *pageState) skipRender() bool {
+	b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
+		segments.SegmentMatcherFields{
+			Path:   p.Path(),
+			Kind:   p.Kind(),
+			Lang:   p.Lang(),
+			Output: p.pageOutput.f.Name,
+		},
+	)
+
+	return b
+}
+
 func (po *pageState) isRenderedAny() bool {
 	for _, o := range po.pageOutputs {
 		if o.isRendered() {
diff --git a/hugolib/segments/segments.go b/hugolib/segments/segments.go
new file mode 100644
index 00000000000..8f7c18121cf
--- /dev/null
+++ b/hugolib/segments/segments.go
@@ -0,0 +1,257 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package segments
+
+import (
+	"fmt"
+
+	"github.com/gobwas/glob"
+	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/common/predicate"
+	"github.com/gohugoio/hugo/config"
+	hglob "github.com/gohugoio/hugo/hugofs/glob"
+	"github.com/mitchellh/mapstructure"
+)
+
+// Segments is a collection of named segments.
+type Segments struct {
+	s map[string]excludeInclude
+}
+
+type excludeInclude struct {
+	exclude predicate.P[SegmentMatcherFields]
+	include predicate.P[SegmentMatcherFields]
+}
+
+// ShouldExcludeCoarse returns whether the given fields should be excluded.
+// This is used for the coarser grained checks, e.g. language and output format.
+// Note that ShouldExcludeCoarse(fields) == ShouldExcludeFine(fields) may
+// not always be true, but ShouldExcludeCoarse(fields) == true == ShouldExcludeFine(fields)
+// will always be truthful.
+func (e excludeInclude) ShouldExcludeCoarse(fields SegmentMatcherFields) bool {
+	return e.exclude != nil && e.exclude(fields)
+}
+
+// ShouldExcludeFine returns whether the given fields should be excluded.
+// This is used for the finer grained checks, e.g. on invididual pages.
+func (e excludeInclude) ShouldExcludeFine(fields SegmentMatcherFields) bool {
+	if e.exclude != nil && e.exclude(fields) {
+		return true
+	}
+	return e.include != nil && !e.include(fields)
+}
+
+type SegmentFilter interface {
+	// ShouldExcludeCoarse returns whether the given fields should be excluded on a coarse level.
+	ShouldExcludeCoarse(SegmentMatcherFields) bool
+
+	// ShouldExcludeFine returns whether the given fields should be excluded on a fine level.
+	ShouldExcludeFine(SegmentMatcherFields) bool
+}
+
+type segmentFilter struct {
+	coarse predicate.P[SegmentMatcherFields]
+	fine   predicate.P[SegmentMatcherFields]
+}
+
+func (f segmentFilter) ShouldExcludeCoarse(field SegmentMatcherFields) bool {
+	return f.coarse(field)
+}
+
+func (f segmentFilter) ShouldExcludeFine(fields SegmentMatcherFields) bool {
+	return f.fine(fields)
+}
+
+var (
+	matchAll     = func(SegmentMatcherFields) bool { return true }
+	matchNothing = func(SegmentMatcherFields) bool { return false }
+)
+
+// Get returns a SegmentFilter for the given segments.
+func (sms Segments) Get(onNotFound func(s string), ss ...string) SegmentFilter {
+	if ss == nil {
+		return segmentFilter{coarse: matchNothing, fine: matchNothing}
+	}
+	var sf segmentFilter
+	for _, s := range ss {
+		if seg, ok := sms.s[s]; ok {
+			if sf.coarse == nil {
+				sf.coarse = seg.ShouldExcludeCoarse
+			} else {
+				sf.coarse = sf.coarse.Or(seg.ShouldExcludeCoarse)
+			}
+			if sf.fine == nil {
+				sf.fine = seg.ShouldExcludeFine
+			} else {
+				sf.fine = sf.fine.Or(seg.ShouldExcludeFine)
+			}
+		} else if onNotFound != nil {
+			onNotFound(s)
+		}
+	}
+
+	if sf.coarse == nil {
+		sf.coarse = matchAll
+	}
+	if sf.fine == nil {
+		sf.fine = matchAll
+	}
+
+	return sf
+}
+
+type SegmentConfig struct {
+	Excludes []SegmentMatcherFields
+	Includes []SegmentMatcherFields
+}
+
+// SegmentMatcherFields is a matcher for a segment include or exclude.
+// All of these are Glob patterns.
+type SegmentMatcherFields struct {
+	Kind   string
+	Path   string
+	Lang   string
+	Output string
+}
+
+func getGlob(s string) (glob.Glob, error) {
+	if s == "" {
+		return nil, nil
+	}
+	g, err := hglob.GetGlob(s)
+	if err != nil {
+		return nil, fmt.Errorf("failed to compile Glob %q: %w", s, err)
+	}
+	return g, nil
+}
+
+func compileSegments(f []SegmentMatcherFields) (predicate.P[SegmentMatcherFields], error) {
+	if f == nil {
+		return func(SegmentMatcherFields) bool { return false }, nil
+	}
+	var (
+		result  predicate.P[SegmentMatcherFields]
+		section predicate.P[SegmentMatcherFields]
+	)
+
+	addToSection := func(matcherFields SegmentMatcherFields, f func(fields SegmentMatcherFields) string) error {
+		s1 := f(matcherFields)
+		g, err := getGlob(s1)
+		if err != nil {
+			return err
+		}
+		matcher := func(fields SegmentMatcherFields) bool {
+			s2 := f(fields)
+			if s2 == "" {
+				return false
+			}
+			return g.Match(s2)
+		}
+		if section == nil {
+			section = matcher
+		} else {
+			section = section.And(matcher)
+		}
+		return nil
+	}
+
+	for _, fields := range f {
+		if fields.Kind != "" {
+			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Kind }); err != nil {
+				return result, err
+			}
+		}
+		if fields.Path != "" {
+			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Path }); err != nil {
+				return result, err
+			}
+		}
+		if fields.Lang != "" {
+			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Lang }); err != nil {
+				return result, err
+			}
+		}
+		if fields.Output != "" {
+			if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Output }); err != nil {
+				return result, err
+			}
+		}
+
+		if result == nil {
+			result = section
+		} else {
+			result = result.Or(section)
+		}
+		section = nil
+
+	}
+
+	return result, nil
+}
+
+func DecodeSegments(in map[string]any) (*config.ConfigNamespace[map[string]SegmentConfig, Segments], error) {
+	buildConfig := func(in any) (Segments, any, error) {
+		sms := Segments{
+			s: map[string]excludeInclude{},
+		}
+		m, err := maps.ToStringMapE(in)
+		if err != nil {
+			return sms, nil, err
+		}
+		if m == nil {
+			m = map[string]any{}
+		}
+		m = maps.CleanConfigStringMap(m)
+
+		var scfgm map[string]SegmentConfig
+		if err := mapstructure.Decode(m, &scfgm); err != nil {
+			return sms, nil, err
+		}
+
+		for k, v := range scfgm {
+			var (
+				include predicate.P[SegmentMatcherFields]
+				exclude predicate.P[SegmentMatcherFields]
+				err     error
+			)
+			if v.Excludes != nil {
+				exclude, err = compileSegments(v.Excludes)
+				if err != nil {
+					return sms, nil, err
+				}
+			}
+			if v.Includes != nil {
+				include, err = compileSegments(v.Includes)
+				if err != nil {
+					return sms, nil, err
+				}
+			}
+
+			ei := excludeInclude{
+				exclude: exclude,
+				include: include,
+			}
+			sms.s[k] = ei
+
+		}
+
+		return sms, nil, nil
+	}
+
+	ns, err := config.DecodeNamespace[map[string]SegmentConfig](in, buildConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode segments: %w", err)
+	}
+	return ns, nil
+}
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 1cf4b9a61b4..8fab39cac38 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -271,7 +271,7 @@ func (s *Site) renderAliases() error {
 			p := n.(*pageState)
 
 			// We cannot alias a page that's not rendered.
-			if p.m.noLink() {
+			if p.m.noLink() || p.skipRender() {
 				return false, nil
 			}
 


Test patch:
diff --git a/hugolib/segments/segments_integration_test.go b/hugolib/segments/segments_integration_test.go
new file mode 100644
index 00000000000..465a7abe027
--- /dev/null
+++ b/hugolib/segments/segments_integration_test.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package segments_test
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestSegments(t *testing.T) {
+	files := `
+-- hugo.toml --
+baseURL = "https://example.org/"
+renderSegments = ["docs"]
+[languages]
+[languages.en]
+weight = 1
+[languages.no]
+weight = 2
+[languages.nb]
+weight = 3
+[segments]
+[segments.docs]
+[[segments.docs.includes]]
+kind = "{home,taxonomy,term}"
+[[segments.docs.includes]]
+path = "{/docs,/docs/**}"
+[[segments.docs.excludes]]
+path = "/blog/**"
+[[segments.docs.excludes]]
+lang = "n*"
+output = "rss"
+[[segments.docs.excludes]]
+output = "json"
+-- layouts/_default/single.html --
+Single: {{ .Title }}|{{ .RelPermalink }}|
+-- layouts/_default/list.html --
+List: {{ .Title }}|{{ .RelPermalink }}|
+-- content/docs/_index.md --
+-- content/docs/section1/_index.md --
+-- content/docs/section1/page1.md --
+---
+title: "Docs Page 1"
+tags: ["tag1", "tag2"]
+---
+-- content/blog/_index.md --
+-- content/blog/section1/page1.md --
+---
+title: "Blog Page 1"
+tags: ["tag1", "tag2"]
+---
+`
+
+	b := hugolib.Test(t, files)
+	b.Assert(b.H.Configs.Base.RootConfig.RenderSegments, qt.DeepEquals, []string{"docs"})
+
+	b.AssertFileContent("public/docs/section1/page1/index.html", "Docs Page 1")
+	b.AssertFileExists("public/blog/section1/page1/index.html", false)
+	b.AssertFileExists("public/index.html", true)
+	b.AssertFileExists("public/index.xml", true)
+	b.AssertFileExists("public/no/index.html", true)
+	b.AssertFileExists("public/no/index.xml", false)
+}
diff --git a/hugolib/segments/segments_test.go b/hugolib/segments/segments_test.go
new file mode 100644
index 00000000000..1a2dfb97bad
--- /dev/null
+++ b/hugolib/segments/segments_test.go
@@ -0,0 +1,115 @@
+package segments
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestCompileSegments(t *testing.T) {
+	c := qt.New(t)
+
+	c.Run("excludes", func(c *qt.C) {
+		fields := []SegmentMatcherFields{
+			{
+				Lang:   "n*",
+				Output: "rss",
+			},
+		}
+
+		match, err := compileSegments(fields)
+		c.Assert(err, qt.IsNil)
+
+		check := func() {
+			c.Assert(match, qt.IsNotNil)
+			c.Assert(match(SegmentMatcherFields{Lang: "no"}), qt.Equals, false)
+			c.Assert(match(SegmentMatcherFields{Lang: "no", Kind: "page"}), qt.Equals, false)
+			c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss"}), qt.Equals, true)
+			c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "html"}), qt.Equals, false)
+			c.Assert(match(SegmentMatcherFields{Kind: "page"}), qt.Equals, false)
+			c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss", Kind: "page"}), qt.Equals, true)
+		}
+
+		check()
+
+		fields = []SegmentMatcherFields{
+			{
+				Path: "/blog/**",
+			},
+			{
+				Lang:   "n*",
+				Output: "rss",
+			},
+		}
+
+		match, err = compileSegments(fields)
+		c.Assert(err, qt.IsNil)
+		check()
+		c.Assert(match(SegmentMatcherFields{Path: "/blog/foo"}), qt.Equals, true)
+	})
+
+	c.Run("includes", func(c *qt.C) {
+		fields := []SegmentMatcherFields{
+			{
+				Path: "/docs/**",
+			},
+			{
+				Lang:   "no",
+				Output: "rss",
+			},
+		}
+
+		match, err := compileSegments(fields)
+		c.Assert(err, qt.IsNil)
+		c.Assert(match, qt.IsNotNil)
+		c.Assert(match(SegmentMatcherFields{Lang: "no"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Kind: "page"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/blog/foo"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Lang: "en"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss"}), qt.Equals, true)
+		c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "html"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/docs/foo"}), qt.Equals, true)
+	})
+
+	c.Run("includes variant1", func(c *qt.C) {
+		c.Skip()
+
+		fields := []SegmentMatcherFields{
+			{
+				Kind: "home",
+			},
+			{
+				Path: "{/docs,/docs/**}",
+			},
+		}
+
+		match, err := compileSegments(fields)
+		c.Assert(err, qt.IsNil)
+		c.Assert(match, qt.IsNotNil)
+		c.Assert(match(SegmentMatcherFields{Path: "/blog/foo"}), qt.Equals, false)
+		c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/docs/foo"}), qt.Equals, true)
+		c.Assert(match(SegmentMatcherFields{Kind: "home", Path: "/"}), qt.Equals, true)
+	})
+}
+
+func BenchmarkSegmentsMatch(b *testing.B) {
+	fields := []SegmentMatcherFields{
+		{
+			Path: "/docs/**",
+		},
+		{
+			Lang:   "no",
+			Output: "rss",
+		},
+	}
+
+	match, err := compileSegments(fields)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		match(SegmentMatcherFields{Lang: "no", Output: "rss"})
+	}
+}