edgepress 2026.9.5 → 2026.9.8
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 +5 -3
- package/config.yml +121 -69
- package/content/pages/home/en.md +15 -0
- package/content/pages/home/zh-cn.md +16 -1
- package/content/pages/plugin-development/en.md +5 -4
- package/content/pages/plugin-development/zh-cn.md +5 -4
- package/content/pages/privacy-policy/en.md +8 -5
- package/content/pages/privacy-policy/zh-cn.md +8 -5
- package/content/pages/theme-development/en.md +24 -0
- package/content/pages/theme-development/zh-cn.md +24 -0
- package/content/posts/2026-09-23-edgepress-on-the-edge/en.md +12 -2
- package/content/posts/2026-09-23-edgepress-on-the-edge/zh-cn.md +12 -2
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +2 -0
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-2/en.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-2/zh-cn.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-3/en.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-3/zh-cn.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-4/en.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-4/zh-cn.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-5/en.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-5/zh-cn.md +2 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-6/en.md +31 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-6/zh-cn.md +31 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-7/en.md +26 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-7/zh-cn.md +26 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-8/en.md +26 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-8/zh-cn.md +26 -0
- package/languages/base/en.json +9 -2
- package/languages/packs/zh-CN.json +9 -2
- package/package.json +2 -1
- package/plugins/consent/index.js +4 -2
- package/project-compatibility.json +26 -2
- package/src/build.js +5 -4
- package/src/cli.js +8 -13
- package/src/config.js +72 -14
- package/src/content.js +55 -1
- package/src/generators.js +125 -23
- package/src/i18n.js +6 -0
- package/src/icons.js +16 -0
- package/src/maintenance.js +0 -27
- package/src/markdown.js +88 -9
- package/src/page-blocks.js +84 -18
- package/src/page-check.js +79 -25
- package/src/report-visuals.js +111 -16
- package/src/theme.js +13 -2
- package/static/edgepress/brand/edgepress-mark.svg +7 -0
- package/static/edgepress/brand/mascot-209.webp +0 -0
- package/static/edgepress/brand/mascot-418.png +0 -0
- package/static/edgepress/brand/mascot-418.webp +0 -0
- package/static/edgepress/brand/mascot.png +0 -0
- package/static/edgepress/brand/og-image.png +0 -0
- package/static/edgepress/brand/og-image.svg +46 -0
- package/static/edgepress/code-copy.js +55 -0
- package/static/edgepress/icons.svg +24 -0
- package/static/edgepress/plugins/consent/manager.js +204 -71
- package/static/edgepress/plugins/statistics/cloudflare-web-analytics.js +1 -0
- package/themes/atelier/assets/style.css +98 -20
- package/themes/atelier/layouts/partials/head.html +5 -0
- package/themes/atelier/layouts/partials/header.html +1 -1
- package/themes/default/assets/style.css +99 -20
- package/themes/default/layouts/partials/head.html +3 -0
- package/themes/default/layouts/partials/header.html +1 -1
- package/themes/signal/assets/style.css +98 -20
- package/themes/signal/layouts/partials/head.html +5 -0
- package/themes/signal/layouts/partials/header.html +1 -1
package/src/report-visuals.js
CHANGED
|
@@ -127,13 +127,42 @@ async function startHeadlessBrowser(executable, profile) {
|
|
|
127
127
|
await withTimeout(loaded, 20000, 'Screenshot page load timed out');
|
|
128
128
|
await send('Runtime.evaluate', { expression: 'new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 150)))', awaitPromise: true });
|
|
129
129
|
};
|
|
130
|
-
const screenshot = async () => {
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const screenshot = async ({ hideConsent = false, clipSelector = '' } = {}) => {
|
|
131
|
+
let previousDisplay;
|
|
132
|
+
if (hideConsent) {
|
|
133
|
+
const hidden = await send('Runtime.evaluate', {
|
|
134
|
+
expression: '(() => { const node = document.querySelector("[data-consent-ui]"); if (!node) return null; const old = node.style.display; node.style.display = "none"; return old; })()',
|
|
135
|
+
returnByValue: true
|
|
136
|
+
});
|
|
137
|
+
previousDisplay = hidden.result.value;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
let clip;
|
|
141
|
+
if (clipSelector) {
|
|
142
|
+
const measured = await send('Runtime.evaluate', {
|
|
143
|
+
expression: '(() => { const node = document.querySelector("' + clipSelector + '"); if (!node) return null; const rect = node.getBoundingClientRect(); return JSON.stringify({x:Math.max(0,rect.left),y:Math.max(0,rect.top),width:Math.min(window.innerWidth,rect.right)-Math.max(0,rect.left),height:Math.min(window.innerHeight,rect.bottom)-Math.max(0,rect.top)}); })()',
|
|
144
|
+
returnByValue: true
|
|
145
|
+
});
|
|
146
|
+
if (!measured.result.value) return null;
|
|
147
|
+
const rect = JSON.parse(measured.result.value);
|
|
148
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
149
|
+
clip = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, scale: 1 };
|
|
150
|
+
}
|
|
151
|
+
const result = await send('Page.captureScreenshot', {
|
|
152
|
+
format: 'png', fromSurface: true, captureBeyondViewport: false, ...(clip ? { clip } : {})
|
|
153
|
+
});
|
|
154
|
+
return Buffer.from(result.data, 'base64');
|
|
155
|
+
} finally {
|
|
156
|
+
if (hideConsent && previousDisplay !== undefined && previousDisplay !== null) {
|
|
157
|
+
await send('Runtime.evaluate', {
|
|
158
|
+
expression: '(() => { const node = document.querySelector("[data-consent-ui]"); if (node) node.style.display = ' + JSON.stringify(previousDisplay) + '; })()'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
133
162
|
};
|
|
134
163
|
const metrics = async () => {
|
|
135
164
|
const result = await send('Runtime.evaluate', {
|
|
136
|
-
expression: 'JSON.stringify({viewportWidth:
|
|
165
|
+
expression: '(() => { const manager = document.querySelector("[data-consent-ui]"); const oldDisplay = manager?.style.display; if (manager) manager.style.display = "none"; const pageDocumentWidth = Math.max(document.documentElement.scrollWidth,document.body?.scrollWidth || 0); if (manager) manager.style.display = oldDisplay; const panel = manager?.querySelector(".privacy-panel"); const rect = manager?.getBoundingClientRect(); const panelRect = panel?.getBoundingClientRect(); const buttons = [...(panel?.querySelectorAll("button") || [])]; const buttonsFit = !panelRect || buttons.every(button => { const r = button.getBoundingClientRect(); return r.left >= panelRect.left - 1 && r.right <= panelRect.right + 1; }); const consentInnerNoOverflow = !panel || (panel.scrollWidth <= panel.clientWidth + 1 && buttonsFit); const consentWithinViewport = !rect || (rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1); const accept = panel?.querySelector(".privacy-accept"); const reject = panel?.querySelector(".privacy-reject"); const ar = accept?.getBoundingClientRect(); const rr = reject?.getBoundingClientRect(); const consentActionsBalanced = Boolean(accept && reject && Math.abs(ar.width-rr.width)<1 && Math.abs(ar.height-rr.height)<1 && getComputedStyle(accept).backgroundColor===getComputedStyle(reject).backgroundColor); const switches = [...(panel?.querySelectorAll(".privacy-service-toggle input") || [])]; const consentOptInDefault = switches.every(input => !input.checked); const serviceCards = [...(panel?.querySelectorAll(".privacy-service-setting") || [])]; const consentServiceDisclosures = serviceCards.length===0 || serviceCards.every(card => card.querySelectorAll(".privacy-service-disclosures dt").length>=4); const consentDetails = Boolean(panel?.querySelector(".privacy-details>summary")); return JSON.stringify({viewportWidth:innerWidth,documentWidth:document.documentElement.scrollWidth,pageDocumentWidth,consentExists:Boolean(manager),consentWithinViewport,consentInnerNoOverflow,consentActionsBalanced,consentOptInDefault,consentServiceDisclosures,consentDetails,consentWidth:rect?.width || 0,consentHeight:rect?.height || 0}); })()',
|
|
137
166
|
returnByValue: true
|
|
138
167
|
});
|
|
139
168
|
return JSON.parse(result.result.value);
|
|
@@ -189,7 +218,11 @@ const mimeTypes = {
|
|
|
189
218
|
function reportHtml(report, screenshots) {
|
|
190
219
|
const documents = report.documents || [];
|
|
191
220
|
const reportReading = report.visualEvidence?.reportAccessibility || { status: 'pending', checks: 0, passed: 0 };
|
|
192
|
-
const
|
|
221
|
+
const consentChecks = report.consentUi?.checks || 0;
|
|
222
|
+
const consentPassed = report.consentUi?.passed || 0;
|
|
223
|
+
const consentErrors = Math.max(0, consentChecks - consentPassed);
|
|
224
|
+
const consentScore = consentChecks ? Math.floor(consentPassed / consentChecks * 100) + '%' : '—';
|
|
225
|
+
const typeLabel = (type) => ({ post: 'Posts', page: 'Pages', homepage: 'Homepages', system: 'System routes', consent: 'Consent interface' })[type] || type;
|
|
193
226
|
const statusTable = '<table><caption>Audit results</caption><thead><tr><th scope="col">Audit</th><th scope="col">Status</th>' +
|
|
194
227
|
'<th scope="col">Score</th><th scope="col">Checks</th><th scope="col">Errors</th><th scope="col">Warnings</th></tr></thead><tbody>' +
|
|
195
228
|
'<tr><th scope="row">Accessibility</th><td>' + escapeHtml(report.accessibility.status) + '</td><td>' + report.accessibility.score +
|
|
@@ -198,6 +231,8 @@ function reportHtml(report, screenshots) {
|
|
|
198
231
|
'%</td><td>' + report.agentFriendliness.checks + '</td><td>' + report.agentFriendliness.errors + '</td><td>' + report.agentFriendliness.warnings + '</td></tr>' +
|
|
199
232
|
'<tr><th scope="row">Markdown rendering</th><td>' + escapeHtml(report.markdownRendering?.status || 'not run') + '</td><td>—</td><td>' +
|
|
200
233
|
(report.markdownRendering?.checks || 0) + '</td><td>' + ((report.markdownRendering?.checks || 0) - (report.markdownRendering?.passed || 0)) + '</td><td>0</td></tr>' +
|
|
234
|
+
'<tr><th scope="row">Consent interface</th><td>' + escapeHtml(report.consentUi?.status || 'not checked') + '</td><td>' + consentScore +
|
|
235
|
+
'</td><td>' + consentChecks + '</td><td>' + consentErrors + '</td><td>0</td></tr>' +
|
|
201
236
|
'<tr><th scope="row">Report reading structure</th><td>' + escapeHtml(reportReading.status) + '</td><td>' +
|
|
202
237
|
Math.floor(reportReading.passed / Math.max(1, reportReading.checks) * 100) + '%</td><td>' + reportReading.checks +
|
|
203
238
|
'</td><td>' + (reportReading.checks - reportReading.passed) + '</td><td>0</td></tr>' +
|
|
@@ -217,11 +252,13 @@ function reportHtml(report, screenshots) {
|
|
|
217
252
|
? '<ul>' + report.issues.map((item) => '<li><strong>' + escapeHtml(item.severity + ' · ' + item.category) +
|
|
218
253
|
'</strong> ' + escapeHtml(item.page) + ' — ' + escapeHtml(item.message) + '</li>').join('') + '</ul>'
|
|
219
254
|
: '<p>No findings.</p>';
|
|
220
|
-
const screenshotGroups = ['post', 'page', 'homepage', 'system'].map((type) => {
|
|
255
|
+
const screenshotGroups = ['post', 'page', 'homepage', 'system', 'consent'].map((type) => {
|
|
221
256
|
const items = screenshots.filter((item) => item.type === type);
|
|
222
257
|
if (!items.length) return '';
|
|
223
258
|
return '<section><h3>' + typeLabel(type) + ' screenshots</h3>' + items.map((item) => {
|
|
224
|
-
const dimensions = item.
|
|
259
|
+
const dimensions = item.capture === 'consent'
|
|
260
|
+
? 'Consent component ' + Math.round(item.consentWidth || 0) + ' by ' + Math.round(item.consentHeight || 0) + ' CSS pixels; within viewport: ' + Boolean(item.consentWithinViewport)
|
|
261
|
+
: item.viewportWidth + ' CSS pixels wide; page content width ' + item.documentWidth + ' CSS pixels with consent hidden';
|
|
225
262
|
const alt = 'Screenshot of ' + (item.title || item.page) + ' at the ' + item.viewport + ' viewport, ' +
|
|
226
263
|
item.width + ' by ' + item.height + ' pixels; ' + dimensions + '.';
|
|
227
264
|
return '<h4>' + escapeHtml(item.title || item.page) + ' · ' + escapeHtml(item.viewport) + '</h4><figure><img src="./page-check-screenshots/' +
|
|
@@ -232,12 +269,15 @@ function reportHtml(report, screenshots) {
|
|
|
232
269
|
}).join('');
|
|
233
270
|
const markdownDetails = (report.markdownRendering?.documents || []).map((document) => '<section><h3>' + escapeHtml(document.path) +
|
|
234
271
|
'</h4><ul>' + document.checks.map((item) => '<li>' + (item.passed ? 'Pass' : 'Fail') + ' — ' + escapeHtml(item.feature) + '</li>').join('') + '</ul></section>').join('');
|
|
272
|
+
const consentDetails = [...(report.consentUi?.results || []), ...(report.consentUi?.visualChecks || [])]
|
|
273
|
+
.map((item) => '<li>' + (item.passed ? 'Pass' : 'Fail') + ' — ' + escapeHtml(item.name ||
|
|
274
|
+
(item.page + ' at ' + item.viewport + ' viewport')) + '</li>').join('');
|
|
235
275
|
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' +
|
|
236
276
|
'<title>EdgePress accessibility and content report</title><style>@page{size:A4;margin:15mm}*{box-sizing:border-box}html{scroll-behavior:smooth}body{font:12px/1.55 Arial,sans-serif;color:#202520;background:#fff;margin:0 auto;padding:16px;max-width:1100px}header,main,footer{display:block}h1{font-size:25px;line-height:1.2}h2{font-size:19px;margin:26px 0 8px}h3{font-size:15px;margin:20px 0 8px}h4{font-size:12px;margin:16px 0 6px}p{margin:6px 0}a{color:#174c35;text-decoration:underline;text-underline-offset:2px}a:focus-visible{outline:3px solid #9a481e;outline-offset:3px}.skip-link{position:absolute;left:-10000px;top:auto}.skip-link:focus{left:16px;top:16px;padding:8px;background:#fff;border:2px solid #174c35;z-index:2}.report-nav ul{display:flex;gap:18px;flex-wrap:wrap;padding-left:20px}table{width:100%;border-collapse:collapse;margin:10px 0 20px;table-layout:fixed}caption{text-align:left;font-weight:bold;padding:0 0 6px}th,td{padding:7px;border:1px solid #879188;text-align:left;vertical-align:top;overflow-wrap:anywhere}thead th{background:#e9eee9}tbody th{background:#f5f6f3}li{margin:4px 0;overflow-wrap:anywhere}code{overflow-wrap:anywhere}.status{font-weight:bold;text-transform:uppercase}.appendix{break-before:page}figure{margin:0 0 20px;break-inside:avoid}figure img{display:block;max-width:100%;max-height:220mm;width:auto;height:auto;margin:0 auto;border:1px solid #879188;object-fit:contain}figcaption{font-size:10px;color:#39443b;margin-top:5px}section{break-inside:auto}footer{border-top:1px solid #879188;margin-top:24px;padding-top:10px}@media(max-width:600px){body{padding:12px}.report-nav ul{display:block}table{font-size:11px}th,td{padding:5px}}@media print{body{max-width:none;padding:0}.report-nav{display:none}a{color:#202520}.appendix{break-before:page}h2,h3,h4{break-after:avoid}figure{break-inside:avoid}}</style></head>' +
|
|
237
277
|
'<body><a class="skip-link" href="#main">Skip to report</a><header><h1>EdgePress accessibility and content report</h1><p>Generated ' +
|
|
238
278
|
escapeHtml(report.generatedAt) + ' · Overall status: <span class="status">' + escapeHtml(report.status) + '</span>.</p>' +
|
|
239
279
|
'<nav class="report-nav" aria-label="Report sections"><ul><li><a href="#summary">Summary</a></li><li><a href="#content">Content reviewed</a></li>' +
|
|
240
|
-
'<li><a href="#findings">Findings</a></li><li><a href="#markdown">Markdown rendering</a></li><li><a href="#evidence">Screenshots</a></li><li><a href="#scope">Scope</a></li></ul></nav></header>' +
|
|
280
|
+
'<li><a href="#findings">Findings</a></li><li><a href="#markdown">Markdown rendering</a></li><li><a href="#consent">Consent interface</a></li><li><a href="#evidence">Screenshots</a></li><li><a href="#scope">Scope</a></li></ul></nav></header>' +
|
|
241
281
|
'<main id="main"><section id="summary"><h2>Audit summary</h2>' + statusTable + summary + '<p>Generated HTML documents: ' + report.pages +
|
|
242
282
|
' · HTML bytes: ' + report.metrics.htmlBytes + ' · Stylesheet references: ' + report.metrics.stylesheets +
|
|
243
283
|
' · Script references: ' + report.metrics.scripts + '.</p></section>' +
|
|
@@ -245,7 +285,9 @@ function reportHtml(report, screenshots) {
|
|
|
245
285
|
documentSections + '</section><section id="findings"><h2>Findings</h2>' + findings + '</section>' +
|
|
246
286
|
'<section id="markdown"><h2>Markdown rendering verification</h2><p>' + escapeHtml(String(report.markdownRendering?.passed || 0)) + ' of ' +
|
|
247
287
|
escapeHtml(String(report.markdownRendering?.checks || 0)) + ' syntax checks passed across the tutorial post locales.</p>' + markdownDetails + '</section>' +
|
|
248
|
-
'<section id="
|
|
288
|
+
'<section id="consent"><h2>Consent interface checks</h2><p>Consent component checks run separately from page-content layout checks.</p>' +
|
|
289
|
+
(consentDetails ? '<ul>' + consentDetails + '</ul>' : '<p>No consent UI checks were run.</p>') + '</section>' +
|
|
290
|
+
'<section id="evidence" class="appendix"><h2>Responsive and consent screenshots</h2><p>Page screenshots use a 390 by 844 CSS pixel mobile viewport and hide the consent component while measuring page content. Separate consent screenshots crop the component at desktop and mobile sizes.</p>' +
|
|
249
291
|
screenshotGroups + '</section><section id="scope"><h2>Scope and limits</h2><ul>' + report.scope.map((item) => '<li>' + escapeHtml(item) + '</li>').join('') +
|
|
250
292
|
'</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
293
|
}
|
|
@@ -407,11 +449,25 @@ export async function addVisualEvidence(config, report) {
|
|
|
407
449
|
const digest = createHash('sha256').update(relativeFile + ':' + viewport.name).digest('hex').slice(0, 10);
|
|
408
450
|
const file = 'page-' + digest + '.png';
|
|
409
451
|
const target = resolve(evidenceDirectory, file);
|
|
410
|
-
screenshots.push({ page: relativeFile, type: document.type, title: document.title, viewport: viewport.name, width: viewport.width,
|
|
452
|
+
screenshots.push({ page: relativeFile, type: document.type, capture: 'page', title: document.title, viewport: viewport.name, width: viewport.width,
|
|
411
453
|
height: viewport.height, file, target,
|
|
412
454
|
label: relativeFile + ' · ' + viewport.name + ' (' + viewport.width + '×' + viewport.height + ')' });
|
|
413
455
|
}
|
|
414
456
|
}
|
|
457
|
+
if (config.browserPlugins.consent.enabled) {
|
|
458
|
+
const availableSet = new Set(available);
|
|
459
|
+
for (const relativeFile of new Set([...homePaths, ...privacyPaths])) {
|
|
460
|
+
if (!availableSet.has(relativeFile)) continue;
|
|
461
|
+
for (const viewport of [desktop, mobile]) {
|
|
462
|
+
const digest = createHash('sha256').update('consent:' + relativeFile + ':' + viewport.name).digest('hex').slice(0, 10);
|
|
463
|
+
const file = 'consent-' + digest + '.png';
|
|
464
|
+
screenshots.push({ page: relativeFile, type: 'consent', capture: 'consent', title: 'Privacy choices',
|
|
465
|
+
viewport: viewport.name, width: viewport.width, height: viewport.height, file,
|
|
466
|
+
target: resolve(evidenceDirectory, file), label: 'Privacy choices · ' + relativeFile + ' · ' + viewport.name +
|
|
467
|
+
' (' + viewport.width + '×' + viewport.height + ')' });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
415
471
|
reportServer = await startReportServer(config.resolvedPaths.output, evidenceDirectory, htmlPath);
|
|
416
472
|
browserSession = await startHeadlessBrowser(browser, profile);
|
|
417
473
|
for (const screenshot of screenshots) {
|
|
@@ -419,19 +475,57 @@ export async function addVisualEvidence(config, report) {
|
|
|
419
475
|
await browserSession.setViewport(viewport);
|
|
420
476
|
await browserSession.navigate('http://127.0.0.1:' + reportServer.port + siteAddress(screenshot.page));
|
|
421
477
|
const dimensions = await browserSession.metrics();
|
|
422
|
-
|
|
478
|
+
if (screenshot.capture === 'consent') {
|
|
479
|
+
report.consentUi.visualChecks ||= [];
|
|
480
|
+
const checks = [
|
|
481
|
+
['component stays inside the viewport', dimensions.consentExists && dimensions.consentWithinViewport],
|
|
482
|
+
['component content stays inside its own box', dimensions.consentExists && dimensions.consentInnerNoOverflow],
|
|
483
|
+
['accept and reject actions have equal visual weight', dimensions.consentExists && dimensions.consentActionsBalanced],
|
|
484
|
+
['optional services begin unchecked', dimensions.consentExists && dimensions.consentOptInDefault],
|
|
485
|
+
['expanded service details disclose all configured fields', dimensions.consentExists && dimensions.consentServiceDisclosures && dimensions.consentDetails]
|
|
486
|
+
];
|
|
487
|
+
report.consentUi.visualChecks.push(...checks.map(([name, passed]) => ({ page: screenshot.page, viewport: viewport.name,
|
|
488
|
+
name, passed, consentWidth: dimensions.consentWidth, consentHeight: dimensions.consentHeight })));
|
|
489
|
+
const withinViewport = dimensions.consentExists && dimensions.consentWithinViewport;
|
|
490
|
+
const noInnerOverflow = dimensions.consentExists && dimensions.consentInnerNoOverflow;
|
|
491
|
+
if (!dimensions.consentExists) {
|
|
492
|
+
report.issues.push({ severity: 'error', category: 'consent-ui', page: screenshot.page,
|
|
493
|
+
message: 'The consent manager did not appear in the browser.' });
|
|
494
|
+
} else if (!withinViewport || !noInnerOverflow) {
|
|
495
|
+
report.issues.push({ severity: 'error', category: 'consent-ui', page: screenshot.page,
|
|
496
|
+
message: 'Consent UI exceeded its viewport or its own content box at the ' + viewport.name + ' size.' });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const image = screenshot.capture === 'consent'
|
|
500
|
+
? await browserSession.screenshot({ clipSelector: '[data-consent-ui]' })
|
|
501
|
+
: await browserSession.screenshot({ hideConsent: config.browserPlugins.consent.enabled });
|
|
502
|
+
if (!image) {
|
|
503
|
+
report.issues.push({ severity: 'error', category: screenshot.capture === 'consent' ? 'consent-ui' : 'visual-evidence',
|
|
504
|
+
page: screenshot.page, message: 'The requested screenshot area was not present in the browser.' });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
await writeFile(screenshot.target, image);
|
|
423
508
|
const imageInfo = await lstat(screenshot.target);
|
|
424
509
|
if (!imageInfo.isFile() || imageInfo.size < 1000) throw new Error('Headless browser produced an empty screenshot');
|
|
425
510
|
screenshot.viewportWidth = dimensions.viewportWidth;
|
|
426
|
-
screenshot.documentWidth = dimensions.
|
|
511
|
+
screenshot.documentWidth = dimensions.pageDocumentWidth;
|
|
512
|
+
screenshot.consentWidth = dimensions.consentWidth;
|
|
513
|
+
screenshot.consentHeight = dimensions.consentHeight;
|
|
514
|
+
screenshot.consentWithinViewport = dimensions.consentWithinViewport;
|
|
427
515
|
if (viewport.name === 'mobile' && dimensions.viewportWidth !== viewport.width) {
|
|
428
516
|
report.issues.push({ severity: 'error', category: 'responsive-layout', page: screenshot.page,
|
|
429
517
|
message: 'Mobile viewport measured ' + dimensions.viewportWidth + ' CSS pixels instead of ' + viewport.width + '.' });
|
|
430
|
-
} else if (viewport.name === 'mobile' && dimensions.
|
|
518
|
+
} else if (screenshot.capture === 'page' && viewport.name === 'mobile' && dimensions.pageDocumentWidth > dimensions.viewportWidth) {
|
|
431
519
|
report.issues.push({ severity: 'error', category: 'responsive-layout', page: screenshot.page,
|
|
432
|
-
message: '
|
|
520
|
+
message: 'Page content width is ' + dimensions.pageDocumentWidth + ' CSS pixels for a ' + dimensions.viewportWidth + ' pixel viewport; consent UI is measured separately.' });
|
|
433
521
|
}
|
|
434
522
|
}
|
|
523
|
+
if (report.consentUi?.visualChecks) {
|
|
524
|
+
const visualChecks = report.consentUi.visualChecks;
|
|
525
|
+
report.consentUi.checks += visualChecks.length;
|
|
526
|
+
report.consentUi.passed += visualChecks.filter((item) => item.passed).length;
|
|
527
|
+
report.consentUi.status = report.consentUi.passed === report.consentUi.checks ? 'pass' : 'fail';
|
|
528
|
+
}
|
|
435
529
|
report.errors = report.issues.filter((item) => item.severity === 'error').length;
|
|
436
530
|
report.warnings = report.issues.filter((item) => item.severity === 'warning').length;
|
|
437
531
|
report.status = report.errors ? 'fail' : report.warnings ? 'warning' : 'pass';
|
|
@@ -440,9 +534,10 @@ export async function addVisualEvidence(config, report) {
|
|
|
440
534
|
if (findings.some((item) => item.severity === 'error')) document.status = 'fail';
|
|
441
535
|
else if (findings.length) document.status = 'warning';
|
|
442
536
|
}
|
|
443
|
-
report.visualEvidence = { status: 'complete', pdf: 'tools/page-check.pdf', screenshots: screenshots.map((item) => ({
|
|
444
|
-
page: item.page, type: item.type, title: item.title, viewport: item.viewport, width: item.width, height: item.height,
|
|
537
|
+
report.visualEvidence = { status: 'complete', pdf: 'tools/page-check.pdf', screenshots: screenshots.filter((item) => item.viewportWidth).map((item) => ({
|
|
538
|
+
page: item.page, type: item.type, capture: item.capture, title: item.title, viewport: item.viewport, width: item.width, height: item.height,
|
|
445
539
|
viewportWidth: item.viewportWidth, documentWidth: item.documentWidth,
|
|
540
|
+
consentWidth: item.consentWidth, consentHeight: item.consentHeight, consentWithinViewport: item.consentWithinViewport,
|
|
446
541
|
label: item.label
|
|
447
542
|
})) };
|
|
448
543
|
const initialHtml = reportHtml(report, screenshots);
|
package/src/theme.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { localePrefix, localizedUrl, translate } from './i18n.js';
|
|
4
|
+
import { isIconName, renderIcon } from './icons.js';
|
|
5
|
+
const defaultNavigationIcons = {
|
|
6
|
+
home: 'home', guides: 'book-open', 'quick-start': 'rocket', posts: 'newspaper',
|
|
7
|
+
search: 'search', privacy: 'shield-check', feed: 'rss'
|
|
8
|
+
};
|
|
4
9
|
|
|
5
10
|
function isInside(root, target) {
|
|
6
11
|
const rel = relative(root, target);
|
|
@@ -27,7 +32,9 @@ function navigationItems(config, locale, items) {
|
|
|
27
32
|
item.url === '@search' ? localizedUrl(config, locale, 'search/') :
|
|
28
33
|
item.url.startsWith('/') ? localizedUrl(config, locale, item.url) : item.url;
|
|
29
34
|
const label = item.labels?.[locale] ?? item.labels?.[config.i18n.defaultLocale] ?? translate(config, locale, item.key);
|
|
30
|
-
|
|
35
|
+
const iconName = item.icon || defaultNavigationIcons[item.key];
|
|
36
|
+
const icon = isIconName(iconName) ? renderIcon(iconName, 'navigation-icon') : '';
|
|
37
|
+
return '<a href="' + escapeHtml(path) + '">' + icon + escapeHtml(label) + '</a>';
|
|
31
38
|
}).join('');
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -157,5 +164,9 @@ export async function renderLayout(config, extensions, page, body) {
|
|
|
157
164
|
const html = '<!doctype html>' + rendered;
|
|
158
165
|
const filtered = await extensions.filter('html:afterLayout', html, context);
|
|
159
166
|
if (typeof filtered !== 'string') throw new Error('html:afterLayout filters must return a string');
|
|
160
|
-
return filtered;
|
|
167
|
+
if (/src=["']\/edgepress\/code-copy\.js["']/i.test(filtered)) return filtered;
|
|
168
|
+
const codeCopyScript = '<script defer src="/edgepress/code-copy.js"></script>';
|
|
169
|
+
return /<\/body\s*>/i.test(filtered)
|
|
170
|
+
? filtered.replace(/<\/body\s*>/i, codeCopyScript + '</body>')
|
|
171
|
+
: filtered + codeCopyScript;
|
|
161
172
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<rect width="48" height="48" rx="14" fill="#315E49"/>
|
|
3
|
+
<path d="M12 10.5a2 2 0 0 1 2-2h13.5L36 17v20.5a2 2 0 0 1-2 2H14a2 2 0 0 1-2-2z" fill="#FBF8EF"/>
|
|
4
|
+
<path d="M27.5 8.5V17H36" fill="#E98263"/>
|
|
5
|
+
<path d="M18 23h12m-12 6h12m-12 6h8" stroke="#315E49" stroke-width="2.6" stroke-linecap="round"/>
|
|
6
|
+
<path d="M27.5 8.5 36 17" stroke="#D6E4D8" stroke-width="1.2"/>
|
|
7
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fbfaf5"/><stop offset="1" stop-color="#eee9dd"/></linearGradient>
|
|
4
|
+
<linearGradient id="field" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#dfeae0"/><stop offset="1" stop-color="#c9ddcf"/></linearGradient>
|
|
5
|
+
<filter id="card-shadow" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#183729" flood-opacity=".12"/></filter>
|
|
6
|
+
<pattern id="dots" width="25" height="25" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1.4" fill="#8da891" opacity=".42"/></pattern>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="1200" height="630" rx="28" fill="url(#paper)"/>
|
|
9
|
+
<circle cx="1100" cy="62" r="245" fill="#f1e5d3" opacity=".75"/>
|
|
10
|
+
<circle cx="964" cy="333" r="242" fill="url(#field)"/>
|
|
11
|
+
<circle cx="964" cy="333" r="217" fill="url(#dots)" opacity=".7"/>
|
|
12
|
+
<path d="M680 463c104-93 172-139 274-139s184 47 255 121" fill="none" stroke="#74937b" stroke-width="2" stroke-dasharray="4 10" opacity=".55"/>
|
|
13
|
+
<circle cx="703" cy="442" r="7" fill="#e98263"/>
|
|
14
|
+
<circle cx="1182" cy="442" r="7" fill="#315e49"/>
|
|
15
|
+
<g transform="translate(68 67)">
|
|
16
|
+
<rect width="52" height="52" rx="16" fill="#315e49"/>
|
|
17
|
+
<path d="M13 12a2 2 0 0 1 2-2h15l9 9v21a2 2 0 0 1-2 2H15a2 2 0 0 1-2-2z" fill="#fbf8ef"/>
|
|
18
|
+
<path d="M30 10v9h9" fill="#e98263"/>
|
|
19
|
+
<path d="M19 25h13m-13 6h13m-13 6h9" stroke="#315e49" stroke-width="2.5" stroke-linecap="round"/>
|
|
20
|
+
<text x="70" y="36" fill="#26382d" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="28" font-weight="700" letter-spacing="-.7">EdgePress</text>
|
|
21
|
+
</g>
|
|
22
|
+
<g fill="#26382d" font-family="Inter,Segoe UI,Arial,sans-serif">
|
|
23
|
+
<text x="72" y="235" font-size="60" font-weight="750" letter-spacing="-2.7">Build for the edge.</text>
|
|
24
|
+
<text x="72" y="306" font-size="60" font-weight="750" letter-spacing="-2.7">Publish with clarity.</text>
|
|
25
|
+
<text x="75" y="365" fill="#64756a" font-size="23" font-weight="450">A thoughtful way to build a fast, flexible website.</text>
|
|
26
|
+
<text x="75" y="400" fill="#64756a" font-size="23" font-weight="450">Markdown, editable pages, themes, and Workers.</text>
|
|
27
|
+
</g>
|
|
28
|
+
<g filter="url(#card-shadow)">
|
|
29
|
+
<rect x="72" y="460" width="516" height="91" rx="18" fill="#fffdf8" stroke="#e1ddd3"/>
|
|
30
|
+
<path d="M102 487h24l9 9v28h-33z" fill="#dce9dd" stroke="#315e49" stroke-width="1.5" stroke-linejoin="round"/>
|
|
31
|
+
<path d="M126 487v9h9m-24 8h16m-16 7h16" fill="none" stroke="#315e49" stroke-width="1.7" stroke-linecap="round"/>
|
|
32
|
+
<text x="151" y="510" fill="#26382d" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="17" font-weight="650">Open source. Built for publishing.</text>
|
|
33
|
+
<text x="151" y="532" fill="#748176" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="14">Clear source in. A lightweight site out.</text>
|
|
34
|
+
</g>
|
|
35
|
+
<g transform="translate(950 55)">
|
|
36
|
+
<rect x="12" y="10" width="167" height="41" rx="20.5" fill="#fffdf8" stroke="#c7d7c9"/>
|
|
37
|
+
<circle cx="37" cy="30.5" r="5" fill="#e98263"/>
|
|
38
|
+
<text x="54" y="36" fill="#45634e" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="13" font-weight="700" letter-spacing="1.4">PAGE BY PAGE</text>
|
|
39
|
+
</g>
|
|
40
|
+
<image href="mascot.png" x="755" y="72" width="430" height="478" preserveAspectRatio="xMidYMid meet"/>
|
|
41
|
+
<g transform="translate(936 557)">
|
|
42
|
+
<rect width="208" height="36" rx="18" fill="#315e49"/>
|
|
43
|
+
<text x="104" y="24" text-anchor="middle" fill="#fbf8ef" font-family="Inter,Segoe UI,Arial,sans-serif" font-size="12" font-weight="700" letter-spacing="1.5">PUBLISH WITH PURPOSE</text>
|
|
44
|
+
</g>
|
|
45
|
+
<path d="M1107 125c10-19 14-21 21-23-4 11-4 18-1 27-8-4-13-4-20-4Z" fill="#e98263"/>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const copyLabels = document.documentElement.lang.toLowerCase().startsWith('zh')
|
|
2
|
+
? { copy: '复制', copied: '已复制到剪贴板。', failed: '复制失败,请手动选择并复制代码。', label: '复制代码' }
|
|
3
|
+
: { copy: 'Copy', copied: 'Copied to clipboard.', failed: 'Copy failed. Select and copy the code manually.', label: 'Copy code' };
|
|
4
|
+
|
|
5
|
+
function fallbackCopy(value) {
|
|
6
|
+
const field = document.createElement('textarea');
|
|
7
|
+
field.value = value;
|
|
8
|
+
field.setAttribute('readonly', '');
|
|
9
|
+
field.tabIndex = -1;
|
|
10
|
+
field.setAttribute('aria-hidden', 'true');
|
|
11
|
+
field.style.position = 'fixed';
|
|
12
|
+
field.style.inset = '0 auto auto -10000px';
|
|
13
|
+
document.body.append(field);
|
|
14
|
+
field.select();
|
|
15
|
+
let copied = false;
|
|
16
|
+
try { copied = document.execCommand('copy'); } catch { /* Clipboard permissions may deny the fallback. */ }
|
|
17
|
+
field.remove();
|
|
18
|
+
return copied;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const pre of document.querySelectorAll('pre[data-copyable-code]')) {
|
|
22
|
+
if (pre.parentElement?.classList.contains('code-copy-block')) continue;
|
|
23
|
+
const wrapper = document.createElement('div');
|
|
24
|
+
wrapper.className = 'code-copy-block';
|
|
25
|
+
const toolbar = document.createElement('div');
|
|
26
|
+
toolbar.className = 'code-copy-toolbar';
|
|
27
|
+
const button = document.createElement('button');
|
|
28
|
+
button.className = 'code-copy-button';
|
|
29
|
+
button.type = 'button';
|
|
30
|
+
button.textContent = copyLabels.copy;
|
|
31
|
+
button.setAttribute('aria-label', copyLabels.label);
|
|
32
|
+
const status = document.createElement('span');
|
|
33
|
+
status.className = 'code-copy-status';
|
|
34
|
+
status.setAttribute('role', 'status');
|
|
35
|
+
status.setAttribute('aria-live', 'polite');
|
|
36
|
+
toolbar.append(button);
|
|
37
|
+
pre.before(wrapper);
|
|
38
|
+
wrapper.append(toolbar, pre, status);
|
|
39
|
+
|
|
40
|
+
button.addEventListener('click', async () => {
|
|
41
|
+
const code = pre.querySelector('code') || pre;
|
|
42
|
+
let copied = false;
|
|
43
|
+
try {
|
|
44
|
+
if (navigator.clipboard?.writeText) {
|
|
45
|
+
await navigator.clipboard.writeText(code.textContent || '');
|
|
46
|
+
copied = true;
|
|
47
|
+
} else {
|
|
48
|
+
copied = fallbackCopy(code.textContent || '');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
copied = fallbackCopy(code.textContent || '');
|
|
52
|
+
}
|
|
53
|
+
status.textContent = copied ? copyLabels.copied : copyLabels.failed;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="home" viewBox="0 0 24 24"><path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-6v-7h-4v7H4a1 1 0 0 1-1-1z"/><path d="M8 8h8"/></symbol>
|
|
3
|
+
<symbol id="book-open" viewBox="0 0 24 24"><path d="M12 7v14M12 8c-2.5-2-5.5-2-9-1v13c3.5-1 6.5-1 9 1M12 8c2.5-2 5.5-2 9-1v13c-3.5-1-6.5-1-9 1"/><path d="M6 11h3m6 0h3"/></symbol>
|
|
4
|
+
<symbol id="rocket" viewBox="0 0 24 24"><path d="M12 15 9 12c1-5 4-8 11-9 0 7-4 10-8 11Z"/><path d="m9 12-4 1-2 4 5-1m4-1 1 4 4 2-1-5"/><circle cx="15" cy="8" r="1.5"/><path d="M7 18c-1 2-3 3-4 3 0-2 1-4 3-5"/></symbol>
|
|
5
|
+
<symbol id="newspaper" viewBox="0 0 24 24"><path d="M5 4h15v15a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"/><path d="M3 8h5v5H3m8-5h6m-6 4h6m-11 4h11m-11 3h7"/></symbol>
|
|
6
|
+
<symbol id="search" viewBox="0 0 24 24"><circle cx="10.8" cy="10.8" r="6.8"/><path d="m16 16 5 5"/></symbol>
|
|
7
|
+
<symbol id="shield-check" viewBox="0 0 24 24"><path d="M12 3 20 6v5c0 5-3.2 8.2-8 10-4.8-1.8-8-5-8-10V6z"/><path d="m8.5 12 2.3 2.3 4.8-5"/></symbol>
|
|
8
|
+
<symbol id="rss" viewBox="0 0 24 24"><circle cx="5" cy="19" r="1"/><path d="M4 11a9 9 0 0 1 9 9M4 5a15 15 0 0 1 15 15"/></symbol>
|
|
9
|
+
<symbol id="file-text" viewBox="0 0 24 24"><path d="M6 3h8l5 5v13H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z"/><path d="M14 3v6h5m-10 4h6m-6 4h6"/></symbol>
|
|
10
|
+
<symbol id="layers" viewBox="0 0 24 24"><path d="m12 3 9 5-9 5-9-5z"/><path d="m3 12 9 5 9-5M3 16l9 5 9-5"/></symbol>
|
|
11
|
+
<symbol id="code" viewBox="0 0 24 24"><path d="m8 7-5 5 5 5m8-10 5 5-5 5m-3-12-2 14"/></symbol>
|
|
12
|
+
<symbol id="puzzle" viewBox="0 0 24 24"><path d="M4 4h5a2.5 2.5 0 1 1 5 0h6v6a2.5 2.5 0 1 0 0 5v5h-6a2.5 2.5 0 1 0-5 0H4v-6a2.5 2.5 0 1 1 0-5z"/></symbol>
|
|
13
|
+
<symbol id="plug" viewBox="0 0 24 24"><path d="M9 3v6m6-6v6m-9 0h12v3a6 6 0 0 1-6 6v3m0-9v6"/></symbol>
|
|
14
|
+
<symbol id="cloud" viewBox="0 0 24 24"><path d="M7 18a5 5 0 1 1 1-9.9A6 6 0 0 1 19 10a4 4 0 0 1 0 8z"/><path d="m9 14 3-3 3 3m-3-3v7"/></symbol>
|
|
15
|
+
<symbol id="palette" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 0 18h1.2a2.3 2.3 0 0 0 1.6-4c-.7-.7-.2-2 1-2H18a3 3 0 0 0 3-3c0-5-4-9-9-9Z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7.5" r="1"/><circle cx="15" cy="8" r="1"/></symbol>
|
|
16
|
+
<symbol id="globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18m0-18a14 14 0 0 0 0 18"/></symbol>
|
|
17
|
+
<symbol id="help-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.6 2.6 0 1 1 4.8 1.4c-.8 1-2.3 1.4-2.3 3m0 3h.01"/></symbol>
|
|
18
|
+
<symbol id="arrow-up-right" viewBox="0 0 24 24"><path d="M7 17 17 7M8 7h9v9"/></symbol>
|
|
19
|
+
<symbol id="check-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="m8 12 2.7 2.7L16.5 9"/></symbol>
|
|
20
|
+
<symbol id="layout-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="5" rx="1.5"/><rect x="13" y="10" width="8" height="11" rx="1.5"/><rect x="3" y="13" width="8" height="8" rx="1.5"/></symbol>
|
|
21
|
+
<symbol id="terminal" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="m7 9 3 3-3 3m6 0h4"/></symbol>
|
|
22
|
+
<symbol id="settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="m19.4 15 .1.1 1.1.9-1.1 2-1.4-.4a8 8 0 0 1-1.7 1l-.3 1.4h-2.2l-.3-1.4a8 8 0 0 1-1.7-1l-1.4.4-1.1-2 1.1-.9A8 8 0 0 1 8.5 14L7 13.7v-2.4l1.5-.3a8 8 0 0 1 .9-1.7l-.5-1.4 2-1.2 1 1.1a8 8 0 0 1 2 0l1-1.1 2 1.2-.5 1.4a8 8 0 0 1 .9 1.7l1.5.3v2.4l-1.5.3"/></symbol>
|
|
23
|
+
<symbol id="sparkles" viewBox="0 0 24 24"><path d="m12 3 1.4 5.6L19 10l-5.6 1.4L12 17l-1.4-5.6L5 10l5.6-1.4zM19 16l.7 2.3L22 19l-2.3.7L19 22l-.7-2.3L16 19l2.3-.7z"/></symbol>
|
|
24
|
+
</svg>
|