sitedrift 0.3.4 → 0.3.5

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/AGENTS.md CHANGED
@@ -107,6 +107,6 @@ LIVE render on separate origins. Never expose sitedrift through a public proxy.
107
107
 
108
108
  The optional Cloudflare Pages addon is intentionally public-preview safe: it is
109
109
  installed only on non-production builds, exposes only `/__sitedrift/*`, permits
110
- only `GET` and `HEAD`, allowlists one configured live origin, and sandboxes both
111
- frames without same-origin authority. Production output and existing API
112
- Functions are unchanged.
110
+ only `GET` and `HEAD`, and allowlists one configured live origin. Hosted frames
111
+ execute the compared site's scripts and must be used only with trusted preview
112
+ code. Production output and existing API Functions are unchanged.
package/README.md CHANGED
@@ -97,8 +97,9 @@ when `CF_PAGES=1` and `CF_PAGES_BRANCH` is not `main`. Production builds are
97
97
  left unchanged. Use `--production-branch <name>` when production is another
98
98
  branch.
99
99
 
100
- Hosted proxies are read-only (`GET`/`HEAD`), sandboxed without same-origin
101
- authority, and fixed to the configured live origin. Review notes stay in that
100
+ Hosted proxies are read-only (`GET`/`HEAD`) and fixed to the configured live
101
+ origin. Frames run the compared site's scripts so interactive previews behave
102
+ like the deployment; only enable the addon for preview code you trust. Review notes stay in that
102
103
  browser's `localStorage`; they are not sent to an API, shared with agents, or
103
104
  written to disk. Existing application Functions keep their original routes.
104
105
 
package/assets/viewer.js CHANGED
@@ -4,10 +4,9 @@
4
4
  config.dev = location.origin;
5
5
  config.frameOrigins = { dev: location.origin, live: location.origin };
6
6
  for (const iframe of document.querySelectorAll('iframe[data-side]')) {
7
- // Same-origin is required for Safari to apply `style-src 'self'` inside
8
- // the frame. Scripts stay disabled so framed site code cannot reach
9
- // the same-origin sitedrift parent.
10
- iframe.setAttribute('sandbox', 'allow-downloads allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin');
7
+ // Safari requires same-origin for `style-src 'self'`; scripts are
8
+ // required for the preview to behave like the deployed application.
9
+ iframe.setAttribute('sandbox', 'allow-downloads allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts');
11
10
  }
12
11
  }
13
12
  const root = document.documentElement;
@@ -88,12 +87,72 @@
88
87
  : config[side] + normalizeRoute(route);
89
88
  }
90
89
  function framePost(side, type, data = {}) {
90
+ if (config.hosted) {
91
+ const win = frame(side).contentWindow;
92
+ const scrolling = frame(side).contentDocument?.scrollingElement;
93
+ if (type === 'scroll' && scrolling) scrolling.scrollTop = Number(data.y) || 0;
94
+ if (type === 'reload') win?.location.reload();
95
+ return;
96
+ }
91
97
  frame(side).contentWindow?.postMessage(
92
98
  { source: 'sitedrift-parent', side, type, ...data },
93
99
  config.hosted ? '*' : config.frameOrigins[side],
94
100
  );
95
101
  }
96
102
 
103
+ function hostedSnapshot(side) {
104
+ if (!config.hosted) return;
105
+ const iframe = frame(side);
106
+ const doc = iframe.contentDocument;
107
+ const win = iframe.contentWindow;
108
+ if (!doc || !win) return;
109
+ const q = (selector) => doc.querySelector(selector);
110
+ const images = [...doc.querySelectorAll('img')];
111
+ const title = (doc.title || '').trim();
112
+ const description = q('meta[name="description"]')?.content?.trim() || '';
113
+ const canonical = q('link[rel="canonical"]')?.href || '';
114
+ const checks = [
115
+ ['Title present', !!title],
116
+ ['Title 30–60 chars', title.length >= 30 && title.length <= 60, String(title.length)],
117
+ ['Meta description', !!description],
118
+ ['Description 70–160', description.length >= 70 && description.length <= 160, String(description.length)],
119
+ ['Exactly one H1', doc.querySelectorAll('h1').length === 1, `${doc.querySelectorAll('h1').length} found`],
120
+ ['Canonical link', !!q('link[rel="canonical"]')],
121
+ ['Viewport meta', !!q('meta[name="viewport"]')],
122
+ ['html lang', !!doc.documentElement.lang],
123
+ ['Open Graph title', !!q('meta[property="og:title"]')],
124
+ ['Open Graph image', !!q('meta[property="og:image"]')],
125
+ ['Not noindex', !(q('meta[name="robots"]')?.content || '').toLowerCase().includes('noindex')],
126
+ ['Favicon', !!q('link[rel~="icon"]')],
127
+ ['Images have alt', images.every((image) => image.hasAttribute('alt')), `${images.filter((image) => !image.hasAttribute('alt')).length} missing`],
128
+ ].map(([label, ok, note]) => ({ label, ok, note }));
129
+ const url = new URL(iframe.src);
130
+ const route = (url.pathname.slice(proxyPath(side).length) || '/') + url.search + url.hash;
131
+ renderMetadata(side, {
132
+ route,
133
+ meta: {
134
+ title,
135
+ description,
136
+ canonical,
137
+ heading: q('h1')?.textContent?.trim() || '',
138
+ siteName: q('meta[property="og:site_name"]')?.content?.trim() || '',
139
+ icon: q('link[rel~="icon"]')?.href || '',
140
+ checks,
141
+ },
142
+ });
143
+ fetchStatus(side, route);
144
+ const reportScroll = () => {
145
+ const root = doc.scrollingElement || doc.documentElement;
146
+ frameState[side] = {
147
+ y: Number(win.scrollY) || 0,
148
+ max: Math.max(0, root.scrollHeight - win.innerHeight),
149
+ };
150
+ syncFrom(side);
151
+ };
152
+ win.addEventListener('scroll', reportScroll, { passive: true });
153
+ reportScroll();
154
+ }
155
+
97
156
  function statusBadges(side) {
98
157
  return [
99
158
  document.querySelector('.label[data-label="' + side + '"] .status-badge'),
@@ -506,6 +565,11 @@
506
565
  runFrameKey(message.key, side, message);
507
566
  }
508
567
  });
568
+ if (config.hosted) {
569
+ for (const side of ['dev', 'live']) {
570
+ frame(side).addEventListener('load', () => hostedSnapshot(side));
571
+ }
572
+ }
509
573
 
510
574
  scrollButton.addEventListener('click', () => {
511
575
  syncScroll = !syncScroll;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitedrift",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Catch the drift between dev and live — frame your local site and production side-by-side on the same route, locked scroll, with a difference-blend overlay. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {