@setzkasten-cms/astro 0.4.2

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.
@@ -0,0 +1,184 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { getDrafts } from './draft-store'
3
+
4
+ // We avoid importing from 'astro:middleware' because it's a virtual module
5
+ // that only exists at Astro build time — importing it breaks standalone tsc.
6
+ type MiddlewareContext = {
7
+ request: Request
8
+ cookies: { get(name: string): { value: string } | undefined }
9
+ }
10
+ type MiddlewareNext = () => Promise<Response>
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // AsyncLocalStorage for passing draft data through the SSR render pipeline.
14
+ // Uses Symbol.for() so the virtual module (setzkasten:content) can access
15
+ // the same ALS instance without importing this file directly.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const ALS_KEY = Symbol.for('setzkasten.preview.als')
19
+
20
+ type DraftMap = Record<string, Record<string, unknown>>
21
+
22
+ // Initialize once
23
+ if (!(globalThis as any)[ALS_KEY]) {
24
+ ;(globalThis as any)[ALS_KEY] = new AsyncLocalStorage<DraftMap>()
25
+ }
26
+
27
+ const als: AsyncLocalStorage<DraftMap> = (globalThis as any)[ALS_KEY]
28
+
29
+ /**
30
+ * Script injected into preview pages for scroll tracking and navigation.
31
+ * Communicates with the admin Page Builder via postMessage.
32
+ */
33
+ const PREVIEW_SCRIPT = `
34
+ <script data-sk-preview>
35
+ (function() {
36
+ // Listen for commands from the Page Builder
37
+ window.addEventListener('message', function(e) {
38
+ if (!e.data || !e.data.type) return;
39
+
40
+ // Scroll to a section
41
+ if (e.data.type === 'sk:scroll-to') {
42
+ var el = document.getElementById('section-' + e.data.section);
43
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
44
+ }
45
+
46
+ // Live DOM patching: update text content of elements with data-sk-field
47
+ // Supports nested paths for arrays/objects (e.g. "features.items.0.title")
48
+ if (e.data.type === 'sk:draft-update' && e.data.section && e.data.values) {
49
+ var section = e.data.section;
50
+ function patchValues(obj, prefix) {
51
+ Object.keys(obj).forEach(function(key) {
52
+ var val = obj[key];
53
+ var path = prefix ? prefix + '.' + key : key;
54
+ if (typeof val === 'string') {
55
+ var selector = '[data-sk-field="' + section + '.' + path + '"]';
56
+ var els = document.querySelectorAll(selector);
57
+ els.forEach(function(el) { el.innerHTML = val; });
58
+ } else if (Array.isArray(val)) {
59
+ val.forEach(function(item, i) {
60
+ if (item && typeof item === 'object') {
61
+ patchValues(item, path + '.' + i);
62
+ }
63
+ });
64
+ } else if (val && typeof val === 'object') {
65
+ patchValues(val, path);
66
+ }
67
+ });
68
+ }
69
+ patchValues(e.data.values, '');
70
+ }
71
+
72
+ // Reorder and toggle sections in the DOM (no reload needed)
73
+ if (e.data.type === 'sk:update-sections' && Array.isArray(e.data.sections)) {
74
+ var config = e.data.sections;
75
+ var parent = null;
76
+ // Find the parent container from the first existing section
77
+ for (var i = 0; i < config.length; i++) {
78
+ var found = document.getElementById('section-' + config[i].key);
79
+ if (found && found.parentElement) { parent = found.parentElement; break; }
80
+ }
81
+ if (parent) {
82
+ // Toggle visibility
83
+ config.forEach(function(s) {
84
+ var el = document.getElementById('section-' + s.key);
85
+ if (el) el.style.display = s.enabled ? '' : 'none';
86
+ });
87
+ // Reorder: append in the new order (moves existing DOM nodes)
88
+ config.forEach(function(s) {
89
+ var el = document.getElementById('section-' + s.key);
90
+ if (el) parent.appendChild(el);
91
+ });
92
+ }
93
+ }
94
+ });
95
+
96
+ // Track which section is currently visible via scroll position
97
+ var sections = document.querySelectorAll('[id^="section-"]');
98
+ if (sections.length > 0) {
99
+ var currentSection = null;
100
+ function updateVisibleSection() {
101
+ var best = null;
102
+ var bestDist = Infinity;
103
+ sections.forEach(function(s) {
104
+ var rect = s.getBoundingClientRect();
105
+ // Section whose top is closest to (but not far below) the viewport top
106
+ var dist = Math.abs(rect.top);
107
+ if (rect.top <= window.innerHeight * 0.4 && dist < bestDist) {
108
+ bestDist = dist;
109
+ best = s.id.replace('section-', '');
110
+ }
111
+ });
112
+ // Fallback: if nothing matched, use first section
113
+ if (!best) best = sections[0].id.replace('section-', '');
114
+ if (best !== currentSection) {
115
+ currentSection = best;
116
+ window.parent.postMessage({ type: 'sk:visible-section', section: best }, '*');
117
+ }
118
+ }
119
+ window.addEventListener('scroll', updateVisibleSection, { passive: true });
120
+ // Send initial section immediately
121
+ updateVisibleSection();
122
+ }
123
+
124
+ // Hover highlight for sections
125
+ var style = document.createElement('style');
126
+ style.textContent =
127
+ '[id^="section-"] { transition: outline 0.15s; }' +
128
+ '[id^="section-"]:hover { outline: 2px solid rgba(196,93,62,0.15); outline-offset: -2px; }';
129
+ document.head.appendChild(style);
130
+ })();
131
+ </script>`
132
+
133
+ /**
134
+ * Astro middleware that intercepts preview requests.
135
+ *
136
+ * When a request has `?_sk_preview=1`, it:
137
+ * 1. Validates the user session (same cookie as admin)
138
+ * 2. Loads draft content from the in-memory store
139
+ * 3. Wraps the page render in AsyncLocalStorage.run() so that
140
+ * getSection() from the virtual module returns draft data
141
+ * 4. Injects scroll tracking script into the rendered HTML
142
+ */
143
+ export const onRequest = async (context: MiddlewareContext, next: MiddlewareNext) => {
144
+ const url = new URL(context.request.url)
145
+
146
+ // Only intercept preview requests
147
+ if (!url.searchParams.has('_sk_preview')) {
148
+ return next()
149
+ }
150
+
151
+ // Validate session
152
+ const session = context.cookies.get('setzkasten_session')?.value
153
+ if (!session) {
154
+ return next()
155
+ }
156
+
157
+ // Load drafts for this session
158
+ const drafts = getDrafts(session)
159
+
160
+ // Render the page (with or without drafts)
161
+ const renderPage = async () => {
162
+ const response = await next()
163
+
164
+ // Only modify HTML responses
165
+ const contentType = response.headers.get('content-type') ?? ''
166
+ if (!contentType.includes('text/html')) return response
167
+
168
+ // Inject the preview script before </body>
169
+ const html = await response.text()
170
+ const modifiedHtml = html.replace('</body>', PREVIEW_SCRIPT + '\n</body>')
171
+
172
+ return new Response(modifiedHtml, {
173
+ status: response.status,
174
+ headers: response.headers,
175
+ })
176
+ }
177
+
178
+ // If we have drafts, run within ALS context
179
+ if (drafts) {
180
+ return als.run(drafts, renderPage)
181
+ }
182
+
183
+ return renderPage()
184
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "jsx": "react-jsx"
7
+ },
8
+ "include": ["src"]
9
+ }