hugo-cms/providers/hugo/provider.go
2025-03-30 11:15:46 +11:00

182 lines
4.5 KiB
Go

package hugo
import (
"context"
"gopkg.in/yaml.v3"
"lmika.dev/lmika/hugo-cms/models"
"log"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
)
type Provider struct {
stagingDir string
previewDir string
previewBaseURL *url.URL
scratchDir string
}
func New(stagingDir, previewBaseURL, previewDir, scratchDir string) (*Provider, error) {
baseURL, err := url.Parse(previewBaseURL)
if err != nil {
return nil, err
}
return &Provider{
stagingDir: stagingDir,
previewBaseURL: baseURL,
previewDir: previewDir,
scratchDir: scratchDir,
}, nil
}
func (p *Provider) SiteStagingDir(site models.Site, what SiteDir) string {
switch what {
case BaseSiteDir:
return filepath.Join(p.stagingDir, site.Name)
case ThemeSiteDir:
return filepath.Join(p.stagingDir, site.Name, "themes", site.Theme)
}
return filepath.Join(p.stagingDir, site.Name, string(what))
}
func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
log.Printf(" .. %v", p.SiteStagingDir(site, BaseSiteDir))
if err := os.MkdirAll(p.SiteStagingDir(site, BaseSiteDir), 0755); err != nil {
return err
}
// Create the new site
if err := exec.CommandContext(ctx, "hugo", "new", "site",
"--config=hugo.yaml",
p.SiteStagingDir(site, BaseSiteDir)).Run(); err != nil {
return err
}
return nil
}
func (p *Provider) PreviewSite(ctx context.Context, site models.Site) (outDir string, err error) {
previewURL, err := p.previewBaseURL.Parse("preview/" + site.Name)
if err != nil {
return "", err
}
previewTarget := models.PublishTarget{
URL: previewURL.String(),
}
return p.publishSiteAt(ctx, p.previewDir, site, previewTarget, "hugoPreview.yaml")
}
func (p *Provider) PublishSite(ctx context.Context, site models.Site, target models.PublishTarget) (outDir string, err error) {
return p.publishSiteAt(ctx, p.scratchDir, site, target, "hugo.yaml")
}
func (p *Provider) publishSiteAt(ctx context.Context, dir string, site models.Site, target models.PublishTarget, configFile string) (outDir string, err error) {
baseSiteDir, err := filepath.Abs(p.SiteStagingDir(site, BaseSiteDir))
if err != nil {
return "", err
}
outDir, err = filepath.Abs(filepath.Join(dir, site.Name))
if err != nil {
return "", err
}
if err := os.MkdirAll(outDir, 0755); err != nil {
return "", err
}
cmd := exec.CommandContext(ctx, "hugo",
"--source", baseSiteDir,
"--destination", outDir,
"--quiet",
"--config", filepath.Join(baseSiteDir, configFile),
"--baseURL", target.URL)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
return "", err
}
if err := cmd.Wait(); err != nil {
return "", err
}
return outDir, nil
}
func (p *Provider) ReconfigureSite(ctx context.Context, isPreviewConfig bool, configBase string, site models.Site, meta models.ThemeMeta) error {
hugoCfg := hugoConfig{
Title: site.Title,
LanguageCode: "en",
Theme: site.Theme,
Permalinks: permalinksConfig{
Page: map[string]string{
meta.BlogPostBundle: meta.BlogPostBundle + "/:year/:month/:day/:title",
},
},
Markup: hugoConfigMarkup{
Goldmark: hugoGoldmarkConfig{
Renderer: hugoGoldmarkRendererConfig{
Unsafe: true,
},
},
},
Params: p.configureParams(ctx, site, meta),
}
if isPreviewConfig {
hugoCfg.CanonifyURLs = true
}
ymlBytes, err := yaml.Marshal(hugoCfg)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".yaml"), ymlBytes, 0644); err != nil {
return err
}
if _, err := os.Stat(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".toml")); err == nil {
if err := os.Remove(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".toml")); err != nil {
return err
}
}
return nil
}
func (p *Provider) configureParams(ctx context.Context, site models.Site, meta models.ThemeMeta) map[string]any {
// Options from the theme
themeOptions := make(map[string]any)
for _, option := range meta.Options {
var typedVal any
strVal := option.DefaultValue
switch option.Type {
case models.ThemeOptionTypeString:
typedVal = strVal
case models.ThemeOptionTypeInt:
intVal, err := strconv.Atoi(strVal)
if err != nil {
log.Printf("invalid theme option value: name %v, type %v, value %v", option.Name, option.Type, strVal)
continue
}
typedVal = intVal
default:
log.Printf("unknown theme option type: name %v, type %v", option.Name, option.Type)
continue
}
themeOptions[option.Name] = typedVal
}
return map[string]any{
"theme_options": themeOptions,
}
}