getfilepress 0.1.32 → 0.1.33
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/package.json +9 -1
- package/packages/app/src/lib/genie/store.ts +1 -1
- package/packages/core/src/lib/config.ts +3 -3
- package/packages/core/src/lib/paths-shared.ts +14 -3
- package/packages/core/src/lib/redirects.ts +5 -3
- package/packages/import/src/ollama.ts +6 -4
- package/packages/import/src/wordpress.ts +6 -12
- package/scripts/preview.mjs +19 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getfilepress",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "FilePress — file-based Markdown blog engine. Link this package from a content-only site (config + posts), then run `filepress build`.",
|
|
@@ -106,6 +106,14 @@
|
|
|
106
106
|
"node": ">=20"
|
|
107
107
|
},
|
|
108
108
|
"packageManager": "pnpm@10.30.1",
|
|
109
|
+
"pnpm": {
|
|
110
|
+
"overrides": {
|
|
111
|
+
"js-yaml": "3.15.2",
|
|
112
|
+
"nanoid": "3.3.18",
|
|
113
|
+
"postcss": "8.5.23",
|
|
114
|
+
"sharp": "0.35.4"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
109
117
|
"devDependencies": {
|
|
110
118
|
"wrangler": "^4.120.1"
|
|
111
119
|
}
|
|
@@ -33,7 +33,7 @@ function ensureGenieDirs(siteRoot: string) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function newVersionId(): string {
|
|
36
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
36
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
37
37
|
return `${ts}-${randomBytes(2).toString('hex')}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* keeps `@filepress/core` app-agnostic and importable from both client and
|
|
8
8
|
* server code.
|
|
9
9
|
*/
|
|
10
|
-
import { normalizePathMounts, type PathMount } from './paths-shared';
|
|
10
|
+
import { normalizePathMounts, stripTrailingSlashes, type PathMount } from './paths-shared';
|
|
11
11
|
import { writingPostRedirects, type RedirectRule } from './redirects';
|
|
12
12
|
export type { PathMount } from './paths-shared';
|
|
13
13
|
export type { RedirectRule } from './redirects';
|
|
@@ -228,7 +228,7 @@ export function defineFilepressConfig(input: SiteConfigInput): SiteConfig {
|
|
|
228
228
|
lede: (input.lede ?? '').trim() || null,
|
|
229
229
|
logo,
|
|
230
230
|
ogImage: (input.ogImage ?? '').trim() || logo,
|
|
231
|
-
url: url
|
|
231
|
+
url: stripTrailingSlashes(url),
|
|
232
232
|
author: (input.author ?? '').trim() || title,
|
|
233
233
|
postsPerPage:
|
|
234
234
|
Number.isFinite(input.postsPerPage) && (input.postsPerPage as number) > 0
|
|
@@ -279,7 +279,7 @@ function normalizeRedirects(rules: RedirectRule[] | undefined): RedirectRule[] {
|
|
|
279
279
|
|
|
280
280
|
/** Join the site origin with a path, guarding against double slashes. */
|
|
281
281
|
export function absoluteUrl(site: Pick<SiteConfig, 'url'>, path: string): string {
|
|
282
|
-
const base = site.url
|
|
282
|
+
const base = stripTrailingSlashes(site.url);
|
|
283
283
|
const suffix = path.startsWith('/') ? path : `/${path}`;
|
|
284
284
|
return `${base}${suffix}`;
|
|
285
285
|
}
|
|
@@ -12,6 +12,17 @@ export interface PathMount {
|
|
|
12
12
|
url: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function toPosixPath(path: string): string {
|
|
16
|
+
return path.split('\\').join('/');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Drop trailing `/` without a regular expression. */
|
|
20
|
+
export function stripTrailingSlashes(path: string): string {
|
|
21
|
+
let out = path;
|
|
22
|
+
while (out.length > 1 && out.endsWith('/')) out = out.slice(0, -1);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
/** Engine route prefixes a mount must not steal. */
|
|
16
27
|
const ENGINE_URL_PREFIXES = new Set([
|
|
17
28
|
...RESERVED_PAGE_SLUGS,
|
|
@@ -38,8 +49,8 @@ export function normalizePathMounts(input: PathMount[] | undefined): PathMount[]
|
|
|
38
49
|
if (!raw || typeof raw !== 'object') {
|
|
39
50
|
throw new Error('filepress.config: each `paths` entry must be an object { url, dir }.');
|
|
40
51
|
}
|
|
41
|
-
const dir = String(raw.dir ?? '').trim()
|
|
42
|
-
let url = String(raw.url ?? '').trim()
|
|
52
|
+
const dir = stripTrailingSlashes(toPosixPath(String(raw.dir ?? '').trim()));
|
|
53
|
+
let url = toPosixPath(String(raw.url ?? '').trim());
|
|
43
54
|
if (!dir) {
|
|
44
55
|
throw new Error('filepress.config: `paths[].dir` must be a non-empty site-relative path.');
|
|
45
56
|
}
|
|
@@ -51,7 +62,7 @@ export function normalizePathMounts(input: PathMount[] | undefined): PathMount[]
|
|
|
51
62
|
if (!url.startsWith('/')) {
|
|
52
63
|
throw new Error(`filepress.config: \`paths[].url\` must start with / (got "${raw.url}").`);
|
|
53
64
|
}
|
|
54
|
-
url = url
|
|
65
|
+
url = stripTrailingSlashes(url) || '/';
|
|
55
66
|
if (url === '/') {
|
|
56
67
|
throw new Error('filepress.config: `paths[].url` cannot be `/` (that is the site home).');
|
|
57
68
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Cloudflare Pages / Netlify `_redirects` lines.
|
|
3
3
|
* Code owns the file shape; sites and import only supply from/to pairs.
|
|
4
4
|
*/
|
|
5
|
+
import { stripTrailingSlashes } from './paths-shared';
|
|
5
6
|
export type RedirectStatus = 301 | 302 | 308;
|
|
6
7
|
|
|
7
8
|
export type RedirectRule = {
|
|
@@ -18,11 +19,11 @@ export function normalizeRedirectPath(path: string): string {
|
|
|
18
19
|
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
19
20
|
const url = new URL(trimmed);
|
|
20
21
|
const out = url.pathname || '/';
|
|
21
|
-
return out === '/' ? '/' : out
|
|
22
|
+
return out === '/' ? '/' : stripTrailingSlashes(out) || '/';
|
|
22
23
|
}
|
|
23
24
|
const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
24
25
|
if (withSlash.includes('*') || withSlash.includes(':splat')) return withSlash;
|
|
25
|
-
return withSlash === '/' ? '/' : withSlash
|
|
26
|
+
return withSlash === '/' ? '/' : stripTrailingSlashes(withSlash) || '/';
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export function serializeRedirects(rules: RedirectRule[]): string {
|
|
@@ -33,7 +34,8 @@ export function serializeRedirects(rules: RedirectRule[]): string {
|
|
|
33
34
|
export function parseRedirectsFile(text: string): RedirectRule[] {
|
|
34
35
|
const rules: RedirectRule[] = [];
|
|
35
36
|
for (const raw of text.split(/\r?\n/)) {
|
|
36
|
-
const
|
|
37
|
+
const hash = raw.indexOf('#');
|
|
38
|
+
const line = (hash === -1 ? raw : raw.slice(0, hash)).trim();
|
|
37
39
|
if (!line) continue;
|
|
38
40
|
const parts = line.split(/\s+/);
|
|
39
41
|
if (parts.length < 2) continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { parseHTML } from 'linkedom';
|
|
3
4
|
import type { DesignBrief, SiteIR } from './ir.ts';
|
|
4
5
|
import type { InspirationSignals } from './inspire.ts';
|
|
5
6
|
import { DEFAULT_BRIEF, parseBriefJson } from './theme.ts';
|
|
@@ -430,10 +431,11 @@ export async function generateDesignBrief(opts: {
|
|
|
430
431
|
|
|
431
432
|
/** Short text summary of an inspiration homepage for the brief prompt. */
|
|
432
433
|
export function summarizeHtmlForBrief(html: string, max = 1200): string {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
.
|
|
436
|
-
|
|
434
|
+
const { document } = parseHTML(html);
|
|
435
|
+
for (const node of document.querySelectorAll('script, style, noscript')) {
|
|
436
|
+
node.remove();
|
|
437
|
+
}
|
|
438
|
+
const text = (document.body?.textContent ?? document.documentElement?.textContent ?? '')
|
|
437
439
|
.replace(/\s+/g, ' ')
|
|
438
440
|
.trim();
|
|
439
441
|
return text.slice(0, max);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* WordPress REST overlay for filepress import.
|
|
3
3
|
* Sitemap + HTML still own the crawl; this fills categories → tags and nav.
|
|
4
4
|
*/
|
|
5
|
+
import { parseHTML } from 'linkedom';
|
|
5
6
|
import { fetchText, originOf } from './fetch.ts';
|
|
6
7
|
|
|
7
8
|
export type WpTerm = {
|
|
@@ -40,18 +41,11 @@ const SKIP_CATEGORY = new Set(['uncategorized']);
|
|
|
40
41
|
const SKIP_PAGE = new Set(['sample-page']);
|
|
41
42
|
|
|
42
43
|
export function decodeWpText(raw: string): string {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.replace(/</g, '<')
|
|
49
|
-
.replace(/>/g, '>')
|
|
50
|
-
.replace(/"/g, '"')
|
|
51
|
-
.replace(/'|'/g, "'")
|
|
52
|
-
.replace(/ /g, ' ')
|
|
53
|
-
.replace(/\s+/g, ' ')
|
|
54
|
-
.trim();
|
|
44
|
+
const { document } = parseHTML('<div id="fp-wp"></div>');
|
|
45
|
+
const el = document.getElementById('fp-wp');
|
|
46
|
+
if (!el) return raw.replace(/\s+/g, ' ').trim();
|
|
47
|
+
el.innerHTML = raw;
|
|
48
|
+
return (el.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
export function termsToTags(categories: WpTerm[], tags: WpTerm[], categoryIds: number[], tagIds: number[]): string[] {
|
package/scripts/preview.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createServer } from 'node:http';
|
|
8
8
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
9
|
-
import { extname, join, resolve, sep } from 'node:path';
|
|
9
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
|
|
12
12
|
const MIME = {
|
|
@@ -32,16 +32,28 @@ const MIME = {
|
|
|
32
32
|
const isMain =
|
|
33
33
|
Boolean(process.argv[1]) && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
|
|
35
|
+
function containedUnder(root, abs) {
|
|
36
|
+
const rel = relative(root, abs);
|
|
37
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/** Map a request path to a file under `root`, or null. */
|
|
40
41
|
export function resolveBuildFile(root, urlPath) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const base = resolve(root);
|
|
43
|
+
let pathname;
|
|
44
|
+
try {
|
|
45
|
+
pathname = decodeURIComponent((urlPath.split('?')[0] || '/'));
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const seg of pathname.split('/')) {
|
|
51
|
+
if (!seg || seg === '.') continue;
|
|
52
|
+
if (seg === '..') return null;
|
|
53
|
+
parts.push(seg);
|
|
54
|
+
}
|
|
55
|
+
const abs = parts.length ? join(base, ...parts) : base;
|
|
56
|
+
if (!containedUnder(base, abs)) return null;
|
|
45
57
|
try {
|
|
46
58
|
const st = statSync(abs);
|
|
47
59
|
if (st.isFile()) return abs;
|