From ee35039582e32d6b102345fcf0f43d338e515009 Mon Sep 17 00:00:00 2001 From: Zach Hammer Date: Sat, 24 Jan 2026 14:05:44 -0500 Subject: [PATCH 1/3] Refactor ViewData to use functional options pattern Add NewViewData constructor with WithData, WithTitle, and WithPageClass options. Update all call sites in main.go to use the new pattern. Co-Authored-By: Claude Opus 4.5 --- cmd/site/main.go | 41 ++++++++++++++++++++--------------------- cmd/site/template.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/cmd/site/main.go b/cmd/site/main.go index 27918af..54b68d3 100644 --- a/cmd/site/main.go +++ b/cmd/site/main.go @@ -17,6 +17,7 @@ type Config struct { cmd.Config Port int `default:"8000"` SessionSecretKey string `envconfig:"session_secret_key" default:"dev_session_secret"` + SiteURL string `envconfig:"site_url"` } // func main() { @@ -55,7 +56,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { errorPage := func(w http.ResponseWriter, r *http.Request, e error) error { fmt.Printf("error on request %s: %s\n", r.URL.String(), e.Error()) - return embedded.WebTemplates.ExecuteTemplate(w, "exception.tmpl", ViewData{e, r, "Uh Oh!", ""}) + return embedded.WebTemplates.ExecuteTemplate(w, "exception.tmpl", NewViewData(r, cfg, WithData(e), WithTitle("Uh Oh!"))) } router.HandleFunc("/exception", func(w http.ResponseWriter, r *http.Request) { @@ -69,7 +70,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "index.tmpl", ViewData{nil, r, "", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "index.tmpl", NewViewData(r, cfg)); err != nil { errorPage(w, r, err) } }).Methods("GET") @@ -80,7 +81,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "login.tmpl", ViewData{nil, r, "Scribly - Log in", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "login.tmpl", NewViewData(r, cfg, WithTitle("Scribly - Log in"))); err != nil { errorPage(w, r, err) } }).Methods("GET") @@ -118,7 +119,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { }).Methods("POST") router.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) { - if err := embedded.WebTemplates.ExecuteTemplate(w, "signup.tmpl", ViewData{nil, r, "Scribly - Sign up", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "signup.tmpl", NewViewData(r, cfg, WithTitle("Scribly - Sign up"))); err != nil { errorPage(w, r, err) } }).Methods("GET") @@ -173,7 +174,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "me.tmpl", ViewData{me, r, "", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "me.tmpl", NewViewData(r, cfg, WithData(me))); err != nil { errorPage(w, r, err) return } @@ -200,20 +201,19 @@ func MakeRouter(cfg Config) (http.Handler, error) { switch story.Story.State { case internal.StoryStateDraft: userSuggestions, _ := scribly.UserSuggestions(r.Context(), *user) - viewData := ViewData{ - Data: map[string]interface{}{ + viewData := NewViewData(r, cfg, + WithData(map[string]interface{}{ "UserStory": story, "UserSuggestions": userSuggestions, - }, - Request: r, - title: fmt.Sprintf("%s - Add Cowriters", story.Story.Title), - } + }), + WithTitle(fmt.Sprintf("%s - Add Cowriters", story.Story.Title)), + ) if err := embedded.WebTemplates.ExecuteTemplate(w, "addpeopletostory.tmpl", viewData); err != nil { errorPage(w, r, err) return } default: - if err := embedded.WebTemplates.ExecuteTemplate(w, "story.tmpl", ViewData{story, r, fmt.Sprintf("Scribly - %s", story.Story.Title), ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "story.tmpl", NewViewData(r, cfg, WithData(story), WithTitle(fmt.Sprintf("Scribly - %s", story.Story.Title)))); err != nil { errorPage(w, r, err) return } @@ -286,7 +286,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "newstory.tmpl", ViewData{nil, r, "Scribly - New Story", "page page-tall"}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "newstory.tmpl", NewViewData(r, cfg, WithTitle("Scribly - New Story"), WithPageClass("page page-tall"))); err != nil { errorPage(w, r, err) return } @@ -381,14 +381,13 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - data := ViewData{ - Request: r, - Data: map[string]interface{}{ + data := NewViewData(r, cfg, + WithData(map[string]interface{}{ "User": user, "StoryID": storyID, - }, - title: "Nudge delivered", - } + }), + WithTitle("Nudge delivered"), + ) if err := embedded.WebTemplates.ExecuteTemplate(w, "nudged.tmpl", data); err != nil { errorPage(w, r, err) return @@ -408,7 +407,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "emailverificationrequested.tmpl", ViewData{user, r, "Email Verification Sent", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "emailverificationrequested.tmpl", NewViewData(r, cfg, WithData(user), WithTitle("Email Verification Sent"))); err != nil { errorPage(w, r, err) return } @@ -428,7 +427,7 @@ func MakeRouter(cfg Config) (http.Handler, error) { return } - if err := embedded.WebTemplates.ExecuteTemplate(w, "emailverificationsuccess.tmpl", ViewData{user, r, "Email Verified!", ""}); err != nil { + if err := embedded.WebTemplates.ExecuteTemplate(w, "emailverificationsuccess.tmpl", NewViewData(r, cfg, WithData(user), WithTitle("Email Verified!"))); err != nil { errorPage(w, r, err) return } diff --git a/cmd/site/template.go b/cmd/site/template.go index 64c7da2..d81f64b 100644 --- a/cmd/site/template.go +++ b/cmd/site/template.go @@ -15,6 +15,35 @@ type ViewData struct { Request *http.Request title string pageClass string + cfg Config +} + +type ViewDataOption func(*ViewData) + +func NewViewData(r *http.Request, cfg Config, opts ...ViewDataOption) ViewData { + vd := ViewData{Request: r, cfg: cfg} + for _, opt := range opts { + opt(&vd) + } + return vd +} + +func WithData(data interface{}) ViewDataOption { + return func(vd *ViewData) { + vd.Data = data + } +} + +func WithTitle(title string) ViewDataOption { + return func(vd *ViewData) { + vd.title = title + } +} + +func WithPageClass(pageClass string) ViewDataOption { + return func(vd *ViewData) { + vd.pageClass = pageClass + } } // v *ViewData doesn't seem to work. i guess go templates don't have the same @@ -35,6 +64,10 @@ func (v ViewData) Title() string { return v.title } +func (v ViewData) SiteURL() string { + return v.cfg.SiteURL +} + func (v ViewData) PageClass() string { if v.pageClass == "" { return "page" @@ -80,6 +113,7 @@ func (v ViewData) Propogate(data interface{}) ViewData { return ViewData{ Data: data, Request: v.Request, + cfg: v.cfg, } } From b578a612c1f341590b6b12ef3b4fdf9fb4ba65d1 Mon Sep 17 00:00:00 2001 From: Zach Hammer Date: Sat, 24 Jan 2026 14:07:47 -0500 Subject: [PATCH 2/3] Refactor email viewData to use functional options and config - Add newViewData constructor with withData option for email templates - Pass websiteURL as parameter instead of reading from os.Getenv - Add websiteURL field to Scribly struct and NewScribly constructor - Add SiteURL field to cmd.Config (centralized for both site and emails) Co-Authored-By: Claude Opus 4.5 --- cmd/config.go | 3 ++- cmd/site/main.go | 1 - internal/emails.go | 48 +++++++++++++++++++++++++++++---------------- internal/scribly.go | 12 +++++++----- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 5952c0e..c7f0984 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -14,6 +14,7 @@ type Config struct { DatabaseURL string `envconfig:"database_url" default:"postgres://scribly:pass@localhost/scribly?sslmode=disable"` ResendBaseURL string `envconfig:"resend_base_url" default:"https://api.resend.com"` ResendAPIKey string `envconfig:"resend_api_key" default:"test_resend_api_key"` + SiteURL string `envconfig:"site_url"` Debug bool } @@ -34,7 +35,7 @@ func (c *Config) MakeScribly() (*internal.Scribly, error) { resend := internal.NewResendClient(c.ResendBaseURL, c.ResendAPIKey) messageGateway := internal.GoroutineMessageGateway{} - scribly, err := internal.NewScribly(db, resend, &messageGateway) + scribly, err := internal.NewScribly(db, resend, &messageGateway, c.SiteURL) if err != nil { return nil, err } diff --git a/cmd/site/main.go b/cmd/site/main.go index 54b68d3..b50bf0e 100644 --- a/cmd/site/main.go +++ b/cmd/site/main.go @@ -17,7 +17,6 @@ type Config struct { cmd.Config Port int `default:"8000"` SessionSecretKey string `envconfig:"session_secret_key" default:"dev_session_secret"` - SiteURL string `envconfig:"site_url"` } // func main() { diff --git a/internal/emails.go b/internal/emails.go index e57e987..89ba50a 100644 --- a/internal/emails.go +++ b/internal/emails.go @@ -4,25 +4,39 @@ import ( "bytes" "fmt" "html/template" - "os" "scribly/embed" "strings" "github.com/vanng822/go-premailer/premailer" ) -var websiteURL = os.Getenv("WEBSITE_URL") - type viewData struct { - Data interface{} + Data interface{} + websiteURL string +} + +type viewDataOption func(*viewData) + +func newViewData(websiteURL string, opts ...viewDataOption) viewData { + vd := viewData{websiteURL: websiteURL} + for _, opt := range opts { + opt(&vd) + } + return vd +} + +func withData(data interface{}) viewDataOption { + return func(vd *viewData) { + vd.Data = data + } } func (v viewData) StoryLink(story Story) template.HTML { - return template.HTML(fmt.Sprintf(`%s`, websiteURL, story.ID, story.Title)) + return template.HTML(fmt.Sprintf(`%s`, v.websiteURL, story.ID, story.Title)) } func (v viewData) WebsiteURL() string { - return websiteURL + return v.websiteURL } func (v viewData) Replace(original string, pattern string, replacement string) string { @@ -45,13 +59,13 @@ func (v viewData) CSS() template.HTML { return template.HTML("") } -func BuildNudgeEmail(nudger User, nudgee User, story Story) (*Email, error) { +func BuildNudgeEmail(websiteURL string, nudger User, nudgee User, story Story) (*Email, error) { subject := fmt.Sprintf("%s nudged you to take your turn on %s", nudger.Username, story.Title) data := map[string]interface{}{ "Story": story, "Nudger": nudger, } - body, err := renderTemplateWithCSS("nudge.tmpl", data) + body, err := renderTemplateWithCSS("nudge.tmpl", websiteURL, data) if err != nil { return nil, err } @@ -63,7 +77,7 @@ func BuildNudgeEmail(nudger User, nudgee User, story Story) (*Email, error) { }, nil } -func BuildAddedToStoryEmails(story Story) ([]Email, error) { +func BuildAddedToStoryEmails(websiteURL string, story Story) ([]Email, error) { var recipients []User for _, cowriter := range story.Cowriters { if cowriter.User.ID != story.CreatedByID && cowriter.User.EmailVerificationStatus == EmailVerificationStateVerified { @@ -77,7 +91,7 @@ func BuildAddedToStoryEmails(story Story) ([]Email, error) { "Story": story, "Recipient": recipient, } - body, err := renderTemplateWithCSS("addedtostory.tmpl", data) + body, err := renderTemplateWithCSS("addedtostory.tmpl", websiteURL, data) if err != nil { return nil, err } @@ -91,7 +105,7 @@ func BuildAddedToStoryEmails(story Story) ([]Email, error) { return emails, nil } -func BuildTurnNotificationEmails(story Story, turnNumber int) ([]Email, error) { +func BuildTurnNotificationEmails(websiteURL string, story Story, turnNumber int) ([]Email, error) { turn := story.Turns[turnNumber-1] var recipients []User for _, cowriter := range story.Cowriters { @@ -108,7 +122,7 @@ func BuildTurnNotificationEmails(story Story, turnNumber int) ([]Email, error) { "Turn": &turn, "Recipient": recipient, } - body, err := renderTemplateWithCSS("storyturnnotification.tmpl", data) + body, err := renderTemplateWithCSS("storyturnnotification.tmpl", websiteURL, data) if err != nil { return nil, err } @@ -134,12 +148,12 @@ func BuildTurnNotificationEmails(story Story, turnNumber int) ([]Email, error) { return emails, nil } -func BuildEmailVerificationEmail(user User, token string) (*Email, error) { +func BuildEmailVerificationEmail(websiteURL string, user User, token string) (*Email, error) { data := map[string]interface{}{ "Recipient": user, "VerificationToken": token, } - body, err := renderTemplateWithCSS("verification.tmpl", data) + body, err := renderTemplateWithCSS("verification.tmpl", websiteURL, data) if err != nil { return nil, err } @@ -151,10 +165,10 @@ func BuildEmailVerificationEmail(user User, token string) (*Email, error) { }, nil } -func renderTemplateWithCSS(templateName string, data interface{}) (string, error) { +func renderTemplateWithCSS(templateName string, websiteURL string, data interface{}) (string, error) { var buffer bytes.Buffer - viewData := viewData{Data: data} - if err := embed.EmailTemplates.ExecuteTemplate(&buffer, templateName, viewData); err != nil { + vd := newViewData(websiteURL, withData(data)) + if err := embed.EmailTemplates.ExecuteTemplate(&buffer, templateName, vd); err != nil { return "", err } prem, err := premailer.NewPremailerFromBytes(buffer.Bytes(), premailer.NewOptions()) diff --git a/internal/scribly.go b/internal/scribly.go index de8abb7..ca48ef2 100644 --- a/internal/scribly.go +++ b/internal/scribly.go @@ -14,6 +14,7 @@ type Scribly struct { db *bun.DB emailer EmailGateway messageGateway MessageGateway + websiteURL string } func (s *Scribly) LogIn(ctx context.Context, input LoginInput) (*User, error) { @@ -271,7 +272,7 @@ func (s *Scribly) Nudge(ctx context.Context, nudger User, nudgeeID int, storyID return err } - email, err := BuildNudgeEmail(nudger, nudgee, story) + email, err := BuildNudgeEmail(s.websiteURL, nudger, nudgee, story) if err != nil { return err } @@ -292,7 +293,7 @@ func (s *Scribly) SendAddedToStoryEmails(ctx context.Context, storyID int) error return err } - emails, err := BuildAddedToStoryEmails(story) + emails, err := BuildAddedToStoryEmails(s.websiteURL, story) if err != nil { return err } @@ -318,7 +319,7 @@ func (s *Scribly) SendTurnEmailNotifications(ctx context.Context, storyID int, t return err } - emails, err := BuildTurnNotificationEmails(story, turnNumber) + emails, err := BuildTurnNotificationEmails(s.websiteURL, story, turnNumber) if err != nil { return err } @@ -347,7 +348,7 @@ func (s *Scribly) SendVerificationEmail(ctx context.Context, userID int) error { return err } - email, err := BuildEmailVerificationEmail(user, verificationToken) + email, err := BuildEmailVerificationEmail(s.websiteURL, user, verificationToken) if err != nil { return err } @@ -377,10 +378,11 @@ func (s *Scribly) VerifyEmail(ctx context.Context, user User, token string) erro return nil } -func NewScribly(db *bun.DB, emailer EmailGateway, messageGateway MessageGateway) (*Scribly, error) { +func NewScribly(db *bun.DB, emailer EmailGateway, messageGateway MessageGateway, websiteURL string) (*Scribly, error) { return &Scribly{ db: db, emailer: emailer, messageGateway: messageGateway, + websiteURL: websiteURL, }, nil } From c078397421ab723b718712a2f236f56f9dceedd8 Mon Sep 17 00:00:00 2001 From: Zach Hammer Date: Sat, 24 Jan 2026 14:11:00 -0500 Subject: [PATCH 3/3] use vercel env var for url --- api/index.go | 10 ++++++++++ docker-compose.yml | 2 +- embed/templates/web/layout.tmpl | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/index.go b/api/index.go index 28b54e9..9308ae3 100644 --- a/api/index.go +++ b/api/index.go @@ -4,14 +4,24 @@ import ( "fmt" "log" "net/http" + "os" "scribly/cmd/site" + "strings" "github.com/kelseyhightower/envconfig" ) var router http.Handler +func url() string { + url := os.Getenv("VERCEL_PROJECT_PRODUCTION_URL") + url, _ = strings.CutPrefix(url, "www.") + return "https://" + url +} + func init() { + os.Setenv("SITE_URL", url()) + cfg := site.Config{} err := envconfig.Process("", &cfg) if err != nil { diff --git a/docker-compose.yml b/docker-compose.yml index c43d48f..d0b885f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - "./static:/project/static" environment: DATABASE_URL: postgres://scribly:pass@db/scribly?sslmode=disable - WEBSITE_URL: http://127.0.0.1:8000 + SITE_URL: http://127.0.0.1:8000 RESEND_BASE_URL: http://mockresend:1080 command: > bash -c " diff --git a/embed/templates/web/layout.tmpl b/embed/templates/web/layout.tmpl index c74a489..9ca1c4f 100644 --- a/embed/templates/web/layout.tmpl +++ b/embed/templates/web/layout.tmpl @@ -14,7 +14,7 @@ - +