@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.
- package/LICENSE +37 -0
- package/package.json +33 -0
- package/src/admin-page.astro +146 -0
- package/src/api-routes/asset-proxy.ts +76 -0
- package/src/api-routes/auth-callback.ts +105 -0
- package/src/api-routes/auth-login.ts +87 -0
- package/src/api-routes/auth-logout.ts +9 -0
- package/src/api-routes/auth-session.ts +36 -0
- package/src/api-routes/config.ts +30 -0
- package/src/api-routes/draft.ts +61 -0
- package/src/api-routes/github-proxy.ts +82 -0
- package/src/api-routes/pages.ts +17 -0
- package/src/draft-store.ts +61 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +11 -0
- package/src/integration.ts +378 -0
- package/src/preview-middleware.ts +184 -0
- package/tsconfig.json +9 -0
|
@@ -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
|
+
}
|