Skip to content

Commit b67a8e4

Browse files
committed
feat(email): Add email digest templates, styles, and components
This commit introduces a comprehensive email delivery system, complete with structured templates, reusable components, and a dedicated styling system. New email templates, including a `defaultEmailTemplate`, have been added, leveraging Go's `html/template` package for dynamic content rendering. These templates are designed with a component-based architecture, making them modular and easy to maintain. A key feature of this commit is the introduction of reusable email components, such as badges, banners, and stat boxes. These components are defined in `workers/email/pkg/digest/components.go` and are designed to be easily configurable and reusable across different email types. To ensure a consistent and polished look, a dedicated styling system has been implemented in `workers/email/pkg/digest/styles.go`. This file contains all the CSS styles used in the email templates, defined as template snippets. This approach centralizes all styling information, making it easy to manage and update the visual appearance of the emails. A golden file, `digest.golden.html`, has been added to the test data to ensure that the email rendering remains consistent and predictable. The `.prettierignore` and `Makefile` have been updated to exclude this file from formatting and license checks. Additionally, a suite of email icons and logos has been added to `frontend/src/static/img/email` to enhance the visual appeal of the emails.
1 parent fb69239 commit b67a8e4

File tree

15 files changed

+1527
-1
lines changed

15 files changed

+1527
-1
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ jsonschema/mdn_browser-compat-data
77
jsonschema/web-platform-dx_web-features
88
jsonschema/web-platform-dx_web-features-mappings
99
docs/schema
10+
workers/email/pkg/digest/testdata/digest.golden.html
1011
workflows/steps/services/bcd_consumer/pkg/data/testdata/data.json
1112
workflows/steps/services/web_feature_consumer/pkg/data/testdata/v3.data.json
1213
workflows/steps/services/developer_signals_consumer/pkg/data/testdata/web-features-signals.json

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ ADDLICENSE_ARGS := -c "${COPYRIGHT_NAME}" \
337337
-ignore 'node_modules/**' \
338338
-ignore 'infra/storage/spanner/schema.sql' \
339339
-ignore 'antlr/.antlr/**' \
340-
-ignore '.devcontainer/cache/**'
340+
-ignore '.devcontainer/cache/**' \
341+
-ignore 'workers/email/pkg/digest/testdata/digest.golden.html'
341342

342343
license-check: go-install-tools
343344
go tool addlicense -check $(ADDLICENSE_ARGS) .
4.12 KB
Loading
4.86 KB
Loading
5.76 KB
Loading
2.01 KB
Loading
2.04 KB
Loading
6.95 KB
Loading
2.1 KB
Loading
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// nolint:lll // WONTFIX - for readability
16+
package digest
17+
18+
const componentStyles = `{{- define "style_badge_wrapper" -}}align-self: stretch; padding-top: 12px; padding-bottom: 11px; padding-left: 15px; padding-right: 16px; overflow: hidden; border-top-left-radius: 4px; border-top-right-radius: 4px; justify-content: flex-start; align-items: center; display: flex;{{- end -}}
19+
{{- define "style_badge_inner_wrapper" -}}flex: 1 1 0; flex-direction: column; justify-content: center; align-items: flex-start; display: inline-flex;{{- end -}}
20+
{{- define "style_change_detail_wrapper" -}}align-self: stretch; justify-content: flex-start; align-items: center; gap: 10px; display: inline-flex; width: 100%;{{- end -}}
21+
{{- define "style_change_detail_inner" -}}flex: 1 1 0;{{- end -}}
22+
{{- define "style_banner_wrapper" -}}align-self: stretch; height: 50px; padding-top: 12px; padding-bottom: 11px; padding-left: 15px; padding-right: 16px; overflow: hidden; border-top-left-radius: 4px; border-top-right-radius: 4px; justify-content: flex-start; align-items: center; gap: 8px; display: flex;{{- end -}}
23+
{{- define "style_banner_icon_wrapper_28" -}}height: 28px; position: relative; overflow: hidden; display: flex; align-items: center;{{- end -}}
24+
{{- define "style_banner_icon_wrapper_20" -}}height: 20px; position: relative; margin-right: 4px;{{- end -}}
25+
{{- define "style_img_responsive" -}}display: block; width: auto;{{- end -}}
26+
{{- define "style_banner_text_wrapper" -}}flex: 1 1 0;{{- end -}}
27+
{{- define "style_banner_browser_logos_wrapper" -}}justify-content: flex-start; align-items: center; display: flex; margin-right: 8px;{{- end -}}
28+
{{- define "style_browser_item_row" -}}align-self: stretch; justify-content: flex-start; align-items: center; gap: 10px; display: flex;{{- end -}}
29+
{{- define "style_browser_item_logo_wrapper" -}}justify-content: flex-start; align-items: center; display: flex;{{- end -}}
30+
{{- define "style_browser_item_feature_link_wrapper" -}}align-self: stretch; justify-content: flex-start; align-items: center; gap: 10px; display: inline-flex; margin-top: 8px;{{- end -}}
31+
{{- define "style_button_wrapper" -}}margin: 20px 0; text-align: center;{{- end -}}
32+
{{- define "style_footer_wrapper" -}}align-self: stretch; padding-top: 16px; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 12px; display: flex; {{- template "font_family_main" -}};{{- end -}}
33+
{{- define "style_footer_hr" -}}align-self: stretch; height: 1px; background: #E4E4E7;{{- end -}}
34+
{{- define "style_footer_text_wrapper" -}}align-self: stretch;{{- end -}}
35+
{{- define "style_feature_title_row_wrapper" -}}align-self: stretch; justify-content: flex-start; align-items: center; gap: 10px; display: flex;{{- end -}}
36+
{{- define "style_feature_title_row_inner" -}}flex: 1 1 0;{{- end -}}`
37+
38+
const badgeComponent = `{{- define "badge" -}}
39+
<div style='{{- template "style_badge_wrapper" -}}; background: {{- badgeBackgroundColor .Title -}};'>
40+
<div style='{{- template "style_badge_inner_wrapper" -}}'>
41+
<div style='{{- template "style_text_badge_title" -}}'>{{.Title}}</div>
42+
{{- if .Description -}}
43+
<div style='{{- template "style_text_badge_description" -}}'>{{.Description}}</div>
44+
{{- end -}}
45+
</div>
46+
</div>
47+
{{- end -}}`
48+
49+
const introTextComponent = `{{- define "intro_text" -}}
50+
<div style='{{- template "style_section_wrapper" -}}'>
51+
<h2 style='{{- template "style_subject_header" -}}'>{{.Subject}}</h2>
52+
<div style='{{- template "style_query_text" -}}'>
53+
Here is your update for the saved search <strong style='font-weight: bold;'>'{{.Query}}'</strong>.
54+
{{.SummaryText}}.
55+
</div>
56+
</div>
57+
{{- end -}}`
58+
59+
const changeDetailComponent = `{{- define "change_detail" -}}
60+
<div style='{{- template "style_change_detail_wrapper" -}}'>
61+
<div style='{{- template "style_change_detail_inner" -}}'>
62+
<span style='{{- template "style_text_body" -}}'>{{.Label}}</span>
63+
<span style='{{- template "style_text_body_subtle" -}}'> ({{.From}} &rarr; {{.To}})</span>
64+
</div>
65+
</div>
66+
{{- end -}}`
67+
68+
const baselineChangeItemComponent = `{{- define "baseline_change_item" -}}
69+
<div style='{{- template "style_section_wrapper" -}}'>
70+
<div style='{{- template "style_banner_wrapper" -}}; {{- template "color_bg_success" -}}'>
71+
<div style='{{- template "style_banner_icon_wrapper_28" -}}'>
72+
<img src="{{.ToURL}}" height="28" alt="{{.To}}" style='{{- template "style_img_responsive" -}}' />
73+
</div>
74+
<div style='{{- template "style_banner_text_wrapper" -}}'>
75+
<span style='{{- template "style_text_banner_bold" -}}'>Baseline</span>
76+
<span style='{{- template "style_text_banner_normal" -}}'> {{.To}} </span>
77+
</div>
78+
</div>
79+
<div style='{{- template "style_card_body" -}}'>
80+
<div style='{{- template "style_browser_item_row" -}}'>
81+
<div style='{{- template "style_banner_text_wrapper" -}}'>
82+
<span style='{{- template "style_text_feature_link" -}}'>{{.FeatureName}}</span>
83+
</div>
84+
<!-- Optional Date logic could go here if passed -->
85+
</div>
86+
</div>
87+
</div>
88+
{{- end -}}`
89+
90+
const browserItemComponent = `{{- define "browser_item" -}}
91+
<div style='{{- template "style_card_body" -}}'>
92+
<div style='{{- template "style_browser_item_row" -}}'>
93+
<div style='{{- template "style_browser_item_logo_wrapper" -}}'>
94+
<img src="{{.LogoURL}}" height="20" alt="{{.Name}}" style='{{- template "style_img_responsive" -}}' />
95+
</div>
96+
<div style='{{- template "style_text_browser_item" -}}'>
97+
{{.Name}}: {{ template "browser_status_detail" .From }} &rarr; {{ template "browser_status_detail" .To -}}
98+
</div>
99+
</div>
100+
{{- if .FeatureName -}}
101+
<div style='{{- template "style_browser_item_feature_link_wrapper" -}}'>
102+
<div style='{{- template "style_banner_text_wrapper" -}}'>
103+
<a href="{{.FeatureURL}}" style='{{- template "style_text_feature_link" -}}'>{{.FeatureName}}</a>
104+
</div>
105+
</div>
106+
{{- end -}}
107+
</div>
108+
{{- end -}}`
109+
110+
const buttonComponent = `{{- define "button" -}}
111+
<div style='{{- template "style_button_wrapper" -}}'>
112+
<a href="{{.URL}}" style='{{- template "style_button_link" -}}'>
113+
{{.Text}}
114+
</a>
115+
</div>
116+
{{- end -}}`
117+
118+
const footerComponent = `{{- define "footer" -}}
119+
<div style='{{- template "style_footer_wrapper" -}}'>
120+
<div style='{{- template "style_footer_hr" -}}'></div>
121+
<div style='{{- template "style_footer_text_wrapper" -}}'>
122+
<span style='{{- template "style_text_footer" -}}'>You can </span>
123+
<a href="{{.UnsubscribeURL}}" style='{{- template "style_text_footer_link" -}}'>unsubscribe</a>
124+
<span style='{{- template "style_text_footer" -}}'> or change any of your alerts on </span>
125+
<a href="{{.ManageURL}}" style='{{- template "style_text_footer_link" -}}'>webstatus.dev</a>
126+
</div>
127+
</div>
128+
{{- end -}}`
129+
130+
const bannerComponents = `{{- define "banner_baseline_widely" -}}
131+
<div style='{{- template "style_banner_wrapper" -}}{{- template "color_bg_success" -}}'>
132+
<div style='{{- template "style_banner_icon_wrapper_28" -}}'>
133+
<img src="{{.LogoURL}}" height="28" alt="Widely Available" style='{{- template "style_img_responsive" -}}' />
134+
</div>
135+
<div style='{{- template "style_banner_text_wrapper" -}}'>
136+
<span style='{{- template "style_text_banner_bold" -}}'>Baseline</span>
137+
<span style='{{- template "style_text_banner_normal" -}}'> Widely available </span>
138+
</div>
139+
</div>
140+
{{- end -}}
141+
{{- define "banner_baseline_newly" -}}
142+
<div style='{{- template "style_banner_wrapper" -}}{{- template "color_bg_info" -}}'>
143+
<div style='{{- template "style_banner_icon_wrapper_28" -}}'>
144+
<img src="{{.LogoURL}}" height="28" alt="Newly Available" style='{{- template "style_img_responsive" -}}' />
145+
</div>
146+
<div style='{{- template "style_banner_text_wrapper" -}}'>
147+
<span style='{{- template "style_text_banner_bold" -}}'>Baseline</span>
148+
<span style='{{- template "style_text_banner_normal" -}}'> Newly available </span>
149+
</div>
150+
</div>
151+
{{- end -}}
152+
{{- define "banner_baseline_regression" -}}
153+
<div style='{{- template "style_banner_wrapper" -}}{{- template "color_bg_neutral" -}}'>
154+
<div style='{{- template "style_banner_icon_wrapper_28" -}}'>
155+
<img src="{{.LogoURL}}" height="28" alt="Regressed" style='{{- template "style_img_responsive" -}}' />
156+
</div>
157+
<div style='{{- template "style_banner_text_wrapper" -}}'>
158+
<span style='{{- template "style_text_banner_bold" -}}'>Regressed</span>
159+
<span style='{{- template "style_text_banner_normal" -}}'> to limited availability</span>
160+
</div>
161+
</div>
162+
{{- end -}}
163+
{{- define "banner_browser_implementation" -}}
164+
<div style='{{- template "style_banner_wrapper" -}}{{- template "color_bg_neutral" -}}'>
165+
<div style='{{- template "style_banner_browser_logos_wrapper" -}}'>
166+
{{- /* Always display the 4 main browser logos as requested */ -}}
167+
<div style='{{- template "style_banner_icon_wrapper_20" -}}'>
168+
<img src="{{browserLogoURL "chrome"}}" height="20" style='{{- template "style_img_responsive" -}}' />
169+
</div>
170+
<div style='{{- template "style_banner_icon_wrapper_20" -}}'>
171+
<img src="{{browserLogoURL "edge"}}" height="20" style='{{- template "style_img_responsive" -}}' />
172+
</div>
173+
<div style='{{- template "style_banner_icon_wrapper_20" -}}'>
174+
<img src="{{browserLogoURL "firefox"}}" height="20" style='{{- template "style_img_responsive" -}}' />
175+
</div>
176+
<div style='{{- template "style_banner_icon_wrapper_20" -}}'>
177+
<img src="{{browserLogoURL "safari"}}" height="20" style='{{- template "style_img_responsive" -}}' />
178+
</div>
179+
</div>
180+
<div style='{{- template "style_banner_text_wrapper" -}}; {{- template "style_text_banner_normal" -}}'>Browser support changed</div>
181+
</div>
182+
{{- end -}}
183+
{{- define "banner_generic" -}}
184+
<div style='{{- template "style_banner_wrapper" -}}{{- template "color_bg_neutral" -}}'>
185+
<div style='{{- template "style_banner_text_wrapper" -}}'>
186+
<span style='{{- template "style_text_banner_bold" -}}'>{{.Type}}</span>
187+
</div>
188+
</div>
189+
{{- end -}}`
190+
const featureTitleRowComponent = `{{- define "feature_title_row" -}}
191+
<div style='{{- template "style_feature_title_row_wrapper" -}}'>
192+
<div style='{{- template "style_feature_title_row_inner" -}}'>
193+
<a href="{{.URL}}" style='{{- template "style_text_feature_link" -}}'>{{.Name}}</a>
194+
{{- with .Docs -}}
195+
{{- if .MDNDocs -}}
196+
<span style='{{- template "style_text_doc_punctuation" -}}'> (</span>
197+
{{- range $i, $doc := .MDNDocs }}
198+
{{- if $i }}, {{ end -}}
199+
<a href="{{$doc.URL}}" style='{{- template "style_text_doc_link" -}}'>MDN</a>
200+
{{- end -}}
201+
<span style='{{- template "style_text_doc_punctuation" -}}'>)</span>
202+
{{- end -}}
203+
{{- end -}}
204+
</div>
205+
{{- if .Date -}}
206+
<div style='{{- template "style_text_date" -}}'>{{.Date}}</div>
207+
{{- end -}}
208+
</div>
209+
{{- end -}}`
210+
211+
const browserStatusDetailComponent = `{{- define "browser_status_detail" -}}
212+
{{- formatBrowserStatus .Status -}}
213+
{{- if .Version -}}
214+
<span style='{{- template "color_text_medium" -}}'> in {{.Version}}</span>
215+
{{- end -}}
216+
{{- if .Date -}}
217+
<span style='{{- template "color_text_medium" -}}'> (on {{ formatDate .Date -}})</span>
218+
{{- end -}}
219+
{{- end -}}`
220+
221+
const EmailComponents = badgeComponent +
222+
introTextComponent +
223+
changeDetailComponent +
224+
baselineChangeItemComponent +
225+
browserItemComponent +
226+
buttonComponent +
227+
footerComponent +
228+
bannerComponents +
229+
featureTitleRowComponent +
230+
browserStatusDetailComponent

0 commit comments

Comments
 (0)