backend-manager 5.0.203 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/CLAUDE.md +100 -1529
- package/TODO-CHARGEBLAST.md +32 -0
- package/TODO-email-auth.md +14 -0
- package/docs/admin-post-route.md +24 -0
- package/docs/ai-library.md +23 -0
- package/docs/architecture.md +31 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/cli-firestore-auth.md +59 -0
- package/docs/cli-logs.md +67 -0
- package/docs/code-patterns.md +67 -0
- package/docs/common-mistakes.md +11 -0
- package/docs/common-operations.md +64 -0
- package/docs/directory-structure.md +119 -0
- package/docs/environment-detection.md +7 -0
- package/docs/file-naming.md +11 -0
- package/docs/key-files.md +36 -0
- package/docs/marketing-campaigns.md +244 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +95 -0
- package/docs/payment-system.md +325 -0
- package/docs/response-headers.md +7 -0
- package/docs/routes.md +126 -0
- package/docs/sanitization.md +61 -0
- package/docs/schemas.md +39 -0
- package/docs/stripe-webhook-forwarding.md +18 -0
- package/docs/testing.md +129 -0
- package/docs/usage-rate-limiting.md +67 -0
- package/package.json +8 -4
- package/scripts/update-disposable-domains.js +1 -1
- package/src/defaults/CHANGELOG.md +15 -0
- package/src/defaults/CLAUDE.md +8 -4
- package/src/defaults/docs/README.md +17 -0
- package/src/defaults/test/README.md +33 -0
- package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
- package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +5 -0
- package/src/manager/helpers/utilities.js +21 -0
- package/src/manager/index.js +1 -1
- package/src/manager/libraries/ai/index.js +162 -0
- package/src/manager/libraries/ai/providers/anthropic.js +193 -0
- package/src/manager/libraries/ai/providers/claude-code.js +206 -0
- package/src/manager/libraries/ai/providers/openai.js +934 -0
- package/src/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/src/manager/libraries/email/data/blocked-local-patterns.js +11 -0
- package/src/manager/libraries/email/data/corporate-domains.json +23 -0
- package/src/manager/libraries/{disposable-domains.json → email/data/disposable-domains.json} +3 -0
- package/src/manager/libraries/email/generators/lib/filter.js +179 -0
- package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
- package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
- package/src/manager/libraries/email/generators/lib/structure.js +278 -0
- package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
- package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
- package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
- package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
- package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
- package/src/manager/libraries/email/generators/newsletter.js +377 -95
- package/src/manager/libraries/email/marketing/index.js +16 -2
- package/src/manager/libraries/email/providers/beehiiv.js +7 -3
- package/src/manager/libraries/email/validation.js +53 -38
- package/src/manager/libraries/openai.js +13 -932
- package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/src/manager/routes/admin/post/post.js +10 -17
- package/src/manager/routes/marketing/contact/post.js +5 -1
- package/templates/_.env +4 -0
- package/templates/_.gitignore +1 -0
- package/templates/backend-manager-config.json +48 -4
- package/test/helpers/email-validation.js +141 -3
- package/test/helpers/slugify.js +394 -0
- package/test/marketing/fixtures/clean.json +31 -0
- package/test/marketing/fixtures/editorial.json +31 -0
- package/test/marketing/fixtures/field-report.json +54 -0
- package/test/marketing/newsletter-generate.js +731 -0
- package/test/marketing/newsletter-templates.js +512 -0
- package/test/routes/admin/deduplicate-image-alts.js +190 -0
- /package/src/manager/libraries/{custom-disposable-domains.json → email/data/custom-disposable-domains.json} +0 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: helpers/utilities.slugify()
|
|
3
|
+
* Unit tests for the canonical URL slug builder.
|
|
4
|
+
*
|
|
5
|
+
* Run: npx mgr test helpers/slugify
|
|
6
|
+
*
|
|
7
|
+
* slugify is the SSOT used by:
|
|
8
|
+
* - BEM admin/post (legacy + modern) for URL + image filenames
|
|
9
|
+
* - Sponsorship platform validator.buildFormatted()
|
|
10
|
+
*
|
|
11
|
+
* Contract:
|
|
12
|
+
* - Strip all non-alphanumeric characters → replace with `-`
|
|
13
|
+
* - Collapse runs of `-` into a single `-`
|
|
14
|
+
* - Trim leading/trailing `-`
|
|
15
|
+
* - Lowercase the result
|
|
16
|
+
* - Non-string input → empty string
|
|
17
|
+
*/
|
|
18
|
+
const Utilities = require('../../src/manager/helpers/utilities.js');
|
|
19
|
+
|
|
20
|
+
const Manager = { libraries: {} };
|
|
21
|
+
const utilities = new Utilities(Manager);
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
description: 'Utilities.slugify()',
|
|
25
|
+
type: 'group',
|
|
26
|
+
|
|
27
|
+
tests: [
|
|
28
|
+
// ─── Basic happy path ───
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
name: 'lowercases-simple-string',
|
|
32
|
+
async run({ assert }) {
|
|
33
|
+
assert.equal(utilities.slugify('Hello World'), 'hello-world');
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
name: 'preserves-numbers',
|
|
39
|
+
async run({ assert }) {
|
|
40
|
+
assert.equal(utilities.slugify('Top 10 Products 2025'), 'top-10-products-2025');
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
name: 'already-a-slug-passes-through',
|
|
46
|
+
async run({ assert }) {
|
|
47
|
+
assert.equal(utilities.slugify('already-a-slug'), 'already-a-slug');
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
name: 'single-word-unchanged',
|
|
53
|
+
async run({ assert }) {
|
|
54
|
+
assert.equal(utilities.slugify('hello'), 'hello');
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// ─── The bug that drove this: collapse runs of `-` ───
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
name: 'collapses-double-dashes-from-literal-hyphen-space',
|
|
62
|
+
async run({ assert }) {
|
|
63
|
+
// The real-world example: "Copy- Paste" produced "copy--paste" before the fix
|
|
64
|
+
assert.equal(
|
|
65
|
+
utilities.slugify('AI Study Prompts That Work: Copy- Paste Questions for Every Subject'),
|
|
66
|
+
'ai-study-prompts-that-work-copy-paste-questions-for-every-subject',
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
name: 'collapses-many-consecutive-dashes',
|
|
73
|
+
async run({ assert }) {
|
|
74
|
+
assert.equal(utilities.slugify('foo---bar'), 'foo-bar');
|
|
75
|
+
assert.equal(utilities.slugify('foo-----bar'), 'foo-bar');
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
name: 'collapses-mixed-runs-of-special-chars',
|
|
81
|
+
async run({ assert }) {
|
|
82
|
+
// " / - / " → multiple non-alphanum → all become one `-`
|
|
83
|
+
assert.equal(utilities.slugify('foo / - / bar'), 'foo-bar');
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// ─── Leading/trailing trim ───
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
name: 'strips-leading-and-trailing-spaces',
|
|
91
|
+
async run({ assert }) {
|
|
92
|
+
assert.equal(utilities.slugify(' hello world '), 'hello-world');
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
name: 'strips-leading-and-trailing-dashes',
|
|
98
|
+
async run({ assert }) {
|
|
99
|
+
assert.equal(utilities.slugify('---hello---'), 'hello');
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
name: 'strips-leading-slash',
|
|
105
|
+
async run({ assert }) {
|
|
106
|
+
assert.equal(utilities.slugify('/foo/bar'), 'foo-bar');
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
name: 'strips-trailing-slash',
|
|
112
|
+
async run({ assert }) {
|
|
113
|
+
assert.equal(utilities.slugify('foo/bar/'), 'foo-bar');
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
name: 'strips-both-slashes',
|
|
119
|
+
async run({ assert }) {
|
|
120
|
+
assert.equal(utilities.slugify('/foo/bar/'), 'foo-bar');
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// ─── Punctuation ───
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
name: 'colon-becomes-dash',
|
|
128
|
+
async run({ assert }) {
|
|
129
|
+
assert.equal(utilities.slugify('Title: Subtitle'), 'title-subtitle');
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
name: 'comma-becomes-dash',
|
|
135
|
+
async run({ assert }) {
|
|
136
|
+
assert.equal(utilities.slugify('one, two, three'), 'one-two-three');
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
name: 'question-mark-becomes-dash',
|
|
142
|
+
async run({ assert }) {
|
|
143
|
+
assert.equal(utilities.slugify('What is AI?'), 'what-is-ai');
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
name: 'exclamation-becomes-dash',
|
|
149
|
+
async run({ assert }) {
|
|
150
|
+
assert.equal(utilities.slugify('Hello World!'), 'hello-world');
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
name: 'period-becomes-dash',
|
|
156
|
+
async run({ assert }) {
|
|
157
|
+
assert.equal(utilities.slugify('v1.2.3'), 'v1-2-3');
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
name: 'apostrophe-becomes-dash',
|
|
163
|
+
async run({ assert }) {
|
|
164
|
+
assert.equal(utilities.slugify("don't stop"), 'don-t-stop');
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
name: 'quotes-become-dashes',
|
|
170
|
+
async run({ assert }) {
|
|
171
|
+
assert.equal(utilities.slugify('"Hello" said the dog'), 'hello-said-the-dog');
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
name: 'parens-become-dashes',
|
|
177
|
+
async run({ assert }) {
|
|
178
|
+
assert.equal(utilities.slugify('Function (advanced)'), 'function-advanced');
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
name: 'brackets-become-dashes',
|
|
184
|
+
async run({ assert }) {
|
|
185
|
+
assert.equal(utilities.slugify('Array[0] access'), 'array-0-access');
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
name: 'ampersand-becomes-dash',
|
|
191
|
+
async run({ assert }) {
|
|
192
|
+
assert.equal(utilities.slugify('Salt & Pepper'), 'salt-pepper');
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
name: 'hash-becomes-dash',
|
|
198
|
+
async run({ assert }) {
|
|
199
|
+
assert.equal(utilities.slugify('issue #42'), 'issue-42');
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
name: 'at-sign-becomes-dash',
|
|
205
|
+
async run({ assert }) {
|
|
206
|
+
assert.equal(utilities.slugify('user@example.com'), 'user-example-com');
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// ─── Unicode / non-ASCII ───
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
name: 'em-dash-becomes-dash',
|
|
214
|
+
async run({ assert }) {
|
|
215
|
+
assert.equal(utilities.slugify('Title — Subtitle'), 'title-subtitle');
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
name: 'curly-quotes-become-dashes',
|
|
221
|
+
async run({ assert }) {
|
|
222
|
+
assert.equal(utilities.slugify('“Hello”'), 'hello');
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
name: 'emoji-becomes-dash',
|
|
228
|
+
async run({ assert }) {
|
|
229
|
+
assert.equal(utilities.slugify('Rocket 🚀 launch'), 'rocket-launch');
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
name: 'accented-chars-become-dashes',
|
|
235
|
+
async run({ assert }) {
|
|
236
|
+
// Slugify is ASCII-only by design — accents get stripped to dashes
|
|
237
|
+
// (Future improvement could be to transliterate, but current behavior is documented here)
|
|
238
|
+
assert.equal(utilities.slugify('café'), 'caf');
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// ─── Whitespace variants ───
|
|
243
|
+
|
|
244
|
+
{
|
|
245
|
+
name: 'tab-becomes-dash',
|
|
246
|
+
async run({ assert }) {
|
|
247
|
+
assert.equal(utilities.slugify('foo\tbar'), 'foo-bar');
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
name: 'newline-becomes-dash',
|
|
253
|
+
async run({ assert }) {
|
|
254
|
+
assert.equal(utilities.slugify('foo\nbar'), 'foo-bar');
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
name: 'multiple-spaces-collapse',
|
|
260
|
+
async run({ assert }) {
|
|
261
|
+
assert.equal(utilities.slugify('foo bar'), 'foo-bar');
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
{
|
|
266
|
+
name: 'mixed-whitespace-collapses',
|
|
267
|
+
async run({ assert }) {
|
|
268
|
+
assert.equal(utilities.slugify('foo \t \n bar'), 'foo-bar');
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// ─── Edge cases / non-string input ───
|
|
273
|
+
|
|
274
|
+
{
|
|
275
|
+
name: 'empty-string-returns-empty',
|
|
276
|
+
async run({ assert }) {
|
|
277
|
+
assert.equal(utilities.slugify(''), '');
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
{
|
|
282
|
+
name: 'all-punctuation-returns-empty',
|
|
283
|
+
async run({ assert }) {
|
|
284
|
+
assert.equal(utilities.slugify('!@#$%^&*()'), '');
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
{
|
|
289
|
+
name: 'only-dashes-returns-empty',
|
|
290
|
+
async run({ assert }) {
|
|
291
|
+
assert.equal(utilities.slugify('-----'), '');
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
name: 'only-whitespace-returns-empty',
|
|
297
|
+
async run({ assert }) {
|
|
298
|
+
assert.equal(utilities.slugify(' '), '');
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
name: 'null-returns-empty-string',
|
|
304
|
+
async run({ assert }) {
|
|
305
|
+
assert.equal(utilities.slugify(null), '');
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
name: 'undefined-returns-empty-string',
|
|
311
|
+
async run({ assert }) {
|
|
312
|
+
assert.equal(utilities.slugify(undefined), '');
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
{
|
|
317
|
+
name: 'number-returns-empty-string',
|
|
318
|
+
async run({ assert }) {
|
|
319
|
+
// Non-string input → empty string (caller is responsible for stringifying)
|
|
320
|
+
assert.equal(utilities.slugify(42), '');
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
{
|
|
325
|
+
name: 'object-returns-empty-string',
|
|
326
|
+
async run({ assert }) {
|
|
327
|
+
assert.equal(utilities.slugify({ foo: 'bar' }), '');
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
{
|
|
332
|
+
name: 'array-returns-empty-string',
|
|
333
|
+
async run({ assert }) {
|
|
334
|
+
assert.equal(utilities.slugify(['hello']), '');
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// ─── Real-world examples ───
|
|
339
|
+
|
|
340
|
+
{
|
|
341
|
+
name: 'real-blog-post-title',
|
|
342
|
+
async run({ assert }) {
|
|
343
|
+
assert.equal(
|
|
344
|
+
utilities.slugify('10 Best Productivity Apps for Students in 2025'),
|
|
345
|
+
'10-best-productivity-apps-for-students-in-2025',
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
name: 'real-blog-post-with-colon-and-slash',
|
|
352
|
+
async run({ assert }) {
|
|
353
|
+
assert.equal(
|
|
354
|
+
utilities.slugify('Beginner\'s Guide: Python vs JavaScript'),
|
|
355
|
+
'beginner-s-guide-python-vs-javascript',
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
{
|
|
361
|
+
name: 'real-image-alt-text',
|
|
362
|
+
async run({ assert }) {
|
|
363
|
+
// The downloadImage() caller uses slugify(alt) to build a tmp filename
|
|
364
|
+
assert.equal(
|
|
365
|
+
utilities.slugify('Diagram showing the user flow (v2)'),
|
|
366
|
+
'diagram-showing-the-user-flow-v2',
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
{
|
|
372
|
+
name: 'real-url-with-blog-prefix-removed-first',
|
|
373
|
+
async run({ assert }) {
|
|
374
|
+
// Mimics how callers strip "blog/" before slugifying
|
|
375
|
+
const url = 'blog/some-existing-post';
|
|
376
|
+
assert.equal(
|
|
377
|
+
utilities.slugify(url.replace(/blog\//ig, '')),
|
|
378
|
+
'some-existing-post',
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// ─── Idempotency ───
|
|
384
|
+
|
|
385
|
+
{
|
|
386
|
+
name: 'idempotent-double-slugify',
|
|
387
|
+
async run({ assert }) {
|
|
388
|
+
const once = utilities.slugify('Hello World: Foo & Bar!');
|
|
389
|
+
const twice = utilities.slugify(once);
|
|
390
|
+
assert.equal(twice, once, 'Applying slugify twice should equal applying it once');
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Predefined fixture for the `clean` template. Loaded via NEWSLETTER_FIXTURE=clean. Edit freely — no AI involved. Classic content shape: intro + sections[{title, body, cta, image_prompt}].",
|
|
3
|
+
"subject": "Identity is the new growth lever",
|
|
4
|
+
"preheader": "Three platform shifts to lock in this week.",
|
|
5
|
+
"intro": "Identity matters because trust is the new growth lever. Teams that handle this well will spend less time cleaning up later.",
|
|
6
|
+
"sections": [
|
|
7
|
+
{
|
|
8
|
+
"title": "LinkedIn ships verified-profile gates for business accounts",
|
|
9
|
+
"body": "LinkedIn now requires verified identity attestations on any business profile claiming more than 500 followers. Accounts with documented attribution histories sail through verification in under twelve hours. The practical implication is to lock down your attribution log this week, not next.",
|
|
10
|
+
"cta": { "label": "Read the brief", "url": "https://somiibo.com/blog/linkedin-verification" },
|
|
11
|
+
"image_prompt": "Abstract geometric illustration of a verification checkmark inside a layered profile card."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"title": "Documentation is the new operator moat",
|
|
15
|
+
"body": "Operator playbooks are now the difference between an account that recovers from a flag and one that gets permanently restricted. Teams running documented processes recover in days. The ones without burn weeks negotiating with support queues.",
|
|
16
|
+
"cta": { "label": "See the playbook", "url": "https://somiibo.com/blog/operator-playbook" },
|
|
17
|
+
"image_prompt": "Stack of geometric document layers casting clean shadows on a flat surface."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "YouTube tests creator-attribution metadata on uploads",
|
|
21
|
+
"body": "A subset of channels saw a new upload checkbox this week asking whether content was assisted by automation. The checkbox is optional today. Reading the room: it will not stay optional. Creators with clear internal workflows will adapt in an afternoon.",
|
|
22
|
+
"cta": null,
|
|
23
|
+
"image_prompt": "Minimalist play button morphing into a label tag, flat geometric style."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"signoff": "Best,\nThe Somiibo Team",
|
|
27
|
+
"citations": [
|
|
28
|
+
{ "note": "LinkedIn flagged 12,000 impersonation attempts in the verified-profile beta.", "source": "Reported by LinkedIn product team, May 2026" },
|
|
29
|
+
{ "note": "Operator playbook adoption up 38% week-over-week in Q2.", "source": "Per platform analytics, May 2026" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Predefined fixture for the `editorial` template. Loaded via NEWSLETTER_FIXTURE=editorial. Same classic content shape as clean.json — both templates render the same data.",
|
|
3
|
+
"subject": "Identity is the new growth lever",
|
|
4
|
+
"preheader": "Three platform shifts to lock in this week.",
|
|
5
|
+
"intro": "Identity matters because trust is the new growth lever. Teams that handle this well will spend less time cleaning up later, and the patterns emerging this week tell you exactly where to focus.",
|
|
6
|
+
"sections": [
|
|
7
|
+
{
|
|
8
|
+
"title": "LinkedIn ships verified-profile gates for business accounts",
|
|
9
|
+
"body": "LinkedIn now requires verified identity attestations on any business profile claiming more than 500 followers. The rollout is fast and uneven — some accounts hit the gate inside two days of profile activity, others sit unchallenged for weeks. The pattern that matters is this: accounts with documented attribution histories sail through verification in under twelve hours. Accounts without get queued. The practical implication is to lock down your attribution log this week, not next.",
|
|
10
|
+
"cta": { "label": "Read the brief", "url": "https://somiibo.com/blog/linkedin-verification" },
|
|
11
|
+
"image_prompt": "Abstract geometric illustration of a verification checkmark inside a layered profile card."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"title": "Documentation is the new operator moat",
|
|
15
|
+
"body": "Operator playbooks — once dismissed as overhead — are now the difference between an account that recovers from a flag and one that gets permanently restricted. The teams already running on documented processes recover in days. The ones without burn weeks negotiating with support queues. Treat your playbook as a compliance artifact, not a knowledge-management nice-to-have.",
|
|
16
|
+
"cta": { "label": "See the playbook", "url": "https://somiibo.com/blog/operator-playbook" },
|
|
17
|
+
"image_prompt": "Stack of geometric document layers casting clean shadows on a flat surface."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "YouTube tests creator-attribution metadata on uploads",
|
|
21
|
+
"body": "A subset of channels saw a new upload checkbox this week asking whether content was assisted by automation. The checkbox is optional today. Reading the room: it will not stay optional. Creators with clear internal workflows already labeled will adapt in an afternoon. Everyone else will spend a quarter retrofitting.",
|
|
22
|
+
"cta": null,
|
|
23
|
+
"image_prompt": "Minimalist play button morphing into a label tag, flat geometric style."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"signoff": "Best,\nThe Somiibo Team",
|
|
27
|
+
"citations": [
|
|
28
|
+
{ "note": "LinkedIn flagged 12,000 impersonation attempts in the verified-profile beta.", "source": "Reported by LinkedIn product team, May 2026" },
|
|
29
|
+
{ "note": "Operator playbook adoption up 38% week-over-week in Q2.", "source": "Per platform analytics, May 2026" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Predefined fixture for the `field-report` template. Loaded via NEWSLETTER_FIXTURE=field-report. Wire-service content shape: tldr + dateline + dispatches[{kicker, headline, byline, location, lede, dispatch, dataPoints, cta, image_prompt}].",
|
|
3
|
+
"subject": "LinkedIn tightens identity — operators document everything",
|
|
4
|
+
"preheader": "Three filings from this week's platform front lines.",
|
|
5
|
+
"tldr": "LinkedIn is rolling out verified-account scrutiny across every business profile. Operators with paper trails will outrun the rest. Documentation is the new growth lever.",
|
|
6
|
+
"dateline": "OAKLAND",
|
|
7
|
+
"signoff": "— Stay sharp,\nThe Somiibo Desk",
|
|
8
|
+
"citations": [
|
|
9
|
+
{ "note": "LinkedIn flagged 12,000 impersonation attempts in the verified-profile beta.", "source": "Reported by LinkedIn product team, May 2026" },
|
|
10
|
+
{ "note": "Operator playbook adoption up 38% week-over-week in Q2.", "source": "Per platform analytics, May 2026" }
|
|
11
|
+
],
|
|
12
|
+
"dispatches": [
|
|
13
|
+
{
|
|
14
|
+
"kicker": "LEAD DISPATCH",
|
|
15
|
+
"headline": "LinkedIn ships verified-profile gates for every business account",
|
|
16
|
+
"byline": "Filed by The Somiibo platform desk",
|
|
17
|
+
"location": "OAKLAND",
|
|
18
|
+
"lede": "A wave of identity-verification gates is rolling out across LinkedIn business profiles this week — and the platform is not asking politely.",
|
|
19
|
+
"dispatch": "LinkedIn now requires verified identity attestations on any business profile claiming more than 500 followers. The rollout is fast and uneven: some accounts hit the gate inside two days of profile activity, others sit unchallenged for weeks. The pattern that matters is this — accounts with documented attribution histories sail through verification in under twelve hours. Accounts without get queued. The practical implication is to lock down your attribution log this week, not next.",
|
|
20
|
+
"dataPoints": [
|
|
21
|
+
{ "label": "PROFILES FLAGGED", "value": "12.4K" },
|
|
22
|
+
{ "label": "AVG REVIEW TIME", "value": "14HR" },
|
|
23
|
+
{ "label": "WoW APPROVAL", "value": "+38%" }
|
|
24
|
+
],
|
|
25
|
+
"cta": { "label": "READ THE BRIEF", "url": "https://somiibo.com/blog/linkedin-verification" },
|
|
26
|
+
"image_prompt": "Abstract geometric illustration of a verification checkmark inside a layered profile card."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"kicker": "FIELD NOTES",
|
|
30
|
+
"headline": "Documentation is the new operator moat",
|
|
31
|
+
"byline": "Filed by The Somiibo growth desk",
|
|
32
|
+
"location": "REMOTE",
|
|
33
|
+
"lede": "The teams surviving this round of platform scrutiny share one habit: they write everything down.",
|
|
34
|
+
"dispatch": "Operator playbooks — once dismissed as overhead — are now the difference between an account that recovers from a flag and one that gets permanently restricted. The teams already running on documented processes recover in days. The ones without burn weeks negotiating with support queues. Treat your playbook as a compliance artifact, not a knowledge-management nice-to-have.",
|
|
35
|
+
"dataPoints": [
|
|
36
|
+
{ "label": "RECOVERY (DOCUMENTED)", "value": "4D" },
|
|
37
|
+
{ "label": "RECOVERY (UNDOC)", "value": "21D" }
|
|
38
|
+
],
|
|
39
|
+
"cta": { "label": "SEE THE PLAYBOOK", "url": "https://somiibo.com/blog/operator-playbook" },
|
|
40
|
+
"image_prompt": "Stack of geometric document layers casting clean shadows on a flat surface."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"kicker": "WATCH",
|
|
44
|
+
"headline": "YouTube tests creator-attribution metadata on uploads",
|
|
45
|
+
"byline": "Filed by The Somiibo signals desk",
|
|
46
|
+
"location": "NEW YORK",
|
|
47
|
+
"lede": "A small pilot suggests YouTube will soon ask creators to declare automated tooling and cross-posting workflows at upload time.",
|
|
48
|
+
"dispatch": "A subset of channels saw a new upload checkbox this week asking whether content was assisted by automation. The checkbox is optional today. Reading the room: it will not stay optional. Creators with clear internal workflows already labeled will adapt in an afternoon. Everyone else will spend a quarter retrofitting.",
|
|
49
|
+
"dataPoints": [],
|
|
50
|
+
"cta": null,
|
|
51
|
+
"image_prompt": "Minimalist play button morphing into a label tag, flat geometric style."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|