Repository: gohugoio/hugo (2026-03-23T01:59:36Z)

Issue:
hugo convert does not copy "non content file" files if --output is set
Hi all,

I probably found another issue concerning _page resources:_

`hugo convert` deletes all page resources if the flag `output` is set, e. g.:

```
hugo convert toTOML --output converted
```

With the `unsafe` flag set everything works as expected:

```
hugo convert toTOML --unsafe
```


Discussion:
I've just run `hugo convert toTOML --output converted` and all it did was copy the md files to the specified directory - no deleting, but also no converting.
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.

I just checked this, and yes, it does not copy any non-content files. Maybe it should. Not sure.
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.



Patch:
diff --git a/commands/convert.go b/commands/convert.go
index ebf81cfb3e2..06982984935 100644
--- a/commands/convert.go
+++ b/commands/convert.go
@@ -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 {


Test patch:
diff --git a/testscripts/commands/convert.txt b/testscripts/commands/convert.txt
index 811aeecc8ac..4c325f17b9c 100644
--- a/testscripts/commands/convert.txt
+++ b/testscripts/commands/convert.txt
@@ -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