feat: add configurable html minification #23
No files matched your search
@@ -2,7 +2,11 @@ build:
|
||||
go build -mod=vendor -o miniflux-digest ./cmd/miniflux-digest
|
||||
|
||||
preview-html:
|
||||
ifdef minify
|
||||
go run -mod=vendor ./scripts/preview-html/main.go -minify=${minify}
|
||||
else
|
||||
go run -mod=vendor ./scripts/preview-html/main.go
|
||||
endif
|
||||
./scripts/open-preview.sh
|
||||
|
||||
preview-miniflux-email:
|
||||
|
||||
@@ -14,3 +14,4 @@ digest:
|
||||
from: "SENDER_EMAIL@example.com"
|
||||
schedule: "@every 24h" # Cron schedule for digest generation
|
||||
host: "https://your-digest-host.com" # URL where HTML archives will be served
|
||||
compress: true # Compress HTML before sending
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/v2 v2.2.2
|
||||
github.com/tdewolff/minify/v2 v2.23.8
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
miniflux.app/v2 v2.2.10
|
||||
)
|
||||
@@ -17,11 +18,13 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
|
||||
@@ -16,6 +16,8 @@ github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpb
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4=
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
|
||||
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
|
||||
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||
@@ -38,6 +40,12 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.23.8 h1:tvjHzRer46kwOfpdCBCWsDblCw3QtnLJRd61pTVkyZ8=
|
||||
github.com/tdewolff/minify/v2 v2.23.8/go.mod h1:VW3ISUd3gDOZuQ/jwZr4sCzsuX+Qvsx87FDMjk6Rvno=
|
||||
github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
|
||||
github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type ArchiveService interface {
|
||||
MakeArchiveHTML(data *models.HTMLTemplateData) (*os.File, error)
|
||||
MakeArchiveHTML(data *models.HTMLTemplateData, compress bool) (*os.File, error)
|
||||
CleanArchive(maxAge time.Duration)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"miniflux-digest/internal/app"
|
||||
"miniflux-digest/internal/digest"
|
||||
"miniflux-digest/internal/models"
|
||||
"miniflux-digest/internal/templates"
|
||||
"miniflux-digest/internal/utils"
|
||||
@@ -35,17 +36,15 @@ type ArchiveServiceImpl struct{}
|
||||
|
||||
var _ app.ArchiveService = (*ArchiveServiceImpl)(nil)
|
||||
|
||||
func getHTML(data *models.HTMLTemplateData) (string, error) {
|
||||
func getHTML(data *models.HTMLTemplateData, compress bool) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
var htmlOutput string
|
||||
|
||||
err := templates.ArchiveTemplate.Execute(&buf, data)
|
||||
|
||||
if err == nil {
|
||||
htmlOutput = buf.String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return htmlOutput, err
|
||||
return digest.MinifyHTML(buf.Bytes(), compress)
|
||||
}
|
||||
|
||||
func makeArchiveFile(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
@@ -62,7 +61,7 @@ func makeArchiveFile(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *ArchiveServiceImpl) MakeArchiveHTML(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
func (s *ArchiveServiceImpl) MakeArchiveHTML(data *models.HTMLTemplateData, compress bool) (*os.File, error) {
|
||||
file, err := makeArchiveFile(data)
|
||||
|
||||
if err != nil {
|
||||
@@ -70,14 +69,14 @@ func (s *ArchiveServiceImpl) MakeArchiveHTML(data *models.HTMLTemplateData) (*os
|
||||
return nil, err
|
||||
}
|
||||
|
||||
htmlOutput, err := getHTML(data)
|
||||
htmlOutput, err := getHTML(data, compress)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error generating HTML for category %s: %v", data.Category.Title, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
_, err = file.WriteString(htmlOutput)
|
||||
_, err = file.Write(htmlOutput)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error writing HTML to file for category '%s': %v", data.Category.Title, err)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestGetHTML(t *testing.T) {
|
||||
data := testutil.NewMockHTMLTemplateData()
|
||||
html, err := getHTML(data)
|
||||
html, err := getHTML(data, true)
|
||||
if err != nil {
|
||||
t.Fatalf("getHTML failed: %v", err)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestMakeArchiveHTML(t *testing.T) {
|
||||
|
||||
data := testutil.NewMockHTMLTemplateData()
|
||||
archiveService := &ArchiveServiceImpl{}
|
||||
file, err := archiveService.MakeArchiveHTML(data)
|
||||
file, err := archiveService.MakeArchiveHTML(data, true)
|
||||
if err != nil {
|
||||
t.Fatalf("MakeArchiveHTML failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/v2"
|
||||
)
|
||||
@@ -19,16 +18,17 @@ type Config struct {
|
||||
DigestEmailFrom string
|
||||
DigestSchedule string
|
||||
DigestHost string
|
||||
DigestCompress bool
|
||||
}
|
||||
|
||||
var k = koanf.New(".")
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
k.Load(confmap.Provider(map[string]interface{}{
|
||||
"digest.compress": true,
|
||||
}, "."), nil)
|
||||
|
||||
err := k.Load(file.Provider(path), yaml.Parser())
|
||||
|
||||
if err != nil {
|
||||
log.Printf("error loading config file: %v", err)
|
||||
if err := k.Load(file.Provider(path), yaml.Parser()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func Load(path string) (*Config, error) {
|
||||
DigestEmailFrom: k.String("digest.email.from"),
|
||||
DigestSchedule: k.String("digest.schedule"),
|
||||
DigestHost: k.String("digest.host"),
|
||||
DigestCompress: k.Bool("digest.compress"),
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestLoad(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
name: "compress not set",
|
||||
content: `
|
||||
miniflux:
|
||||
host: "miniflux.example.com"
|
||||
@@ -42,20 +42,43 @@ digest:
|
||||
DigestEmailFrom: "from@example.com",
|
||||
DigestSchedule: "@daily",
|
||||
DigestHost: "https://example.com",
|
||||
DigestCompress: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing config file",
|
||||
content: "",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid yaml",
|
||||
content: "invalid-yaml",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
name: "compress set to false",
|
||||
content: `
|
||||
miniflux:
|
||||
host: "miniflux.example.com"
|
||||
api_token: "test-token"
|
||||
smtp:
|
||||
host: "smtp.example.com"
|
||||
port: 587
|
||||
user: "test-user"
|
||||
password: "test-password"
|
||||
digest:
|
||||
email:
|
||||
to: "to@example.com"
|
||||
from: "from@example.com"
|
||||
schedule: "@daily"
|
||||
host: "https://example.com"
|
||||
compress: false
|
||||
`,
|
||||
want: &Config{
|
||||
MinifluxHost: "miniflux.example.com",
|
||||
MinifluxApiToken: "test-token",
|
||||
SmtpHost: "smtp.example.com",
|
||||
SmtpPort: 587,
|
||||
SmtpUser: "test-user",
|
||||
SmtpPassword: "test-password",
|
||||
DigestEmailTo: "to@example.com",
|
||||
DigestEmailFrom: "from@example.com",
|
||||
DigestSchedule: "@daily",
|
||||
DigestHost: "https://example.com",
|
||||
DigestCompress: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,15 +93,12 @@ digest:
|
||||
t.Errorf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
if tt.content != "" {
|
||||
if err := os.WriteFile(configPath, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write dummy config file: %v", err)
|
||||
}
|
||||
} else {
|
||||
configPath = "non-existent-file.yaml"
|
||||
if err := os.WriteFile(configPath, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write dummy config file: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
@@ -86,42 +106,9 @@ digest:
|
||||
return
|
||||
}
|
||||
|
||||
if tt.want != nil {
|
||||
if *cfg != *tt.want {
|
||||
t.Errorf("Load() = %v, want %v", *cfg, *tt.want)
|
||||
}
|
||||
if *cfg != *tt.want {
|
||||
t.Errorf("Load() = %v, want %v", *cfg, *tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_fileNotFound(t *testing.T) {
|
||||
_, err := Load("non-existent-file.yaml")
|
||||
if !os.IsNotExist(err) {
|
||||
t.Errorf("Expected IsNotExist error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_invalidYaml(t *testing.T) {
|
||||
content := "invalid-yaml"
|
||||
tmpFile, err := os.CreateTemp("", "config.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
t.Errorf("Failed to remove temp file: %v", err)
|
||||
}
|
||||
}()
|
||||
if _, err := tmpFile.WriteString(content); err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("Failed to close temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = Load(tmpFile.Name())
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid YAML, got nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package digest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/css"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
"github.com/tdewolff/minify/v2/js"
|
||||
)
|
||||
|
||||
func MinifyHTML(input []byte, enabled bool) ([]byte, error) {
|
||||
if !enabled {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
m := minify.New()
|
||||
m.AddFunc("text/css", css.Minify)
|
||||
m.AddFunc("application/javascript", js.Minify)
|
||||
m.AddFunc("text/html", html.Minify)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := m.Minify("text/html", &buf, bytes.NewReader(input)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ func CategoryDigestJob(application *app.App, rawData *app.RawCategoryData, markA
|
||||
data := application.DigestService.BuildDigestData(rawData.Category, rawData.Entries, rawData.Icons)
|
||||
|
||||
if len(*data.Entries) > 0 {
|
||||
file, err := application.ArchiveService.MakeArchiveHTML(data)
|
||||
file, err := application.ArchiveService.MakeArchiveHTML(data, application.Config.DigestCompress)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error generating File for category %s: %v", data.Category.Title, err)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestCategoryDigestJob(t *testing.T) {
|
||||
},
|
||||
EmailService: &testutil.MockEmailService{},
|
||||
ArchiveService: &testutil.MockArchiveService{
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData, minify bool) (*os.File, error) {
|
||||
return nil, errors.New("failed to make archive html")
|
||||
},
|
||||
},
|
||||
@@ -73,7 +73,7 @@ func TestCategoryDigestJob(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ArchiveService: &testutil.MockArchiveService{
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData, minify bool) (*os.File, error) {
|
||||
return os.CreateTemp("", "test-archive-*.html")
|
||||
},
|
||||
},
|
||||
@@ -112,7 +112,7 @@ func TestCategoryDigestJob(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ArchiveService: &testutil.MockArchiveService{
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData, minify bool) (*os.File, error) {
|
||||
return os.CreateTemp("", "test-archive-*.html")
|
||||
},
|
||||
},
|
||||
@@ -149,7 +149,7 @@ func TestCategoryDigestJob(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ArchiveService: &testutil.MockArchiveService{
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
MakeArchiveHTMLFunc: func(data *models.HTMLTemplateData, minify bool) (*os.File, error) {
|
||||
return os.CreateTemp("", "test-archive-*.html")
|
||||
},
|
||||
},
|
||||
|
||||
@@ -68,11 +68,11 @@ func (m *MockMinifluxClient) StreamAllCategoryData() <-chan *app.RawCategoryData
|
||||
|
||||
type MockArchiveService struct {
|
||||
app.ArchiveService
|
||||
MakeArchiveHTMLFunc func(data *models.HTMLTemplateData) (*os.File, error)
|
||||
MakeArchiveHTMLFunc func(data *models.HTMLTemplateData, minify bool) (*os.File, error)
|
||||
}
|
||||
|
||||
func (m *MockArchiveService) MakeArchiveHTML(data *models.HTMLTemplateData) (*os.File, error) {
|
||||
return m.MakeArchiveHTMLFunc(data)
|
||||
func (m *MockArchiveService) MakeArchiveHTML(data *models.HTMLTemplateData, minify bool) (*os.File, error) {
|
||||
return m.MakeArchiveHTMLFunc(data, minify)
|
||||
}
|
||||
|
||||
func (m *MockArchiveService) CleanArchive(maxAge time.Duration) {}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"miniflux-digest/internal/digest"
|
||||
"miniflux-digest/internal/templates"
|
||||
"miniflux-digest/internal/testutil"
|
||||
)
|
||||
|
||||
func main() {
|
||||
minify := flag.Bool("minify", true, "Minify the HTML output")
|
||||
flag.Parse()
|
||||
|
||||
data := testutil.NewMockHTMLTemplateData()
|
||||
|
||||
file, err := os.Create("web/preview.html")
|
||||
@@ -21,9 +27,19 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := templates.ArchiveTemplate.Execute(file, data); err != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := templates.ArchiveTemplate.Execute(&buf, data); err != nil {
|
||||
log.Fatalf("Failed to execute template: %v", err)
|
||||
}
|
||||
|
||||
html, err := digest.MinifyHTML(buf.Bytes(), *minify)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to minify HTML: %v", err)
|
||||
}
|
||||
|
||||
if _, err := file.Write(html); err != nil {
|
||||
log.Fatalf("Failed to write to preview file: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Successfully generated web/preview.html")
|
||||
}
|
||||
Reference in new issue
Block a user