getfilepress 0.1.8 → 0.1.10
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 +21 -4
- package/package.json +110 -102
- package/packages/app/src/lib/genie/GeniePanel.svelte +286 -21
- package/packages/app/src/lib/genie/ops.ts +7 -1
- package/packages/app/src/lib/genie/store.ts +54 -1
- package/packages/app/src/lib/theme-entry.ts +2 -1
- package/packages/app/src/routes/+error.svelte +38 -0
- package/packages/app/src/routes/posts/+page.server.ts +12 -0
- package/packages/app/src/routes/posts/+page.svelte +38 -0
- package/packages/app/src/routes/posts/[slug]/+page.svelte +2 -1
- package/packages/app/src/routes/writing/+page.server.ts +2 -6
- package/packages/app/src/routes/writing/+page.svelte +1 -38
- package/packages/app/src/site-theme.d.ts +3 -0
- package/packages/app/vite-plugin-genie.ts +45 -1
- package/packages/app/vite.config.ts +38 -7
- package/packages/core/src/lib/components/PostCard.svelte +2 -1
- package/packages/core/src/lib/components/PostIndex.svelte +1 -1
- package/packages/core/src/lib/config.ts +51 -5
- package/packages/core/src/lib/content/feeds.ts +1 -1
- package/packages/core/src/lib/content/parse.ts +2 -0
- package/packages/core/src/lib/content/types.ts +2 -0
- package/packages/core/src/lib/format.ts +11 -0
- package/packages/core/src/lib/index.ts +7 -3
- package/packages/core/src/lib/redirects.ts +88 -0
- package/packages/core/src/lib/server.ts +9 -2
- package/packages/core/src/lib/styles/presets/essay.css +2 -0
- package/packages/core/src/lib/styles/presets/folio.css +27 -0
- package/packages/core/src/lib/styles/presets/ink.css +28 -0
- package/packages/core/src/lib/styles/theme.css +37 -0
- package/packages/import/src/cli.ts +1 -0
- package/packages/import/src/extract.ts +15 -2
- package/packages/import/src/ir.ts +2 -0
- package/packages/import/src/ollama.ts +119 -27
- package/packages/import/src/redirects.ts +17 -0
- package/packages/import/src/write-site.ts +25 -3
- package/scripts/copy-path-mounts.mjs +30 -1
- package/scripts/create-site.mjs +1 -0
- package/scripts/filepress.mjs +24 -14
- package/scripts/new-post.ts +125 -0
- package/scripts/preview.mjs +95 -0
- package/packages/app/README.md +0 -19
- package/packages/core/README.md +0 -29
|
@@ -66,6 +66,33 @@
|
|
|
66
66
|
let cfgLede = $state('');
|
|
67
67
|
let cfgTagline = $state('');
|
|
68
68
|
let cfgLogo = $state('');
|
|
69
|
+
let renamingId = $state<string | null>(null);
|
|
70
|
+
let renameDraft = $state('');
|
|
71
|
+
let jobNote = $state('');
|
|
72
|
+
let jobSecs = $state(0);
|
|
73
|
+
let jobTimer: ReturnType<typeof setInterval> | null = null;
|
|
74
|
+
|
|
75
|
+
function formatWait(secs: number): string {
|
|
76
|
+
const m = Math.floor(secs / 60);
|
|
77
|
+
const r = secs % 60;
|
|
78
|
+
return m ? `${m}m ${String(r).padStart(2, '0')}s` : `${r}s`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function beginJob(note: string) {
|
|
82
|
+
jobNote = note;
|
|
83
|
+
jobSecs = 0;
|
|
84
|
+
if (jobTimer) clearInterval(jobTimer);
|
|
85
|
+
jobTimer = setInterval(() => {
|
|
86
|
+
jobSecs += 1;
|
|
87
|
+
}, 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function endJob() {
|
|
91
|
+
if (jobTimer) clearInterval(jobTimer);
|
|
92
|
+
jobTimer = null;
|
|
93
|
+
jobNote = '';
|
|
94
|
+
jobSecs = 0;
|
|
95
|
+
}
|
|
69
96
|
|
|
70
97
|
async function api(path: string, init?: RequestInit) {
|
|
71
98
|
const res = await fetch(`/__filepress/genie${path}`, {
|
|
@@ -129,6 +156,89 @@
|
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
function versionWhen(v: VersionRow) {
|
|
160
|
+
if (v.id === 'baseline') return 'pre-Genie snapshot';
|
|
161
|
+
const d = new Date(v.createdAt);
|
|
162
|
+
if (Number.isNaN(d.getTime())) return v.createdAt;
|
|
163
|
+
return d.toLocaleString(undefined, {
|
|
164
|
+
month: 'short',
|
|
165
|
+
day: 'numeric',
|
|
166
|
+
hour: '2-digit',
|
|
167
|
+
minute: '2-digit'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startRename(v: VersionRow) {
|
|
172
|
+
renamingId = v.id;
|
|
173
|
+
renameDraft = v.label;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function toggleStar(v: VersionRow) {
|
|
177
|
+
if (v.id === 'baseline') return;
|
|
178
|
+
loading = true;
|
|
179
|
+
error = '';
|
|
180
|
+
try {
|
|
181
|
+
await api('/star', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: JSON.stringify({ versionId: v.id, starred: !v.starred })
|
|
184
|
+
});
|
|
185
|
+
await refresh();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
error = e instanceof Error ? e.message : String(e);
|
|
188
|
+
loading = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function saveLabel(v: VersionRow) {
|
|
193
|
+
const label = renameDraft.trim();
|
|
194
|
+
if (!label || label === v.label) {
|
|
195
|
+
renamingId = null;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
loading = true;
|
|
199
|
+
error = '';
|
|
200
|
+
try {
|
|
201
|
+
await api('/label', { method: 'POST', body: JSON.stringify({ versionId: v.id, label }) });
|
|
202
|
+
renamingId = null;
|
|
203
|
+
await refresh();
|
|
204
|
+
} catch (e) {
|
|
205
|
+
error = e instanceof Error ? e.message : String(e);
|
|
206
|
+
loading = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function duplicate(v: VersionRow) {
|
|
211
|
+
loading = true;
|
|
212
|
+
error = '';
|
|
213
|
+
try {
|
|
214
|
+
await api('/duplicate', { method: 'POST', body: JSON.stringify({ versionId: v.id }) });
|
|
215
|
+
await refresh();
|
|
216
|
+
} catch (e) {
|
|
217
|
+
error = e instanceof Error ? e.message : String(e);
|
|
218
|
+
loading = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function removeVersion(v: VersionRow) {
|
|
223
|
+
if (v.id === 'baseline') return;
|
|
224
|
+
if (
|
|
225
|
+
!confirm(
|
|
226
|
+
`Delete “${v.label}”? This only removes the snapshot under .filepress-genie/. The working tree stays as it is.`
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
loading = true;
|
|
232
|
+
error = '';
|
|
233
|
+
try {
|
|
234
|
+
await api('/delete', { method: 'POST', body: JSON.stringify({ versionId: v.id }) });
|
|
235
|
+
await refresh();
|
|
236
|
+
} catch (e) {
|
|
237
|
+
error = e instanceof Error ? e.message : String(e);
|
|
238
|
+
loading = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
132
242
|
async function applyStock() {
|
|
133
243
|
loading = true;
|
|
134
244
|
error = '';
|
|
@@ -253,6 +363,11 @@
|
|
|
253
363
|
async function runInspire() {
|
|
254
364
|
loading = true;
|
|
255
365
|
error = '';
|
|
366
|
+
beginJob(
|
|
367
|
+
useLlm
|
|
368
|
+
? `Inspire + ${selectedModel || 'Ollama'} — first load of a 12B model can take several minutes`
|
|
369
|
+
: 'Crawling inspiration URLs'
|
|
370
|
+
);
|
|
256
371
|
try {
|
|
257
372
|
await api('/inspire', {
|
|
258
373
|
method: 'POST',
|
|
@@ -270,12 +385,17 @@
|
|
|
270
385
|
} catch (e) {
|
|
271
386
|
error = e instanceof Error ? e.message : String(e);
|
|
272
387
|
loading = false;
|
|
388
|
+
} finally {
|
|
389
|
+
endJob();
|
|
273
390
|
}
|
|
274
391
|
}
|
|
275
392
|
|
|
276
393
|
async function runRefine() {
|
|
277
394
|
loading = true;
|
|
278
395
|
error = '';
|
|
396
|
+
beginJob(
|
|
397
|
+
`Asking ${selectedModel || 'Ollama'} — first load of a 12B model can take several minutes`
|
|
398
|
+
);
|
|
279
399
|
try {
|
|
280
400
|
await api('/refine', {
|
|
281
401
|
method: 'POST',
|
|
@@ -289,6 +409,8 @@
|
|
|
289
409
|
} catch (e) {
|
|
290
410
|
error = e instanceof Error ? e.message : String(e);
|
|
291
411
|
loading = false;
|
|
412
|
+
} finally {
|
|
413
|
+
endJob();
|
|
292
414
|
}
|
|
293
415
|
}
|
|
294
416
|
|
|
@@ -324,12 +446,18 @@
|
|
|
324
446
|
<strong>Genie</strong>
|
|
325
447
|
<p class="genie-tagline">Try a look → activate → commit baked files</p>
|
|
326
448
|
</div>
|
|
327
|
-
<button type="button" class="genie-x" onclick={() =>
|
|
449
|
+
<button type="button" class="genie-x" onclick={() => { open = false; endJob(); }}>Close</button>
|
|
328
450
|
</header>
|
|
329
451
|
|
|
330
452
|
{#if error}
|
|
331
453
|
<p class="genie-err">{error}</p>
|
|
332
454
|
{/if}
|
|
455
|
+
{#if jobNote}
|
|
456
|
+
<p class="genie-progress" aria-live="polite">
|
|
457
|
+
{jobNote} · {formatWait(jobSecs)} elapsed. Watch the <code>filepress dev</code> terminal for
|
|
458
|
+
“still generating…”.
|
|
459
|
+
</p>
|
|
460
|
+
{/if}
|
|
333
461
|
|
|
334
462
|
{#if health}
|
|
335
463
|
<div class="genie-shell">
|
|
@@ -423,10 +551,13 @@
|
|
|
423
551
|
disabled={loading || scanning || !hostIsReady() || !refinePrompt.trim()}
|
|
424
552
|
onclick={runRefine}
|
|
425
553
|
>
|
|
426
|
-
{loading ?
|
|
554
|
+
{loading ? `Asking Ollama… ${formatWait(jobSecs)}` : 'Refine & activate'}
|
|
427
555
|
</button>
|
|
428
556
|
<p class="genie-muted">
|
|
429
|
-
|
|
557
|
+
<code>gemma4:12b</code> often spends the first few minutes loading into VRAM — that is
|
|
558
|
+
normal. Progress prints in the filepress terminal every 15s. Raise the budget with
|
|
559
|
+
<code>FILEPRESS_OLLAMA_TIMEOUT_MS</code> (default 10 minutes). Tip: set
|
|
560
|
+
<code>FILEPRESS_OLLAMA_MODEL</code> / <code>OLLAMA_HOST</code> for defaults;
|
|
430
561
|
<a href="https://ollanet.dev" target="_blank" rel="noreferrer">ollanet</a> finds other
|
|
431
562
|
boxes. Finetuna can tune a named variant.
|
|
432
563
|
</p>
|
|
@@ -605,25 +736,82 @@
|
|
|
605
736
|
<h3>Versions</h3>
|
|
606
737
|
<p class="genie-howto">
|
|
607
738
|
Every Genie action saves a snapshot under <code>.filepress-genie/</code> (gitignored).
|
|
608
|
-
|
|
609
|
-
the pre-Genie look.
|
|
739
|
+
Star keepers, rename, duplicate, then activate to bake into the working tree.
|
|
740
|
+
<code>baseline</code> is the pre-Genie look and cannot be deleted.
|
|
610
741
|
</p>
|
|
611
742
|
<p class="genie-muted">
|
|
612
743
|
Active: {health.active?.versionId ?? '(none)'}
|
|
744
|
+
{#if health.versions.length}
|
|
745
|
+
· {health.versions.length} saved
|
|
746
|
+
{/if}
|
|
613
747
|
</p>
|
|
614
748
|
<ul class="genie-versions">
|
|
615
749
|
{#each health.versions as v (v.id)}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
750
|
+
{@const isActive = health.active?.versionId === v.id}
|
|
751
|
+
<li class:active={isActive} class:starred={v.starred}>
|
|
752
|
+
<div class="genie-ver-head">
|
|
753
|
+
<button
|
|
754
|
+
type="button"
|
|
755
|
+
class="genie-star"
|
|
756
|
+
class:on={v.starred}
|
|
757
|
+
disabled={loading || v.id === 'baseline'}
|
|
758
|
+
aria-pressed={v.starred}
|
|
759
|
+
aria-label={v.starred ? 'Unstar version' : 'Star version'}
|
|
760
|
+
onclick={() => toggleStar(v)}
|
|
761
|
+
>
|
|
762
|
+
{v.starred ? '★' : '☆'}
|
|
763
|
+
</button>
|
|
764
|
+
{#if renamingId === v.id}
|
|
765
|
+
<input
|
|
766
|
+
class="genie-rename"
|
|
767
|
+
type="text"
|
|
768
|
+
bind:value={renameDraft}
|
|
769
|
+
disabled={loading}
|
|
770
|
+
aria-label="Version label"
|
|
771
|
+
onkeydown={(e) => {
|
|
772
|
+
if (e.key === 'Enter') void saveLabel(v);
|
|
773
|
+
if (e.key === 'Escape') renamingId = null;
|
|
774
|
+
}}
|
|
775
|
+
/>
|
|
776
|
+
{:else}
|
|
777
|
+
<strong>{v.label}</strong>
|
|
778
|
+
{/if}
|
|
779
|
+
{#if isActive}
|
|
780
|
+
<span class="genie-ver-badge">Active</span>
|
|
781
|
+
{/if}
|
|
782
|
+
</div>
|
|
783
|
+
<p class="genie-ver-when">{versionWhen(v)}</p>
|
|
784
|
+
<div class="genie-ver-actions">
|
|
785
|
+
{#if renamingId === v.id}
|
|
786
|
+
<button type="button" disabled={loading} onclick={() => saveLabel(v)}>
|
|
787
|
+
Save name
|
|
788
|
+
</button>
|
|
789
|
+
<button type="button" disabled={loading} onclick={() => (renamingId = null)}>
|
|
790
|
+
Cancel
|
|
791
|
+
</button>
|
|
792
|
+
{:else}
|
|
793
|
+
<button
|
|
794
|
+
type="button"
|
|
795
|
+
disabled={loading || isActive}
|
|
796
|
+
onclick={() => activate(v.id)}
|
|
797
|
+
>
|
|
798
|
+
Activate
|
|
799
|
+
</button>
|
|
800
|
+
<button type="button" disabled={loading} onclick={() => startRename(v)}>
|
|
801
|
+
Rename
|
|
802
|
+
</button>
|
|
803
|
+
<button type="button" disabled={loading} onclick={() => duplicate(v)}>
|
|
804
|
+
Duplicate
|
|
805
|
+
</button>
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
disabled={loading || v.id === 'baseline' || isActive}
|
|
809
|
+
onclick={() => removeVersion(v)}
|
|
810
|
+
>
|
|
811
|
+
Delete
|
|
812
|
+
</button>
|
|
813
|
+
{/if}
|
|
814
|
+
</div>
|
|
627
815
|
</li>
|
|
628
816
|
{/each}
|
|
629
817
|
</ul>
|
|
@@ -816,6 +1004,16 @@
|
|
|
816
1004
|
flex-shrink: 0;
|
|
817
1005
|
}
|
|
818
1006
|
|
|
1007
|
+
.genie-progress {
|
|
1008
|
+
margin: 0 0.25rem 0.65rem;
|
|
1009
|
+
padding: 0.5rem 0.65rem;
|
|
1010
|
+
border-radius: 6px;
|
|
1011
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 40%, var(--rule, #444));
|
|
1012
|
+
font-size: 0.82rem;
|
|
1013
|
+
color: var(--ink-soft, #ccc);
|
|
1014
|
+
flex-shrink: 0;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
819
1017
|
.genie-row {
|
|
820
1018
|
display: grid;
|
|
821
1019
|
gap: 0.35rem;
|
|
@@ -898,19 +1096,86 @@
|
|
|
898
1096
|
margin: 0.5rem 0;
|
|
899
1097
|
padding: 0;
|
|
900
1098
|
display: grid;
|
|
901
|
-
gap: 0.
|
|
1099
|
+
gap: 0.55rem;
|
|
902
1100
|
}
|
|
903
1101
|
|
|
904
1102
|
.genie-versions li {
|
|
1103
|
+
display: grid;
|
|
1104
|
+
gap: 0.3rem;
|
|
1105
|
+
padding: 0.5rem 0.55rem;
|
|
1106
|
+
border: 1px solid var(--rule, #444);
|
|
1107
|
+
border-radius: 8px;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.genie-versions li.active {
|
|
1111
|
+
border-color: color-mix(in srgb, var(--accent, #f0c040) 55%, var(--rule, #444));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.genie-ver-head {
|
|
905
1115
|
display: flex;
|
|
906
1116
|
align-items: center;
|
|
907
|
-
|
|
908
|
-
|
|
1117
|
+
gap: 0.4rem;
|
|
1118
|
+
min-width: 0;
|
|
909
1119
|
}
|
|
910
1120
|
|
|
911
|
-
.genie-
|
|
912
|
-
|
|
1121
|
+
.genie-ver-head strong {
|
|
1122
|
+
flex: 1 1 auto;
|
|
1123
|
+
min-width: 0;
|
|
1124
|
+
overflow: hidden;
|
|
1125
|
+
text-overflow: ellipsis;
|
|
1126
|
+
white-space: nowrap;
|
|
1127
|
+
font-size: 0.84rem;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.genie-star {
|
|
1131
|
+
flex: 0 0 auto;
|
|
1132
|
+
width: 1.7rem;
|
|
1133
|
+
padding: 0.15rem 0;
|
|
1134
|
+
border: 1px solid transparent;
|
|
1135
|
+
background: none;
|
|
1136
|
+
color: var(--ink-soft, #999);
|
|
1137
|
+
font-size: 0.95rem;
|
|
1138
|
+
line-height: 1;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.genie-star.on {
|
|
1142
|
+
color: var(--accent, #f0c040);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.genie-ver-badge {
|
|
1146
|
+
flex: 0 0 auto;
|
|
1147
|
+
font-size: 0.62rem;
|
|
1148
|
+
letter-spacing: 0.08em;
|
|
1149
|
+
text-transform: uppercase;
|
|
913
1150
|
color: var(--accent, #f0c040);
|
|
1151
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 45%, var(--rule, #444));
|
|
1152
|
+
border-radius: 999px;
|
|
1153
|
+
padding: 0.08rem 0.4rem;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.genie-rename {
|
|
1157
|
+
flex: 1 1 auto;
|
|
1158
|
+
min-width: 0;
|
|
1159
|
+
box-sizing: border-box;
|
|
1160
|
+
font: inherit;
|
|
1161
|
+
font-size: 0.82rem;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.genie-ver-when {
|
|
1165
|
+
margin: 0;
|
|
1166
|
+
font-size: 0.72rem;
|
|
1167
|
+
color: var(--ink-soft, #999);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.genie-ver-actions {
|
|
1171
|
+
display: flex;
|
|
1172
|
+
flex-wrap: wrap;
|
|
1173
|
+
gap: 0.3rem;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.genie-ver-actions button {
|
|
1177
|
+
padding: 0.28rem 0.5rem;
|
|
1178
|
+
font-size: 0.72rem;
|
|
914
1179
|
}
|
|
915
1180
|
|
|
916
1181
|
.genie-linkish {
|
|
@@ -313,6 +313,7 @@ function stubSiteIr(siteRoot: string): SiteIR {
|
|
|
313
313
|
nav: [],
|
|
314
314
|
topics: [],
|
|
315
315
|
lede: null,
|
|
316
|
+
homeMarkdown: null,
|
|
316
317
|
notes: [],
|
|
317
318
|
assets: []
|
|
318
319
|
};
|
|
@@ -405,13 +406,17 @@ export async function refineWithOllama(
|
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
const seed = loadWorkingBrief(siteRoot);
|
|
409
|
+
const logLabel = `filepress genie: refine ${model} @ ${host}`;
|
|
410
|
+
console.log(`${logLabel}: prompt “${prompt.slice(0, 80)}${prompt.length > 80 ? '…' : ''}”`);
|
|
408
411
|
const brief = await generateDesignBrief({
|
|
409
412
|
host,
|
|
410
413
|
model,
|
|
411
414
|
ir: stubSiteIr(siteRoot),
|
|
412
415
|
inspireSummaries: [`Author steer: ${prompt}`],
|
|
413
416
|
inspireSignals: [],
|
|
414
|
-
seed
|
|
417
|
+
seed,
|
|
418
|
+
strictParse: true,
|
|
419
|
+
logLabel
|
|
415
420
|
});
|
|
416
421
|
const themeCss = themeCssFromBrief(brief);
|
|
417
422
|
const meta = writeSnapshot(siteRoot, {
|
|
@@ -423,6 +428,7 @@ export async function refineWithOllama(
|
|
|
423
428
|
llm: { used: true, model, host },
|
|
424
429
|
steers: [{ type: 'refine', prompt }]
|
|
425
430
|
});
|
|
431
|
+
console.log(`${logLabel}: wrote ${meta.id}`);
|
|
426
432
|
const active =
|
|
427
433
|
opts.activate !== false ? activateVersion(siteRoot, meta.id) : getActive(siteRoot);
|
|
428
434
|
return { meta, active, brief, llm: { used: true, model, host } };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
copyFileSync,
|
|
3
|
+
cpSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
readFileSync,
|
|
@@ -62,7 +63,10 @@ export function listVersions(siteRoot: string): GenieVersionMeta[] {
|
|
|
62
63
|
const meta = readJson<GenieVersionMeta | null>(join(dir, id, 'meta.json'), null);
|
|
63
64
|
if (meta) out.push(meta);
|
|
64
65
|
}
|
|
65
|
-
return out.sort((a, b) =>
|
|
66
|
+
return out.sort((a, b) => {
|
|
67
|
+
if (a.starred !== b.starred) return a.starred ? -1 : 1;
|
|
68
|
+
return b.createdAt.localeCompare(a.createdAt);
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export function readVersionBrief(siteRoot: string, id: string): DesignBrief | null {
|
|
@@ -161,6 +165,55 @@ export function activateVersion(siteRoot: string, versionId: string): GenieActiv
|
|
|
161
165
|
return active;
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
export function readVersionMeta(siteRoot: string, versionId: string): GenieVersionMeta {
|
|
169
|
+
const meta = readJson<GenieVersionMeta | null>(join(versionPath(siteRoot, versionId), 'meta.json'), null);
|
|
170
|
+
if (!meta) throw new Error(`Unknown Genie version: ${versionId}`);
|
|
171
|
+
return meta;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function updateVersionMeta(
|
|
175
|
+
siteRoot: string,
|
|
176
|
+
versionId: string,
|
|
177
|
+
patch: { label?: string; starred?: boolean }
|
|
178
|
+
): GenieVersionMeta {
|
|
179
|
+
const meta = readVersionMeta(siteRoot, versionId);
|
|
180
|
+
if (typeof patch.label === 'string') {
|
|
181
|
+
const label = patch.label.trim();
|
|
182
|
+
if (!label) throw new Error('label cannot be empty');
|
|
183
|
+
meta.label = label.slice(0, 80);
|
|
184
|
+
}
|
|
185
|
+
if (typeof patch.starred === 'boolean') {
|
|
186
|
+
if (versionId === 'baseline' && patch.starred === false) {
|
|
187
|
+
throw new Error('Cannot unstar the baseline version');
|
|
188
|
+
}
|
|
189
|
+
meta.starred = patch.starred;
|
|
190
|
+
}
|
|
191
|
+
writeJson(join(versionPath(siteRoot, versionId), 'meta.json'), meta);
|
|
192
|
+
return meta;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Copy a snapshot folder to a new id. Does not activate. */
|
|
196
|
+
export function duplicateVersion(siteRoot: string, sourceId: string, label?: string): GenieVersionMeta {
|
|
197
|
+
const src = versionPath(siteRoot, sourceId);
|
|
198
|
+
if (!existsSync(join(src, 'theme.css'))) {
|
|
199
|
+
throw new Error(`Unknown Genie version: ${sourceId}`);
|
|
200
|
+
}
|
|
201
|
+
const source = readVersionMeta(siteRoot, sourceId);
|
|
202
|
+
const id = newVersionId();
|
|
203
|
+
const dest = versionPath(siteRoot, id);
|
|
204
|
+
cpSync(src, dest, { recursive: true });
|
|
205
|
+
const meta: GenieVersionMeta = {
|
|
206
|
+
...source,
|
|
207
|
+
id,
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
parentId: sourceId,
|
|
210
|
+
label: (label?.trim() || `Copy of ${source.label}`).slice(0, 80),
|
|
211
|
+
starred: false
|
|
212
|
+
};
|
|
213
|
+
writeJson(join(dest, 'meta.json'), meta);
|
|
214
|
+
return meta;
|
|
215
|
+
}
|
|
216
|
+
|
|
164
217
|
export function deleteVersion(siteRoot: string, versionId: string) {
|
|
165
218
|
if (versionId === 'baseline') {
|
|
166
219
|
throw new Error('Cannot delete the baseline version');
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Essay chrome + fonts, then site theme.
|
|
2
|
+
* Essay chrome + fonts, then a named preset, then the site theme.
|
|
3
3
|
* Site rules use higher-specificity selectors (`:root:root`, `header.site-header …`)
|
|
4
4
|
* so they win even if Vite emits Essay CSS after the site sheet.
|
|
5
5
|
*/
|
|
6
6
|
import '@filepress/core/theme';
|
|
7
|
+
import '$site-preset';
|
|
7
8
|
import '$site-theme';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/stores';
|
|
3
|
+
import { postsIndexPath } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
const homeHref = $derived(postsIndexPath(config));
|
|
7
|
+
const missing = $derived($page.status === 404);
|
|
8
|
+
const detail = $derived.by(() => {
|
|
9
|
+
const raw = $page.error?.message?.trim() ?? '';
|
|
10
|
+
if (!raw || /^not found$/i.test(raw)) return '';
|
|
11
|
+
return raw;
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<svelte:head>
|
|
16
|
+
<title>{missing ? 'Not found' : 'Error'} — {config.title}</title>
|
|
17
|
+
<meta name="robots" content="noindex" />
|
|
18
|
+
</svelte:head>
|
|
19
|
+
|
|
20
|
+
<section class="empty-state error-page">
|
|
21
|
+
<p class="error-code">{$page.status}</p>
|
|
22
|
+
<h1>{missing ? 'This page is not here.' : 'Something went wrong.'}</h1>
|
|
23
|
+
<p>
|
|
24
|
+
{detail ||
|
|
25
|
+
(missing
|
|
26
|
+
? 'The address may have changed, or the file was never published.'
|
|
27
|
+
: 'The build or request failed. Try the index, or check the terminal if you are in dev.')}
|
|
28
|
+
</p>
|
|
29
|
+
<p class="error-actions">
|
|
30
|
+
{#if config.homePage}
|
|
31
|
+
<a href="/">Home</a>
|
|
32
|
+
<span aria-hidden="true"> · </span>
|
|
33
|
+
<a href={homeHref}>Posts</a>
|
|
34
|
+
{:else}
|
|
35
|
+
<a href={homeHref}>Back to the index</a>
|
|
36
|
+
{/if}
|
|
37
|
+
</p>
|
|
38
|
+
</section>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit';
|
|
2
|
+
import type { PageServerLoad } from './$types';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
export const load: PageServerLoad = () => {
|
|
7
|
+
// Without homePage, the post index stays at `/`.
|
|
8
|
+
if (!config.homePage) {
|
|
9
|
+
redirect(302, '/');
|
|
10
|
+
}
|
|
11
|
+
return content.getIndexPage(1, config.postsPerPage);
|
|
12
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { PostIndex, absoluteUrl, ogImageUrl, postsIndexPath } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
|
|
8
|
+
const pageTitle = $derived(`Posts — ${config.title}`);
|
|
9
|
+
const canonical = $derived(absoluteUrl(config, postsIndexPath(config)));
|
|
10
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
11
|
+
const description = $derived(
|
|
12
|
+
`Posts from ${config.title}. ${config.description}`.trim()
|
|
13
|
+
);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<svelte:head>
|
|
17
|
+
<title>{pageTitle}</title>
|
|
18
|
+
<meta name="description" content={description} />
|
|
19
|
+
<link rel="canonical" href={canonical} />
|
|
20
|
+
<meta property="og:type" content="website" />
|
|
21
|
+
<meta property="og:title" content="Posts — {config.title}" />
|
|
22
|
+
<meta property="og:description" content={description} />
|
|
23
|
+
<meta property="og:url" content={canonical} />
|
|
24
|
+
{#if ogImage}
|
|
25
|
+
<meta property="og:image" content={ogImage} />
|
|
26
|
+
<meta name="twitter:card" content="summary" />
|
|
27
|
+
<meta name="twitter:image" content={ogImage} />
|
|
28
|
+
{/if}
|
|
29
|
+
</svelte:head>
|
|
30
|
+
|
|
31
|
+
<PostIndex
|
|
32
|
+
site={config}
|
|
33
|
+
featured={data.featured}
|
|
34
|
+
posts={data.posts}
|
|
35
|
+
page={data.page}
|
|
36
|
+
totalPages={data.totalPages}
|
|
37
|
+
indexHref={postsIndexPath(config)}
|
|
38
|
+
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PageData } from './$types';
|
|
3
|
-
import { Newsletter, absoluteUrl, formatDate, ogImageUrl } from '@filepress/core';
|
|
3
|
+
import { Newsletter, absoluteUrl, formatDate, formatReadingTime, ogImageUrl } from '@filepress/core';
|
|
4
4
|
import config from '$site-config';
|
|
5
5
|
|
|
6
6
|
let { data }: { data: PageData } = $props();
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
{#if showUpdated}
|
|
52
52
|
· updated <time datetime={post.updated}>{formatDate(post.updated!)}</time>
|
|
53
53
|
{/if}
|
|
54
|
+
· <span class="reading-time">{formatReadingTime(post.readingMinutes)}</span>
|
|
54
55
|
</p>
|
|
55
56
|
{#if data.tags.length}
|
|
56
57
|
<ul class="tag-list" style="margin-top:0.75rem">
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { redirect } from '@sveltejs/kit';
|
|
2
2
|
import type { PageServerLoad } from './$types';
|
|
3
|
-
import {
|
|
3
|
+
import { postsIndexPath } from '@filepress/core';
|
|
4
4
|
import config from '$site-config';
|
|
5
5
|
|
|
6
6
|
export const load: PageServerLoad = () => {
|
|
7
|
-
|
|
8
|
-
if (!config.homePage) {
|
|
9
|
-
redirect(302, '/');
|
|
10
|
-
}
|
|
11
|
-
return content.getIndexPage(1, config.postsPerPage);
|
|
7
|
+
redirect(308, postsIndexPath(config));
|
|
12
8
|
};
|
|
@@ -1,38 +1 @@
|
|
|
1
|
-
<
|
|
2
|
-
import type { PageData } from './$types';
|
|
3
|
-
import { PostIndex, absoluteUrl, ogImageUrl, postsIndexPath } from '@filepress/core';
|
|
4
|
-
import config from '$site-config';
|
|
5
|
-
|
|
6
|
-
let { data }: { data: PageData } = $props();
|
|
7
|
-
|
|
8
|
-
const pageTitle = $derived(`Posts — ${config.title}`);
|
|
9
|
-
const canonical = $derived(absoluteUrl(config, postsIndexPath(config)));
|
|
10
|
-
const ogImage = $derived(ogImageUrl(config));
|
|
11
|
-
const description = $derived(
|
|
12
|
-
`Posts from ${config.title}. ${config.description}`.trim()
|
|
13
|
-
);
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<svelte:head>
|
|
17
|
-
<title>{pageTitle}</title>
|
|
18
|
-
<meta name="description" content={description} />
|
|
19
|
-
<link rel="canonical" href={canonical} />
|
|
20
|
-
<meta property="og:type" content="website" />
|
|
21
|
-
<meta property="og:title" content="Posts — {config.title}" />
|
|
22
|
-
<meta property="og:description" content={description} />
|
|
23
|
-
<meta property="og:url" content={canonical} />
|
|
24
|
-
{#if ogImage}
|
|
25
|
-
<meta property="og:image" content={ogImage} />
|
|
26
|
-
<meta name="twitter:card" content="summary" />
|
|
27
|
-
<meta name="twitter:image" content={ogImage} />
|
|
28
|
-
{/if}
|
|
29
|
-
</svelte:head>
|
|
30
|
-
|
|
31
|
-
<PostIndex
|
|
32
|
-
site={config}
|
|
33
|
-
featured={data.featured}
|
|
34
|
-
posts={data.posts}
|
|
35
|
-
page={data.page}
|
|
36
|
-
totalPages={data.totalPages}
|
|
37
|
-
indexHref={postsIndexPath(config)}
|
|
38
|
-
/>
|
|
1
|
+
<p>Moved to <a href="/posts">/posts</a>.</p>
|