@cparkerwebm/webmonterey 1.1.0 → 1.2.0

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.
@@ -1,111 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { readFileSync } from 'node:fs';
4
-
5
- import { contentTag, CREDIT_TEXT, creditUrl } from './credit.ts';
6
-
7
- /*
8
- * Credit.astro read as SOURCE, because it cannot be imported here: it pulls in ../site.ts, which
9
- * imports virtual:webm/site, which only resolves inside an Astro build. A source assertion is the
10
- * weaker tool and it is the one available - and the alternative in place until now was nothing.
11
- *
12
- * WHY IT NEEDS ONE AT ALL. This anchor once carried rel="noopener" and no target, which does
13
- * nothing whatsoever - noopener only means anything alongside a target - so the credit navigated
14
- * the visitor away from the client's site in the same tab. That shipped to live sites. The
15
- * regression test written afterwards guards the EMAIL footer, in emails/footer.test.ts; the
16
- * component where the bug actually happened had no test until this one.
17
- */
18
- const componentSource = readFileSync(new URL('./Credit.astro', import.meta.url), 'utf8');
19
- /** The template with its {/* *\/} comment blocks removed, so prose about the rule is not read as the rule. */
20
- const template = componentSource.replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, '');
21
- const anchors: string[] = template.match(/<a\s[^>]*>/g) ?? [];
22
- /** The single credit anchor. Asserted to be the only one below. */
23
- const anchor = (): string => anchors[0] ?? '';
24
-
25
- test('creditUrl points at the main site, not a /credits page', () => {
26
- const url = new URL(creditUrl('example.com'));
27
- assert.equal(url.origin, 'https://webmonterey.com');
28
- assert.equal(url.pathname, '/', 'the credit link must not reintroduce a /credits path');
29
- });
30
-
31
- test('creditUrl carries all four UTM parameters', () => {
32
- const { searchParams } = new URL(creditUrl('example.com'));
33
- assert.equal(searchParams.get('utm_source'), 'client');
34
- assert.equal(searchParams.get('utm_campaign'), 'credits');
35
- assert.equal(searchParams.get('utm_content'), 'example_com');
36
- });
37
-
38
- test('utm_medium separates the two surfaces', () => {
39
- const site = new URL(creditUrl('example.com', 'website'));
40
- const email = new URL(creditUrl('example.com', 'email'));
41
- assert.equal(site.searchParams.get('utm_medium'), 'website');
42
- assert.equal(email.searchParams.get('utm_medium'), 'email');
43
- });
44
-
45
- test('utm_medium defaults to website', () => {
46
- assert.equal(new URL(creditUrl('example.com')).searchParams.get('utm_medium'), 'website');
47
- });
48
-
49
- test('utm_content is the production domain, with every dot as an underscore', () => {
50
- // Not the host the page is served from — a preview build must not fragment attribution.
51
- const url = new URL(creditUrl('sub.example.co.uk', 'email'));
52
- assert.equal(url.searchParams.get('utm_content'), 'sub_example_co_uk');
53
- });
54
-
55
- test('both surfaces tag the same client the same way', () => {
56
- const site = new URL(creditUrl('example.com', 'website'));
57
- const email = new URL(creditUrl('example.com', 'email'));
58
- assert.equal(site.searchParams.get('utm_content'), email.searchParams.get('utm_content'));
59
- });
60
-
61
- test('no dot survives into the credit URL, so nothing can linkify it', () => {
62
- const url = new URL(creditUrl('example.com', 'email'));
63
- assert.equal(url.searchParams.get('utm_content'), 'example_com');
64
- /*
65
- * Scoped to the QUERY STRING, not the whole URL: the destination host is webmonterey.com
66
- * and is allowed to look like a host. It is the parameter value that must not.
67
- */
68
- assert.equal(url.search.includes('.'), false);
69
- });
70
-
71
- test('contentTag leaves a domain that has no dots alone', () => {
72
- assert.equal(contentTag('localhost'), 'localhost');
73
- });
74
-
75
- test('contentTag changes nothing but the dots', () => {
76
- // The value still has to be recognisably the site it came from in the report.
77
- assert.equal(contentTag('steven-glaze.com'), 'steven-glaze_com');
78
- });
79
-
80
- test('CREDIT_TEXT is the shared wording', () => {
81
- assert.equal(CREDIT_TEXT, 'Powered by WebMonterey');
82
- });
83
-
84
- /* ── the footer credit's link, mirroring emails/footer.test.ts ────────────────────────────── */
85
-
86
- test('the site credit renders exactly one link', () => {
87
- assert.equal(anchors.length, 1, `expected one anchor, found ${anchors.length}`);
88
- });
89
-
90
- test('the site credit opens in a new tab', () => {
91
- // The half that was missing. Without it the visitor leaves the client's site to read ours.
92
- assert.match(anchor(), /target="_blank"/);
93
- });
94
-
95
- test('the new tab cannot reach back into the client page', () => {
96
- assert.match(anchor(), /rel="[^"]*noopener/);
97
- });
98
-
99
- /*
100
- * Locking a decision rather than an implementation detail: the referrer IS the attribution, so
101
- * stripping it would leave only utm_content. This is the kind of thing someone adds in good faith
102
- * while "hardening" a target=_blank link.
103
- */
104
- test('the credit link deliberately does not send noreferrer', () => {
105
- assert.doesNotMatch(anchor(), /noreferrer/);
106
- });
107
-
108
- test('the new tab is announced, not just implemented', () => {
109
- // A link that moves the user to another tab without saying so is a semantics bug.
110
- assert.match(template, /webm-visually-hidden[^>]*>\s*\(opens in a new tab\)/);
111
- });
@@ -1,59 +0,0 @@
1
- /*
2
- * The agency credit's wording and link, in ONE place.
3
- *
4
- * Two surfaces render this credit and they cannot share a component: the site footer is an
5
- * .astro component with scoped CSS, and transactional email is a string with inline styles
6
- * (email clients strip <style> blocks). Without this module the wording exists twice, and the
7
- * next time it changes one copy gets missed — which is exactly how "a WebMonterey website"
8
- * would have survived in email after the footer moved to "Powered by WebMonterey".
9
- *
10
- * utm_content is ALWAYS the production domain from webmonterey.json, never the host the page
11
- * happens to be served from. A preview build at `feature-x-webm-example-com.workers.dev`
12
- * still reports the client's own domain, so staging traffic does not fragment the attribution.
13
- *
14
- * IT IS WRITTEN WITH UNDERSCORES — `example_com`, not `example.com`. A dot makes the value look
15
- * like a hostname, and analytics UIs read it as one rather than as the label it is. Underscores
16
- * keep it a label. Every dot goes, subdomains included: `sub.example.co.uk` is `sub_example_co_uk`.
17
- *
18
- * This comment used to also claim a mail client would linkify `example.com` inside the query
19
- * string and truncate the URL there. That does not apply: auto-linkification acts on plain text,
20
- * and here the URL only ever appears inside an href. Corrected rather than deleted, because a
21
- * justification that does not survive scrutiny invites someone to reverse the decision it
22
- * defends — and the analytics reason on its own is enough.
23
- *
24
- * The link points at the WebMonterey home page, not a /credits landing page. The UTM
25
- * parameters carry the whole story, so the destination stays the main site and there is no
26
- * separate page to keep alive. utm_medium is what separates the two surfaces: `website` from
27
- * the footer, `email` from a transactional template.
28
- */
29
-
30
- /** The credit wording. Rendered verbatim on the site and in email. */
31
- export const CREDIT_TEXT = 'Powered by WebMonterey';
32
-
33
- /**
34
- * Which surface the click came from. Kept separate from utm_content so the report can tell
35
- * a footer click from an email click without splitting it per client.
36
- */
37
- export type CreditMedium = 'website' | 'email';
38
-
39
- /**
40
- * The client's domain as a UTM label rather than as a domain. Dots only — the value is
41
- * otherwise left exactly as webmonterey.json wrote it, so what lands in the report is still
42
- * recognisably the site it came from.
43
- */
44
- export function contentTag(domain: string): string {
45
- return domain.replace(/\./g, '_');
46
- }
47
-
48
- /** The attributed credit link for one client site. */
49
- export function creditUrl(domain: string, medium: CreditMedium = 'website'): string {
50
- const params = new URLSearchParams({
51
- utm_source: 'client',
52
- utm_medium: medium,
53
- utm_campaign: 'credits',
54
- /* See the note above: a dot here is a link waiting to be made out of the query string. */
55
- utm_content: contentTag(domain),
56
- });
57
-
58
- return `https://webmonterey.com/?${params}`;
59
- }
Binary file