]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
commands: Preserve non-content files in convert output
authorAlexandre Vaz <xndvaz@gmail.com>
Mon, 23 Mar 2026 16:49:13 +0000 (13:49 -0300)
committerGitHub <noreply@github.com>
Mon, 23 Mar 2026 16:49:13 +0000 (17:49 +0100)
When running `hugo convert` with `--output`, copy the content tree first so non-content bundle resources are kept in the destination, then overwrite converted content files.

Also avoid recursive self-copy when the output path points inside the content tree by skipping output directories during copy.

Fixes #4621

commands/convert.go
testscripts/commands/convert.txt

index ebf81cfb3e23a9972473939d7c1ff966d526fc4a..06982984935fc7a422854e4a9fec53a424e34b30 100644 (file)
@@ -18,10 +18,12 @@ import (
        "context"
        "fmt"
        "path/filepath"
+       "slices"
        "strings"
        "time"
 
        "github.com/bep/simplecobra"
+       "github.com/gohugoio/hugo/common/hugio"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugofs"
@@ -200,6 +202,56 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
        return nil
 }
 
+func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
+       contentDirs := make(map[string]bool)
+       for _, p := range pagesBackedByFile {
+               filename := p.File().Filename()
+               contentDir := strings.TrimSuffix(filename, p.File().Path())
+               if contentDir == filename {
+                       continue
+               }
+               contentDirs[filepath.Clean(contentDir)] = true
+       }
+
+       var contentDirList []string
+       for contentDir := range contentDirs {
+               contentDirList = append(contentDirList, contentDir)
+       }
+       slices.Sort(contentDirList)
+
+       outputDirAbs, err := filepath.Abs(c.outputDir)
+       if err != nil {
+               return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
+       }
+
+       for _, contentDir := range contentDirList {
+               outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
+
+               skipDirs := make(map[string]bool)
+               relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
+               if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
+                       skipDirs[filepath.Clean(outputDirAbs)] = true
+               }
+               relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
+               if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
+                       skipDirs[filepath.Clean(outputContentDirAbs)] = true
+               }
+
+               var shouldCopy func(filename string) bool
+               if len(skipDirs) > 0 {
+                       shouldCopy = func(filename string) bool {
+                               return !skipDirs[filepath.Clean(filename)]
+                       }
+               }
+
+               if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
+                       return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
+               }
+       }
+
+       return nil
+}
+
 func (c *convertCommand) convertContents(format metadecoders.Format) error {
        if c.outputDir == "" && !c.unsafe {
                return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
@@ -219,6 +271,12 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
                pagesBackedByFile = append(pagesBackedByFile, p)
        }
 
+       if c.outputDir != "" {
+               if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
+                       return err
+               }
+       }
+
        site.Log.Println("processing", len(pagesBackedByFile), "content files")
        for _, p := range site.AllPages() {
                if err := c.convertAndSavePage(p, site, format); err != nil {
index 811aeecc8ac93d45b8f0483c82869d470471fcfb..4c325f17b9cc8262e31ad1fd2a8b1cf7efd8a980 100644 (file)
@@ -10,14 +10,21 @@ hugo convert toYAML -h
 stdout 'to use YAML for the front matter'
 
 hugo convert toJSON -o myjsoncontent
-stdout 'processing 3 content files'
+stdout 'processing 4 content files'
 grep '^{' myjsoncontent/content/mytoml.md
 grep '^{' myjsoncontent/content/myjson.md
 grep '^{' myjsoncontent/content/myyaml.md
+grep '^{' myjsoncontent/content/bundle/index.md
+exists myjsoncontent/content/bundle/data.txt
+exists myjsoncontent/content/bundle/nested/asset.dat
 hugo convert toYAML -o myyamlcontent
-stdout 'processing 3 content files'
+stdout 'processing 4 content files'
+exists myyamlcontent/content/bundle/data.txt
+exists myyamlcontent/content/bundle/nested/asset.dat
 hugo convert toTOML -o mytomlcontent
-stdout 'processing 3 content files'
+stdout 'processing 4 content files'
+exists mytomlcontent/content/bundle/data.txt
+exists mytomlcontent/content/bundle/nested/asset.dat
 
 
 
@@ -40,3 +47,12 @@ JSON content
 title: YAML
 ---
 YAML content
+-- content/bundle/index.md --
+---
+title: Bundle
+---
+Bundle content
+-- content/bundle/data.txt --
+bundle resource
+-- content/bundle/nested/asset.dat --
+nested resource