edgepress 2026.9.8 → 2026.9.10
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 +4 -4
- package/config.yml +14 -3
- package/content/pages/home/zh-cn.md +9 -9
- package/content/pages/plugin-development/en.md +1 -1
- package/content/pages/plugin-development/zh-cn.md +1 -1
- package/content/pages/privacy-policy/en.md +45 -28
- package/content/pages/privacy-policy/zh-cn.md +50 -33
- package/content/pages/project-introduction/en.md +1 -1
- package/content/pages/project-introduction/zh-cn.md +1 -1
- package/content/pages/theme-development/en.md +1 -0
- package/content/pages/theme-development/zh-cn.md +1 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-9/en.md +30 -0
- package/content/posts/2026-09-26-edgepress-release-2026-9-9/zh-cn.md +30 -0
- package/content/posts/2026-09-27-edgepress-release-2026-9-10/en.md +31 -0
- package/content/posts/2026-09-27-edgepress-release-2026-9-10/zh-cn.md +31 -0
- package/languages/base/en.json +13 -9
- package/languages/packs/zh-CN.json +12 -8
- package/package.json +1 -1
- package/plugins/consent/index.js +18 -4
- package/project-compatibility.json +16 -2
- package/src/config.js +10 -1
- package/src/generators.js +13 -6
- package/src/page-blocks.js +29 -11
- package/src/page-check.js +6 -4
- package/src/theme.js +12 -4
- package/static/edgepress/icons.svg +2 -2
- package/static/edgepress/plugins/consent/manager.js +78 -58
- package/static/edgepress/post-toc.js +37 -0
- package/themes/atelier/assets/style.css +69 -49
- package/themes/default/assets/style.css +69 -49
- package/themes/signal/assets/style.css +69 -49
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: EdgePress 2026 年 9 月更新:第 10 次需求
|
|
3
|
+
author: toewpq
|
|
4
|
+
date: 2026-09-27
|
|
5
|
+
lang: zh-CN
|
|
6
|
+
slug: edgepress-release-2026-9-10
|
|
7
|
+
tags:
|
|
8
|
+
- 发布
|
|
9
|
+
- 多语言
|
|
10
|
+
- 隐私
|
|
11
|
+
- 主题
|
|
12
|
+
description: 修复跨语言切换后的隐私选择,支持本地化站内链接,改进文章标签、搜索脚本和 SVG 图标。
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
这是 EdgePress 在 2026 年 9 月针对第 10 次需求的更新。
|
|
16
|
+
|
|
17
|
+
## 切换语言后保留隐私选择
|
|
18
|
+
|
|
19
|
+
隐私选择现在使用跨语言一致的配置校验,切换页面语言时会继续生效。现有的旧版选择会自动迁移;当告知日期或服务配置变化时,仍会重新征求选择。
|
|
20
|
+
|
|
21
|
+
## 按当前语言生成站内链接
|
|
22
|
+
|
|
23
|
+
页面块链接可使用 `/[launge]/quick-start/`。生成页面时,这个标记会替换为当前语言路径;默认语言会省略语言段。中文首页的指南链接现在会打开对应的中文页面。
|
|
24
|
+
|
|
25
|
+
## 改进文章标签和搜索
|
|
26
|
+
|
|
27
|
+
文章底部会以清楚可见的链接显示标签,并可打开对应的标签页。搜索脚本只在搜索页面加载,标签使用原生链接,无需 JavaScript。
|
|
28
|
+
|
|
29
|
+
## 重绘图标并简化隐私说明
|
|
30
|
+
|
|
31
|
+
重绘了设置和插头 SVG 图标。隐私政策以日常用语说明选择保存在访客设备上,不再显示内部存储键名称。
|
package/languages/base/en.json
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"searchResultCount": "Matching articles: {count}.",
|
|
25
25
|
"markdownTaskComplete": "Task complete",
|
|
26
26
|
"markdownTaskIncomplete": "Task incomplete",
|
|
27
|
-
"postContents": "
|
|
27
|
+
"postContents": "Contents",
|
|
28
|
+
"closeContents": "Close contents",
|
|
28
29
|
"feed": "Atom feed",
|
|
29
30
|
"languageName": "English",
|
|
30
31
|
"mainNavigationLabel": "Main navigation",
|
|
@@ -37,26 +38,29 @@
|
|
|
37
38
|
"postAuthor": "By {author}",
|
|
38
39
|
"privacySettings": "Privacy settings",
|
|
39
40
|
"privacyNotice": "Privacy choices",
|
|
40
|
-
"privacyIntro": "Optional services stay off until you choose.
|
|
41
|
-
"acceptOptional": "
|
|
41
|
+
"privacyIntro": "Optional services stay off until you choose.",
|
|
42
|
+
"acceptOptional": "Allow all",
|
|
42
43
|
"rejectOptional": "Reject optional",
|
|
43
44
|
"savePreferences": "Save choices",
|
|
44
45
|
"optionalServices": "Optional services",
|
|
45
|
-
"noIntegrations": "No optional services are
|
|
46
|
+
"noIntegrations": "No optional services are active.",
|
|
46
47
|
"privacyPolicy": "Privacy policy",
|
|
47
|
-
"privacyController": "
|
|
48
|
+
"privacyController": "Operator",
|
|
49
|
+
"privacyContact": "Contact",
|
|
48
50
|
"servicePurpose": "Purpose",
|
|
49
51
|
"serviceDataCategories": "Data used",
|
|
50
52
|
"serviceRecipient": "Recipient",
|
|
51
53
|
"serviceRetention": "Retention",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
54
|
+
"servicePrivacyDetails": "Privacy information",
|
|
55
|
+
"servicePrivacyLink": "Open policy",
|
|
56
|
+
"reviewDetails": "Details",
|
|
57
|
+
"essentialStorage": "Choice saved on this device for up to {days} days.",
|
|
55
58
|
"pluginTracking": "Tracking",
|
|
56
59
|
"pluginStatistics": "Statistics",
|
|
57
60
|
"pluginAdvertising": "Advertising",
|
|
58
61
|
"pluginCaptcha": "Human verification",
|
|
59
62
|
"consentReload": "Your choice was saved. Reloading to apply the updated settings.",
|
|
60
63
|
"agentSummary": "Project guides and reference pages for the EdgePress website builder.",
|
|
61
|
-
"closePrivacy": "Close settings"
|
|
64
|
+
"closePrivacy": "Close settings",
|
|
65
|
+
"consentStoredOnDevice": "This device"
|
|
62
66
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"markdownTaskComplete": "任务已完成",
|
|
26
26
|
"markdownTaskIncomplete": "任务未完成",
|
|
27
27
|
"postContents": "文章目录",
|
|
28
|
+
"closeContents": "关闭目录",
|
|
28
29
|
"feed": "Atom 订阅",
|
|
29
30
|
"languageName": "简体中文",
|
|
30
31
|
"mainNavigationLabel": "主导航",
|
|
@@ -37,26 +38,29 @@
|
|
|
37
38
|
"postAuthor": "作者:{author}",
|
|
38
39
|
"privacySettings": "隐私设置",
|
|
39
40
|
"privacyNotice": "隐私选择",
|
|
40
|
-
"privacyIntro": "
|
|
41
|
-
"acceptOptional": "
|
|
41
|
+
"privacyIntro": "可选服务仅在你选择后启用。",
|
|
42
|
+
"acceptOptional": "全部允许",
|
|
42
43
|
"rejectOptional": "拒绝可选服务",
|
|
43
44
|
"savePreferences": "保存选择",
|
|
44
45
|
"optionalServices": "可选服务",
|
|
45
|
-
"noIntegrations": "
|
|
46
|
+
"noIntegrations": "当前没有启用可选服务。",
|
|
46
47
|
"privacyPolicy": "隐私政策",
|
|
47
|
-
"privacyController": "
|
|
48
|
+
"privacyController": "运营者",
|
|
49
|
+
"privacyContact": "联系邮箱",
|
|
48
50
|
"servicePurpose": "用途",
|
|
49
51
|
"serviceDataCategories": "涉及的数据",
|
|
50
52
|
"serviceRecipient": "接收方",
|
|
51
53
|
"serviceRetention": "保留期限",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
54
|
+
"servicePrivacyDetails": "隐私说明",
|
|
55
|
+
"servicePrivacyLink": "查看政策",
|
|
56
|
+
"reviewDetails": "详情",
|
|
57
|
+
"essentialStorage": "选择保存在此设备,最长 {days} 天。",
|
|
55
58
|
"pluginTracking": "跟踪",
|
|
56
59
|
"pluginStatistics": "统计",
|
|
57
60
|
"pluginAdvertising": "广告",
|
|
58
61
|
"pluginCaptcha": "人机验证",
|
|
59
62
|
"consentReload": "选择已保存,正在重新载入以应用设置。",
|
|
60
63
|
"agentSummary": "EdgePress 网站构建器的项目指南与参考文档。",
|
|
61
|
-
"closePrivacy": "关闭设置"
|
|
64
|
+
"closePrivacy": "关闭设置",
|
|
65
|
+
"consentStoredOnDevice": "此设备"
|
|
62
66
|
}
|
package/package.json
CHANGED
package/plugins/consent/index.js
CHANGED
|
@@ -20,9 +20,9 @@ export default function consentManager(api) {
|
|
|
20
20
|
const locale = page?.locale || config.i18n.defaultLocale;
|
|
21
21
|
const ui = Object.fromEntries([
|
|
22
22
|
'privacySettings', 'privacyNotice', 'privacyIntro', 'acceptOptional', 'rejectOptional',
|
|
23
|
-
'savePreferences', 'optionalServices', 'noIntegrations', 'privacyPolicy', 'privacyController',
|
|
24
|
-
'
|
|
25
|
-
'closePrivacy', 'reviewDetails', '
|
|
23
|
+
'savePreferences', 'optionalServices', 'noIntegrations', 'privacyPolicy', 'privacyController', 'privacyContact',
|
|
24
|
+
'serviceDataCategories', 'serviceRecipient', 'serviceRetention', 'servicePrivacyDetails', 'servicePrivacyLink', 'consentReload',
|
|
25
|
+
'closePrivacy', 'reviewDetails', 'essentialStorage', 'pluginTracking',
|
|
26
26
|
'pluginStatistics', 'pluginAdvertising', 'pluginCaptcha'
|
|
27
27
|
].map((key) => [key, translate(config, locale, key)]));
|
|
28
28
|
const services = ['tracking', 'statistics', 'advertising', 'captcha'].flatMap((group) =>
|
|
@@ -34,7 +34,21 @@ export default function consentManager(api) {
|
|
|
34
34
|
integrations: services
|
|
35
35
|
};
|
|
36
36
|
if (privacy.policyUrl.startsWith('/')) privacy.policyUrl = localizedUrl(config, locale, privacy.policyUrl);
|
|
37
|
-
const
|
|
37
|
+
const choiceFingerprint = JSON.stringify({
|
|
38
|
+
controller: config.privacy.controller,
|
|
39
|
+
policyUrl: config.privacy.policyUrl,
|
|
40
|
+
consent: config.browserPlugins.consent,
|
|
41
|
+
integrations: services
|
|
42
|
+
});
|
|
43
|
+
const legacyChoiceFingerprints = config.i18n.locales.map((legacyLocale) => JSON.stringify({
|
|
44
|
+
controller: config.privacy.controller,
|
|
45
|
+
policyUrl: config.privacy.policyUrl.startsWith('/')
|
|
46
|
+
? localizedUrl(config, legacyLocale, config.privacy.policyUrl)
|
|
47
|
+
: config.privacy.policyUrl,
|
|
48
|
+
consent: config.browserPlugins.consent,
|
|
49
|
+
integrations: services
|
|
50
|
+
}));
|
|
51
|
+
const payload = safeJson({ privacy, ui, siteLanguage: locale, defaultLocale: config.i18n.defaultLocale, choiceFingerprint, legacyChoiceFingerprints });
|
|
38
52
|
const integration = '<script type="application/json" id="edgepress-privacy-config">' + payload + '</script>' +
|
|
39
53
|
'<script src="/edgepress/plugins/consent/manager.js" defer></script>';
|
|
40
54
|
return html.replace(/<\/body\s*>/i, integration + '</body>');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"frameworkVersion": "2026.9.
|
|
3
|
+
"frameworkVersion": "2026.9.10",
|
|
4
4
|
"requirements": {
|
|
5
5
|
"nodeMajor": 22,
|
|
6
6
|
"nodeMinimumVersion": "22.12.0",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
{ "name": "Consent-nested browser services and generated privacy disclosures", "stage": "experimental", "since": "2026.9.7" },
|
|
32
32
|
{ "name": "Responsive homepage imagery and chronological all-post archives", "stage": "experimental", "since": "2026.9.7" },
|
|
33
33
|
{ "name": "Date-versioned localized consent disclosures with isolated responsive checks", "stage": "experimental", "since": "2026.9.8" },
|
|
34
|
-
{ "name": "Markdown video syntax and locale-deduplicated chronological post lists", "stage": "experimental", "since": "2026.9.8" }
|
|
34
|
+
{ "name": "Markdown video syntax and locale-deduplicated chronological post lists", "stage": "experimental", "since": "2026.9.8" },
|
|
35
|
+
{ "name": "Floating post contents and compact consent controls", "stage": "experimental", "since": "2026.9.9" },
|
|
36
|
+
{ "name": "Locale-stable consent choices and localized page-block links", "stage": "experimental", "since": "2026.9.10" }
|
|
35
37
|
],
|
|
36
38
|
"releaseHistory": [
|
|
37
39
|
{
|
|
@@ -81,6 +83,18 @@
|
|
|
81
83
|
"date": "2026-09-26",
|
|
82
84
|
"request": 8,
|
|
83
85
|
"notes": "Replace consent version counters with proposed and effective dates; localize service data disclosures; audit the consent component independently from page layout; add Markdown video rendering; fix unique localized archive ordering and Markdown excerpts."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"version": "2026.9.9",
|
|
89
|
+
"date": "2026-09-26",
|
|
90
|
+
"request": 9,
|
|
91
|
+
"notes": "Add a floating post contents widget, compact consent controls, supplier privacy links, and fuller bilingual privacy disclosures."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"version": "2026.9.10",
|
|
95
|
+
"date": "2026-09-27",
|
|
96
|
+
"request": 10,
|
|
97
|
+
"notes": "Keep consent choices across locale changes and migrate existing records; add localized page-block links; make article tags clear; load search only where needed; redraw icons and simplify privacy wording."
|
|
84
98
|
}
|
|
85
99
|
]
|
|
86
100
|
}
|
package/src/config.js
CHANGED
|
@@ -260,7 +260,16 @@ function validateBrowserPlugins(config) {
|
|
|
260
260
|
validateLocalizedServiceText(service.dataCategories, 'Service ' + id + '.dataCategories', 600, locales);
|
|
261
261
|
validateLocalizedServiceText(service.recipient, 'Service ' + id + '.recipient', 200, locales);
|
|
262
262
|
validateLocalizedServiceText(service.retention, 'Service ' + id + '.retention', 300, locales);
|
|
263
|
-
|
|
263
|
+
if (typeof service.privacyUrl !== 'string' || service.privacyUrl.length > 2048 || /[\x00-\x20]/.test(service.privacyUrl)) {
|
|
264
|
+
throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
|
|
265
|
+
}
|
|
266
|
+
let safePrivacyUrl = false;
|
|
267
|
+
try {
|
|
268
|
+
const parsed = new URL(service.privacyUrl);
|
|
269
|
+
safePrivacyUrl = parsed.protocol === 'https:' && !parsed.username && !parsed.password;
|
|
270
|
+
} catch { safePrivacyUrl = false; }
|
|
271
|
+
if (!safePrivacyUrl) throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
|
|
272
|
+
const allowedKeys = new Set(['id', 'provider', 'name', 'purpose', 'dataCategories', 'recipient', 'retention', 'privacyUrl', definition.credential]);
|
|
264
273
|
for (const key of Object.keys(service)) if (!allowedKeys.has(key)) throw new Error('Unsupported option for service ' + id + ': ' + key);
|
|
265
274
|
const credential = service[definition.credential];
|
|
266
275
|
if (typeof credential !== 'string' || !definition.pattern.test(credential)) throw new Error('Service ' + id + ' needs a valid public ' + definition.credential);
|
package/src/generators.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderLayout } from './theme.js';
|
|
|
4
4
|
import { localizedUrl, translate } from './i18n.js';
|
|
5
5
|
import { postsForLocale, slugify, sortPostsNewest } from './content.js';
|
|
6
6
|
import { mapLimit } from './concurrency.js';
|
|
7
|
+
import { renderIcon } from './icons.js';
|
|
7
8
|
|
|
8
9
|
function escapeHtml(value) {
|
|
9
10
|
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
@@ -72,8 +73,14 @@ function addPostContents(html, locale, config) {
|
|
|
72
73
|
const items = headings.map((heading) => '<li class="post-toc-level-' + heading.level + '"><a href="#' +
|
|
73
74
|
escapeHtml(heading.id) + '">' + escapeHtml(heading.label) + '</a></li>').join('');
|
|
74
75
|
const contentsLabel = translate(config, locale, 'postContents');
|
|
75
|
-
const
|
|
76
|
-
|
|
76
|
+
const closeContents = translate(config, locale, 'closeContents');
|
|
77
|
+
const toc = '<aside class="post-toc-widget" data-post-toc hidden>' +
|
|
78
|
+
'<button class="post-toc-toggle" type="button" aria-expanded="false" aria-controls="post-toc-panel" aria-label="' +
|
|
79
|
+
escapeHtml(contentsLabel) + '">' + renderIcon('book-open', 'post-toc-icon') + '<span>' + escapeHtml(contentsLabel) + '</span></button>' +
|
|
80
|
+
'<nav class="post-toc-panel" id="post-toc-panel" aria-labelledby="post-toc-title" aria-label="' +
|
|
81
|
+
escapeHtml(contentsLabel) + '" hidden><div class="post-toc-heading"><h2 id="post-toc-title">' + escapeHtml(contentsLabel) +
|
|
82
|
+
'</h2><button class="post-toc-close" type="button" aria-label="' + escapeHtml(closeContents) + '"><span aria-hidden="true">×</span></button></div>' +
|
|
83
|
+
'<ol>' + items + '</ol></nav></aside>';
|
|
77
84
|
return { html: content, toc };
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -231,8 +238,9 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
231
238
|
const body = '<article class="post"><header><h1>' + escapeHtml(post.title) + '</h1><p class="meta"><time datetime="' + post.date.toISOString() + '">' +
|
|
232
239
|
escapeHtml(dateLabel(post.date, locale)) + '</time>' + (post.author ? ' · ' + renderPostAuthor(post, locale, config) : '') +
|
|
233
240
|
'</p></header>' + renderPostVideo(post.video, locale) + renderedMarkdown.toc + '<div class="post-content">' + renderedMarkdown.html + '</div>' +
|
|
234
|
-
(post.tags.length ? '<
|
|
235
|
-
escapeHtml(localizedUrl(config, locale, 'tags/' + tagSlug(tag) + '/')) + '">' + escapeHtml(tag) + '</a>').join('
|
|
241
|
+
(post.tags.length ? '<nav class="post-tags" aria-label="' + escapeHtml(label('tags')) + '"><span class="post-tags-label">' + escapeHtml(label('tags')) + ':</span><ul>' +
|
|
242
|
+
post.tags.map((tag) => '<li><a rel="tag" href="' + escapeHtml(localizedUrl(config, locale, 'tags/' + tagSlug(tag) + '/')) + '">' + escapeHtml(tag) + '</a></li>').join('') +
|
|
243
|
+
'</ul></nav>' : '') + '</article>';
|
|
236
244
|
const canonicalPath = urlFor(post.path);
|
|
237
245
|
return pageRoute(post.path + 'index.html', post.title, post.description || plainText(post.markdown).slice(0, 160), body,
|
|
238
246
|
locale, config, extensions, {
|
|
@@ -308,8 +316,7 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
308
316
|
'aria-describedby="edgepress-search-hint"><button type="submit">' + escapeHtml(label('searchButton')) + '</button></div>' +
|
|
309
317
|
'<p id="edgepress-search-hint">' + escapeHtml(label('searchHint')) + '</p></form><p id="edgepress-search-status" role="status" aria-live="polite">' +
|
|
310
318
|
escapeHtml(label('searchPrompt')) + '</p><ol id="edgepress-search-results" class="local-search-results"></ol>' +
|
|
311
|
-
'<noscript><p>' + escapeHtml(label('searchNeedsJavaScript')) + '</p></noscript></section>'
|
|
312
|
-
'<script defer src="/edgepress/search.js"></script>';
|
|
319
|
+
'<noscript><p>' + escapeHtml(label('searchNeedsJavaScript')) + '</p></noscript></section>';
|
|
313
320
|
routes.push(await pageRoute(prefix + 'search/index.html', label('searchPosts'), label('searchIntro'), searchBody,
|
|
314
321
|
locale, config, extensions));
|
|
315
322
|
|
package/src/page-blocks.js
CHANGED
|
@@ -29,6 +29,19 @@ function safeUrl(value, label) {
|
|
|
29
29
|
return url;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function localizedSiteUrl(value, label, context) {
|
|
33
|
+
const url = safeUrl(value, label);
|
|
34
|
+
if (!url.startsWith('/')) return url;
|
|
35
|
+
const localeToken = /^\/\[launge\](?=\/|$)/i;
|
|
36
|
+
if (/\[launge\]/i.test(url) && !localeToken.test(url)) {
|
|
37
|
+
throw new Error(label + ' may use [launge] only as the first site-path segment');
|
|
38
|
+
}
|
|
39
|
+
if (!localeToken.test(url)) return url;
|
|
40
|
+
const suffix = url.replace(localeToken, '');
|
|
41
|
+
const prefix = context.locale === context.config.i18n.defaultLocale ? '' : '/' + context.locale;
|
|
42
|
+
return prefix + (suffix || '/');
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
function list(value, label, min = 1, max = 50) {
|
|
33
46
|
if (!Array.isArray(value) || value.length < min || value.length > max) throw new Error(label + ' must contain ' + min + ' to ' + max + ' items');
|
|
34
47
|
return value;
|
|
@@ -86,7 +99,9 @@ function renderPrivacyServices(context) {
|
|
|
86
99
|
escapeHtml(translate(context.config, context.locale, 'serviceRecipient')) + ':</strong> ' +
|
|
87
100
|
escapeHtml(translateValue(context.config, context.locale, service.recipient)) + '</p><p><strong>' +
|
|
88
101
|
escapeHtml(translate(context.config, context.locale, 'serviceRetention')) + ':</strong> ' +
|
|
89
|
-
escapeHtml(translateValue(context.config, context.locale, service.retention)) + '</p
|
|
102
|
+
escapeHtml(translateValue(context.config, context.locale, service.retention)) + '</p><p><a class="privacy-vendor-link" href="' +
|
|
103
|
+
escapeHtml(service.privacyUrl) + '" rel="noopener noreferrer">' +
|
|
104
|
+
escapeHtml(translate(context.config, context.locale, 'servicePrivacyDetails')) + '</a></p></li>').join('') + '</ul>';
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
async function renderLatestPosts(block, context) {
|
|
@@ -113,7 +128,7 @@ async function renderLatestPosts(block, context) {
|
|
|
113
128
|
(cards ? '<div class="post-list">' + cards + '</div>' : '<p>' + escapeHtml(translate(context.config, context.locale, 'noPosts')) + '</p>') + pagination + '</section>';
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
function renderMediaText(block) {
|
|
131
|
+
function renderMediaText(block, context) {
|
|
117
132
|
const mediaType = text(block.mediaType, 'media-text.mediaType', 10);
|
|
118
133
|
if (!['image', 'video'].includes(mediaType)) throw new Error('media-text.mediaType must be image or video');
|
|
119
134
|
const src = escapeHtml(safeUrl(block.src, 'media-text.src'));
|
|
@@ -138,7 +153,7 @@ function renderMediaText(block) {
|
|
|
138
153
|
const content = text(block.text, 'media-text.text', 2000);
|
|
139
154
|
let action = '';
|
|
140
155
|
if (block.cta) {
|
|
141
|
-
action = '<p><a class="button" href="' + escapeHtml(
|
|
156
|
+
action = '<p><a class="button" href="' + escapeHtml(localizedSiteUrl(block.cta.url, 'media-text.cta.url', context)) + '">' +
|
|
142
157
|
escapeHtml(text(block.cta.label, 'media-text.cta.label', 120)) + '</a></p>';
|
|
143
158
|
}
|
|
144
159
|
return '<article class="media-text-block media-text-block--' + placement + '"><figure>' + media +
|
|
@@ -175,7 +190,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
175
190
|
let action = '';
|
|
176
191
|
if (block.cta) {
|
|
177
192
|
const label = escapeHtml(text(block.cta.label, 'hero.cta.label', 120));
|
|
178
|
-
const url = escapeHtml(
|
|
193
|
+
const url = escapeHtml(localizedSiteUrl(block.cta.url, 'hero.cta.url', context));
|
|
179
194
|
action = '<p><a class="button" href="' + url + '">' + label + '</a></p>';
|
|
180
195
|
}
|
|
181
196
|
const highlights = block.highlights === undefined ? [] : list(block.highlights, 'hero.highlights', 1, 4);
|
|
@@ -229,7 +244,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
229
244
|
if (!Array.isArray(paragraphs) || paragraphs.length < 1 || paragraphs.length > 30) throw new Error('text needs a text value or 1 to 30 paragraphs');
|
|
230
245
|
return '<div class="text-widget">' + paragraphs.map((paragraph) => '<p>' + escapeHtml(text(paragraph, 'text paragraph', 4000)) + '</p>').join('') + '</div>';
|
|
231
246
|
}
|
|
232
|
-
case 'media-text': return renderMediaText(block);
|
|
247
|
+
case 'media-text': return renderMediaText(block, context);
|
|
233
248
|
case 'display-text': {
|
|
234
249
|
const value = escapeHtml(text(block.text, 'display-text.text', 1000));
|
|
235
250
|
const style = block.style ?? 'editorial';
|
|
@@ -244,7 +259,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
244
259
|
return '<nav class="link-list"' + (title ? ' aria-label="' + escapeHtml(title) + '"' : '') + '>' +
|
|
245
260
|
(title ? '<h3>' + escapeHtml(title) + '</h3>' : '') + '<ul>' + items.map((item) => {
|
|
246
261
|
const label = escapeHtml(text(item?.label, 'link-list label', 200));
|
|
247
|
-
const url = escapeHtml(
|
|
262
|
+
const url = escapeHtml(localizedSiteUrl(item?.url, 'link-list.url', context));
|
|
248
263
|
const description = text(item?.text, 'link-list.text', 500, true);
|
|
249
264
|
return '<li><a href="' + url + '">' + label + '</a>' + (description ? '<p>' + escapeHtml(description) + '</p>' : '') + '</li>';
|
|
250
265
|
}).join('') + '</ul></nav>';
|
|
@@ -306,7 +321,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
306
321
|
const expiryText = text(block.expiryText, 'privacy-consent.expiryText', 120);
|
|
307
322
|
if (!expiryText.includes('{days}')) throw new Error('privacy-consent.expiryText must include a {days} placeholder');
|
|
308
323
|
const consent = context.config.browserPlugins.consent;
|
|
309
|
-
return '<dl class="privacy-controller-details"><div><dt>' + storageLabel + '</dt><dd>
|
|
324
|
+
return '<dl class="privacy-controller-details"><div><dt>' + storageLabel + '</dt><dd>' + escapeHtml(translate(context.config, context.locale, 'consentStoredOnDevice')) + '</dd></div>' +
|
|
310
325
|
'<div><dt>' + expiryLabel + '</dt><dd>' + escapeHtml(expiryText.replace('{days}', String(consent.expiresDays))) + '</dd></div>' +
|
|
311
326
|
'<div><dt>' + proposedDateLabel + '</dt><dd>' + escapeHtml(dateOnlyLabel(consent.proposedDate, context.locale)) + '</dd></div>' +
|
|
312
327
|
(consent.effectiveDate ? '<div><dt>' + effectiveDateLabel + '</dt><dd>' + escapeHtml(dateOnlyLabel(consent.effectiveDate, context.locale)) + '</dd></div>' : '') + '</dl>';
|
|
@@ -322,8 +337,11 @@ async function renderBlock(block, context, depth, index) {
|
|
|
322
337
|
if (!name || !contact) {
|
|
323
338
|
return '<aside class="notice-widget notice-widget--warning"><h3>' + missingTitle + '</h3><p>' + missingText + '</p></aside>';
|
|
324
339
|
}
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contact);
|
|
341
|
+
const contactValue = email ? '<a href="mailto:' + escapeHtml(contact) + '">' + escapeHtml(contact) + '</a>' : escapeHtml(contact);
|
|
342
|
+
return '<p class="privacy-controller-inline"><span><strong>' + nameLabel + ':</strong> ' + escapeHtml(name) +
|
|
343
|
+
'</span><span class="privacy-controller-separator" aria-hidden="true">·</span><span><strong>' + contactLabel +
|
|
344
|
+
':</strong> ' + contactValue + '</span></p>';
|
|
327
345
|
}
|
|
328
346
|
case 'feature-grid': {
|
|
329
347
|
const items = list(block.items, 'feature-grid.items', 1, 12);
|
|
@@ -331,7 +349,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
331
349
|
const cells = items.map((item) => {
|
|
332
350
|
const title = escapeHtml(text(item?.title, 'feature title', 200));
|
|
333
351
|
const description = escapeHtml(text(item?.text, 'feature text', 1000, true));
|
|
334
|
-
const href = item?.url ?
|
|
352
|
+
const href = item?.url ? localizedSiteUrl(item.url, 'feature.url', context) : '';
|
|
335
353
|
const iconName = item?.icon === undefined ? '' : text(item.icon, 'feature icon', 40);
|
|
336
354
|
if (iconName && !isIconName(iconName)) throw new Error('Unsupported feature icon: ' + iconName);
|
|
337
355
|
const icon = iconName ? renderIcon(iconName, 'feature-icon') : '';
|
|
@@ -356,7 +374,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
356
374
|
const title = escapeHtml(text(block.title, 'cta.title', 240));
|
|
357
375
|
const description = text(block.text, 'cta.text', 1000, true);
|
|
358
376
|
const label = escapeHtml(text(block.label, 'cta.label', 120));
|
|
359
|
-
const url = escapeHtml(
|
|
377
|
+
const url = escapeHtml(localizedSiteUrl(block.url, 'cta.url', context));
|
|
360
378
|
return '<section class="cta-block"><h2>' + title + '</h2>' + (description ? '<p>' + escapeHtml(description) + '</p>' : '') +
|
|
361
379
|
'<p><a class="button" href="' + url + '">' + label + '</a></p></section>';
|
|
362
380
|
}
|
package/src/page-check.js
CHANGED
|
@@ -304,14 +304,16 @@ export async function checkPages(config) {
|
|
|
304
304
|
});
|
|
305
305
|
consentCheck('privacy controls are injected on every generated page', files.length > 0 && htmlWithConsent.length === files.length);
|
|
306
306
|
consentCheck('consent manager script was generated', Boolean(managerScript));
|
|
307
|
-
consentCheck('service disclosure supports data
|
|
308
|
-
managerScript.includes('serviceDataCategories') && managerScript.includes('serviceRecipient') &&
|
|
307
|
+
consentCheck('service disclosure supports data, recipient, retention, and supplier privacy links',
|
|
308
|
+
managerScript.includes('serviceDataCategories') && managerScript.includes('serviceRecipient') &&
|
|
309
|
+
managerScript.includes('serviceRetention') && managerScript.includes('servicePrivacyDetails') &&
|
|
310
|
+
managerScript.includes('integration.privacyUrl'));
|
|
309
311
|
consentCheck('stored choices are tied to proposed and effective dates',
|
|
310
312
|
managerScript.includes('proposedDate: consent.proposedDate') && managerScript.includes('effectiveDate: consent.effectiveDate'));
|
|
311
313
|
consentCheck('new visitors start with every optional service off', managerScript.includes('checkbox.checked = false'));
|
|
312
314
|
consentCheck('accept and reject controls share the same component styling',
|
|
313
|
-
/\.privacy-panel \.privacy-accept
|
|
314
|
-
const componentClass = /(?:^|[\s>+~])\.privacy-(?:manager|panel|settings-button|close|intro|essential|details|
|
|
315
|
+
/\.privacy-manager \.privacy-panel \.privacy-accept\s*,\s*\.privacy-manager \.privacy-panel \.privacy-reject\s*\{[^}]*background:[^}]*\}/.test(styles));
|
|
316
|
+
const componentClass = /(?:^|[\s>+~])\.privacy-(?:manager|panel|settings-button|close|intro|essential|details|category|service-settings|service-setting|service-toggle|service-toggle-text|service-purpose|service-disclosures|actions|accept|reject|save|empty|policy-link|footer|controller)(?=$|[^\w-])/;
|
|
315
317
|
let scoped = true;
|
|
316
318
|
for (const match of styles.matchAll(/([^{}]+)\{[^{}]*\}/g)) {
|
|
317
319
|
const selector = match[1].trim();
|
package/src/theme.js
CHANGED
|
@@ -164,9 +164,17 @@ export async function renderLayout(config, extensions, page, body) {
|
|
|
164
164
|
const html = '<!doctype html>' + rendered;
|
|
165
165
|
const filtered = await extensions.filter('html:afterLayout', html, context);
|
|
166
166
|
if (typeof filtered !== 'string') throw new Error('html:afterLayout filters must return a string');
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
const scripts = [];
|
|
168
|
+
if (!/src=["']\/edgepress\/code-copy\.js["']/i.test(filtered)) scripts.push('<script defer src="/edgepress/code-copy.js"></script>');
|
|
169
|
+
if (/data-post-toc(?:\s|>)/i.test(filtered) && !/src=["']\/edgepress\/post-toc\.js["']/i.test(filtered)) {
|
|
170
|
+
scripts.push('<script defer src="/edgepress/post-toc.js"></script>');
|
|
171
|
+
}
|
|
172
|
+
if (/data-edgepress-search(?:\s|=|>)/i.test(filtered) && !/src=["']\/edgepress\/search\.js["']/i.test(filtered)) {
|
|
173
|
+
scripts.push('<script defer src="/edgepress/search.js"></script>');
|
|
174
|
+
}
|
|
175
|
+
if (!scripts.length) return filtered;
|
|
176
|
+
const integration = scripts.join('');
|
|
169
177
|
return /<\/body\s*>/i.test(filtered)
|
|
170
|
-
? filtered.replace(/<\/body\s*>/i,
|
|
171
|
-
: filtered +
|
|
178
|
+
? filtered.replace(/<\/body\s*>/i, integration + '</body>')
|
|
179
|
+
: filtered + integration;
|
|
172
180
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
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
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
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-
|
|
13
|
+
<symbol id="plug" viewBox="0 0 24 24"><path d="M9 3v6m6-6v6M6 9h12v2a6 6 0 0 1-12 0V9Z"/><path d="M12 17v4"/></symbol>
|
|
14
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
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
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>
|
|
@@ -19,6 +19,6 @@
|
|
|
19
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
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
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"><
|
|
22
|
+
<symbol id="settings" viewBox="0 0 24 24"><path d="M10 2h4l.7 2.5a8 8 0 0 1 1.6.7l2.4-1.1 2 3.5-1.9 1.6a8 8 0 0 1 0 1.6l1.9 1.6-2 3.5-2.4-1.1a8 8 0 0 1-1.6.7L14 22h-4l-.7-2.5a8 8 0 0 1-1.6-.7l-2.4 1.1-2-3.5 1.9-1.6a8 8 0 0 1 0-1.6l-1.9-1.6 2-3.5 2.4 1.1a8 8 0 0 1 1.6-.7z"/><circle cx="12" cy="12" r="2.7"/></symbol>
|
|
23
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
24
|
</svg>
|