siteplane 0.2.0-next.0 → 0.2.0-rc.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.
package/README.md CHANGED
@@ -8,18 +8,21 @@ content as fallback in your code, a Draft Mode preview for the editor, Booking a
8
8
  Run this in the website's folder, or tell your coding agent "Install Siteplane":
9
9
 
10
10
  ```bash
11
- npx siteplane@next init
11
+ npx siteplane@latest init
12
12
  ```
13
13
 
14
- Version 0.2 is on the `next` tag until it becomes `latest`.
15
-
16
14
  `init` opens the approval page in the browser. There you sign up or sign in, create the website in Siteplane or pick
17
15
  an existing one, and click Connect. If you created the website in Siteplane first, its prompt names the site:
18
16
  `npx siteplane@<version> init --site <site ID>`.
19
17
 
20
18
  `init` then writes `siteplane.config.json`, installs this package, wraps `next.config` in `withSiteplane`, adds the
21
19
  route `app/api/siteplane/[...siteplane]/route.ts` and `<SiteplaneBridge />`, and prints the task for the agent.
22
- Siteplane needs no environment variables or secrets in your project.
20
+ Siteplane needs no environment variables or secrets in your project, and it never deploys: deploy the way the project
21
+ always does.
22
+
23
+ The approval creates an agent connection for this one website. It may read the task, sync fields and check the
24
+ status; it cannot publish. It is listed under Settings → Connected agents in Siteplane, where it can be revoked, and
25
+ expires after 90 days without use.
23
26
 
24
27
  ## Field API
25
28
 
@@ -49,6 +52,13 @@ A website with `basePath` in `next.config` needs no extra setup. Check it with t
49
52
  (`npx siteplane status --url https://www.example.com/docs`); Siteplane saves that address as the website URL and
50
53
  reaches the website below it.
51
54
 
55
+ ## Analytics
56
+
57
+ With Analytics on for the site, `init` also adds `<SiteplaneAnalytics siteId="…" />` to the root layout. It counts
58
+ page views, App Router navigations and clicks on elements with `data-siteplane-event="<name>"`, on links to other
59
+ websites, phone numbers and email addresses. It sets no cookies and uses no browser storage; Siteplane counts visitors
60
+ per day on its server. Development builds and the editor's preview send nothing.
61
+
52
62
  ## Booking
53
63
 
54
64
  ```ts
@@ -70,3 +80,9 @@ npx siteplane booking test
70
80
  approval. The agent connection is stored in the user config directory (`$SITEPLANE_CONFIG_DIR`,
71
81
  `$XDG_CONFIG_HOME/siteplane`, `%APPDATA%\siteplane` or `~/.config/siteplane`), never in the project; `--no-browser`
72
82
  skips opening the browser.
83
+
84
+ ## More
85
+
86
+ - Instructions for coding agents: https://siteplane.io/agents and https://siteplane.io/llms.txt
87
+ - Removing Siteplane: Settings → Export in Siteplane downloads every field with its published value and gives your
88
+ coding agent a task that writes the values into the code and removes this package.
@@ -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-rc.0";
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-rc.0";
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-rc.0",
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",