meowpress 0.3.0-alpha.2 → 0.3.0-alpha.4
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 +71 -29
- package/dist/src/build.d.ts +21 -15
- package/dist/src/build.js +64 -42
- package/dist/src/cli.js +105 -14
- package/dist/src/content.d.ts +9 -1
- package/dist/src/content.js +86 -22
- package/dist/src/fonts.js +1 -1
- package/dist/src/identity.d.ts +6 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/output.d.ts +7 -0
- package/dist/src/output.js +111 -0
- package/dist/src/preview.d.ts +1 -0
- package/dist/src/preview.js +61 -0
- package/dist/src/site.d.ts +3 -2
- package/dist/src/site.js +154 -281
- package/dist/src/skills.d.ts +1 -0
- package/dist/src/skills.js +13 -7
- package/dist/src/theme.d.ts +12 -43
- package/dist/src/theme.js +301 -2046
- package/package.json +10 -5
package/dist/src/content.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compile, run } from "@mdx-js/mdx";
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
4
|
-
import { relative, resolve, sep } from "node:path";
|
|
3
|
+
import { lstat, readdir, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parse as parseYaml } from "yaml";
|
|
6
6
|
import { Fragment, h } from "meowjsx/runtime";
|
|
7
7
|
import { renderToHtml } from "meowjsx/web";
|
|
@@ -42,16 +42,24 @@ function parseDate(value) {
|
|
|
42
42
|
function makeDateKey(value, date) {
|
|
43
43
|
const text = asText(value);
|
|
44
44
|
const explicitDate = text?.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
45
|
-
if (explicitDate)
|
|
46
|
-
|
|
45
|
+
if (explicitDate) {
|
|
46
|
+
const parsed = new Date(`${explicitDate}T00:00:00Z`);
|
|
47
|
+
if (!Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(explicitDate)) {
|
|
48
|
+
return explicitDate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
47
51
|
if (!date)
|
|
48
52
|
return "undated";
|
|
49
|
-
|
|
53
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
50
54
|
timeZone: "Asia/Shanghai",
|
|
51
55
|
year: "numeric",
|
|
52
56
|
month: "2-digit",
|
|
53
57
|
day: "2-digit",
|
|
54
|
-
}).
|
|
58
|
+
}).formatToParts(date);
|
|
59
|
+
const year = parts.find((part) => part.type === "year").value;
|
|
60
|
+
const month = parts.find((part) => part.type === "month").value;
|
|
61
|
+
const day = parts.find((part) => part.type === "day").value;
|
|
62
|
+
return `${year.padStart(4, "0")}-${month}-${day}`;
|
|
55
63
|
}
|
|
56
64
|
function makeTimeLabel(value, date) {
|
|
57
65
|
const text = asText(value);
|
|
@@ -126,14 +134,14 @@ async function renderMdx(source, components) {
|
|
|
126
134
|
return renderToHtml(component({ components: { ...MDX_COMPONENTS, ...components } }));
|
|
127
135
|
}
|
|
128
136
|
async function renderSource(source, sourcePath, components) {
|
|
129
|
-
if (sourcePath.endsWith(".md")) {
|
|
137
|
+
if (sourcePath.toLowerCase().endsWith(".md")) {
|
|
130
138
|
return Bun.markdown.html(source, {
|
|
131
139
|
tagFilter: true,
|
|
132
140
|
});
|
|
133
141
|
}
|
|
134
142
|
return renderMdx(source, components);
|
|
135
143
|
}
|
|
136
|
-
async function collectFiles(directory) {
|
|
144
|
+
async function collectFiles(directory, assetRoots, skipNestedAssets) {
|
|
137
145
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
138
146
|
const files = [];
|
|
139
147
|
for (const entry of entries) {
|
|
@@ -141,10 +149,13 @@ async function collectFiles(directory) {
|
|
|
141
149
|
continue;
|
|
142
150
|
const entryPath = resolve(directory, entry.name);
|
|
143
151
|
if (entry.isDirectory()) {
|
|
144
|
-
|
|
152
|
+
if (assetRoots.has(entryPath) || (skipNestedAssets && entry.name === "assets"))
|
|
153
|
+
continue;
|
|
154
|
+
files.push(...(await collectFiles(entryPath, assetRoots, skipNestedAssets)));
|
|
145
155
|
continue;
|
|
146
156
|
}
|
|
147
|
-
|
|
157
|
+
const extension = entry.name.slice(entry.name.lastIndexOf(".")).toLowerCase();
|
|
158
|
+
if (entry.isFile() && CONTENT_EXTENSIONS.has(extension)) {
|
|
148
159
|
files.push(entryPath);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
@@ -164,39 +175,90 @@ function makeSlug(sourcePath, sectionDirectory) {
|
|
|
164
175
|
.join("/");
|
|
165
176
|
}
|
|
166
177
|
function normalizeSlugPrefix(prefix) {
|
|
167
|
-
|
|
178
|
+
const normalized = (prefix ?? "")
|
|
168
179
|
.replaceAll("\\", "/")
|
|
169
180
|
.replace(/^\/+|\/+$/g, "")
|
|
170
181
|
.replaceAll(/\/+/g, "/");
|
|
182
|
+
if (normalized.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
183
|
+
throw new Error(`Content slug prefix must not contain "." or "..": ${prefix}`);
|
|
184
|
+
}
|
|
185
|
+
return normalized;
|
|
171
186
|
}
|
|
172
187
|
function withSlugPrefix(prefix, slug) {
|
|
173
188
|
const normalizedPrefix = normalizeSlugPrefix(prefix);
|
|
174
189
|
return normalizedPrefix ? `${normalizedPrefix}/${slug}` : slug;
|
|
175
190
|
}
|
|
176
|
-
export function normalizeContentSources(input) {
|
|
191
|
+
export function normalizeContentSources(input, baseDirectory = process.cwd()) {
|
|
177
192
|
const sources = Array.isArray(input) ? input : [input];
|
|
178
193
|
return sources.map((source) => {
|
|
179
194
|
const normalized = typeof source === "string" ? { directory: source } : source;
|
|
195
|
+
const directory = resolve(baseDirectory, normalized.directory);
|
|
196
|
+
for (const mapping of [
|
|
197
|
+
normalized.postsDirectory,
|
|
198
|
+
normalized.thoughtsDirectory,
|
|
199
|
+
normalized.assetsDirectory,
|
|
200
|
+
normalized.postAssetsDirectory,
|
|
201
|
+
]) {
|
|
202
|
+
if (mapping !== undefined)
|
|
203
|
+
sourceDirectory(directory, mapping);
|
|
204
|
+
}
|
|
180
205
|
return {
|
|
181
|
-
|
|
206
|
+
...normalized,
|
|
207
|
+
directory,
|
|
182
208
|
slugPrefix: normalizeSlugPrefix(normalized.slugPrefix) || undefined,
|
|
183
209
|
};
|
|
184
210
|
});
|
|
185
211
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
212
|
+
function sourceDirectory(root, path) {
|
|
213
|
+
const target = resolve(root, path);
|
|
214
|
+
const local = relative(root, target);
|
|
215
|
+
if (isAbsolute(path) || local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local)) {
|
|
216
|
+
throw new Error(`Content directory must stay inside its source root: ${path}`);
|
|
217
|
+
}
|
|
218
|
+
return target;
|
|
188
219
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let sourceFiles;
|
|
220
|
+
/** Check every segment below a source root before walking a configured directory. */
|
|
221
|
+
async function isSourceDirectory(root, target) {
|
|
192
222
|
try {
|
|
193
|
-
|
|
223
|
+
let path = root;
|
|
224
|
+
if (!(await lstat(path)).isDirectory())
|
|
225
|
+
return false;
|
|
226
|
+
for (const segment of relative(root, target).split(sep).filter(Boolean)) {
|
|
227
|
+
path = resolve(path, segment);
|
|
228
|
+
if (!(await lstat(path)).isDirectory())
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
194
232
|
}
|
|
195
233
|
catch (error) {
|
|
196
234
|
if (error.code === "ENOENT")
|
|
197
|
-
return
|
|
235
|
+
return false;
|
|
198
236
|
throw error;
|
|
199
237
|
}
|
|
238
|
+
}
|
|
239
|
+
function assetDirectory(source, directory) {
|
|
240
|
+
const mapping = directory === "assets"
|
|
241
|
+
? source.assetsDirectory
|
|
242
|
+
: directory === "posts/assets"
|
|
243
|
+
? source.postAssetsDirectory
|
|
244
|
+
: undefined;
|
|
245
|
+
return sourceDirectory(source.directory, mapping ?? directory);
|
|
246
|
+
}
|
|
247
|
+
function duplicateContentError(kind, slug) {
|
|
248
|
+
return new Error(`Duplicate ${kind} slug "${slug}" while merging content sources.`);
|
|
249
|
+
}
|
|
250
|
+
async function loadSectionFromSource(source, kind, options) {
|
|
251
|
+
const mapping = kind === "post" ? source.postsDirectory : source.thoughtsDirectory;
|
|
252
|
+
const sectionDirectory = sourceDirectory(source.directory, mapping ?? `${kind}s`);
|
|
253
|
+
if (!(await isSourceDirectory(source.directory, sectionDirectory)))
|
|
254
|
+
return [];
|
|
255
|
+
const assetRoots = new Set([
|
|
256
|
+
assetDirectory(source, "assets"),
|
|
257
|
+
assetDirectory(source, "posts/assets"),
|
|
258
|
+
]);
|
|
259
|
+
if (assetRoots.has(sectionDirectory))
|
|
260
|
+
return [];
|
|
261
|
+
const sourceFiles = await collectFiles(sectionDirectory, assetRoots, mapping !== undefined);
|
|
200
262
|
const items = [];
|
|
201
263
|
for (const sourcePath of sourceFiles) {
|
|
202
264
|
if (isDraft(sourcePath, sectionDirectory, kind))
|
|
@@ -278,7 +340,7 @@ async function collectFilesForAssets(directory) {
|
|
|
278
340
|
if (entry.isDirectory()) {
|
|
279
341
|
files.push(...(await collectFilesForAssets(entryPath)));
|
|
280
342
|
}
|
|
281
|
-
else {
|
|
343
|
+
else if (entry.isFile()) {
|
|
282
344
|
files.push(entryPath);
|
|
283
345
|
}
|
|
284
346
|
}
|
|
@@ -289,7 +351,9 @@ export async function collectContentAssets(sources, options = {}) {
|
|
|
289
351
|
const duplicatePolicy = options.duplicate ?? "first-wins";
|
|
290
352
|
const assets = new Map();
|
|
291
353
|
for (const source of normalizeContentSources(sources)) {
|
|
292
|
-
const assetRoot =
|
|
354
|
+
const assetRoot = assetDirectory(source, directory);
|
|
355
|
+
if (!(await isSourceDirectory(source.directory, assetRoot)))
|
|
356
|
+
continue;
|
|
293
357
|
const sourceFiles = await collectAssetFiles(assetRoot);
|
|
294
358
|
for (const sourcePath of sourceFiles) {
|
|
295
359
|
const localPath = relative(assetRoot, sourcePath).split(sep).join("/");
|
package/dist/src/fonts.js
CHANGED
|
@@ -7,7 +7,7 @@ export const FONT_DIRECTORY = "fonts";
|
|
|
7
7
|
export const FONT_EXTERNAL = `./${FONT_DIRECTORY}/*`;
|
|
8
8
|
/**
|
|
9
9
|
* Resolve a package file from the site first (where a host installs its theme's fonts), then from
|
|
10
|
-
* this package (which carries the default
|
|
10
|
+
* this package (which carries the default Journal fonts).
|
|
11
11
|
*/
|
|
12
12
|
function resolveFontFile(specifier) {
|
|
13
13
|
for (const from of [process.cwd(), dirname(fileURLToPath(import.meta.url))]) {
|
package/dist/src/identity.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface SiteAbout {
|
|
|
14
14
|
linksHeading?: string;
|
|
15
15
|
links?: readonly SiteProfileLink[];
|
|
16
16
|
}
|
|
17
|
+
/** Optional copy for the built-in 404 page. */
|
|
18
|
+
export interface SiteNotFound {
|
|
19
|
+
title?: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
homeLabel?: string;
|
|
22
|
+
}
|
|
17
23
|
/** Site-owned identity and copy. The application supplies it; the library keeps neutral defaults. */
|
|
18
24
|
export interface SiteIdentity {
|
|
19
25
|
/** Display name used in header, footer and document titles. */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, nor
|
|
|
2
2
|
export { buildSite, meowSite, type ResolvedSiteConfig, type SiteConfig } from "./build.js";
|
|
3
3
|
export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
|
|
4
4
|
export { DEFAULT_SITE, type SiteAbout, type SiteIdentity, type SiteProfileLink } from "./identity.js";
|
|
5
|
-
export { blogFonts, blogTheme
|
|
5
|
+
export { blogFonts, blogTheme } from "./theme.js";
|
|
6
6
|
export type { SiteFont } from "./fonts.js";
|
|
7
7
|
export { loadSkills, type LoadSkillsOptions, type Skill, type SkillFile, type SkillScope, } from "./skills.js";
|
package/dist/src/index.js
CHANGED
|
@@ -2,5 +2,5 @@ export { collectContentAssets, collectAssetFiles, isDraftAsset, loadSection, nor
|
|
|
2
2
|
export { buildSite, meowSite } from "./build.js";
|
|
3
3
|
export { aboutPage, agentMarkdown, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
|
|
4
4
|
export { DEFAULT_SITE } from "./identity.js";
|
|
5
|
-
export { blogFonts, blogTheme
|
|
5
|
+
export { blogFonts, blogTheme } from "./theme.js";
|
|
6
6
|
export { loadSkills, } from "./skills.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedSiteConfig } from "./build.js";
|
|
2
|
+
export declare function validateOutputDirectory(config: ResolvedSiteConfig): Promise<void>;
|
|
3
|
+
/** Both agent indexes have been emitted by Meowpress since before the conventional CLI. */
|
|
4
|
+
export declare function hasBuiltSite(directory: string): Promise<boolean>;
|
|
5
|
+
export declare function validateBuildDestination(directory: string): Promise<void>;
|
|
6
|
+
/** Move a complete build into place and preserve any previous build if that move fails. */
|
|
7
|
+
export declare function publishSite(staged: string, destination: string): Promise<void>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { lstat, mkdtemp, readdir, realpath, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { normalizeContentSources } from "./content.js";
|
|
4
|
+
function isMissing(error) {
|
|
5
|
+
return error.code === "ENOENT";
|
|
6
|
+
}
|
|
7
|
+
async function existingPath(path) {
|
|
8
|
+
try {
|
|
9
|
+
return await lstat(path);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!isMissing(error))
|
|
13
|
+
throw error;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Resolve symlinked ancestors even if the output directory has not been created yet. */
|
|
18
|
+
async function filesystemPath(path) {
|
|
19
|
+
try {
|
|
20
|
+
return await realpath(path);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!isMissing(error))
|
|
24
|
+
throw error;
|
|
25
|
+
return join(await filesystemPath(dirname(path)), basename(path));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function containsPath(directory, path) {
|
|
29
|
+
const fromDirectory = relative(directory, path);
|
|
30
|
+
return (!isAbsolute(fromDirectory) && fromDirectory !== ".." && !fromDirectory.startsWith(`..${sep}`));
|
|
31
|
+
}
|
|
32
|
+
export async function validateOutputDirectory(config) {
|
|
33
|
+
const project = process.cwd();
|
|
34
|
+
const sources = normalizeContentSources(config.content);
|
|
35
|
+
const sourceRoots = [
|
|
36
|
+
project,
|
|
37
|
+
config.skillsDirectory,
|
|
38
|
+
...sources.map((source) => source.directory),
|
|
39
|
+
];
|
|
40
|
+
if (config.projectRootDirectory)
|
|
41
|
+
sourceRoots.push(config.projectRootDirectory);
|
|
42
|
+
const inputs = sources.flatMap((source) => [
|
|
43
|
+
resolve(source.directory, source.postsDirectory ?? "posts"),
|
|
44
|
+
resolve(source.directory, source.thoughtsDirectory ?? "thoughts"),
|
|
45
|
+
resolve(source.directory, source.assetsDirectory ?? "assets"),
|
|
46
|
+
resolve(source.directory, source.postAssetsDirectory ?? "posts/assets"),
|
|
47
|
+
]);
|
|
48
|
+
inputs.push(join(config.skillsDirectory, "skills"), join(project, ".agents"), join(project, ".git"));
|
|
49
|
+
if (config.includeProjectSkills)
|
|
50
|
+
inputs.push(join(config.projectRootDirectory ?? config.skillsDirectory, ".agents/skills"));
|
|
51
|
+
const [output, roots, directories] = await Promise.all([
|
|
52
|
+
filesystemPath(config.outputDirectory),
|
|
53
|
+
Promise.all(sourceRoots.map(filesystemPath)),
|
|
54
|
+
Promise.all(inputs.map(filesystemPath)),
|
|
55
|
+
]);
|
|
56
|
+
if (roots.some((root) => containsPath(output, root)) ||
|
|
57
|
+
directories.some((input) => containsPath(output, input) || containsPath(input, output))) {
|
|
58
|
+
throw new Error(`Output directory overlaps project files: ${config.outputDirectory}`);
|
|
59
|
+
}
|
|
60
|
+
const info = await existingPath(config.outputDirectory);
|
|
61
|
+
if (info && !info.isDirectory())
|
|
62
|
+
throw new Error(`Site output must be a directory: ${config.outputDirectory}`);
|
|
63
|
+
}
|
|
64
|
+
/** Both agent indexes have been emitted by Meowpress since before the conventional CLI. */
|
|
65
|
+
export async function hasBuiltSite(directory) {
|
|
66
|
+
const markers = await Promise.all(["agents.md", "llms.txt"].map((name) => existingPath(join(directory, name))));
|
|
67
|
+
return markers.every((marker) => marker?.isFile());
|
|
68
|
+
}
|
|
69
|
+
export async function validateBuildDestination(directory) {
|
|
70
|
+
if (directory === resolve("dist") &&
|
|
71
|
+
(await existingPath(directory)) &&
|
|
72
|
+
(await readdir(directory)).length &&
|
|
73
|
+
!(await hasBuiltSite(directory))) {
|
|
74
|
+
throw new Error(`Refusing to replace existing files in ${directory}. Move them or choose another outputDirectory.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Move a complete build into place and preserve any previous build if that move fails. */
|
|
78
|
+
export async function publishSite(staged, destination) {
|
|
79
|
+
const existing = await existingPath(destination);
|
|
80
|
+
if (!existing) {
|
|
81
|
+
await rename(staged, destination);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!existing.isDirectory())
|
|
85
|
+
throw new Error(`Site output must be a directory: ${destination}`);
|
|
86
|
+
const backupDirectory = await mkdtemp(join(dirname(destination), ".meowpress-previous-"));
|
|
87
|
+
const backup = join(backupDirectory, basename(destination));
|
|
88
|
+
let restoreFailed = false;
|
|
89
|
+
try {
|
|
90
|
+
await rename(destination, backup);
|
|
91
|
+
try {
|
|
92
|
+
await rename(staged, destination);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
try {
|
|
96
|
+
await rename(backup, destination);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
restoreFailed = true;
|
|
100
|
+
throw new Error(`Could not replace ${destination}. Previous output remains in ${backup}.`, {
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (!restoreFailed)
|
|
109
|
+
await rm(backupDirectory, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serveSite(outputDirectory: string, port?: number): Promise<Bun.Server<undefined>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
async function statIfPresent(path) {
|
|
4
|
+
try {
|
|
5
|
+
return await stat(path);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
const code = error.code;
|
|
9
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
10
|
+
return undefined;
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function serveSite(outputDirectory, port = 4173) {
|
|
15
|
+
if (!(await statIfPresent(outputDirectory))?.isDirectory()) {
|
|
16
|
+
throw new Error(`Built site not found at ${outputDirectory}. Run meowpress build first.`);
|
|
17
|
+
}
|
|
18
|
+
const root = await realpath(outputDirectory);
|
|
19
|
+
const isInside = (path) => path === root || path.startsWith(root + sep);
|
|
20
|
+
async function fileResponse(path, method, status) {
|
|
21
|
+
if (!(await statIfPresent(path))?.isFile())
|
|
22
|
+
return undefined;
|
|
23
|
+
const target = await realpath(path);
|
|
24
|
+
if (!isInside(target))
|
|
25
|
+
return undefined;
|
|
26
|
+
const file = Bun.file(target);
|
|
27
|
+
return new Response(method === "HEAD" ? null : file, {
|
|
28
|
+
status,
|
|
29
|
+
headers: { "Content-Type": file.type, "Cache-Control": "no-store" },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return Bun.serve({
|
|
33
|
+
hostname: "127.0.0.1",
|
|
34
|
+
port,
|
|
35
|
+
async fetch(request) {
|
|
36
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
37
|
+
return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, HEAD" } });
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
let pathname;
|
|
40
|
+
try {
|
|
41
|
+
pathname = decodeURIComponent(url.pathname);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return new Response("Invalid path", { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
if (pathname.includes("\0"))
|
|
47
|
+
return new Response("Invalid path", { status: 400 });
|
|
48
|
+
const target = resolve(root, `.${pathname}`);
|
|
49
|
+
if (!isInside(target))
|
|
50
|
+
return new Response("Forbidden", { status: 403 });
|
|
51
|
+
if (!pathname.endsWith("/") && (await statIfPresent(target))?.isDirectory()) {
|
|
52
|
+
url.pathname += "/";
|
|
53
|
+
return Response.redirect(url, 301);
|
|
54
|
+
}
|
|
55
|
+
const path = pathname.endsWith("/") ? join(target, "index.html") : target;
|
|
56
|
+
return ((await fileResponse(path, request.method, 200)) ??
|
|
57
|
+
(await fileResponse(join(root, "404.html"), request.method, 404)) ??
|
|
58
|
+
new Response(request.method === "HEAD" ? null : "Page not found", { status: 404 }));
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/src/site.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { type ThemeDefinition } from "meowcss";
|
|
2
2
|
import { type ContentItem } from "./content.js";
|
|
3
|
-
import { type SiteIdentity } from "./identity.js";
|
|
3
|
+
import { type SiteIdentity, type SiteNotFound } from "./identity.js";
|
|
4
4
|
import { type SitePage } from "./pages.js";
|
|
5
5
|
import type { Skill } from "./skills.js";
|
|
6
6
|
/** The name of the theme stylesheet the package writes by default. */
|
|
7
7
|
export declare const DEFAULT_THEME_STYLESHEET = "blog-theme.css";
|
|
8
8
|
export declare function homePage(items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
9
9
|
export declare function listPage(kind: "post" | "thought", items: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
10
|
+
export declare function notFoundPage(copy: SiteNotFound, theme: ThemeDefinition, site: SiteIdentity, stylesheet: string): string;
|
|
10
11
|
export declare function postPage(item: ContentItem, posts: ContentItem[], thoughts: ContentItem[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
11
12
|
export declare function thoughtPage(item: ContentItem, theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
12
13
|
export declare function aboutPage(theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
13
14
|
export declare function skillsIndexPage(skills: Skill[], theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
14
15
|
export declare function skillPage(skill: Skill, theme?: ThemeDefinition, site?: SiteIdentity, stylesheet?: string): string;
|
|
15
|
-
export declare function agentMarkdown(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[]): string;
|
|
16
|
+
export declare function agentMarkdown(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[], mappedSections?: boolean): string;
|
|
16
17
|
export declare function llmText(posts: ContentItem[], thoughts: ContentItem[], site?: SiteIdentity, pages?: readonly SitePage[], agentGuide?: string): string;
|
|
17
18
|
/**
|
|
18
19
|
* Every page's Markdown in one file, after the same summary and guide as llms.txt. Pages without
|