getfilepress 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +178 -4
- package/package.json +89 -7
- package/packages/app/package.json +31 -0
- package/packages/app/src/app.d.ts +12 -0
- package/packages/app/src/app.html +12 -0
- package/packages/app/src/critical-theme.d.ts +4 -0
- package/packages/app/src/lib/content.server.ts +8 -0
- package/packages/app/src/lib/empty-theme.css +1 -0
- package/packages/app/src/lib/genie/GenieHost.svelte +16 -0
- package/packages/app/src/lib/genie/GeniePanel.svelte +429 -0
- package/packages/app/src/lib/genie/ops.ts +237 -0
- package/packages/app/src/lib/genie/store.ts +217 -0
- package/packages/app/src/lib/genie/types.ts +61 -0
- package/packages/app/src/lib/pages.server.ts +7 -0
- package/packages/app/src/lib/site.server.ts +52 -0
- package/packages/app/src/lib/theme-entry.ts +7 -0
- package/packages/app/src/routes/+layout.svelte +29 -0
- package/packages/app/src/routes/+layout.ts +10 -0
- package/packages/app/src/routes/+page.server.ts +28 -0
- package/packages/app/src/routes/+page.svelte +67 -0
- package/packages/app/src/routes/[slug]/+page.server.ts +20 -0
- package/packages/app/src/routes/[slug]/+page.svelte +47 -0
- package/packages/app/src/routes/page/[n]/+page.server.ts +21 -0
- package/packages/app/src/routes/page/[n]/+page.svelte +36 -0
- package/packages/app/src/routes/posts/[slug]/+page.server.ts +24 -0
- package/packages/app/src/routes/posts/[slug]/+page.svelte +95 -0
- package/packages/app/src/routes/robots.txt/+server.ts +11 -0
- package/packages/app/src/routes/rss.xml/+server.ts +13 -0
- package/packages/app/src/routes/sitemap.xml/+server.ts +19 -0
- package/packages/app/src/routes/tags/+page.server.ts +4 -0
- package/packages/app/src/routes/tags/+page.svelte +26 -0
- package/packages/app/src/routes/tags/[tag]/+page.server.ts +15 -0
- package/packages/app/src/routes/tags/[tag]/+page.svelte +23 -0
- package/packages/app/src/routes/topics/+page.server.ts +28 -0
- package/packages/app/src/routes/topics/+page.svelte +47 -0
- package/packages/app/src/routes/writing/+page.server.ts +12 -0
- package/packages/app/src/routes/writing/+page.svelte +38 -0
- package/packages/app/src/site-theme.d.ts +2 -0
- package/packages/app/static/.gitkeep +0 -0
- package/packages/app/tsconfig.json +15 -0
- package/packages/app/vite-plugin-critical-theme.ts +70 -0
- package/packages/app/vite-plugin-genie.ts +114 -0
- package/packages/app/vite.config.ts +141 -0
- package/packages/core/package.json +51 -0
- package/packages/core/src/lib/assets/favicon.svg +1 -0
- package/packages/core/src/lib/components/Newsletter.svelte +13 -0
- package/packages/core/src/lib/components/PostCard.svelte +34 -0
- package/packages/core/src/lib/components/PostIndex.svelte +86 -0
- package/packages/core/src/lib/components/SiteFooter.svelte +15 -0
- package/packages/core/src/lib/components/SiteHeader.svelte +28 -0
- package/packages/core/src/lib/config.ts +154 -0
- package/packages/core/src/lib/content/content.ts +193 -0
- package/packages/core/src/lib/content/feeds.ts +102 -0
- package/packages/core/src/lib/content/markdown.ts +78 -0
- package/packages/core/src/lib/content/pages.ts +103 -0
- package/packages/core/src/lib/content/parse.ts +214 -0
- package/packages/core/src/lib/content/rehype-figure.ts +79 -0
- package/packages/core/src/lib/content/types.ts +88 -0
- package/packages/core/src/lib/format.ts +25 -0
- package/packages/core/src/lib/index.ts +27 -0
- package/packages/core/src/lib/server.ts +33 -0
- package/packages/core/src/lib/styles/fonts.css +72 -0
- package/packages/core/src/lib/styles/theme.css +762 -0
- package/packages/core/src/lib/theme.ts +5 -0
- package/packages/import/package.json +27 -0
- package/packages/import/src/cli.ts +346 -0
- package/packages/import/src/discover.ts +190 -0
- package/packages/import/src/extract.ts +378 -0
- package/packages/import/src/fetch.ts +63 -0
- package/packages/import/src/html-to-md.ts +21 -0
- package/packages/import/src/images.ts +247 -0
- package/packages/import/src/inspire.ts +417 -0
- package/packages/import/src/ir.ts +109 -0
- package/packages/import/src/ollama.ts +145 -0
- package/packages/import/src/stock.ts +218 -0
- package/packages/import/src/theme.ts +478 -0
- package/packages/import/src/write-site.ts +371 -0
- package/packages/import/tsconfig.json +13 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/create-site.mjs +260 -0
- package/scripts/filepress.mjs +273 -0
- package/scripts/link-embedded-packages.mjs +40 -0
- package/scripts/postinstall.mjs +56 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import type { DesignBrief, GenieActive, GenieVersionMeta } from './types.ts';
|
|
13
|
+
|
|
14
|
+
const GENIE_DIR = '.filepress-genie';
|
|
15
|
+
|
|
16
|
+
export function genieRoot(siteRoot: string): string {
|
|
17
|
+
return join(siteRoot, GENIE_DIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function versionsDir(siteRoot: string): string {
|
|
21
|
+
return join(genieRoot(siteRoot), 'versions');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function versionPath(siteRoot: string, id: string): string {
|
|
25
|
+
return join(versionsDir(siteRoot), id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureGenieDirs(siteRoot: string) {
|
|
29
|
+
mkdirSync(versionsDir(siteRoot), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function newVersionId(): string {
|
|
33
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, 'Z');
|
|
34
|
+
return `${ts}-${randomBytes(2).toString('hex')}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readJson<T>(path: string, fallback: T): T {
|
|
38
|
+
if (!existsSync(path)) return fallback;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
41
|
+
} catch {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeJson(path: string, data: unknown) {
|
|
47
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
48
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getActive(siteRoot: string): GenieActive | null {
|
|
52
|
+
return readJson<GenieActive | null>(join(genieRoot(siteRoot), 'active.json'), null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listVersions(siteRoot: string): GenieVersionMeta[] {
|
|
56
|
+
ensureGenieDirs(siteRoot);
|
|
57
|
+
const dir = versionsDir(siteRoot);
|
|
58
|
+
if (!existsSync(dir)) return [];
|
|
59
|
+
const out: GenieVersionMeta[] = [];
|
|
60
|
+
for (const id of readdirSync(dir)) {
|
|
61
|
+
const meta = readJson<GenieVersionMeta | null>(join(dir, id, 'meta.json'), null);
|
|
62
|
+
if (meta) out.push(meta);
|
|
63
|
+
}
|
|
64
|
+
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readVersionBrief(siteRoot: string, id: string): DesignBrief | null {
|
|
68
|
+
return readJson<DesignBrief | null>(join(versionPath(siteRoot, id), 'design-brief.json'), null);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readVersionTheme(siteRoot: string, id: string): string | null {
|
|
72
|
+
const p = join(versionPath(siteRoot, id), 'theme.css');
|
|
73
|
+
if (!existsSync(p)) return null;
|
|
74
|
+
return readFileSync(p, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type SnapshotInput = {
|
|
78
|
+
id?: string;
|
|
79
|
+
label: string;
|
|
80
|
+
prompt?: string;
|
|
81
|
+
parentId?: string | null;
|
|
82
|
+
brief: DesignBrief;
|
|
83
|
+
themeCss: string;
|
|
84
|
+
steers?: Array<Record<string, unknown>>;
|
|
85
|
+
/** Absolute or site-relative files to copy into version images/ as basename */
|
|
86
|
+
imageFiles?: Array<{ absPath: string; destName: string }>;
|
|
87
|
+
attribution?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function writeSnapshot(siteRoot: string, input: SnapshotInput): GenieVersionMeta {
|
|
91
|
+
ensureGenieDirs(siteRoot);
|
|
92
|
+
const id = input.id || newVersionId();
|
|
93
|
+
const dir = versionPath(siteRoot, id);
|
|
94
|
+
mkdirSync(join(dir, 'images'), { recursive: true });
|
|
95
|
+
|
|
96
|
+
const meta: GenieVersionMeta = {
|
|
97
|
+
id,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
parentId: input.parentId ?? getActive(siteRoot)?.versionId ?? null,
|
|
100
|
+
label: input.label,
|
|
101
|
+
starred: false,
|
|
102
|
+
prompt: input.prompt || '',
|
|
103
|
+
steers: input.steers || [],
|
|
104
|
+
inspireUrls: [],
|
|
105
|
+
llm: { used: false, model: null, host: null }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
writeJson(join(dir, 'meta.json'), meta);
|
|
109
|
+
writeJson(join(dir, 'design-brief.json'), input.brief);
|
|
110
|
+
writeJson(join(dir, 'config-patch.json'), {});
|
|
111
|
+
writeFileSync(join(dir, 'theme.css'), input.themeCss);
|
|
112
|
+
if (input.attribution) {
|
|
113
|
+
writeFileSync(join(dir, 'attribution.md'), input.attribution);
|
|
114
|
+
}
|
|
115
|
+
for (const img of input.imageFiles || []) {
|
|
116
|
+
if (!existsSync(img.absPath)) continue;
|
|
117
|
+
copyFileSync(img.absPath, join(dir, 'images', img.destName));
|
|
118
|
+
}
|
|
119
|
+
return meta;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Copy version theme + images into the site working tree. */
|
|
123
|
+
export function activateVersion(siteRoot: string, versionId: string): GenieActive {
|
|
124
|
+
const dir = versionPath(siteRoot, versionId);
|
|
125
|
+
if (!existsSync(join(dir, 'theme.css'))) {
|
|
126
|
+
throw new Error(`Unknown Genie version: ${versionId}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const themeCss = readFileSync(join(dir, 'theme.css'), 'utf8');
|
|
130
|
+
writeFileSync(join(siteRoot, 'theme.css'), themeCss);
|
|
131
|
+
|
|
132
|
+
const imgDir = join(dir, 'images');
|
|
133
|
+
const staticImg = join(siteRoot, 'static', 'images');
|
|
134
|
+
mkdirSync(staticImg, { recursive: true });
|
|
135
|
+
if (existsSync(imgDir)) {
|
|
136
|
+
for (const name of readdirSync(imgDir)) {
|
|
137
|
+
copyFileSync(join(imgDir, name), join(staticImg, name));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const attr = join(dir, 'attribution.md');
|
|
142
|
+
if (existsSync(attr)) {
|
|
143
|
+
mkdirSync(join(siteRoot, '.filepress-import'), { recursive: true });
|
|
144
|
+
copyFileSync(attr, join(siteRoot, '.filepress-import', 'IMAGE_ATTRIBUTION.md'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const active: GenieActive = {
|
|
148
|
+
versionId,
|
|
149
|
+
activatedAt: new Date().toISOString()
|
|
150
|
+
};
|
|
151
|
+
writeJson(join(genieRoot(siteRoot), 'active.json'), active);
|
|
152
|
+
return active;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function deleteVersion(siteRoot: string, versionId: string) {
|
|
156
|
+
if (versionId === 'baseline') {
|
|
157
|
+
throw new Error('Cannot delete the baseline version');
|
|
158
|
+
}
|
|
159
|
+
const active = getActive(siteRoot);
|
|
160
|
+
if (active?.versionId === versionId) {
|
|
161
|
+
throw new Error('Cannot delete the active version — activate another first');
|
|
162
|
+
}
|
|
163
|
+
const dir = versionPath(siteRoot, versionId);
|
|
164
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function ensureBaseline(
|
|
168
|
+
siteRoot: string,
|
|
169
|
+
opts: { brief: DesignBrief; themeCss: string }
|
|
170
|
+
): GenieVersionMeta {
|
|
171
|
+
ensureGenieDirs(siteRoot);
|
|
172
|
+
const baselineDir = versionPath(siteRoot, 'baseline');
|
|
173
|
+
if (existsSync(join(baselineDir, 'meta.json'))) {
|
|
174
|
+
return readJson<GenieVersionMeta>(join(baselineDir, 'meta.json'), {
|
|
175
|
+
id: 'baseline',
|
|
176
|
+
createdAt: new Date().toISOString(),
|
|
177
|
+
parentId: null,
|
|
178
|
+
label: 'baseline',
|
|
179
|
+
starred: true,
|
|
180
|
+
prompt: '',
|
|
181
|
+
steers: [],
|
|
182
|
+
inspireUrls: [],
|
|
183
|
+
llm: { used: false, model: null, host: null }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Capture current static chrome images if present
|
|
188
|
+
const imageFiles: SnapshotInput['imageFiles'] = [];
|
|
189
|
+
const staticImg = join(siteRoot, 'static', 'images');
|
|
190
|
+
if (existsSync(staticImg)) {
|
|
191
|
+
for (const name of readdirSync(staticImg)) {
|
|
192
|
+
if (!/\.(jpe?g|png|webp|gif|svg)$/i.test(name)) continue;
|
|
193
|
+
imageFiles.push({ absPath: join(staticImg, name), destName: name });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const meta = writeSnapshot(siteRoot, {
|
|
198
|
+
id: 'baseline',
|
|
199
|
+
label: 'baseline',
|
|
200
|
+
prompt: 'Snapshot of working tree before Genie edits',
|
|
201
|
+
parentId: null,
|
|
202
|
+
brief: opts.brief,
|
|
203
|
+
themeCss: opts.themeCss,
|
|
204
|
+
imageFiles,
|
|
205
|
+
steers: [{ type: 'baseline' }]
|
|
206
|
+
});
|
|
207
|
+
meta.starred = true;
|
|
208
|
+
writeJson(join(baselineDir, 'meta.json'), meta);
|
|
209
|
+
|
|
210
|
+
if (!getActive(siteRoot)) {
|
|
211
|
+
writeJson(join(genieRoot(siteRoot), 'active.json'), {
|
|
212
|
+
versionId: 'baseline',
|
|
213
|
+
activatedAt: new Date().toISOString()
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return meta;
|
|
217
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Mirrors @filepress/import DesignBrief — kept local so the panel never pulls Node import code. */
|
|
2
|
+
export type DesignBrief = {
|
|
3
|
+
mood: string;
|
|
4
|
+
do: string[];
|
|
5
|
+
dont: string[];
|
|
6
|
+
tokens: {
|
|
7
|
+
accent: string;
|
|
8
|
+
accentStrong: string;
|
|
9
|
+
bg?: string;
|
|
10
|
+
ink?: string;
|
|
11
|
+
inkSoft?: string;
|
|
12
|
+
surface?: string;
|
|
13
|
+
rule?: string;
|
|
14
|
+
ruleStrong?: string;
|
|
15
|
+
accentWash?: string;
|
|
16
|
+
};
|
|
17
|
+
density: 'sparse' | 'balanced' | 'dense';
|
|
18
|
+
paletteMode?: 'dark' | 'light';
|
|
19
|
+
fonts?: {
|
|
20
|
+
serif: string;
|
|
21
|
+
sans: string;
|
|
22
|
+
mono: string;
|
|
23
|
+
googleHref: string | null;
|
|
24
|
+
};
|
|
25
|
+
hero?: 'editorial' | 'bold';
|
|
26
|
+
atmosphere?: 'none' | 'noise';
|
|
27
|
+
navStyle?: 'soft' | 'uppercase-tracked';
|
|
28
|
+
elevatedCards?: boolean;
|
|
29
|
+
images?: {
|
|
30
|
+
hero?: string | null;
|
|
31
|
+
header?: string | null;
|
|
32
|
+
background?: string | null;
|
|
33
|
+
logo?: string | null;
|
|
34
|
+
portrait?: string | null;
|
|
35
|
+
};
|
|
36
|
+
cssNotes: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type GenieVersionMeta = {
|
|
40
|
+
id: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
parentId: string | null;
|
|
43
|
+
label: string;
|
|
44
|
+
starred: boolean;
|
|
45
|
+
prompt: string;
|
|
46
|
+
steers: Array<Record<string, unknown>>;
|
|
47
|
+
inspireUrls: string[];
|
|
48
|
+
llm: { used: boolean; model: string | null; host: string | null };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type GenieActive = {
|
|
52
|
+
versionId: string;
|
|
53
|
+
activatedAt: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type GenieSteerPatch = {
|
|
57
|
+
label?: string;
|
|
58
|
+
prompt?: string;
|
|
59
|
+
brief?: Partial<DesignBrief>;
|
|
60
|
+
activate?: boolean;
|
|
61
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const appRoot = dirname(fileURLToPath(new URL('../..', import.meta.url)));
|
|
6
|
+
const defaultSiteRoot = resolve(appRoot, '../../sites/demo');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the active site root from FILEPRESS_SITE_ROOT (set by scripts/filepress.mjs).
|
|
10
|
+
* Falls back to sites/demo so `svelte-kit sync` / prepare can run without a site flag.
|
|
11
|
+
*/
|
|
12
|
+
export function getSiteRoot(): string {
|
|
13
|
+
const raw = process.env.FILEPRESS_SITE_ROOT?.trim();
|
|
14
|
+
const root = raw
|
|
15
|
+
? isAbsolute(raw)
|
|
16
|
+
? raw
|
|
17
|
+
: resolve(process.cwd(), raw)
|
|
18
|
+
: defaultSiteRoot;
|
|
19
|
+
if (!existsSync(root)) {
|
|
20
|
+
throw new Error(`Site root does not exist: ${root}`);
|
|
21
|
+
}
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Absolute path to the site's posts/ directory (or FILEPRESS_CONTENT_DIR override). */
|
|
26
|
+
export function getContentDir(): string {
|
|
27
|
+
const override = process.env.FILEPRESS_CONTENT_DIR?.trim();
|
|
28
|
+
if (override) {
|
|
29
|
+
return isAbsolute(override) ? override : resolve(getSiteRoot(), override);
|
|
30
|
+
}
|
|
31
|
+
return join(getSiteRoot(), 'posts');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Absolute path to the site's pages/ directory (static Markdown pages). */
|
|
35
|
+
export function getPagesDir(): string {
|
|
36
|
+
return join(getSiteRoot(), 'pages');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Absolute path to filepress.config.ts at the site root. */
|
|
40
|
+
export function getSiteConfigPath(): string {
|
|
41
|
+
return join(getSiteRoot(), 'filepress.config.ts');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Absolute path to the site's static/ assets (may not exist yet). */
|
|
45
|
+
export function getSiteStaticDir(): string {
|
|
46
|
+
return join(getSiteRoot(), 'static');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Absolute path where adapter-static writes this site's build. */
|
|
50
|
+
export function getSiteBuildDir(): string {
|
|
51
|
+
return join(getSiteRoot(), 'build');
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '$lib/theme-entry';
|
|
3
|
+
import criticalTheme from '$critical-theme';
|
|
4
|
+
import { SiteHeader, SiteFooter } from '@filepress/core';
|
|
5
|
+
import config from '$site-config';
|
|
6
|
+
import GenieHost from '$lib/genie/GenieHost.svelte';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
let { children } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<svelte:head>
|
|
13
|
+
{#if criticalTheme}
|
|
14
|
+
{@html `<style data-filepress-critical>${criticalTheme}</style>`}
|
|
15
|
+
{/if}
|
|
16
|
+
<link rel="icon" href="/favicon.svg" />
|
|
17
|
+
<link rel="alternate" type="application/rss+xml" title={config.title} href="/rss.xml" />
|
|
18
|
+
</svelte:head>
|
|
19
|
+
|
|
20
|
+
<SiteHeader site={config} />
|
|
21
|
+
|
|
22
|
+
<main class="wrap">
|
|
23
|
+
{@render children()}
|
|
24
|
+
</main>
|
|
25
|
+
|
|
26
|
+
<SiteFooter site={config} />
|
|
27
|
+
|
|
28
|
+
<!-- Dev-only Genie Mode (tree-shaken when import.meta.env.DEV is false). -->
|
|
29
|
+
<GenieHost />
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dev } from '$app/environment';
|
|
2
|
+
|
|
3
|
+
// Fully static site: prerender every route at build time (D2, adapter-static).
|
|
4
|
+
export const prerender = true;
|
|
5
|
+
export const trailingSlash = 'never';
|
|
6
|
+
|
|
7
|
+
// Production ships plain HTML (no hydration). That avoids intermittent SvelteKit
|
|
8
|
+
// "500 Internal Error" pages when CDN asset hashes race during deploys or when
|
|
9
|
+
// client navigation can't load __data.json. Keep CSR in `pnpm dev` for Genie.
|
|
10
|
+
export const csr = dev;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import type { PageServerLoad } from './$types';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import { pages } from '$lib/pages.server';
|
|
5
|
+
import config from '$site-config';
|
|
6
|
+
|
|
7
|
+
export const load: PageServerLoad = async () => {
|
|
8
|
+
if (config.homePage) {
|
|
9
|
+
const page = await pages.getRenderedPage(config.homePage);
|
|
10
|
+
if (!page) {
|
|
11
|
+
error(
|
|
12
|
+
500,
|
|
13
|
+
`homePage "${config.homePage}" not found — add pages/${config.homePage}.md`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
mode: 'page' as const,
|
|
18
|
+
page,
|
|
19
|
+
isDraft: page.draft
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const index = content.getIndexPage(1, config.postsPerPage);
|
|
24
|
+
return {
|
|
25
|
+
mode: 'posts' as const,
|
|
26
|
+
...index
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
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 isHomePage = $derived(data.mode === 'page');
|
|
9
|
+
const page = $derived(data.mode === 'page' ? data.page : null);
|
|
10
|
+
const canonical = $derived(absoluteUrl(config, '/'));
|
|
11
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
12
|
+
const description = $derived(
|
|
13
|
+
(page?.description || config.description || '').trim() || null
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<svelte:head>
|
|
18
|
+
<title>{config.title}</title>
|
|
19
|
+
{#if description}
|
|
20
|
+
<meta name="description" content={description} />
|
|
21
|
+
{/if}
|
|
22
|
+
{#if isHomePage}
|
|
23
|
+
<link rel="canonical" href={canonical} />
|
|
24
|
+
<meta property="og:type" content="website" />
|
|
25
|
+
<meta property="og:title" content={config.title} />
|
|
26
|
+
{#if description}
|
|
27
|
+
<meta property="og:description" content={description} />
|
|
28
|
+
{/if}
|
|
29
|
+
<meta property="og:url" content={canonical} />
|
|
30
|
+
{#if ogImage}
|
|
31
|
+
<meta property="og:image" content={ogImage} />
|
|
32
|
+
<meta name="twitter:card" content="summary" />
|
|
33
|
+
<meta name="twitter:image" content={ogImage} />
|
|
34
|
+
{/if}
|
|
35
|
+
{#if data.mode === 'page' && data.isDraft}
|
|
36
|
+
<meta name="robots" content="noindex" />
|
|
37
|
+
{/if}
|
|
38
|
+
{/if}
|
|
39
|
+
</svelte:head>
|
|
40
|
+
|
|
41
|
+
{#if data.mode === 'page' && page}
|
|
42
|
+
<article class="static-page home-page">
|
|
43
|
+
<header class="page-header">
|
|
44
|
+
{#if data.isDraft}
|
|
45
|
+
<p class="draft-banner">
|
|
46
|
+
Draft — shown in local listings; excluded from production sitemap.
|
|
47
|
+
</p>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if config.lede}
|
|
50
|
+
<p class="hero-lede">{config.lede}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
<h1>{page.title}</h1>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<div class="prose">{@html page.html}</div>
|
|
56
|
+
</article>
|
|
57
|
+
{:else if data.mode === 'posts'}
|
|
58
|
+
<PostIndex
|
|
59
|
+
site={config}
|
|
60
|
+
hero
|
|
61
|
+
featured={data.featured}
|
|
62
|
+
posts={data.posts}
|
|
63
|
+
page={data.page}
|
|
64
|
+
totalPages={data.totalPages}
|
|
65
|
+
indexHref={postsIndexPath(config)}
|
|
66
|
+
/>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import type { EntryGenerator, PageServerLoad } from './$types';
|
|
3
|
+
import { pages } from '$lib/pages.server';
|
|
4
|
+
|
|
5
|
+
export const prerender = true;
|
|
6
|
+
|
|
7
|
+
export const entries: EntryGenerator = () => {
|
|
8
|
+
return pages.getBuildableSlugs().map((slug) => ({ slug }));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
12
|
+
const page = await pages.getRenderedPage(params.slug);
|
|
13
|
+
if (!page) error(404, `Page not found: ${params.slug}`);
|
|
14
|
+
|
|
15
|
+
const isDraft = page.draft;
|
|
16
|
+
return {
|
|
17
|
+
page,
|
|
18
|
+
isDraft
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { absoluteUrl, ogImageUrl } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
|
|
8
|
+
const page = $derived(data.page);
|
|
9
|
+
const pageTitle = $derived(`${page.title} — ${config.title}`);
|
|
10
|
+
const canonical = $derived(absoluteUrl(config, `/${page.slug}`));
|
|
11
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<svelte:head>
|
|
15
|
+
<title>{pageTitle}</title>
|
|
16
|
+
{#if page.description}
|
|
17
|
+
<meta name="description" content={page.description} />
|
|
18
|
+
{/if}
|
|
19
|
+
<link rel="canonical" href={canonical} />
|
|
20
|
+
<meta property="og:type" content="website" />
|
|
21
|
+
<meta property="og:title" content={page.title} />
|
|
22
|
+
{#if page.description}
|
|
23
|
+
<meta property="og:description" content={page.description} />
|
|
24
|
+
{/if}
|
|
25
|
+
<meta property="og:url" content={canonical} />
|
|
26
|
+
{#if ogImage}
|
|
27
|
+
<meta property="og:image" content={ogImage} />
|
|
28
|
+
<meta name="twitter:card" content="summary" />
|
|
29
|
+
<meta name="twitter:image" content={ogImage} />
|
|
30
|
+
{/if}
|
|
31
|
+
{#if data.isDraft}
|
|
32
|
+
<meta name="robots" content="noindex" />
|
|
33
|
+
{/if}
|
|
34
|
+
</svelte:head>
|
|
35
|
+
|
|
36
|
+
<article class="static-page">
|
|
37
|
+
<header class="page-header">
|
|
38
|
+
{#if data.isDraft}
|
|
39
|
+
<p class="draft-banner">
|
|
40
|
+
Draft — shown in local listings; excluded from production sitemap.
|
|
41
|
+
</p>
|
|
42
|
+
{/if}
|
|
43
|
+
<h1>{page.title}</h1>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<div class="prose">{@html page.html}</div>
|
|
47
|
+
</article>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EntryGenerator, PageServerLoad } from './$types';
|
|
2
|
+
import { error } from '@sveltejs/kit';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
// Page 1 lives at "/"; this route covers pages 2..N only.
|
|
7
|
+
export const entries: EntryGenerator = () => {
|
|
8
|
+
const count = content.getIndexPageCount(config.postsPerPage);
|
|
9
|
+
const pages: { n: string }[] = [];
|
|
10
|
+
for (let n = 2; n <= count; n++) pages.push({ n: String(n) });
|
|
11
|
+
return pages;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const load: PageServerLoad = ({ params }) => {
|
|
15
|
+
const n = Number(params.n);
|
|
16
|
+
const totalPages = content.getIndexPageCount(config.postsPerPage);
|
|
17
|
+
if (!Number.isInteger(n) || n < 2 || n > totalPages) {
|
|
18
|
+
error(404, `No such page "${params.n}".`);
|
|
19
|
+
}
|
|
20
|
+
return content.getIndexPage(n, config.postsPerPage);
|
|
21
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
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 — page ${data.page} — ${config.title}`);
|
|
9
|
+
const canonical = $derived(absoluteUrl(config, `/page/${data.page}`));
|
|
10
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
11
|
+
const description = $derived(`More posts from ${config.title}.`);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<svelte:head>
|
|
15
|
+
<title>{pageTitle}</title>
|
|
16
|
+
<meta name="description" content={description} />
|
|
17
|
+
<link rel="canonical" href={canonical} />
|
|
18
|
+
<meta property="og:type" content="website" />
|
|
19
|
+
<meta property="og:title" content={pageTitle} />
|
|
20
|
+
<meta property="og:description" content={description} />
|
|
21
|
+
<meta property="og:url" content={canonical} />
|
|
22
|
+
{#if ogImage}
|
|
23
|
+
<meta property="og:image" content={ogImage} />
|
|
24
|
+
<meta name="twitter:card" content="summary" />
|
|
25
|
+
<meta name="twitter:image" content={ogImage} />
|
|
26
|
+
{/if}
|
|
27
|
+
</svelte:head>
|
|
28
|
+
|
|
29
|
+
<PostIndex
|
|
30
|
+
site={config}
|
|
31
|
+
featured={data.featured}
|
|
32
|
+
posts={data.posts}
|
|
33
|
+
page={data.page}
|
|
34
|
+
totalPages={data.totalPages}
|
|
35
|
+
indexHref={postsIndexPath(config)}
|
|
36
|
+
/>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntryGenerator, PageServerLoad } from './$types';
|
|
2
|
+
import { error } from '@sveltejs/kit';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
|
|
5
|
+
// Build a page for every published post plus every draft (drafts are unlinked
|
|
6
|
+
// but reachable by direct URL for preview, D7). Future-dated posts are excluded.
|
|
7
|
+
export const entries: EntryGenerator = () => {
|
|
8
|
+
return content.getBuildableSlugs().map((slug) => ({ slug }));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
12
|
+
const post = await content.getRenderedPost(params.slug);
|
|
13
|
+
if (!post) {
|
|
14
|
+
error(404, `No post found for "${params.slug}".`);
|
|
15
|
+
}
|
|
16
|
+
const isDraft = content.loadPostSources().some((p) => p.slug === params.slug && p.draft);
|
|
17
|
+
const adjacent = content.getAdjacentPosts(params.slug);
|
|
18
|
+
// Tag pages come from listed posts. In production that means published only,
|
|
19
|
+
// so a draft can be the only carrier of a tag. Those render unlinked;
|
|
20
|
+
// linking them would 404 and fail prerendering.
|
|
21
|
+
const paged = new Set(content.getAllTags().map((t) => t.tag));
|
|
22
|
+
const tags = post.tags.map((tag) => ({ tag, hasPage: paged.has(tag) }));
|
|
23
|
+
return { post, isDraft, adjacent, tags };
|
|
24
|
+
};
|