diff --git a/cmd/account/account_producer_extension_info_push.go b/cmd/account/account_producer_extension_info_push.go
index 82566d96..f8b80c4d 100644
--- a/cmd/account/account_producer_extension_info_push.go
+++ b/cmd/account/account_producer_extension_info_push.go
@@ -1,7 +1,6 @@
package account
import (
- "bytes"
"context"
"fmt"
"os"
@@ -15,6 +14,7 @@ import (
accountApi "github.com/shopware/shopware-cli/internal/account-api"
"github.com/shopware/shopware-cli/internal/extension"
+ "github.com/shopware/shopware-cli/internal/markdown"
"github.com/shopware/shopware-cli/logging"
)
@@ -312,15 +312,12 @@ func parseInlineablePath(path, extensionDir string) (string, error) {
return string(content), nil
}
- md := extension.GetConfiguredGoldMark()
-
- var buf bytes.Buffer
- err = md.Convert(content, &buf)
+ html, err := markdown.ToHTML(content)
if err != nil {
return "", fmt.Errorf("cannot convert file at path %s from markdown to html with error: %v", filePath, err)
}
- return buf.String(), nil
+ return html, nil
}
func uploadImagesByDirectory(ctx context.Context, extensionId int, directory string, index int, p *accountApi.ProducerEndpoint) error {
diff --git a/internal/extension/changelog.go b/internal/extension/changelog.go
index b04551d0..1186e0aa 100644
--- a/internal/extension/changelog.go
+++ b/internal/extension/changelog.go
@@ -1,16 +1,12 @@
package extension
import (
- "bytes"
"fmt"
"os"
"path/filepath"
"strings"
- "github.com/yuin/goldmark"
- goldmarkExtension "github.com/yuin/goldmark/extension"
- "github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer/html"
+ "github.com/shopware/shopware-cli/internal/markdown"
)
func parseMarkdownChangelogInPath(path string) (map[string]map[string]string, error) {
@@ -64,14 +60,12 @@ func parseMarkdownChangelog(content string) (map[string]string, error) {
versions[currentVersion] = versionText
for key, changelog := range versions {
- var buf bytes.Buffer
-
- err := GetConfiguredGoldMark().Convert([]byte(changelog), &buf)
+ html, err := markdown.ToHTML([]byte(changelog))
if err != nil {
return nil, err
}
- versions[key] = buf.String()
+ versions[key] = html
}
return versions, nil
@@ -121,16 +115,3 @@ func parseExtensionMarkdownChangelog(ext Extension) (*ExtensionChangelog, error)
return &ExtensionChangelog{German: changelogDeVersion, English: changelogEnVersion, Changelogs: allChangelogsInVersion}, nil
}
-
-func GetConfiguredGoldMark() goldmark.Markdown {
- return goldmark.New(
- goldmark.WithExtensions(goldmarkExtension.GFM),
- goldmark.WithParserOptions(
- parser.WithAutoHeadingID(),
- ),
- goldmark.WithRendererOptions(
- html.WithHardWraps(),
- html.WithXHTML(),
- ),
- )
-}
diff --git a/internal/markdown/heading_renderer.go b/internal/markdown/heading_renderer.go
new file mode 100644
index 00000000..e18df0e2
--- /dev/null
+++ b/internal/markdown/heading_renderer.go
@@ -0,0 +1,42 @@
+package markdown
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
+)
+
+// spanHeadingRenderer renders Markdown headings as elements
+// instead of real - tags.
+//
+// The HTML produced by this package is pushed to the Shopware Store
+// (extension description, installation manual and changelog) where it is
+// embedded inside a store page that already owns its heading hierarchy
+// (the is the product title). Emitting real headings for this description
+// chrome would inject competing headings and harm the page's SEO structure, so
+// we keep the visual hierarchy via CSS classes (class="h1" ... "h6") without the
+// heading semantics, matching the Shopware Storefront SEO guideline.
+type spanHeadingRenderer struct{}
+
+func (r *spanHeadingRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindHeading, r.renderHeading)
+}
+
+func (r *spanHeadingRenderer) renderHeading(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.Heading)
+
+ if entering {
+ _, _ = w.WriteString(`')
+ } else {
+ _, _ = w.WriteString("\n")
+ }
+
+ return ast.WalkContinue, nil
+}
diff --git a/internal/markdown/heading_renderer_test.go b/internal/markdown/heading_renderer_test.go
new file mode 100644
index 00000000..11db7339
--- /dev/null
+++ b/internal/markdown/heading_renderer_test.go
@@ -0,0 +1,45 @@
+package markdown
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func renderMarkdown(t *testing.T, source string) string {
+ t.Helper()
+
+ html, err := ToHTML([]byte(source))
+ assert.NoError(t, err)
+
+ return html
+}
+
+func TestMarkdownHeadingsRenderedAsSpans(t *testing.T) {
+ html := renderMarkdown(t, "# Title\n\n## Subtitle\n\nText")
+
+ // Headings must not emit real - tags so they do not compete with
+ // the store page heading hierarchy.
+ assert.NotContains(t, html, "")
+ assert.Contains(t, html, "Subtitle
")
+}
+
+func TestMarkdownAllHeadingLevelsRenderedAsSpans(t *testing.T) {
+ html := renderMarkdown(t, "# A\n\n## B\n\n### C\n\n#### D\n\n##### E\n\n###### F")
+
+ for _, level := range []string{"h1", "h2", "h3", "h4", "h5", "h6"} {
+ assert.Contains(t, html, `bold")
+ assert.Contains(t, html, `link`)
+}
diff --git a/internal/markdown/markdown.go b/internal/markdown/markdown.go
new file mode 100644
index 00000000..3e2902ea
--- /dev/null
+++ b/internal/markdown/markdown.go
@@ -0,0 +1,48 @@
+// Package markdown renders Markdown to HTML for content that is pushed to the
+// Shopware Store (extension description, installation manual and changelog).
+//
+// The generated HTML is embedded inside a store page that already owns its
+// heading hierarchy, so the renderer is configured to follow the Shopware
+// Storefront SEO guidelines (see heading_renderer.go).
+package markdown
+
+import (
+ "bytes"
+
+ "github.com/yuin/goldmark"
+ goldmarkExtension "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
+)
+
+// New returns a goldmark instance configured for rendering Markdown that is
+// pushed to the Shopware Store.
+func New() goldmark.Markdown {
+ return goldmark.New(
+ goldmark.WithExtensions(goldmarkExtension.GFM),
+ goldmark.WithParserOptions(
+ parser.WithAutoHeadingID(),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithHardWraps(),
+ html.WithXHTML(),
+ // Override heading rendering so Markdown headings become
+ // instead of competing - tags in the
+ // store page. Priority < 1000 makes it win over the default
+ // HTML renderer.
+ renderer.WithNodeRenderers(util.Prioritized(&spanHeadingRenderer{}, 100)),
+ ),
+ )
+}
+
+// ToHTML converts Markdown content to HTML using the store-configured renderer.
+func ToHTML(content []byte) (string, error) {
+ var buf bytes.Buffer
+ if err := New().Convert(content, &buf); err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}