edgepress 2026.9.3 → 2026.9.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 CHANGED
@@ -48,23 +48,23 @@ Posts under content/posts/ use the Markdown renderer. Themes own shared head, na
48
48
  - edgepress init: scaffold a new site in a project directory.
49
49
  - edgepress new "Article title": create a Markdown post.
50
50
  - edgepress build and edgepress generate: run the same generator and write static files to dist/ for Cloudflare Workers or static hosting.
51
- - edgepress server: start live local preview, rebuild on source changes, and refresh the audit report.
52
- - edgepress check: build, generate accessibility, agent-friendliness, and Markdown rendering findings, capture post/page desktop and mobile screenshots, and write the PDF report under tools/.
51
+ - edgepress server: start live local preview, rebuild on source changes, and refresh the PDF audit report.
52
+ - edgepress check: build, audit accessibility, agent-friendliness, and Markdown rendering, capture post/page desktop and mobile screenshots temporarily, and write only the PDF report under tools/.
53
53
  - edgepress doctor: inspect runtime and Worker compatibility.
54
54
  - edgepress theme list and edgepress theme use <name>: inspect or select an installed theme.
55
55
  - edgepress theme install <npm-package>[@version]: install a theme package from npm without running its install scripts.
56
56
  - edgepress theme create <name>: make an empty, accessible HTML theme skeleton with editable partials and a blank stylesheet.
57
57
  - edgepress secret put BACKEND_TOKEN: store the backend token in Wrangler.
58
58
  - edgepress deploy: build and deploy to Cloudflare Workers.
59
- - Connect a GitHub repository to Cloudflare Workers Builds to deploy on pushes to main. The build and deploy scripts are detected from package.json.
59
+ - Connect a GitHub repository to Cloudflare Workers Builds to deploy on pushes to main. Set root directory `/`, build command `npm run build`, deploy command `npx wrangler deploy`, and leave build variables empty. See the [Cloudflare build troubleshooting guide](content/pages/quick-start/) if initialization stalls before commands run.
60
60
  - edgepress clean: remove generated site and report files.
61
61
  - edgepress iterate: create a report-only maintenance plan.
62
62
  - edgepress security: check dependency advisories.
63
63
 
64
- The generated report is written to tools/page-check.md and tools/page-check.json. The accessible HTML source is tools/page-check-visual.html; the tagged PDF report is tools/page-check.pdf, with desktop and mobile post/page screenshots in tools/page-check-screenshots/. The mobile entries include their measured viewport and document widths. These automated checks do not replace manual accessibility or legal review.
64
+ The only persisted page-audit report is the accessible PDF at tools/page-check.pdf. Desktop and mobile screenshots are embedded in the PDF and removed from temporary storage afterward. Automated checks do not replace manual accessibility or legal review.
65
65
 
66
66
  The header search page filters Markdown posts from the generated local search index; it does not send search queries to an external service. The first post, “EdgePress and Markdown: a complete writing guide,” documents and verifies the supported Markdown syntax in English and Chinese.
67
67
 
68
68
  Project guides are published from content/pages/: project introduction, quick start, theme development, plugin development, and privacy policy. The privacy policy intentionally reports that the starter site still needs a real site URL, operator name, and privacy contact before publication.
69
69
 
70
- Version 2026.9.3 records the third requested release update for September 2026. It is available as the `edgepress` npm package. See the [request 1 notes](content/posts/2026-09-24-edgepress-release-2026-9-1/), [request 2 notes](content/posts/2026-09-25-edgepress-release-2026-9-2/), and [request 3 notes](content/posts/2026-09-25-edgepress-release-2026-9-3/).
70
+ Version 2026.9.4 records the fourth requested release update for September 2026. It is available as the `edgepress` npm package. See the [request 1 notes](content/posts/2026-09-24-edgepress-release-2026-9-1/), [request 2 notes](content/posts/2026-09-25-edgepress-release-2026-9-2/), [request 3 notes](content/posts/2026-09-25-edgepress-release-2026-9-3/), and [request 4 notes](content/posts/2026-09-25-edgepress-release-2026-9-4/).
@@ -10,7 +10,7 @@ blocks:
10
10
  - type: hero
11
11
  eyebrow: Start here
12
12
  title: Set up the project and open the live preview.
13
- text: Use the edgepress command after installing project dependencies. Local preview rebuilds changed source files and refreshes the accessibility and agent-friendliness report.
13
+ text: Use the edgepress command after installing project dependencies. Local preview rebuilds changed source files and refreshes the PDF accessibility and agent-friendliness report.
14
14
  - columns: 1
15
15
  cells:
16
16
  -
@@ -76,7 +76,7 @@ blocks:
76
76
  - Edit site title, description, canonical URL, agent SEO, site navigation, footer text, and privacy controller details in config.yml. Theme colors live in theme CSS.
77
77
  - Change the selected theme with edgepress theme list and edgepress theme use <theme-name>. Theme paths are stored in edgepress.config.mjs.
78
78
  - Install a theme package with edgepress theme install <npm-package> or start with a blank theme using edgepress theme create <name>.
79
- - edgepress server starts a local Wrangler server, watches source files, rebuilds changed pages, reloads the browser, and rewrites tools/page-check.md and tools/page-check.json.
79
+ - edgepress server starts a local Wrangler server, watches source files, rebuilds changed pages, reloads the browser, and refreshes tools/page-check.pdf. Screenshots are embedded in the PDF and temporary files are removed after generation.
80
80
  - type: code
81
81
  title: Build, inspect, and deploy
82
82
  language: sh
@@ -85,14 +85,15 @@ blocks:
85
85
  edgepress doctor
86
86
  edgepress deploy
87
87
  - type: text
88
- text: Compatibility checks read the Node.js baseline, Worker entry, and compatibility date from project-compatibility.json and wrangler.jsonc. Check the generated report before deploying.
88
+ text: Compatibility checks read the Node.js baseline, Worker entry, and compatibility date from project-compatibility.json and wrangler.jsonc. Review the PDF report before deploying.
89
89
  - type: section
90
90
  title: Publish the source on GitHub
91
91
  blocks:
92
92
  - type: text
93
93
  paragraphs:
94
94
  - Create an empty repository on GitHub, then push the EdgePress source. In the Cloudflare dashboard, create a Worker and connect that repository with Workers Builds. Choose main as the production branch. Set a unique Worker name in wrangler.jsonc before deployment.
95
- - The build and deploy scripts in package.json are detected by Workers Builds. It runs npm run build to generate dist/, then npm run deploy to publish the Worker. dist/ stays out of Git. Cloudflare manages the build authorization for this connection.
95
+ - For this repository, set Workers Builds root directory to /, build command to npm run build, and deploy command to npx wrangler deploy. No build variables are required. The build command generates dist/ before Wrangler publishes the Worker; dist/ stays out of Git.
96
+ - If the log remains at Initializing build environment before cloning the repository or running commands, verify that the Cloudflare Workers and Pages GitHub App still has access to this repository. Reconnect the repository or reinstall and reauthorize the app, then retry. Check that the selected Cloudflare API token is still valid; Workers Builds can create a token automatically, and stale tokens must be replaced in Build settings. This stage runs before the project build command.
96
97
  - type: code
97
98
  title: Upload the source repository
98
99
  language: sh
@@ -108,10 +109,12 @@ blocks:
108
109
  - type: link-list
109
110
  title: Deployment references
110
111
  items:
111
- - label: Connect GitHub to Cloudflare Workers Builds
112
+ - label: Connect or reauthorize the GitHub integration
112
113
  url: https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/
113
114
  - label: Configure Workers Builds commands
114
115
  url: https://developers.cloudflare.com/workers/ci-cd/builds/configuration/
116
+ - label: Troubleshoot Workers Builds
117
+ url: https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/
115
118
  - type: section
116
119
  title: Generate files for OpenResty or Nginx
117
120
  blocks:
@@ -10,7 +10,7 @@ blocks:
10
10
  - type: hero
11
11
  eyebrow: 从这里开始
12
12
  title: 配置项目并打开实时预览。
13
- text: 安装项目依赖后使用 edgepress 命令。本地预览会在源文件变更后自动构建,并刷新无障碍化和 agent 友好度报告。
13
+ text: 安装项目依赖后使用 edgepress 命令。本地预览会在源文件变更后自动构建,并刷新无障碍化和 agent 友好度 PDF 报告。
14
14
  - columns: 1
15
15
  cells:
16
16
  -
@@ -76,7 +76,7 @@ blocks:
76
76
  - 在 config.yml 中编辑站点标题、简介、规范 URL、agent SEO、导航、页脚文字和隐私运营者信息。主题配色保存在各自主题的 CSS 中。
77
77
  - 使用 edgepress theme list 查看主题,使用 edgepress theme use <主题名称> 切换主题。主题路径保存在 edgepress.config.mjs 中。
78
78
  - 使用 edgepress theme install <npm-package> 安装主题包,或使用 edgepress theme create <名称> 创建空白主题。
79
- - edgepress server 会启动本地 Wrangler 服务、监视源文件、重新构建变更页面、刷新浏览器,并更新 tools/page-check.md 和 tools/page-check.json。
79
+ - edgepress server 会启动本地 Wrangler 服务、监视源文件、重新构建变更页面、刷新浏览器,并更新 tools/page-check.pdf。截图会嵌入 PDF,生成后会清理临时文件。
80
80
  - type: code
81
81
  title: 构建、检查和部署
82
82
  language: sh
@@ -85,14 +85,15 @@ blocks:
85
85
  edgepress doctor
86
86
  edgepress deploy
87
87
  - type: text
88
- text: 兼容性检查会读取 project-compatibility.json 和 wrangler.jsonc 中的 Node.js 基线、Worker 入口和兼容日期。部署前请查看生成的报告。
88
+ text: 兼容性检查会读取 project-compatibility.json 和 wrangler.jsonc 中的 Node.js 基线、Worker 入口和兼容日期。部署前请查看 PDF 报告。
89
89
  - type: section
90
90
  title: 将源代码上传到 GitHub
91
91
  blocks:
92
92
  - type: text
93
93
  paragraphs:
94
94
  - 在 GitHub 创建空仓库并推送 EdgePress 源代码。然后在 Cloudflare 控制台创建 Worker,并通过 Workers Builds 连接该仓库,选择 main 作为生产分支。部署前请在 wrangler.jsonc 中设置唯一的 Worker 名称。
95
- - Workers Builds 会读取 package.json 中的 build 和 deploy 脚本,先运行 npm run build 生成 dist/,再运行 npm run deploy 发布 Worker。dist/ 不会提交到 Git;Cloudflare 会管理此连接的构建授权。
95
+ - 此仓库的 Workers Builds 设置为:根目录 /、构建命令 npm run build、部署命令 npx wrangler deploy。无需添加构建变量。构建命令会先生成 dist/,再由 Wrangler 发布 Worker;dist/ 不会提交到 Git。
96
+ - 如果日志在克隆仓库或运行命令前一直停留在 Initializing build environment,请检查 Cloudflare Workers and Pages GitHub App 是否仍有此仓库的访问权限。重新连接仓库,或重新安装并授权该 App 后再试。还需确认所选 Cloudflare API Token 仍有效;Workers Builds 可以自动创建 Token,如果已选 Token 失效,需要在 Build 设置中替换。此阶段尚未运行项目构建命令。
96
97
  - type: code
97
98
  title: 推送源代码仓库
98
99
  language: sh
@@ -108,10 +109,12 @@ blocks:
108
109
  - type: link-list
109
110
  title: 部署参考
110
111
  items:
111
- - label: 将 GitHub 连接到 Cloudflare Workers Builds
112
+ - label: 连接或重新授权 GitHub 集成
112
113
  url: https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/
113
114
  - label: 配置 Workers Builds 命令
114
115
  url: https://developers.cloudflare.com/workers/ci-cd/builds/configuration/
116
+ - label: 排查 Workers Builds
117
+ url: https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/
115
118
  - type: section
116
119
  title: 为 OpenResty 或 Nginx 生成静态文件
117
120
  blocks:
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: "EdgePress September 2026 update: request 4"
3
+ date: 2026-09-25
4
+ lang: en
5
+ slug: edgepress-release-2026-9-4
6
+ tags:
7
+ - release
8
+ - reports
9
+ - cloudflare
10
+ ---
11
+
12
+ This is the fourth requested EdgePress update for September 2026.
13
+
14
+ ## Keep the audit report in one PDF
15
+
16
+ `edgepress check` and `edgepress server` now persist only `tools/page-check.pdf`. Desktop and mobile screenshots are embedded in the PDF; temporary HTML and screenshot files are removed after generation.
17
+
18
+ ## Remove the templates directory
19
+
20
+ `edgepress init` now writes its generated README and `.gitignore` directly from the CLI package. The source and npm package no longer include a `templates/` directory.
21
+
22
+ ## Cloudflare Workers Builds setup
23
+
24
+ The quick-start guide records the repository settings: root directory `/`, build command `npm run build`, deploy command `npx wrangler deploy`, and no required build variables. It also explains that a build stuck at `Initializing build environment` has not run the project build command yet; reconnect the GitHub integration or replace a stale Cloudflare build API token before retrying.
25
+
26
+ Wrangler requires `compatibility_date` for this Worker, so the project pins it to the first day of the month: `2026-09-01`.
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: EdgePress 2026 年 9 月更新:第 4 次需求
3
+ date: 2026-09-25
4
+ lang: zh-CN
5
+ slug: edgepress-release-2026-9-4
6
+ tags:
7
+ - 发布
8
+ - 报告
9
+ - Cloudflare
10
+ ---
11
+
12
+ 这是 EdgePress 在 2026 年 9 月针对第 4 次需求的更新。
13
+
14
+ ## 审核报告只保留 PDF
15
+
16
+ `edgepress check` 和 `edgepress server` 现在只在 `tools/page-check.pdf` 保存报告。桌面和移动端截图会嵌入 PDF,临时 HTML 和截图文件会在生成后清理。
17
+
18
+ ## 移除 templates 目录
19
+
20
+ `edgepress init` 现在由 CLI 包直接写入新项目的 README 和 `.gitignore`。源码与 npm 包不再包含 `templates/` 目录。
21
+
22
+ ## Cloudflare Workers Builds 设置
23
+
24
+ 快速入门指南现已记录本仓库设置:根目录 `/`、构建命令 `npm run build`、部署命令 `npx wrangler deploy`,且无需配置构建变量。指南也说明,构建若停留在 `Initializing build environment`,表示项目构建命令还未运行;应重新连接 GitHub 集成,或替换失效的 Cloudflare 构建 API Token 后再试。
25
+
26
+ Wrangler 要求此 Worker 设置 `compatibility_date`,因此项目将其固定为本月第一天:`2026-09-01`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edgepress",
3
- "version": "2026.9.3",
3
+ "version": "2026.9.4",
4
4
  "description": "A static website builder for Cloudflare Workers with editable pages, Markdown posts, local search, themes, and consent-gated integrations.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,6 @@
12
12
  "plugins",
13
13
  "src",
14
14
  "static",
15
- "templates",
16
15
  "themes",
17
16
  "config.yml",
18
17
  "edgepress.config.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "frameworkVersion": "2026.9.3",
3
+ "frameworkVersion": "2026.9.4",
4
4
  "requirements": {
5
5
  "nodeMajor": 22,
6
6
  "nodeMinimumVersion": "22.12.0",
@@ -15,7 +15,7 @@
15
15
  { "name": "Row-based editable pages and reusable layout components", "stage": "experimental", "since": "2026.9.1" },
16
16
  { "name": "Image/video with text and display typography elements", "stage": "experimental", "since": "2026.9.1" },
17
17
  { "name": "i18n locale routes", "stage": "experimental", "since": "2026.9.1" },
18
- { "name": "Accessibility and agent-friendliness report with screenshots and PDF", "stage": "experimental", "since": "2026.9.1" },
18
+ { "name": "Accessibility and agent-friendliness PDF report with embedded screenshots", "stage": "experimental", "since": "2026.9.1" },
19
19
  { "name": "Consent-gated browser plugins", "stage": "experimental", "since": "2026.9.1" },
20
20
  { "name": "Service binding and URL/token backend proxy", "stage": "experimental", "since": "2026.9.1" },
21
21
  { "name": "Live local preview and theme switching", "stage": "experimental", "since": "2026.9.1" },
@@ -23,7 +23,8 @@
23
23
  { "name": "Local browser search across Markdown posts", "stage": "experimental", "since": "2026.9.2" },
24
24
  { "name": "Markdown syntax verification and responsive post screenshots", "stage": "experimental", "since": "2026.9.2" },
25
25
  { "name": "Compact localized language selector", "stage": "experimental", "since": "2026.9.2" },
26
- { "name": "npm theme installation and blank theme scaffolds", "stage": "experimental", "since": "2026.9.3" }
26
+ { "name": "npm theme installation and blank theme scaffolds", "stage": "experimental", "since": "2026.9.3" },
27
+ { "name": "PDF-only audit output and temporary screenshot capture", "stage": "experimental", "since": "2026.9.4" }
27
28
  ],
28
29
  "releaseHistory": [
29
30
  {
@@ -43,6 +44,12 @@
43
44
  "date": "2026-09-25",
44
45
  "request": 3,
45
46
  "notes": "Published EdgePress for npm installation, added npm theme installation and blank theme packages, documented GitHub and Cloudflare Workers Builds deployment, and added a Cloudflare one-click deployment button."
47
+ },
48
+ {
49
+ "version": "2026.9.4",
50
+ "date": "2026-09-25",
51
+ "request": 4,
52
+ "notes": "Keep audit output as a single accessible PDF with embedded screenshots, remove the templates directory and package archive, document Workers Builds initialization troubleshooting, and pin the Wrangler compatibility date to the first of the month."
46
53
  }
47
54
  ]
48
55
  }
package/src/cli.js CHANGED
@@ -10,6 +10,31 @@ import { slugify } from './content.js';
10
10
  import { checkPages } from './page-check.js';
11
11
  import { checkCompatibility, writeIterationPlan } from './maintenance.js';
12
12
 
13
+ const siteGitignore = ['node_modules/', 'dist/', '.edgepress/', '.wrangler/', '.dev.vars', '*.tgz', ''].join('\n');
14
+ const siteReadme = [
15
+ '# My EdgePress site',
16
+ '',
17
+ 'Static site source built with EdgePress. Use Node.js 22.12 or newer.',
18
+ '',
19
+ '## Start locally',
20
+ '',
21
+ ' npm install',
22
+ ' edgepress server',
23
+ '',
24
+ 'Edit localized pages in `content/pages/` and Markdown posts in `content/posts/`. Choose a shared layout with `edgepress theme list` and `edgepress theme use <name>`.',
25
+ '',
26
+ '## Deploy to Cloudflare Workers',
27
+ '',
28
+ 'Push this source repository to GitHub, then connect the repository to Cloudflare Workers Builds. The `build` and `deploy` scripts in `package.json` generate the static files and deploy the Worker. Set a unique Worker name in `wrangler.jsonc` first.',
29
+ '',
30
+ 'For a local deployment, run `npx wrangler login`, then `edgepress deploy`.',
31
+ '',
32
+ '## Use another static web server',
33
+ '',
34
+ 'Run `edgepress generate` and upload the contents of `dist/` to the server document root. OpenResty and Nginx can serve the generated directories as static files. Worker API routes and service bindings need a Worker or an equivalent server-side route.',
35
+ ''
36
+ ].join('\n');
37
+
13
38
  async function initializeProject() {
14
39
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
40
  const root = process.cwd();
@@ -61,11 +86,11 @@ async function initializeProject() {
61
86
  };
62
87
  for (const directory of directories) await cp(resolve(packageRoot, directory), resolve(root, directory), { recursive: true, errorOnExist: true });
63
88
  for (const file of files) {
64
- const source = file === '.gitignore' ? resolve(packageRoot, 'templates/gitignore') :
65
- file === 'README.md' ? resolve(packageRoot, 'templates/site-readme.md') : resolve(packageRoot, file);
66
89
  const destination = resolve(root, file);
67
90
  await mkdir(dirname(destination), { recursive: true });
68
- await cp(source, destination, { errorOnExist: true });
91
+ if (file === '.gitignore') await writeFile(destination, siteGitignore, { flag: 'wx' });
92
+ else if (file === 'README.md') await writeFile(destination, siteReadme, { flag: 'wx' });
93
+ else await cp(resolve(packageRoot, file), destination, { errorOnExist: true });
69
94
  }
70
95
  await mkdir(resolve(root, 'src'), { recursive: true });
71
96
  await cp(resolve(packageRoot, 'src/worker.js'), resolve(root, 'src/worker.js'), { errorOnExist: true });
@@ -324,7 +349,7 @@ try {
324
349
  await buildSite(config.root);
325
350
  const pageReport = await checkPages(config);
326
351
  const compatibilityReport = await checkCompatibility(config);
327
- await showReport('Page check', pageReport, resolve(config.root, 'tools/page-check.md'));
352
+ await showReport('Page check', pageReport, resolve(config.root, 'tools/page-check.pdf'));
328
353
  await showReport('Compatibility check', compatibilityReport, resolve(config.resolvedPaths.cache, 'reports/compatibility.md'));
329
354
  if (pageReport.errors || compatibilityReport.issues.some((issue) => issue.severity === 'error')) process.exitCode = 1;
330
355
  } else if (command === 'doctor') {
package/src/page-check.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, lstat, mkdir, readFile, readdir, realpath, writeFile } from 'node:fs/promises';
1
+ import { access, lstat, mkdir, readFile, readdir, realpath, rm } from 'node:fs/promises';
2
2
  import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { addVisualEvidence } from './report-visuals.js';
4
4
 
@@ -136,49 +136,6 @@ function auditSummary(checks, findings) {
136
136
  };
137
137
  }
138
138
 
139
- function markdownReport(report) {
140
- const lines = [
141
- '# EdgePress accessibility and agent-friendliness audit',
142
- '',
143
- 'Overall status: **' + report.status + '**',
144
- '',
145
- '| Audit | Status | Score | Checks | Errors | Warnings |',
146
- '| --- | --- | ---: | ---: | ---: | ---: |',
147
- '| Accessibility | ' + report.accessibility.status + ' | ' + report.accessibility.score + '% | ' + report.accessibility.checks + ' | ' + report.accessibility.errors + ' | ' + report.accessibility.warnings + ' |',
148
- '| Agent friendliness | ' + report.agentFriendliness.status + ' | ' + report.agentFriendliness.score + '% | ' + report.agentFriendliness.checks + ' | ' + report.agentFriendliness.errors + ' | ' + report.agentFriendliness.warnings + ' |',
149
- '| Markdown rendering | ' + report.markdownRendering.status + ' | ' + Math.floor(report.markdownRendering.passed / Math.max(1, report.markdownRendering.checks) * 100) + '% | ' + report.markdownRendering.checks + ' | ' + (report.markdownRendering.checks - report.markdownRendering.passed) + ' | 0 |',
150
- '| Report reading structure | ' + (report.visualEvidence?.reportAccessibility?.status || 'pending') + ' | ' +
151
- Math.floor((report.visualEvidence?.reportAccessibility?.passed || 0) / Math.max(1, report.visualEvidence?.reportAccessibility?.checks || 0) * 100) + '% | ' +
152
- (report.visualEvidence?.reportAccessibility?.checks || 0) + ' | ' +
153
- ((report.visualEvidence?.reportAccessibility?.checks || 0) - (report.visualEvidence?.reportAccessibility?.passed || 0)) + ' | 0 |',
154
- '',
155
- 'Generated documents: ' + report.pages + ' | Posts: ' + report.contentSummary.posts + ' | Pages: ' + report.contentSummary.pages +
156
- ' | System routes: ' + report.contentSummary.system + ' | Errors: ' + report.errors + ' | Warnings: ' + report.warnings,
157
- 'HTML bytes: ' + report.metrics.htmlBytes + ' | Stylesheet references: ' + report.metrics.stylesheets +
158
- ' | Potentially render-blocking stylesheets: ' + report.metrics.blockingStylesheets +
159
- ' | Script references: ' + report.metrics.scripts + ' | Blocking scripts: ' + report.metrics.blockingScripts,
160
- '',
161
- report.visualEvidence?.status === 'complete'
162
- ? 'Accessible PDF evidence: [page-check.pdf](page-check.pdf) | HTML report: [page-check-visual.html](page-check-visual.html) | Screenshots: ' +
163
- report.visualEvidence.screenshots.map((item) => '[' + item.title + ' · ' + item.viewport + ' (' + item.viewportWidth + '/' + item.documentWidth + ' px)](' + item.path.replace(/^tools\//, '') + ')').join(' · ')
164
- : 'Visual evidence: ' + (report.visualEvidence?.reason || 'not generated'),
165
- '',
166
- '## Findings',
167
- ''
168
- ];
169
- if (!report.issues.length) lines.push('No findings.');
170
- for (const issue of report.issues) lines.push('- **' + issue.severity.toUpperCase() + ' · ' + issue.category + '** ' + issue.page + ' — ' + issue.message);
171
- lines.push('', '## Markdown rendering', '', 'Verified ' + report.markdownRendering.passed + ' of ' + report.markdownRendering.checks +
172
- ' syntax combinations in the generated tutorial post.');
173
- for (const document of report.markdownRendering.documents) {
174
- lines.push('', '### ' + document.path, '', ...document.checks.map((item) => '- [' + (item.passed ? 'x' : ' ') + '] ' + item.feature));
175
- }
176
- lines.push('', '## Documents reviewed', '', '| Type | Count |', '| --- | ---: |', '| Posts | ' + report.contentSummary.posts + ' |',
177
- '| Customizable pages and homepages | ' + report.contentSummary.pages + ' |', '| System routes | ' + report.contentSummary.system + ' |');
178
- lines.push('', '## Scope', '', ...report.scope.map((item) => '- ' + item), '', 'This report is written outside the public output directory.');
179
- return lines.join('\n');
180
- }
181
-
182
139
  export async function checkPages(config) {
183
140
  const output = config.resolvedPaths.output;
184
141
  const files = await htmlFiles(output);
@@ -380,28 +337,35 @@ export async function checkPages(config) {
380
337
  const root = await realpath(config.root);
381
338
  const directory = await realpath(requestedDirectory);
382
339
  if (!isInside(root, directory)) throw new Error('Page-check report directory must stay inside the project root');
383
- const jsonPath = resolve(directory, 'page-check.json');
384
- const markdownPath = resolve(directory, 'page-check.md');
385
- for (const file of [jsonPath, markdownPath]) {
340
+ for (const name of ['page-check.json', 'page-check.md', 'page-check-visual.html']) {
341
+ const file = resolve(directory, name);
386
342
  try {
387
343
  const info = await lstat(file);
388
- if (info.isSymbolicLink() || !info.isFile()) throw new Error('Refusing an unsafe page-check report path: ' + file);
344
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error('Refusing an unsafe legacy report path: ' + file);
345
+ await rm(file);
389
346
  } catch (error) {
390
347
  if (error.code !== 'ENOENT') throw error;
391
348
  }
392
349
  }
393
- for (const file of [resolve(directory, 'page-check.pdf'), resolve(directory, 'page-check-visual.html')]) {
394
- try {
395
- const info = await lstat(file);
396
- if (info.isSymbolicLink() || !info.isFile()) throw new Error('Refusing an unsafe visual report path: ' + file);
397
- } catch (error) {
398
- if (error.code !== 'ENOENT') throw error;
399
- }
350
+ const screenshotsDirectory = resolve(directory, 'page-check-screenshots');
351
+ try {
352
+ const info = await lstat(screenshotsDirectory);
353
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error('Refusing an unsafe legacy screenshots path: ' + screenshotsDirectory);
354
+ await rm(screenshotsDirectory, { recursive: true });
355
+ } catch (error) {
356
+ if (error.code !== 'ENOENT') throw error;
357
+ }
358
+ const pdfPath = resolve(directory, 'page-check.pdf');
359
+ try {
360
+ const info = await lstat(pdfPath);
361
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error('Refusing an unsafe PDF report path: ' + pdfPath);
362
+ } catch (error) {
363
+ if (error.code !== 'ENOENT') throw error;
400
364
  }
401
365
  try {
402
366
  report.visualEvidence = await addVisualEvidence(config, report);
403
367
  if (report.visualEvidence.status !== 'complete') {
404
- issues.push({ severity: 'warning', category: 'visual-evidence', page: 'report', message: report.visualEvidence.reason || 'Visual evidence was not generated.' });
368
+ issues.push({ severity: 'warning', category: 'visual-evidence', page: 'report', message: report.visualEvidence.reason || 'PDF generation failed.' });
405
369
  }
406
370
  } catch (error) {
407
371
  report.visualEvidence = { status: 'failed', reason: error.message };
@@ -410,7 +374,5 @@ export async function checkPages(config) {
410
374
  report.errors = issues.filter((issue) => issue.severity === 'error').length;
411
375
  report.warnings = issues.filter((issue) => issue.severity === 'warning').length;
412
376
  report.status = report.errors ? 'fail' : report.warnings ? 'warning' : 'pass';
413
- await writeFile(jsonPath, JSON.stringify(report, null, 2), 'utf8');
414
- await writeFile(markdownPath, markdownReport(report), 'utf8');
415
377
  return report;
416
378
  }
package/src/preview.js CHANGED
@@ -57,7 +57,7 @@ async function main() {
57
57
  await buildSite(root, { preview: true });
58
58
  const currentConfig = await loadConfig(root);
59
59
  const report = await checkPages(currentConfig);
60
- console.log('Accessibility and agent-friendliness report: ' + report.status +
60
+ console.log('Accessibility and agent-friendliness PDF report: ' + report.status +
61
61
  ' (' + report.accessibility.status + ', ' + report.agentFriendliness.status + ')');
62
62
  }
63
63
 
@@ -247,7 +247,7 @@ function reportHtml(report, screenshots) {
247
247
  escapeHtml(String(report.markdownRendering?.checks || 0)) + ' syntax checks passed across the tutorial post locales.</p>' + markdownDetails + '</section>' +
248
248
  '<section id="evidence" class="appendix"><h2>Responsive page screenshots</h2><p>Mobile screenshots use a 390 by 844 CSS pixel viewport. Each caption includes the measured document width so horizontal overflow is visible in the report.</p>' +
249
249
  screenshotGroups + '</section><section id="scope"><h2>Scope and limits</h2><ul>' + report.scope.map((item) => '<li>' + escapeHtml(item) + '</li>').join('') +
250
- '</ul></section></main><footer><p>EdgePress local build report. Use the HTML source for reflowable reading; screenshots are evidence and may contain text too small to read when printed.</p></footer></body></html>';
250
+ '</ul></section></main><footer><p>EdgePress local PDF report. Screenshot details are included as evidence and may be small when printed.</p></footer></body></html>';
251
251
  }
252
252
 
253
253
  function checkReportReadingStructure(html) {
@@ -351,73 +351,67 @@ function siteAddress(relativeFile) {
351
351
 
352
352
  export async function addVisualEvidence(config, report) {
353
353
  const root = resolve(config.root, 'tools');
354
- const evidenceDirectory = resolve(root, 'page-check-screenshots');
355
354
  const directoryInfo = await lstat(root);
356
355
  if (directoryInfo.isSymbolicLink() || !directoryInfo.isDirectory()) throw new Error('Report directory must be a real directory');
357
- await mkdir(evidenceDirectory, { recursive: true });
358
- const evidenceInfo = await lstat(evidenceDirectory);
359
- if (evidenceInfo.isSymbolicLink() || !evidenceInfo.isDirectory()) throw new Error('Screenshot directory must be a real directory');
360
356
  const browser = await browserPath();
361
357
  if (!browser) {
362
358
  return { status: 'unavailable', reason: 'Install Microsoft Edge, Chrome, or Chromium to capture page screenshots and PDF evidence.' };
363
359
  }
364
360
 
365
- const documents = new Map((report.documents || []).map((item) => [item.path, item]));
366
- const candidates = new Set((report.documents || [])
367
- .filter((item) => ['post', 'page', 'homepage'].includes(item.type))
368
- .map((item) => item.path));
369
- for (const locale of config.i18n.locales) {
370
- const prefix = locale === config.i18n.defaultLocale ? '' : locale + '/';
371
- candidates.add(prefix + 'index.html');
372
- candidates.add(prefix + 'privacy-policy/index.html');
373
- }
374
- for (const issue of report.issues) {
375
- if (issue.page && issue.page.toLowerCase().endsWith('.html')) candidates.add(issue.page);
376
- }
377
-
378
- const available = [];
379
- for (const relativeFile of candidates) {
380
- const target = resolve(config.resolvedPaths.output, relativeFile);
381
- try {
382
- const info = await lstat(target);
383
- if (info.isFile() && !info.isSymbolicLink()) available.push(relativeFile);
384
- } catch (error) {
385
- if (error.code !== 'ENOENT') throw error;
361
+ const stamp = createHash('sha256').update(report.generatedAt).digest('hex').slice(0, 8);
362
+ const profile = await mkdtemp(resolve(tmpdir(), 'edgepress-report-' + process.pid + '-' + stamp + '-'));
363
+ const evidenceDirectory = resolve(profile, 'page-check-screenshots');
364
+ const htmlPath = resolve(profile, 'report.html');
365
+ const pdfPath = resolve(root, 'page-check.pdf');
366
+ let reportServer;
367
+ let browserSession;
368
+ try {
369
+ await mkdir(evidenceDirectory, { recursive: true });
370
+ const evidenceInfo = await lstat(evidenceDirectory);
371
+ if (evidenceInfo.isSymbolicLink() || !evidenceInfo.isDirectory()) throw new Error('Temporary screenshot directory must be a real directory');
372
+ const documents = new Map((report.documents || []).map((item) => [item.path, item]));
373
+ const candidates = new Set((report.documents || [])
374
+ .filter((item) => ['post', 'page', 'homepage'].includes(item.type))
375
+ .map((item) => item.path));
376
+ for (const locale of config.i18n.locales) {
377
+ const prefix = locale === config.i18n.defaultLocale ? '' : locale + '/';
378
+ candidates.add(prefix + 'index.html');
379
+ candidates.add(prefix + 'privacy-policy/index.html');
380
+ }
381
+ for (const issue of report.issues) {
382
+ if (issue.page && issue.page.toLowerCase().endsWith('.html')) candidates.add(issue.page);
386
383
  }
387
- }
388
384
 
389
- const stamp = createHash('sha256').update(report.generatedAt).digest('hex').slice(0, 8);
390
- const screenshots = [];
391
- const desktop = { name: 'desktop', width: 1440, height: 1100 };
392
- const mobile = { name: 'mobile', width: 390, height: 844 };
393
- const homePaths = new Set(config.i18n.locales.map((locale) => locale === config.i18n.defaultLocale ? 'index.html' : locale + '/index.html'));
394
- const privacyPaths = new Set(config.i18n.locales.map((locale) =>
395
- (locale === config.i18n.defaultLocale ? '' : locale + '/') + 'privacy-policy/index.html'));
396
- for (const relativeFile of available) {
397
- const document = documents.get(relativeFile) || { type: 'system', title: relativeFile };
398
- const viewports = homePaths.has(relativeFile) || privacyPaths.has(relativeFile) || document.type === 'post'
399
- ? [desktop, mobile] : [mobile];
400
- for (const viewport of viewports) {
401
- const digest = createHash('sha256').update(relativeFile + ':' + viewport.name).digest('hex').slice(0, 10);
402
- const file = 'page-' + digest + '.png';
403
- const target = resolve(evidenceDirectory, file);
385
+ const available = [];
386
+ for (const relativeFile of candidates) {
387
+ const target = resolve(config.resolvedPaths.output, relativeFile);
404
388
  try {
405
- const existing = await lstat(target);
406
- if (existing.isSymbolicLink() || !existing.isFile()) throw new Error('Refusing an unsafe screenshot path: ' + target);
389
+ const info = await lstat(target);
390
+ if (info.isFile() && !info.isSymbolicLink()) available.push(relativeFile);
407
391
  } catch (error) {
408
392
  if (error.code !== 'ENOENT') throw error;
409
393
  }
410
- screenshots.push({ page: relativeFile, type: document.type, title: document.title, viewport: viewport.name, width: viewport.width,
411
- height: viewport.height, file, target,
412
- label: relativeFile + ' · ' + viewport.name + ' (' + viewport.width + '×' + viewport.height + ')' });
413
394
  }
414
- }
415
- const htmlPath = resolve(root, 'page-check-visual.html');
416
- const pdfPath = resolve(root, 'page-check.pdf');
417
- const profile = await mkdtemp(resolve(tmpdir(), 'edgepress-report-' + process.pid + '-' + stamp + '-'));
418
- let reportServer;
419
- let browserSession;
420
- try {
395
+
396
+ const screenshots = [];
397
+ const desktop = { name: 'desktop', width: 1440, height: 1100 };
398
+ const mobile = { name: 'mobile', width: 390, height: 844 };
399
+ const homePaths = new Set(config.i18n.locales.map((locale) => locale === config.i18n.defaultLocale ? 'index.html' : locale + '/index.html'));
400
+ const privacyPaths = new Set(config.i18n.locales.map((locale) =>
401
+ (locale === config.i18n.defaultLocale ? '' : locale + '/') + 'privacy-policy/index.html'));
402
+ for (const relativeFile of available) {
403
+ const document = documents.get(relativeFile) || { type: 'system', title: relativeFile };
404
+ const viewports = homePaths.has(relativeFile) || privacyPaths.has(relativeFile) || document.type === 'post'
405
+ ? [desktop, mobile] : [mobile];
406
+ for (const viewport of viewports) {
407
+ const digest = createHash('sha256').update(relativeFile + ':' + viewport.name).digest('hex').slice(0, 10);
408
+ const file = 'page-' + digest + '.png';
409
+ const target = resolve(evidenceDirectory, file);
410
+ screenshots.push({ page: relativeFile, type: document.type, title: document.title, viewport: viewport.name, width: viewport.width,
411
+ height: viewport.height, file, target,
412
+ label: relativeFile + ' · ' + viewport.name + ' (' + viewport.width + '×' + viewport.height + ')' });
413
+ }
414
+ }
421
415
  reportServer = await startReportServer(config.resolvedPaths.output, evidenceDirectory, htmlPath);
422
416
  browserSession = await startHeadlessBrowser(browser, profile);
423
417
  for (const screenshot of screenshots) {
@@ -449,13 +443,13 @@ export async function addVisualEvidence(config, report) {
449
443
  report.visualEvidence = { status: 'complete', pdf: 'tools/page-check.pdf', screenshots: screenshots.map((item) => ({
450
444
  page: item.page, type: item.type, title: item.title, viewport: item.viewport, width: item.width, height: item.height,
451
445
  viewportWidth: item.viewportWidth, documentWidth: item.documentWidth,
452
- path: 'tools/page-check-screenshots/' + item.file, label: item.label
446
+ label: item.label
453
447
  })) };
454
448
  const initialHtml = reportHtml(report, screenshots);
455
449
  const reportAccessibility = checkReportReadingStructure(initialHtml);
456
450
  report.visualEvidence.reportAccessibility = reportAccessibility;
457
451
  for (const check of reportAccessibility.results.filter((item) => !item.passed)) {
458
- report.issues.push({ severity: 'error', category: 'report-accessibility', page: 'tools/page-check-visual.html',
452
+ report.issues.push({ severity: 'error', category: 'report-accessibility', page: 'tools/page-check.pdf',
459
453
  message: 'Report reading requirement failed: ' + check.name + '.' });
460
454
  }
461
455
  report.errors = report.issues.filter((item) => item.severity === 'error').length;
package/wrangler.jsonc CHANGED
@@ -3,7 +3,7 @@
3
3
  "$schema": "./node_modules/wrangler/config-schema.json",
4
4
  "name": "edgepress",
5
5
  "main": "./src/worker.js",
6
- "compatibility_date": "2026-09-24",
6
+ "compatibility_date": "2026-09-01",
7
7
  "assets": {
8
8
  "directory": "./dist",
9
9
  "binding": "ASSETS",
@@ -1,6 +0,0 @@
1
- node_modules/
2
- dist/
3
- .edgepress/
4
- .wrangler/
5
- .dev.vars
6
- *.tgz
@@ -1,20 +0,0 @@
1
- # My EdgePress site
2
-
3
- Static site source built with EdgePress. Use Node.js 22.12 or newer.
4
-
5
- ## Start locally
6
-
7
- npm install
8
- edgepress server
9
-
10
- Edit localized pages in `content/pages/` and Markdown posts in `content/posts/`. Choose a shared layout with `edgepress theme list` and `edgepress theme use <name>`.
11
-
12
- ## Deploy to Cloudflare Workers
13
-
14
- Push this source repository to GitHub, then connect the repository to Cloudflare Workers Builds. The `build` and `deploy` scripts in `package.json` generate the static files and deploy the Worker. Set a unique Worker name in `wrangler.jsonc` first.
15
-
16
- For a local deployment, run `npx wrangler login`, then `edgepress deploy`.
17
-
18
- ## Use another static web server
19
-
20
- Run `edgepress generate` and upload the contents of `dist/` to the server document root. OpenResty and Nginx can serve the generated directories as static files. Worker API routes and service bindings need a Worker or an equivalent server-side route.