diff --git a/Makefile b/Makefile index a5f1d6e..5f19fbd 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/config.yaml.example b/config.yaml.example index 79485ca..3533077 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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 diff --git a/go.mod b/go.mod index d333c0b..1f51669 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e2591f0..2c0a2fe 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/app/interfaces.go b/internal/app/interfaces.go index 1107f3a..b899457 100644 --- a/internal/app/interfaces.go +++ b/internal/app/interfaces.go @@ -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) } diff --git a/internal/archive/archive.go b/internal/archive/archive.go index be3b8ae..92965de 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -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) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 024e48d..83677ac 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -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) } diff --git a/internal/config/config.go b/internal/config/config.go index 6a86757..b088b2e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 76d7d4f..74f0cb3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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") - } -} +} \ No newline at end of file diff --git a/internal/digest/minify.go b/internal/digest/minify.go new file mode 100644 index 0000000..a954e9f --- /dev/null +++ b/internal/digest/minify.go @@ -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 +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index e1ba105..f289cdb 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -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) diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index e11bf15..4ce1310 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -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") }, }, diff --git a/internal/testutil/mocks.go b/internal/testutil/mocks.go index 222aa63..14c7213 100644 --- a/internal/testutil/mocks.go +++ b/internal/testutil/mocks.go @@ -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) {} diff --git a/scripts/preview-html/main.go b/scripts/preview-html/main.go index 5c1d3ec..4390513 100644 --- a/scripts/preview-html/main.go +++ b/scripts/preview-html/main.go @@ -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") } \ No newline at end of file