]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
FIXME: hugolib: markdown_title: add title from markdown
authorNikita Shubin <nikita.shubin@maquefel.me>
Sun, 26 Apr 2026 07:24:59 +0000 (10:24 +0300)
committerNikita Shubin <nikita.shubin@maquefel.me>
Sun, 26 Apr 2026 10:12:12 +0000 (13:12 +0300)
Getting title from the page itself, as first encountered header in
markdown file.

Currently it's inefficient as we parsing page twice, the first in some
ealier point when hugo renders markdown, so it should be embedded
somewhere in that place.

But currently i don't care about if a PoC implementation.

Currently it's required to use '.MarkdownTitleInfo.Title' instead of
'.Title', where appropriate.

TODO:
- make it config controllable
- embed it into .Title:
  - fallback to Meta if no markdown title available
  - or do the opposite and use it if no Meta provided

Signed-off-by: Nikita Shubin <nikita.shubin@maquefel.me>
hugolib/hugo_sites.go
hugolib/markdown_title.go [new file with mode: 0644]
hugolib/page.go
hugolib/page__common.go
hugolib/page__new.go
hugolib/site.go

index 87c04035dffbe4e9f2c2d3ecd30a817e3e5175f7..eea3583a562cf14fa2c99406716aa51908170222 100644 (file)
@@ -84,6 +84,8 @@ type HugoSites struct {
        gitInfo       *gitInfo
        codeownerInfo *codeownerInfo
 
+       markdownTitleInfo *markdownTitleInfo
+
        // As loaded from the /data dirs
        data map[string]any
 
@@ -332,6 +334,8 @@ type hugoSitesInit struct {
 
        // Loads the Git info and CODEOWNERS for all the pages if enabled.
        gitInfo hsync.FuncResetter
+
+       markdownTitleInfo hsync.FuncResetter
 }
 
 func (h *HugoSites) Data() map[string]any {
@@ -401,6 +405,18 @@ func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) {
        return h.codeownerInfo.forPage(p), nil
 }
 
+func (h *HugoSites) markdownTitleInfoForPage(p page.Page) (*MarkdownTitleInfo, error) {
+       if err := h.init.markdownTitleInfo.Do(context.Background()); err != nil {
+               return nil, err
+       }
+
+       if h.markdownTitleInfo == nil {
+               return nil, nil
+       }
+
+       return h.markdownTitleInfo.forPage(p), nil
+}
+
 func (h *HugoSites) reportProgress(f func() (state terminal.ProgressState, progress float64)) {
        h.progressReporter.mu.Lock()
        defer h.progressReporter.mu.Unlock()
@@ -568,6 +584,25 @@ func (h *HugoSites) loadGitInfo() error {
        return nil
 }
 
+func (h *HugoSites) loadMarkdownTitleInfo() error {
+       // TODO: Make it configurable
+       // if h.Configs.Base.EnableMarkdownTitle {
+       //      cfg := gitInfoConfig{
+       //              Deps:         h.Deps,
+       //              Modules:      h.Configs.Modules,
+       //              GitInfoCache: h.Configs.FileCaches.ModuleGitInfoCache(),
+       //              Logger:       h.Log,
+       //      }
+               mt, err := newMarkdownTitleInfo(h.Deps.Conf.DirsBase().ContentDir)
+               if err != nil {
+                       return err
+               } else {
+                       h.markdownTitleInfo = mt
+               }
+       // }
+       return nil
+}
+
 // Reset resets the sites and template caches etc., making it ready for a full rebuild.
 func (h *HugoSites) reset() {
        h.fatalErrorHandler = &fatalErrorHandler{
diff --git a/hugolib/markdown_title.go b/hugolib/markdown_title.go
new file mode 100644 (file)
index 0000000..fe05a75
--- /dev/null
@@ -0,0 +1,68 @@
+package hugolib
+
+import (
+       "path/filepath"
+       "strings"
+       "os"
+
+       "github.com/yuin/goldmark"
+       "github.com/yuin/goldmark/text"
+       "github.com/yuin/goldmark/ast"
+       "github.com/leonhfr/goldmark-frontmatter"
+
+       "github.com/gohugoio/hugo/resources/page"
+)
+
+type MarkdownTitleInfo struct {
+       Title           string    `json:"title"`            // Title
+}
+
+type MarkdownTitleMap map[string]*MarkdownTitleInfo
+
+type markdownTitleInfo struct {
+       contentDir string
+       Map MarkdownTitleMap
+}
+
+func findFirstH1(mdContent []byte) string {
+       md := goldmark.New(
+               goldmark.WithExtensions(
+               &frontmatter.Extender{},
+               ),
+       )
+
+       doc := md.Parser().Parse(text.NewReader(mdContent))
+
+       var headingText string
+       ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
+               if entering && node.Kind() == ast.KindHeading {
+                       if heading, ok := node.(*ast.Heading); ok && heading.Level == 1 {
+                               headingText = string(heading.Text(mdContent))
+                               return ast.WalkStop, nil
+                       }
+               }
+
+               return ast.WalkContinue, nil
+       })
+
+       return strings.TrimSpace(headingText)
+}
+
+func (m *markdownTitleInfo) forPage(p page.Page) *MarkdownTitleInfo {
+       name := strings.TrimPrefix(filepath.ToSlash(p.File().Filename()), m.contentDir)
+
+       source, err := os.ReadFile(name)
+       if err != nil {
+                       return nil
+       }
+
+       title := findFirstH1([]byte(source))
+
+       return &MarkdownTitleInfo{Title: title}
+}
+
+func newMarkdownTitleInfo(_contentDir string) (*markdownTitleInfo, error) {
+       contentDir := _contentDir
+
+       return &markdownTitleInfo{contentDir: contentDir}, nil
+}
\ No newline at end of file
index 759e4fe36ece9430c05f6896245543e3edc816fc..742ff343d8ab52843b39540ba62ecf47c6677d0f 100644 (file)
@@ -325,6 +325,10 @@ func (ps *pageState) CodeOwners() []string {
        return ps.codeowners
 }
 
+func (p *pageState) MarkdownTitleInfo() *MarkdownTitleInfo {
+       return p.markdownTitleInfo
+}
+
 // GetTerms gets the terms defined on this page in the given taxonomy.
 // The pages returned will be ordered according to the front matter.
 func (ps *pageState) GetTerms(taxonomy string) page.Pages {
index 2ea07c9d8f687eaef7aa6011579cbe8153f4fc7c..42965e8a07525b77e2eda180786cd165d9fe4188 100644 (file)
@@ -87,6 +87,8 @@ type pageCommon struct {
        gitInfo    *source.GitInfo
        codeowners []string
 
+       markdownTitleInfo    *MarkdownTitleInfo
+
        // Positional navigation
        posNextPrev        *nextPrev
        posNextPrevSection *nextPrev
index dc3c53e49cc96cf1b66906b63040b34c6bfeaa5c..70925e3b8d3146181c547e2e6524b5384434d25c 100644 (file)
@@ -95,6 +95,12 @@ func (s *Site) doNewPageFromPageMeta(m *pageMeta, cascades *page.PageMatcherPara
                        return nil, fmt.Errorf("failed to load CODEOWNERS: %w", err)
                }
                ps.codeowners = owners
+
+               mt, err := s.h.markdownTitleInfoForPage(ps)
+               if err != nil {
+                       return nil, fmt.Errorf("failed to load MarkdownTitle data: %w", err)
+               }
+               ps.markdownTitleInfo = mt
        }
 
        ps.pageMenus = &pageMenus{p: ps}
index 678fea7cddd817d6f4fa3c0ea825c9e46d804e37..d0431d701816ba0062b4a2266bf680bddc9328ac 100644 (file)
@@ -595,6 +595,10 @@ func newHugoSites(
                return h.loadGitInfo()
        })
 
+       h.init.markdownTitleInfo = hsync.OnceMoreFunc(func(ctx context.Context) error {
+               return h.loadMarkdownTitleInfo()
+       })
+
        return h, nil
 }