@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 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
- import type { JSX } from 'react';
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. Markup carries `core-powered-by` classes so each theme
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="https://usequeek.com/business"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequeek/theme-kit",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "The framework a Queek storefront theme is built against.",
@@ -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
- function render(props?: { className?: string; hidden?: boolean }): string {
7
- return renderToStaticMarkup(React.createElement(PoweredByQueek, props));
7
+ const BASE = 'https://usequeek.com/business?utm_source=powered_by&amp;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('href="https://usequeek.com/business"');
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}&amp;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
+ }