diff --git a/.env.example b/.env.example index aa12f3c..641cd37 100644 --- a/.env.example +++ b/.env.example @@ -40,7 +40,7 @@ BLOG_TITLE=Your Blog Title BLOG_TAGLINE=Your short tagline BLOG_DESCRIPTION=Your blog description goes here BLOG_AUTHOR=Your Name -BLOG_AUTHOR_EMAIL=your.email@example.com +BLOG_AUTHOR_EMAIL= # Blog language (ISO 639-1 code, e.g., 'en', 'en-US') BLOG_LANGUAGE=en diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d3970..89cec55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.15.1] - 2026-05-18 + +### Fixed + +- `BLOG_AUTHOR_EMAIL` default dropped (was `your.email@example.com`). Empty + value now hides the email card on `/about` and the `email` field in + `/about` + article JSON-LD. Operators with the old placeholder in their + `.env` should clear it. (#80) + ## [3.15.0] - 2026-05-18 Theme: **page authoring + reach copy.** Wave-4 of log.1mb.dev feedback diff --git a/internal/commands/init/command.go b/internal/commands/init/command.go index cacf3b1..21d6bda 100644 --- a/internal/commands/init/command.go +++ b/internal/commands/init/command.go @@ -140,7 +140,7 @@ func isAlreadyInitialized(dir string) bool { func getQuickDefaults() BlogConfig { currentUser, err := user.Current() username := "Blog Author" - email := "author@example.com" + email := "" if err == nil && currentUser != nil { username = currentUser.Username diff --git a/internal/config/config.go b/internal/config/config.go index caa4a35..4971377 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -276,7 +276,7 @@ func Load() (*Config, error) { Tagline: getEnv("BLOG_TAGLINE", ""), Description: getEnv("BLOG_DESCRIPTION", "Your blog description goes here"), Author: getEnv("BLOG_AUTHOR", "Your Name"), - AuthorEmail: getEnv("BLOG_AUTHOR_EMAIL", "your.email@example.com"), + AuthorEmail: getEnv("BLOG_AUTHOR_EMAIL", ""), Language: getEnv("BLOG_LANGUAGE", "en"), Theme: getEnv("BLOG_THEME", "default"), Style: getEnv("BLOG_STYLE", "minimal"), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2a5ace6..80f9ae0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -71,7 +71,7 @@ func TestLoad(t *testing.T) { assert.Equal(t, "Your Blog Title", cfg.Blog.Title) assert.Equal(t, "Your blog description goes here", cfg.Blog.Description) assert.Equal(t, "Your Name", cfg.Blog.Author) - assert.Equal(t, "your.email@example.com", cfg.Blog.AuthorEmail) + assert.Equal(t, "", cfg.Blog.AuthorEmail) assert.Equal(t, "en", cfg.Blog.Language) assert.Equal(t, "default", cfg.Blog.Theme) assert.Equal(t, 10, cfg.Blog.PostsPerPage) diff --git a/internal/handlers/about_handler_test.go b/internal/handlers/about_handler_test.go index 0b43206..c78ec0f 100644 --- a/internal/handlers/about_handler_test.go +++ b/internal/handlers/about_handler_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" "github.com/gin-gonic/gin" @@ -12,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/1mb-dev/markgo/internal/config" + "github.com/1mb-dev/markgo/internal/services" ) func createTestAboutHandler(cfg *config.Config) (*AboutHandler, *MockTemplateService) { @@ -176,6 +178,69 @@ func TestAboutHandler_TemplateData(t *testing.T) { } } +// TestAboutHandler_JSONLDEmail locks the contract: about.html JSON-LD emits +// the "email" field only when BLOG_AUTHOR_EMAIL is configured. With an empty +// value, no "email": substring may appear inside the JSON-LD block. +// Sibling of TestArticleHandler_JSONLDEmail — locks the same contract on +// the second emission site surfaced by #80. +func TestAboutHandler_JSONLDEmail(t *testing.T) { + tests := []struct { + name string + authorEmail string + denySubstring string + wantSubstring string + }{ + { + name: "empty email — JSON-LD omits email field", + authorEmail: "", + denySubstring: `"email":`, + }, + { + name: "configured email — JSON-LD includes email field", + authorEmail: "author@example.com", + wantSubstring: `"email": "author@example.com"`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := &config.Config{ + Environment: "test", + BaseURL: "http://localhost:3000", + Blog: config.BlogConfig{Title: "Test Blog", Description: "Test", Author: "Test Author", AuthorEmail: tc.authorEmail}, + } + tplSvc, err := services.NewTemplateService("/nonexistent", cfg) + require.NoError(t, err, "real TemplateService falls back to embedded templates") + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) + base := NewBaseHandler(cfg, logger, tplSvc, &BuildInfo{Version: "test"}, &MockSEOService{}) + handler := NewAboutHandler(base, &MockArticleService{}, &MockMarkdownRenderer{}) + + router := gin.New() + router.GET("/about", handler.ShowAbout) + + w := httptest.NewRecorder() + router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/about", http.NoBody)) + require.Equal(t, http.StatusOK, w.Code) + + body := w.Body.String() + jsonLDStart := strings.Index(body, ``) + require.Greater(t, jsonLDEnd, 0, "JSON-LD script tag must be closed") + jsonLD := body[jsonLDStart : jsonLDStart+jsonLDEnd] + + if tc.denySubstring != "" { + assert.NotContains(t, jsonLD, tc.denySubstring, + "JSON-LD must not emit email field when AuthorEmail is empty") + } + if tc.wantSubstring != "" { + assert.Contains(t, jsonLD, tc.wantSubstring, + "JSON-LD must emit configured email value") + } + }) + } +} + // TestAboutHandler_Identity verifies the identity/social/bio composition // path still produces expected data after the v3.14.0 reach addition. func TestAboutHandler_Identity(t *testing.T) { diff --git a/internal/handlers/article_test.go b/internal/handlers/article_test.go index 8215dba..bd96d31 100644 --- a/internal/handlers/article_test.go +++ b/internal/handlers/article_test.go @@ -80,6 +80,70 @@ func createTestBase() (*BaseHandler, *TestArticleService) { return base, svc } +// TestArticleHandler_JSONLDEmail locks the contract: article.html JSON-LD +// emits the "email" field only when BLOG_AUTHOR_EMAIL is configured. With +// an empty value, no "email": substring may appear inside the JSON-LD block +// (would be invalid Schema.org). Surfaced by #80. +func TestArticleHandler_JSONLDEmail(t *testing.T) { + tests := []struct { + name string + authorEmail string + denySubstring string + wantSubstring string + }{ + { + name: "empty email — JSON-LD omits email field", + authorEmail: "", + denySubstring: `"email":`, + }, + { + name: "configured email — JSON-LD includes email field", + authorEmail: "author@example.com", + wantSubstring: `"email": "author@example.com"`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := &config.Config{ + Environment: "test", + BaseURL: "http://localhost:3000", + Blog: config.BlogConfig{Title: "Test Blog", Description: "Test", Author: "Test Author", AuthorEmail: tc.authorEmail}, + } + tplSvc, err := services.NewTemplateService("/nonexistent", cfg) + require.NoError(t, err, "real TemplateService falls back to embedded templates") + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) + base := NewBaseHandler(cfg, logger, tplSvc, &BuildInfo{Version: "test"}, &MockSEOService{}) + svc := &TestArticleService{articles: []*models.Article{ + {Slug: "test-slug", Title: "Test Article", Date: time.Now(), Content: "body", ProcessedContent: "
body
"}, + }} + + router := gin.New() + router.GET("/writing/:slug", NewPostHandler(base, svc).Article) + + w := httptest.NewRecorder() + router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/writing/test-slug", http.NoBody)) + require.Equal(t, http.StatusOK, w.Code) + + body := w.Body.String() + jsonLDStart := strings.Index(body, ``) + require.Greater(t, jsonLDEnd, 0, "JSON-LD script tag must be closed") + jsonLD := body[jsonLDStart : jsonLDStart+jsonLDEnd] + + if tc.denySubstring != "" { + assert.NotContains(t, jsonLD, tc.denySubstring, + "JSON-LD must not emit email field when AuthorEmail is empty (would be invalid Schema.org)") + } + if tc.wantSubstring != "" { + assert.Contains(t, jsonLD, tc.wantSubstring, + "JSON-LD must emit configured email value") + } + }) + } +} + func TestArticleBySlug(t *testing.T) { tests := []struct { name string diff --git a/web/templates/article.html b/web/templates/article.html index 2d6305c..7ed2765 100644 --- a/web/templates/article.html +++ b/web/templates/article.html @@ -133,8 +133,8 @@