siteplane 0.2.0-next.0 → 0.2.0-next.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/README.md CHANGED
@@ -49,6 +49,13 @@ A website with `basePath` in `next.config` needs no extra setup. Check it with t
49
49
  (`npx siteplane status --url https://www.example.com/docs`); Siteplane saves that address as the website URL and
50
50
  reaches the website below it.
51
51
 
52
+ ## Analytics
53
+
54
+ With Analytics on for the site, `init` also adds `<SiteplaneAnalytics siteId="…" />` to the root layout. It counts
55
+ page views, App Router navigations and clicks on elements with `data-siteplane-event="<name>"`, on links to other
56
+ websites, phone numbers and email addresses. It sets no cookies and uses no browser storage; Siteplane counts visitors
57
+ per day on its server. Development builds and the editor's preview send nothing.
58
+
52
59
  ## Booking
53
60
 
54
61
  ```ts
@@ -2046,7 +2046,7 @@ async function writeStore(connections, options) {
2046
2046
  }
2047
2047
 
2048
2048
  // src/version.ts
2049
- var PACKAGE_VERSION = "0.2.0-next.0";
2049
+ var PACKAGE_VERSION = "0.2.0-next.1";
2050
2050
 
2051
2051
  // src/bin/run-siteplane.ts
2052
2052
  var usage = [
@@ -334,6 +334,7 @@ export {
334
334
  siteCacheTag,
335
335
  siteConfig,
336
336
  signingKeys,
337
+ isDraftMode,
337
338
  readPreviewToken,
338
339
  loadSiteValues
339
340
  };
@@ -8,12 +8,15 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
8
8
  */
9
9
  declare function SiteplaneBridge(): Promise<react_jsx_runtime.JSX.Element | null>;
10
10
  /**
11
- * `<SiteplaneAnalytics siteId="…" />` (Vertrag 54 §10). Page views and clicks
12
- * are measured from Plan 55 J1 on, when Siteplane's collector accepts them;
13
- * until then the component renders nothing and loads no script.
11
+ * `<SiteplaneAnalytics siteId="…" />` (Vertrag 54 §10): page views, App Router
12
+ * navigations and clicks, without cookies or browser storage, sent to
13
+ * Siteplane's collector. In Draft Mode, which only the editor's preview uses,
14
+ * it renders nothing, so previews never count. The client part reads the
15
+ * search parameters and therefore sits in its own Suspense boundary, as with
16
+ * Vercel's `<Analytics />`.
14
17
  */
15
- declare function SiteplaneAnalytics(props: {
18
+ declare function SiteplaneAnalytics({ siteId }: {
16
19
  siteId: string;
17
- }): null;
20
+ }): Promise<react_jsx_runtime.JSX.Element | null>;
18
21
 
19
22
  export { SiteplaneAnalytics, SiteplaneBridge };
@@ -1,12 +1,21 @@
1
1
  import {
2
+ isDraftMode,
2
3
  readPreviewToken,
3
4
  signingKeys,
4
5
  siteConfig,
5
6
  verifySitePreviewToken
6
- } from "./chunk-YDCNEIOB.js";
7
+ } from "./chunk-BLJCWIYD.js";
7
8
  import "./chunk-KZ3AQ6OA.js";
8
9
 
10
+ // ../shared/dist/site-analytics.js
11
+ var SITE_ANALYTICS_SEND_PATH = "/api/analytics/send";
12
+ function siteAnalyticsEndpoint(apiOrigin) {
13
+ return `${apiOrigin}${SITE_ANALYTICS_SEND_PATH}`;
14
+ }
15
+
9
16
  // src/components.tsx
17
+ import { Suspense } from "react";
18
+ import { AnalyticsTracker } from "siteplane/internal/analytics";
10
19
  import { BridgeLoader } from "siteplane/internal/bridge-loader";
11
20
  import { jsx } from "react/jsx-runtime";
12
21
  async function SiteplaneBridge() {
@@ -23,9 +32,10 @@ async function readEditorOrigin() {
23
32
  return null;
24
33
  }
25
34
  }
26
- function SiteplaneAnalytics(props) {
27
- void props;
28
- return null;
35
+ async function SiteplaneAnalytics({ siteId }) {
36
+ if (await isDraftMode()) return null;
37
+ const endpoint = siteAnalyticsEndpoint(siteConfig?.apiOrigin ?? "https://siteplane.io");
38
+ return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AnalyticsTracker, { siteId, endpoint }) });
29
39
  }
30
40
  export {
31
41
  SiteplaneAnalytics,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadSiteValues
3
- } from "./chunk-YDCNEIOB.js";
3
+ } from "./chunk-BLJCWIYD.js";
4
4
  import "./chunk-KZ3AQ6OA.js";
5
5
 
6
6
  // src/index.ts
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ // src/analytics-tracker.tsx
4
+ import { usePathname, useSearchParams } from "next/navigation";
5
+ import { useEffect, useRef } from "react";
6
+
7
+ // ../shared/dist/site-analytics.js
8
+ var SITE_ANALYTICS_EVENT_ATTRIBUTE = "data-siteplane-event";
9
+ var SITE_ANALYTICS_LINK_EVENTS = {
10
+ outbound: "outbound-link",
11
+ tel: "tel-link",
12
+ mailto: "mailto-link"
13
+ };
14
+
15
+ // src/analytics.ts
16
+ function createAnalytics(config, deps = {}) {
17
+ const send = deps.send ?? sendEvent;
18
+ let lastUrl = null;
19
+ const page = () => ({
20
+ website: config.siteId,
21
+ hostname: location.hostname,
22
+ url: location.pathname + location.search,
23
+ screen: `${screen.width}x${screen.height}`
24
+ });
25
+ const emit = (payload) => send(config.endpoint, { type: "event", payload });
26
+ const onClick = (event) => {
27
+ if (event.type === "auxclick" && event.button !== 1) return;
28
+ const element = event.target instanceof Element ? event.target : null;
29
+ const name = element?.closest(`[${SITE_ANALYTICS_EVENT_ATTRIBUTE}]`)?.getAttribute(SITE_ANALYTICS_EVENT_ATTRIBUTE)?.trim();
30
+ if (name) return emit({ ...page(), name: name.slice(0, 100) });
31
+ const link = element?.closest("a[href]");
32
+ if (!(link instanceof HTMLAnchorElement)) return;
33
+ const kind = link.protocol === "tel:" ? SITE_ANALYTICS_LINK_EVENTS.tel : link.protocol === "mailto:" ? SITE_ANALYTICS_LINK_EVENTS.mailto : /^https?:$/u.test(link.protocol) && link.host !== location.host ? SITE_ANALYTICS_LINK_EVENTS.outbound : null;
34
+ if (kind) emit({ ...page(), name: kind, data: { url: link.href } });
35
+ };
36
+ return {
37
+ /** Counts the current page once; a later page names the previous one as referrer. */
38
+ pageView() {
39
+ const current = page();
40
+ if (current.url === lastUrl) return;
41
+ const referrer = lastUrl === null ? document.referrer : `${location.origin}${lastUrl}`;
42
+ lastUrl = current.url;
43
+ emit(referrer ? { ...current, referrer } : current);
44
+ },
45
+ /** Listens for clicks (and middle clicks) until the returned function runs. */
46
+ start() {
47
+ document.addEventListener("click", onClick, true);
48
+ document.addEventListener("auxclick", onClick, true);
49
+ return () => {
50
+ document.removeEventListener("click", onClick, true);
51
+ document.removeEventListener("auxclick", onClick, true);
52
+ };
53
+ }
54
+ };
55
+ }
56
+ function sendEvent(endpoint, body) {
57
+ try {
58
+ void fetch(endpoint, {
59
+ method: "POST",
60
+ body: JSON.stringify(body),
61
+ headers: { "content-type": "text/plain" },
62
+ credentials: "omit",
63
+ keepalive: true
64
+ }).catch(() => void 0);
65
+ } catch {
66
+ }
67
+ }
68
+
69
+ // src/analytics-tracker.tsx
70
+ function AnalyticsTracker({ siteId, endpoint }) {
71
+ const pathname = usePathname();
72
+ const search = useSearchParams()?.toString() ?? "";
73
+ const analytics = useRef(null);
74
+ useEffect(() => {
75
+ if (process.env.NODE_ENV !== "production") return;
76
+ const instance = createAnalytics({ siteId, endpoint });
77
+ analytics.current = instance;
78
+ const stop = instance.start();
79
+ return () => {
80
+ stop();
81
+ analytics.current = null;
82
+ };
83
+ }, [siteId, endpoint]);
84
+ useEffect(() => {
85
+ analytics.current?.pageView();
86
+ }, [pathname, search, siteId, endpoint]);
87
+ return null;
88
+ }
89
+ export {
90
+ AnalyticsTracker
91
+ };
package/dist/route.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  siteConfig,
6
6
  verifySitePreviewToken,
7
7
  verifySiteRevalidation
8
- } from "./chunk-YDCNEIOB.js";
8
+ } from "./chunk-BLJCWIYD.js";
9
9
  import "./chunk-KZ3AQ6OA.js";
10
10
 
11
11
  // src/route.ts
@@ -114,7 +114,7 @@ async function readBoundedText(request) {
114
114
  }
115
115
 
116
116
  // src/version.ts
117
- var PACKAGE_VERSION = "0.2.0-next.0";
117
+ var PACKAGE_VERSION = "0.2.0-next.1";
118
118
 
119
119
  // src/route.ts
120
120
  var embeddable = { httpOnly: true, secure: true, sameSite: "none", partitioned: true, path: "/" };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "siteplane",
3
3
  "private": false,
4
- "version": "0.2.0-next.0",
4
+ "version": "0.2.0-next.1",
5
5
  "description": "Siteplane for Next.js websites: editable content with code fallbacks, Draft Mode preview, Booking and the siteplane CLI.",
6
6
  "type": "module",
7
7
  "license": "SEE LICENSE IN LICENSE",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "./internal/bridge-loader": "./dist/internal/bridge-loader.js",
38
38
  "./internal/bridge": "./dist/internal/bridge.js",
39
+ "./internal/analytics": "./dist/internal/analytics.js",
39
40
  "./package.json": "./package.json"
40
41
  },
41
42
  "types": "./dist/index.d.ts",