@usequeek/theme-kit 0.1.19 → 0.1.20
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/README.md +4 -1
- package/components/powered-by-queek.tsx +12 -3
- package/package.json +1 -1
- package/tests/powered-by-queek.test.tsx +31 -3
- package/tests/powered-by-url.test.ts +49 -0
- package/utils/powered-by.ts +31 -0
package/README.md
CHANGED
|
@@ -203,7 +203,10 @@ export default function Footer() {
|
|
|
203
203
|
it — style the classes in your theme.
|
|
204
204
|
- The href (`https://usequeek.com/business`), `target="_blank"` and
|
|
205
205
|
`rel="noopener noreferrer"` are the contract — never re-implement the
|
|
206
|
-
markup by hand.
|
|
206
|
+
markup by hand. The href is tagged for attribution by `poweredByQueekUrl`
|
|
207
|
+
(`utm_source=powered_by&utm_medium=storefront&utm_campaign=<store slug>`,
|
|
208
|
+
campaign omitted when the store has no slug); the component reads the slug
|
|
209
|
+
from the storefront context, so themes pass nothing.
|
|
207
210
|
- `hidden` (default false) takes the component's visibility from a prop as
|
|
208
211
|
the seam for a possible future plan perk. There is no merchant-facing
|
|
209
212
|
toggle for it in v1 — always render it visible.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useContext, type JSX } from 'react';
|
|
4
|
+
import { StorefrontContext } from '../provider';
|
|
5
|
+
import { poweredByQueekUrl } from '../utils/powered-by';
|
|
2
6
|
|
|
3
7
|
export interface PoweredByQueekProps {
|
|
4
8
|
/** Extra class so a footer can position the link — styling stays theme-owned. */
|
|
@@ -17,15 +21,20 @@ export interface PoweredByQueekProps {
|
|
|
17
21
|
* Required on every footer variant of every theme (enforced by `theme-check`
|
|
18
22
|
* rule `theme/footer-shows-powered-by` in the storefront repo). The link
|
|
19
23
|
* target (`https://usequeek.com/business`) and `rel` are the contract —
|
|
20
|
-
* do not change them.
|
|
24
|
+
* do not change them. The href is tagged `utm_source=powered_by`,
|
|
25
|
+
* `utm_medium=storefront`, `utm_campaign=<store slug>` via `poweredByQueekUrl`
|
|
26
|
+
* (read from the storefront context, so themes pass nothing; no provider means
|
|
27
|
+
* no campaign, never an error). Markup carries `core-powered-by` classes so each theme
|
|
21
28
|
* styles it in its own `theme.css`; no kit CSS ships for it.
|
|
22
29
|
*/
|
|
23
30
|
export function PoweredByQueek({ className, hidden = false }: PoweredByQueekProps): JSX.Element | null {
|
|
31
|
+
const vendor = useContext(StorefrontContext)?.vendor;
|
|
32
|
+
|
|
24
33
|
if (hidden) return null;
|
|
25
34
|
|
|
26
35
|
return (
|
|
27
36
|
<a
|
|
28
|
-
href=
|
|
37
|
+
href={poweredByQueekUrl(vendor?.slug)}
|
|
29
38
|
target="_blank"
|
|
30
39
|
rel="noopener noreferrer"
|
|
31
40
|
className={className ? `core-powered-by ${className}` : 'core-powered-by'}
|
package/package.json
CHANGED
|
@@ -2,9 +2,18 @@ import React from 'react';
|
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { PoweredByQueek } from '../components/powered-by-queek';
|
|
5
|
+
import { StorefrontContext } from '../provider';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const BASE = 'https://usequeek.com/business?utm_source=powered_by&utm_medium=storefront';
|
|
8
|
+
|
|
9
|
+
function render(props?: { className?: string; hidden?: boolean }, vendor?: { id: string; slug: string | null }): string {
|
|
10
|
+
const element = React.createElement(PoweredByQueek, props);
|
|
11
|
+
|
|
12
|
+
if (!vendor) return renderToStaticMarkup(element);
|
|
13
|
+
|
|
14
|
+
return renderToStaticMarkup(
|
|
15
|
+
React.createElement(StorefrontContext.Provider, { value: { vendor } as never }, element),
|
|
16
|
+
);
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
describe('PoweredByQueek', () => {
|
|
@@ -12,11 +21,29 @@ describe('PoweredByQueek', () => {
|
|
|
12
21
|
const html = render();
|
|
13
22
|
|
|
14
23
|
expect(html).toContain('<a');
|
|
15
|
-
expect(html).toContain(
|
|
24
|
+
expect(html).toContain(`href="${BASE}"`);
|
|
16
25
|
expect(html).toContain('target="_blank"');
|
|
17
26
|
expect(html).toContain('rel="noopener noreferrer"');
|
|
18
27
|
});
|
|
19
28
|
|
|
29
|
+
it('tags the link with the store slug as utm_campaign', () => {
|
|
30
|
+
const html = render(undefined, { id: 'uuid-1', slug: 'kili-foods' });
|
|
31
|
+
|
|
32
|
+
expect(html).toContain(`href="${BASE}&utm_campaign=kili-foods"`);
|
|
33
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('omits utm_campaign (never the internal UUID) when the store has no slug', () => {
|
|
37
|
+
const html = render(undefined, { id: 'uuid-1', slug: null });
|
|
38
|
+
|
|
39
|
+
expect(html).not.toContain('utm_campaign');
|
|
40
|
+
expect(html).not.toContain('uuid-1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('omits utm_campaign (never errors) when no storefront provider is mounted', () => {
|
|
44
|
+
expect(render()).not.toContain('utm_campaign');
|
|
45
|
+
});
|
|
46
|
+
|
|
20
47
|
it('renders "Powered by Queek" with the brand hook for theme styling', () => {
|
|
21
48
|
const html = render();
|
|
22
49
|
|
|
@@ -33,5 +60,6 @@ describe('PoweredByQueek', () => {
|
|
|
33
60
|
|
|
34
61
|
it('renders nothing when hidden', () => {
|
|
35
62
|
expect(render({ hidden: true })).toBe('');
|
|
63
|
+
expect(render({ hidden: true }, { id: 'uuid-1', slug: 'kili-foods' })).toBe('');
|
|
36
64
|
});
|
|
37
65
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { POWERED_BY_QUEEK_URL, poweredByQueekUrl } from '../utils/powered-by';
|
|
3
|
+
|
|
4
|
+
function params(url: string): Record<string, string> {
|
|
5
|
+
return Object.fromEntries(new URL(url).searchParams.entries());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('poweredByQueekUrl', () => {
|
|
9
|
+
it('keeps the business-page destination and adds source, medium and campaign', () => {
|
|
10
|
+
const url = poweredByQueekUrl('kili-foods');
|
|
11
|
+
|
|
12
|
+
expect(url.startsWith(`${POWERED_BY_QUEEK_URL}?`)).toBe(true);
|
|
13
|
+
expect(params(url)).toEqual({
|
|
14
|
+
utm_source: 'powered_by',
|
|
15
|
+
utm_medium: 'storefront',
|
|
16
|
+
utm_campaign: 'kili-foods',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it.each([undefined, null, '', ' '])('omits utm_campaign for a blank campaign (%j)', (campaign) => {
|
|
21
|
+
expect(params(poweredByQueekUrl(campaign))).toEqual({
|
|
22
|
+
utm_source: 'powered_by',
|
|
23
|
+
utm_medium: 'storefront',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('preserves an existing query and overrides only its own utm keys', () => {
|
|
28
|
+
const url = poweredByQueekUrl('shop-1', 'https://usequeek.com/business?plan=pro&utm_source=old&utm_campaign=stale');
|
|
29
|
+
|
|
30
|
+
expect(params(url)).toEqual({
|
|
31
|
+
plan: 'pro',
|
|
32
|
+
utm_source: 'powered_by',
|
|
33
|
+
utm_medium: 'storefront',
|
|
34
|
+
utm_campaign: 'shop-1',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('drops a stale utm_campaign when the new campaign is blank', () => {
|
|
39
|
+
expect(params(poweredByQueekUrl(null, 'https://usequeek.com/business?utm_campaign=stale'))).not.toHaveProperty('utm_campaign');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('url-encodes the campaign and is deterministic (SSR-safe, no window/Date)', () => {
|
|
43
|
+
const url = poweredByQueekUrl('a b&c');
|
|
44
|
+
|
|
45
|
+
expect(url).toContain('utm_campaign=a+b%26c');
|
|
46
|
+
expect(params(url).utm_campaign).toBe('a b&c');
|
|
47
|
+
expect(poweredByQueekUrl('a b&c')).toBe(url);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place the "Powered by Queek" destination is built, so every theme's
|
|
3
|
+
* footer link carries the same attribution and the affiliate/referral program
|
|
4
|
+
* can trace a signup back to the store it came from.
|
|
5
|
+
*/
|
|
6
|
+
export const POWERED_BY_QUEEK_URL = 'https://usequeek.com/business';
|
|
7
|
+
|
|
8
|
+
export const POWERED_BY_UTM_SOURCE = 'powered_by';
|
|
9
|
+
export const POWERED_BY_UTM_MEDIUM = 'storefront';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `campaign` is the store's public slug (never an internal UUID) so signups
|
|
13
|
+
* are attributable per store. Existing query parameters on the base
|
|
14
|
+
* URL are kept; the `utm_*` keys are always ours. A blank campaign omits
|
|
15
|
+
* `utm_campaign` rather than sending an empty value.
|
|
16
|
+
*/
|
|
17
|
+
export function poweredByQueekUrl(campaign?: string | null, base: string = POWERED_BY_QUEEK_URL): string {
|
|
18
|
+
const url = new URL(base);
|
|
19
|
+
|
|
20
|
+
url.searchParams.set('utm_source', POWERED_BY_UTM_SOURCE);
|
|
21
|
+
url.searchParams.set('utm_medium', POWERED_BY_UTM_MEDIUM);
|
|
22
|
+
|
|
23
|
+
const trimmed = campaign?.trim();
|
|
24
|
+
if (trimmed) {
|
|
25
|
+
url.searchParams.set('utm_campaign', trimmed);
|
|
26
|
+
} else {
|
|
27
|
+
url.searchParams.delete('utm_campaign');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return url.toString();
|
|
31
|
+
}
|