@we8/astro 0.1.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.
Files changed (45) hide show
  1. package/README.md +157 -0
  2. package/components/ConsentBanner.astro +105 -0
  3. package/components/VisitBeacon.astro +58 -0
  4. package/components/We8Form.astro +94 -0
  5. package/dist/beacon.d.ts +32 -0
  6. package/dist/beacon.d.ts.map +1 -0
  7. package/dist/beacon.js +38 -0
  8. package/dist/beacon.js.map +1 -0
  9. package/dist/config.d.ts +42 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +58 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/consent.d.ts +61 -0
  14. package/dist/consent.d.ts.map +1 -0
  15. package/dist/consent.js +121 -0
  16. package/dist/consent.js.map +1 -0
  17. package/dist/content.d.ts +36 -0
  18. package/dist/content.d.ts.map +1 -0
  19. package/dist/content.js +58 -0
  20. package/dist/content.js.map +1 -0
  21. package/dist/form.d.ts +22 -0
  22. package/dist/form.d.ts.map +1 -0
  23. package/dist/form.js +39 -0
  24. package/dist/form.js.map +1 -0
  25. package/dist/index.d.ts +21 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +14 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/integration.d.ts +40 -0
  30. package/dist/integration.d.ts.map +1 -0
  31. package/dist/integration.js +37 -0
  32. package/dist/integration.js.map +1 -0
  33. package/dist/seo.d.ts +51 -0
  34. package/dist/seo.d.ts.map +1 -0
  35. package/dist/seo.js +63 -0
  36. package/dist/seo.js.map +1 -0
  37. package/dist/session.d.ts +17 -0
  38. package/dist/session.d.ts.map +1 -0
  39. package/dist/session.js +46 -0
  40. package/dist/session.js.map +1 -0
  41. package/dist/sitemap.d.ts +19 -0
  42. package/dist/sitemap.d.ts.map +1 -0
  43. package/dist/sitemap.js +17 -0
  44. package/dist/sitemap.js.map +1 -0
  45. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @we8/astro
2
+
3
+ The Astro integration for we8. It wraps [`@we8/client`](https://www.npmjs.com/package/@we8/client) (itself a
4
+ thin layer over the OpenAPI-documented `/v1` API) with the pieces an Astro site
5
+ needs: build-time content fetching, a code-first SEO merge, sitemap merging, a
6
+ consent-gated visit beacon, and a progressive-enhancement form. The API is the
7
+ product; this package is the ergonomic way to build a site on it.
8
+
9
+ ## Requirements
10
+
11
+ - Astro 7 or newer in the consuming site (declared as an optional peer: the
12
+ helpers run without Astro, the `.astro` components need it).
13
+ - Node 22.12 or newer, which is Astro 7's own floor.
14
+ - `@we8/client` comes with this package as a regular dependency; npm installs
15
+ it for you.
16
+ - A we8 CMS URL and a publishable key (`WE8_API_URL`, `WE8_PUBLISHABLE_KEY`).
17
+ Both absent is fine: `tryResolveConfig` returns `null` so a site can fall
18
+ back to its own data source.
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @we8/astro astro
23
+ ```
24
+
25
+ `astro` is an optional peer dependency: the helpers typecheck and run without
26
+ it, and the `.astro` components need it in the consuming project (which an Astro
27
+ site already has).
28
+
29
+ ## Quick start
30
+
31
+ ```js
32
+ // astro.config.mjs
33
+ import { defineConfig } from 'astro/config';
34
+ import { we8 } from '@we8/astro';
35
+
36
+ export default defineConfig({
37
+ site: process.env.SITE, // your public origin, for canonical URLs and sitemap
38
+ integrations: [we8()], // warns at build start when the env is unset
39
+ });
40
+ ```
41
+
42
+ ```astro
43
+ ---
44
+ // Any page or layout frontmatter (runs in Node during `astro build`).
45
+ import { getPosts, getSiteConfig, mergeSeo, postUrl } from '@we8/astro';
46
+
47
+ const site = await getSiteConfig();
48
+ const { items } = await getPosts({ page: 1, pageSize: 10 });
49
+ const seo = mergeSeo({ title: 'Blog' }, site); // page values win, site fills gaps
50
+ ---
51
+ <title>{seo.title}</title>
52
+ {items.map((post) => <a href={postUrl(post, site)}>{post.title}</a>)}
53
+ ```
54
+
55
+ ## What you get
56
+
57
+ - Config: `resolveConfig(env?)` (throws when unconfigured),
58
+ `tryResolveConfig(env?)` (returns `null` instead, for a template that falls
59
+ back to its own data source), `createBuildClient(config?, fetch?)`.
60
+ - Build-time content: `getPosts`, `getPost`, `getPostSlugs`, `getSiteConfig`,
61
+ `getDocument` (design.md, llms.txt). These run in Node during `astro build`
62
+ and need `WE8_API_URL`.
63
+ - SEO: `mergeSeo(page, site)` (code-first merge), `postUrl(post, site)`,
64
+ `robotsTxt(site, { sitemapUrl? })`.
65
+ - Sitemap: `we8SitemapUrl()` and `mergeSitemap({ map })` to feed
66
+ `@astrojs/sitemap`'s `customPages` with your post URLs.
67
+ - Consent: `readConsent`, `writeConsent`, `recordConsent(client, choice)`,
68
+ `analyticsAllowed`, `onAnalyticsConsent(handler)`, plus `CONSENT_STORAGE_KEY`
69
+ and `CONSENT_EVENT`.
70
+ - Browser: `getSessionId()`, `sendVisit(client)`, `trackVisit(client)` (the
71
+ consent-gated beacon), `enhanceForm(form, client, opts)`.
72
+ - Re-exports `createClient`, `We8ApiError`, `isWe8ApiError` and the key types
73
+ from `@we8/client`, so a template needs only this one dependency.
74
+
75
+ ## Components
76
+
77
+ Import the ready-made components by subpath. Each takes the publishable key as a
78
+ prop and falls back to `PUBLIC_WE8_PUBLISHABLE_KEY`; each degrades rather than
79
+ crashes when no key is available.
80
+
81
+ ```astro
82
+ ---
83
+ import ConsentBanner from '@we8/astro/ConsentBanner.astro';
84
+ import VisitBeacon from '@we8/astro/VisitBeacon.astro';
85
+ import We8Form from '@we8/astro/We8Form.astro';
86
+
87
+ const key = import.meta.env.WE8_PUBLISHABLE_KEY;
88
+ const apiUrl = import.meta.env.WE8_API_URL;
89
+ ---
90
+ <ConsentBanner key={key} apiUrl={apiUrl} />
91
+ <VisitBeacon key={key} apiUrl={apiUrl} />
92
+
93
+ <We8Form formKey="contact" key={key} apiUrl={apiUrl}>
94
+ <input name="email" type="email" required />
95
+ <textarea name="body"></textarea>
96
+ <button type="submit">Send</button>
97
+ </We8Form>
98
+ ```
99
+
100
+ `<We8Form>` progressively enhances a native `<form>`: it POSTs through the
101
+ client and lifts a Cloudflare Turnstile `cf-turnstile-response` field
102
+ automatically. Without a key it stays inert and says so, instead of failing
103
+ silently.
104
+
105
+ `<ConsentBanner>` ships markup only, no styling: three named decisions, a slot
106
+ for your copy, and a `data-we8-consent-*` hook on every element. It stays hidden
107
+ for a visitor who has already answered.
108
+
109
+ `<VisitBeacon>` waits for analytics consent by default. The banner's write
110
+ dispatches `we8:consent` and the beacon fires on that event, with no page
111
+ reload. Pass `requireConsent={false}` only if measuring before consent is lawful
112
+ where your visitors are.
113
+
114
+ ## Consent, end to end
115
+
116
+ ```ts
117
+ import { recordConsent, readConsent, createClient } from '@we8/astro';
118
+
119
+ const client = createClient({ key, baseUrl: apiUrl });
120
+ if (readConsent() === null) showBanner();
121
+
122
+ // On "Accept all":
123
+ await recordConsent(client, { decision: 'accepted', analytics: true, performance: true });
124
+ ```
125
+
126
+ `recordConsent` persists the decision locally FIRST, announces it, and only then
127
+ reports it to the API. It never rejects: a failed report leaves the visitor's
128
+ choice fully in effect.
129
+
130
+ ## Environment contract
131
+
132
+ Two variables, both safe to expose:
133
+
134
+ - `WE8_API_URL`: the absolute API base. For a self-hosted we8 CMS that is its
135
+ origin with no path on the end, for example `https://cms.example.com`, or
136
+ `http://localhost:8787` in development. The CMS serves `/v1` at its own root,
137
+ so a trailing `/api` produces 404s.
138
+ - `WE8_PUBLISHABLE_KEY`: the publishable (`pk_`) key.
139
+
140
+ Astro only puts `PUBLIC_`-prefixed variables on `import.meta.env` by default. The
141
+ starter template widens `envPrefix` to accept `WE8_` as well, so one variable
142
+ serves both build-time fetches and the browser. If you would rather not, set
143
+ `PUBLIC_WE8_PUBLISHABLE_KEY` and let the components pick it up.
144
+
145
+ `SITE` is Astro's own `site` config (your public origin). It drives Astro's
146
+ canonical URLs and `@astrojs/sitemap`; it is distinct from the we8 site config's
147
+ `siteUrl` that `postUrl` reads.
148
+
149
+ A secret (`sk_`) key can be handed to `createBuildClient` directly for a
150
+ server-only build. Never let one reach the browser.
151
+
152
+ ## The contract
153
+
154
+ The full, authoritative interface is the OpenAPI document your CMS serves at
155
+ `/v1/openapi.json`, keyless, and [the API doc](https://github.com/reveriext/we8-package/blob/main/docs/api.md) is the same
156
+ contract in prose. See [the frontend doc](https://github.com/reveriext/we8-package/blob/main/docs/frontend.md) for how this
157
+ package and the starter template fit together.
@@ -0,0 +1,105 @@
1
+ ---
2
+ /**
3
+ * <ConsentBanner /> - ask for analytics consent, record the answer, and get out
4
+ * of the way.
5
+ *
6
+ * The banner is hidden until the script confirms the visitor has NOT decided
7
+ * yet, so a returning visitor never sees a flash of it. Choosing writes the
8
+ * decision locally first, dispatches `we8:consent` (which releases anything
9
+ * gated on consent, such as <VisitBeacon />), and then reports the decision to
10
+ * the analytics API. A failed report never undoes the choice.
11
+ *
12
+ * Markup only, no styling: three named slots carry your copy, and every element
13
+ * has a `data-we8-consent-*` hook to style from your own stylesheet. Renders
14
+ * nothing without a key, so a build with no backend has no dead banner.
15
+ *
16
+ * The three decisions map to the API's vocabulary: `accepted` (analytics and
17
+ * performance), `partial` (analytics only), `declined` (neither).
18
+ */
19
+ interface Props {
20
+ /** The publishable key. Defaults to `PUBLIC_WE8_PUBLISHABLE_KEY`. */
21
+ key?: string | undefined;
22
+ /** Absolute API base. Defaults to same-origin `/api`. */
23
+ apiUrl?: string | undefined;
24
+ acceptLabel?: string;
25
+ essentialLabel?: string;
26
+ declineLabel?: string;
27
+ /** Accessible name for the region. */
28
+ label?: string;
29
+ }
30
+
31
+ const {
32
+ key = import.meta.env.PUBLIC_WE8_PUBLISHABLE_KEY,
33
+ apiUrl,
34
+ acceptLabel = 'Accept analytics',
35
+ essentialLabel = 'Analytics only',
36
+ declineLabel = 'Decline',
37
+ label = 'Cookie choices',
38
+ } = Astro.props;
39
+ ---
40
+
41
+ {
42
+ key && (
43
+ <div
44
+ data-we8-consent
45
+ data-we8-key={key}
46
+ data-we8-api={apiUrl ?? ''}
47
+ role="region"
48
+ aria-label={label}
49
+ hidden
50
+ >
51
+ <div data-we8-consent-body>
52
+ <slot>
53
+ <p>
54
+ We count page views to see which pages are useful. Nothing is sold, and no
55
+ profile is built. You can decline and the site works exactly the same.
56
+ </p>
57
+ </slot>
58
+ </div>
59
+ <div data-we8-consent-actions>
60
+ <button type="button" data-we8-consent-choice="accepted">
61
+ {acceptLabel}
62
+ </button>
63
+ <button type="button" data-we8-consent-choice="partial">
64
+ {essentialLabel}
65
+ </button>
66
+ <button type="button" data-we8-consent-choice="declined">
67
+ {declineLabel}
68
+ </button>
69
+ </div>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ <script>
75
+ import { createClient } from '@we8/client';
76
+ import { readConsent, recordConsent } from '@we8/astro';
77
+
78
+ const banner = document.querySelector<HTMLElement>('[data-we8-consent]');
79
+ const key = banner?.dataset.we8Key;
80
+ if (banner && key && !banner.dataset.we8Bound) {
81
+ banner.dataset.we8Bound = 'true';
82
+
83
+ // Only ask visitors who have not answered. A stored decision of any kind,
84
+ // including a decline, is an answer.
85
+ if (readConsent() === null) {
86
+ banner.hidden = false;
87
+ const apiUrl = banner.dataset.we8Api;
88
+ const client = createClient(apiUrl ? { key, baseUrl: apiUrl } : { key });
89
+
90
+ for (const el of banner.querySelectorAll('[data-we8-consent-choice]')) {
91
+ el.addEventListener('click', () => {
92
+ const choice = (el as HTMLElement).dataset.we8ConsentChoice;
93
+ const decision =
94
+ choice === 'accepted' ? 'accepted' : choice === 'partial' ? 'partial' : 'declined';
95
+ banner.hidden = true;
96
+ void recordConsent(client, {
97
+ decision,
98
+ analytics: decision !== 'declined',
99
+ performance: decision === 'accepted',
100
+ });
101
+ });
102
+ }
103
+ }
104
+ }
105
+ </script>
@@ -0,0 +1,58 @@
1
+ ---
2
+ /**
3
+ * <VisitBeacon /> - fire a single page-visit beacon after the page loads.
4
+ *
5
+ * The key is a publishable (`pk_`) key, safe to embed in a shipped frontend by
6
+ * design. Pass it explicitly from your own config (recommended, and what the
7
+ * starter template does) or let it fall back to `PUBLIC_WE8_PUBLISHABLE_KEY`,
8
+ * which Astro exposes to the browser. It travels to the script through a data
9
+ * attribute, so no build-time env plumbing is needed in the consuming project.
10
+ *
11
+ * By default the beacon waits for analytics consent (see <ConsentBanner> in the
12
+ * starter, or `recordConsent` from this package). Set `requireConsent={false}`
13
+ * only if your jurisdiction and your privacy policy allow measuring first.
14
+ *
15
+ * Renders nothing when no key is available, so a misconfigured build degrades
16
+ * to "no analytics" rather than a runtime error.
17
+ */
18
+ interface Props {
19
+ /** The publishable key. Defaults to `PUBLIC_WE8_PUBLISHABLE_KEY`. */
20
+ key?: string | undefined;
21
+ /** Absolute API base. Defaults to same-origin `/api`. */
22
+ apiUrl?: string | undefined;
23
+ /** Wait for analytics consent before sending. Default true. */
24
+ requireConsent?: boolean;
25
+ }
26
+
27
+ const {
28
+ key = import.meta.env.PUBLIC_WE8_PUBLISHABLE_KEY,
29
+ apiUrl,
30
+ requireConsent = true,
31
+ } = Astro.props;
32
+ ---
33
+
34
+ {
35
+ key && (
36
+ <span
37
+ data-we8-beacon
38
+ data-we8-key={key}
39
+ data-we8-api={apiUrl ?? ''}
40
+ data-we8-require-consent={requireConsent ? 'true' : 'false'}
41
+ hidden
42
+ />
43
+ )
44
+ }
45
+
46
+ <script>
47
+ import { createClient } from '@we8/client';
48
+ import { trackVisit } from '@we8/astro';
49
+
50
+ const el = document.querySelector<HTMLElement>('[data-we8-beacon]');
51
+ const key = el?.dataset.we8Key;
52
+ if (el && key && !el.dataset.we8Bound) {
53
+ el.dataset.we8Bound = 'true';
54
+ const apiUrl = el.dataset.we8Api;
55
+ const client = createClient(apiUrl ? { key, baseUrl: apiUrl } : { key });
56
+ trackVisit(client, { requireConsent: el.dataset.we8RequireConsent !== 'false' });
57
+ }
58
+ </script>
@@ -0,0 +1,94 @@
1
+ ---
2
+ /**
3
+ * <We8Form formKey="contact"> ... form fields ... </We8Form>
4
+ *
5
+ * A progressively-enhanced form. The child fields go in the default slot; on
6
+ * submit, the JS handler POSTs them through the we8 client to
7
+ * `/v1/forms/{formKey}/submissions` and shows a status line. A Cloudflare
8
+ * Turnstile widget's `cf-turnstile-response` field is lifted automatically.
9
+ *
10
+ * The submission endpoint takes JSON, so JS is required to submit; without it
11
+ * the markup is still a valid, accessible form (put a mailto or other fallback
12
+ * in the slot if a no-JS path matters).
13
+ *
14
+ * The key is a publishable (`pk_`) key. Pass it explicitly from your own config
15
+ * (what the starter template does) or let it fall back to
16
+ * `PUBLIC_WE8_PUBLISHABLE_KEY`. With no key at all the form still renders and
17
+ * simply reports that submissions are not configured, so a fixtures-only build
18
+ * shows the real markup instead of crashing.
19
+ */
20
+ interface Props {
21
+ formKey: string;
22
+ /** The publishable key. Defaults to `PUBLIC_WE8_PUBLISHABLE_KEY`. */
23
+ key?: string | undefined;
24
+ /** Absolute API base. Defaults to same-origin `/api`. */
25
+ apiUrl?: string | undefined;
26
+ class?: string | undefined;
27
+ successMessage?: string;
28
+ errorMessage?: string;
29
+ offlineMessage?: string;
30
+ }
31
+
32
+ const {
33
+ formKey,
34
+ key = import.meta.env.PUBLIC_WE8_PUBLISHABLE_KEY,
35
+ apiUrl,
36
+ class: className,
37
+ successMessage = 'Thanks. Your message is on its way.',
38
+ errorMessage = 'Something went wrong. Please try again.',
39
+ offlineMessage = 'This form is not connected to a backend yet.',
40
+ } = Astro.props;
41
+ ---
42
+
43
+ <form
44
+ class={className}
45
+ data-we8-form-key={formKey}
46
+ data-we8-key={key ?? ''}
47
+ data-we8-api={apiUrl ?? ''}
48
+ data-we8-success={successMessage}
49
+ data-we8-error={errorMessage}
50
+ data-we8-offline={offlineMessage}
51
+ >
52
+ <slot />
53
+ <p data-we8-status role="status" aria-live="polite" hidden></p>
54
+ </form>
55
+
56
+ <script>
57
+ import { createClient } from '@we8/client';
58
+ import { enhanceForm } from '@we8/astro';
59
+
60
+ for (const el of document.querySelectorAll('form[data-we8-form-key]')) {
61
+ const form = el as HTMLFormElement;
62
+ // Guard against binding the same form twice if this module ever runs again.
63
+ if (form.dataset.we8Bound) continue;
64
+ form.dataset.we8Bound = 'true';
65
+
66
+ const formKey = form.dataset.we8FormKey;
67
+ if (!formKey) continue;
68
+
69
+ const status = form.querySelector('[data-we8-status]') as HTMLElement | null;
70
+ const show = (message: string) => {
71
+ if (!status) return;
72
+ status.hidden = false;
73
+ status.textContent = message;
74
+ };
75
+
76
+ const key = form.dataset.we8Key;
77
+ if (!key) {
78
+ // No key: keep the form inert but honest rather than silently dead.
79
+ form.addEventListener('submit', (event) => {
80
+ event.preventDefault();
81
+ show(form.dataset.we8Offline ?? 'This form is not connected to a backend yet.');
82
+ });
83
+ continue;
84
+ }
85
+
86
+ const apiUrl = form.dataset.we8Api;
87
+ const client = createClient(apiUrl ? { key, baseUrl: apiUrl } : { key });
88
+ enhanceForm(form, client, {
89
+ formKey,
90
+ onSuccess: () => show(form.dataset.we8Success ?? 'Thanks.'),
91
+ onError: () => show(form.dataset.we8Error ?? 'Something went wrong.'),
92
+ });
93
+ }
94
+ </script>
@@ -0,0 +1,32 @@
1
+ import type { We8Client } from '@we8/client';
2
+ import { type EventTargetLike } from './consent.js';
3
+ import { type StorageLike } from './session.js';
4
+ export interface BeaconOptions {
5
+ /** Defaults to `location.pathname`. */
6
+ pathname?: string;
7
+ /** Defaults to `document.referrer`. */
8
+ referrer?: string;
9
+ }
10
+ /**
11
+ * Send a page-visit beacon for the current page, tagged with the persisted
12
+ * session id. Errors are swallowed: analytics must never break the page it
13
+ * measures.
14
+ */
15
+ export declare function sendVisit(client: We8Client, options?: BeaconOptions): Promise<void>;
16
+ export interface TrackVisitOptions extends BeaconOptions {
17
+ /**
18
+ * Hold the beacon until the visitor has granted analytics consent. Default
19
+ * `true`: measuring before consent is the thing a consent banner exists to
20
+ * prevent, so opting out of the gate has to be deliberate.
21
+ */
22
+ requireConsent?: boolean;
23
+ storage?: StorageLike | null;
24
+ target?: EventTargetLike | null;
25
+ }
26
+ /**
27
+ * Send the visit beacon, gated on analytics consent: now if consent already
28
+ * exists, otherwise the moment the visitor grants it. Returns a cleanup
29
+ * function that cancels a still-waiting beacon.
30
+ */
31
+ export declare function trackVisit(client: We8Client, options?: TrackVisitOptions): () => void;
32
+ //# sourceMappingURL=beacon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAa7F;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,IAAI,CAUzF"}
package/dist/beacon.js ADDED
@@ -0,0 +1,38 @@
1
+ import { onAnalyticsConsent } from './consent.js';
2
+ import { getSessionId } from './session.js';
3
+ /**
4
+ * Send a page-visit beacon for the current page, tagged with the persisted
5
+ * session id. Errors are swallowed: analytics must never break the page it
6
+ * measures.
7
+ */
8
+ export async function sendVisit(client, options = {}) {
9
+ try {
10
+ const pathname = options.pathname ?? (typeof location !== 'undefined' ? location.pathname : '/');
11
+ const referrer = options.referrer ?? (typeof document !== 'undefined' ? document.referrer : '');
12
+ await client.events.visit({
13
+ pathname,
14
+ sessionId: getSessionId(),
15
+ ...(referrer ? { referrer } : {}),
16
+ });
17
+ }
18
+ catch {
19
+ // Intentionally silent: a dropped beacon is invisible to the visitor.
20
+ }
21
+ }
22
+ /**
23
+ * Send the visit beacon, gated on analytics consent: now if consent already
24
+ * exists, otherwise the moment the visitor grants it. Returns a cleanup
25
+ * function that cancels a still-waiting beacon.
26
+ */
27
+ export function trackVisit(client, options = {}) {
28
+ const { requireConsent = true, storage, target, ...beacon } = options;
29
+ if (!requireConsent) {
30
+ void sendVisit(client, beacon);
31
+ return () => undefined;
32
+ }
33
+ return onAnalyticsConsent(() => void sendVisit(client, beacon), {
34
+ ...(storage === undefined ? {} : { storage }),
35
+ ...(target === undefined ? {} : { target }),
36
+ });
37
+ }
38
+ //# sourceMappingURL=beacon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAwB,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,YAAY,EAAoB,MAAM,cAAc,CAAC;AAS9D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAiB,EAAE,UAAyB,EAAE;IAC5E,IAAI,CAAC;QACH,MAAM,QAAQ,GACZ,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAClF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChG,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YACxB,QAAQ;YACR,SAAS,EAAE,YAAY,EAAE;YACzB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;IACxE,CAAC;AACH,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAiB,EAAE,UAA6B,EAAE;IAC3E,MAAM,EAAE,cAAc,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IACtE,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,KAAK,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;IACzB,CAAC;IACD,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;QAC9D,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { type We8Client } from '@we8/client';
2
+ export interface We8AstroConfig {
3
+ /**
4
+ * The site API key. Use a publishable key (`pk_...`) for anything that ends
5
+ * up in the browser (the beacon, the consent banner, the form); a secret key
6
+ * (`sk_...`) is only for build-time server fetches and must never be shipped
7
+ * to a client.
8
+ */
9
+ key: string;
10
+ /**
11
+ * Absolute API base URL for build-time (Node) fetches. For a self-hosted
12
+ * we8 CMS that is its origin with no path on the end, for example
13
+ * `https://cms.example.com`, since the CMS serves `/v1` at its own root.
14
+ * Required by `getPosts`/`getPost`, which run during `astro build` where
15
+ * there is no same-origin to default to.
16
+ */
17
+ apiUrl?: string;
18
+ }
19
+ export type EnvLike = Record<string, string | undefined>;
20
+ /** The environment variable holding the publishable key. */
21
+ export declare const KEY_ENV = "WE8_PUBLISHABLE_KEY";
22
+ /** The environment variable holding the absolute API base URL. */
23
+ export declare const API_URL_ENV = "WE8_API_URL";
24
+ /**
25
+ * Resolve the we8 config from an env record (defaults to `process.env`).
26
+ * `WE8_PUBLISHABLE_KEY` is required; `WE8_API_URL` is optional here and only
27
+ * needed for build-time content fetches, which check it themselves.
28
+ */
29
+ export declare function resolveConfig(env?: EnvLike): We8AstroConfig;
30
+ /**
31
+ * Like `resolveConfig`, but returns `null` instead of throwing when the
32
+ * environment is not configured. A template uses this to fall back to its own
33
+ * data source (fixtures, a different backend) rather than failing the build.
34
+ */
35
+ export declare function tryResolveConfig(env?: EnvLike): We8AstroConfig | null;
36
+ /**
37
+ * Build a `@we8/client` for BUILD-TIME (Node) use. Unlike the browser default
38
+ * (same-origin `/api`), this needs an absolute `apiUrl`, since `astro build`
39
+ * runs off-origin. Pass `fetchImpl` in tests.
40
+ */
41
+ export declare function createBuildClient(config?: We8AstroConfig, fetchImpl?: typeof fetch): We8Client;
42
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEzD,4DAA4D;AAC5D,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAC7C,kEAAkE;AAClE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAYzC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,OAA0B,GAAG,cAAc,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,OAA0B,GAAG,cAAc,GAAG,IAAI,CAKvF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,cAAgC,EACxC,SAAS,CAAC,EAAE,OAAO,KAAK,GACvB,SAAS,CAYX"}
package/dist/config.js ADDED
@@ -0,0 +1,58 @@
1
+ import { createClient } from '@we8/client';
2
+ /** The environment variable holding the publishable key. */
3
+ export const KEY_ENV = 'WE8_PUBLISHABLE_KEY';
4
+ /** The environment variable holding the absolute API base URL. */
5
+ export const API_URL_ENV = 'WE8_API_URL';
6
+ /**
7
+ * Read the default env record without depending on `@types/node`: reach for a
8
+ * `process.env` on `globalThis` if the runtime has one (Node during `astro
9
+ * build`), otherwise an empty record.
10
+ */
11
+ function readDefaultEnv() {
12
+ const g = globalThis;
13
+ return g.process?.env ?? {};
14
+ }
15
+ /**
16
+ * Resolve the we8 config from an env record (defaults to `process.env`).
17
+ * `WE8_PUBLISHABLE_KEY` is required; `WE8_API_URL` is optional here and only
18
+ * needed for build-time content fetches, which check it themselves.
19
+ */
20
+ export function resolveConfig(env = readDefaultEnv()) {
21
+ const key = env[KEY_ENV];
22
+ if (!key) {
23
+ throw new Error(`@we8/astro: ${KEY_ENV} is not set. Add your site's publishable key to the environment (for example in .env).`);
24
+ }
25
+ const apiUrl = env[API_URL_ENV];
26
+ // Build the object with the optional field present only when set, to stay
27
+ // clean under exactOptionalPropertyTypes.
28
+ return apiUrl ? { key, apiUrl } : { key };
29
+ }
30
+ /**
31
+ * Like `resolveConfig`, but returns `null` instead of throwing when the
32
+ * environment is not configured. A template uses this to fall back to its own
33
+ * data source (fixtures, a different backend) rather than failing the build.
34
+ */
35
+ export function tryResolveConfig(env = readDefaultEnv()) {
36
+ const key = env[KEY_ENV];
37
+ const apiUrl = env[API_URL_ENV];
38
+ if (!key || !apiUrl)
39
+ return null;
40
+ return { key, apiUrl };
41
+ }
42
+ /**
43
+ * Build a `@we8/client` for BUILD-TIME (Node) use. Unlike the browser default
44
+ * (same-origin `/api`), this needs an absolute `apiUrl`, since `astro build`
45
+ * runs off-origin. Pass `fetchImpl` in tests.
46
+ */
47
+ export function createBuildClient(config = resolveConfig(), fetchImpl) {
48
+ if (!config.apiUrl) {
49
+ throw new Error(`@we8/astro: ${API_URL_ENV} is required for build-time fetch (getPosts/getPost). ` +
50
+ 'Set it to your CMS origin, for example https://cms.example.com.');
51
+ }
52
+ return createClient({
53
+ key: config.key,
54
+ baseUrl: config.apiUrl,
55
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
56
+ });
57
+ }
58
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAkB,MAAM,aAAa,CAAC;AAsB3D,4DAA4D;AAC5D,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAC7C,kEAAkE;AAClE,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC;;;;GAIG;AACH,SAAS,cAAc;IACrB,MAAM,CAAC,GAAG,UAA6C,CAAC;IACxD,OAAO,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe,cAAc,EAAE;IAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,eAAe,OAAO,wFAAwF,CAC/G,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,0EAA0E;IAC1E,0CAA0C;IAC1C,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe,cAAc,EAAE;IAC9D,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,SAAyB,aAAa,EAAE,EACxC,SAAwB;IAExB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,eAAe,WAAW,wDAAwD;YAChF,iEAAiE,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3C,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { ConsentDecision, We8Client } from '@we8/client';
2
+ import type { StorageLike } from './session.js';
3
+ /** Where a visitor's consent decision is persisted in the browser. */
4
+ export declare const CONSENT_STORAGE_KEY = "we8:consent";
5
+ /**
6
+ * The DOM event dispatched (and listened for) when a decision is recorded, so
7
+ * anything gated on consent (the visit beacon, an embedded map, a third-party
8
+ * script) can start the moment the visitor says yes, without a page reload.
9
+ */
10
+ export declare const CONSENT_EVENT = "we8:consent";
11
+ /** A visitor's stored consent decision. */
12
+ export interface ConsentState {
13
+ decision: ConsentDecision;
14
+ analytics: boolean;
15
+ performance: boolean;
16
+ /** ISO timestamp of when the visitor decided. */
17
+ decidedAt: string;
18
+ }
19
+ /** The minimal event-target surface `recordConsent` and `onConsent` need; `window` satisfies it. */
20
+ export interface EventTargetLike {
21
+ addEventListener(type: string, listener: () => void): void;
22
+ removeEventListener(type: string, listener: () => void): void;
23
+ dispatchEvent?(event: Event): boolean;
24
+ }
25
+ /**
26
+ * Read the stored decision, or `null` when the visitor has not chosen yet (so
27
+ * a banner knows to show itself). A corrupt or unreadable store also reads as
28
+ * `null`: an unparseable decision is not a decision.
29
+ */
30
+ export declare function readConsent(storage?: StorageLike | null): ConsentState | null;
31
+ /** True when the visitor has granted analytics consent. Convenience for gating a beacon. */
32
+ export declare function analyticsAllowed(storage?: StorageLike | null): boolean;
33
+ /** Persist a decision locally and announce it. Storage failures are non-fatal. */
34
+ export declare function writeConsent(state: ConsentState, storage?: StorageLike | null, target?: EventTargetLike | null): void;
35
+ export interface RecordConsentOptions {
36
+ storage?: StorageLike | null;
37
+ target?: EventTargetLike | null;
38
+ /** Override the session id (defaults to the persisted visitor session id). */
39
+ sessionId?: string;
40
+ }
41
+ /**
42
+ * Record a visitor's consent decision: persist it locally FIRST (so the
43
+ * decision is honored even if the network is down), announce it, then report
44
+ * it to the analytics API. Never rejects; a failed report is invisible to the
45
+ * visitor and does not weaken the decision.
46
+ */
47
+ export declare function recordConsent(client: We8Client, choice: {
48
+ decision: ConsentDecision;
49
+ analytics: boolean;
50
+ performance: boolean;
51
+ }, options?: RecordConsentOptions): Promise<ConsentState>;
52
+ /**
53
+ * Run `handler` once analytics consent exists: immediately if it already does,
54
+ * otherwise on the next consent decision that grants it. Returns a cleanup
55
+ * function that detaches the listener.
56
+ */
57
+ export declare function onAnalyticsConsent(handler: () => void, options?: {
58
+ storage?: StorageLike | null;
59
+ target?: EventTargetLike | null;
60
+ }): () => void;
61
+ //# sourceMappingURL=consent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.d.ts","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC3D,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC9D,aAAa,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;CACvC;AAmBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,WAAW,GAAG,IAAuB,GAAG,YAAY,GAAG,IAAI,CAqB/F;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,WAAW,GAAG,IAAuB,GAAG,OAAO,CAExF;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE,WAAW,GAAG,IAAuB,EAC9C,MAAM,GAAE,eAAe,GAAG,IAAsB,GAC/C,IAAI,CAYN;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE;IAAE,QAAQ,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,EAC/E,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;CAAO,GAC9E,MAAM,IAAI,CAgBZ"}