edgepress 2026.9.4 → 2026.9.6
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/CHANGELOG.txt +12 -0
- package/README.md +6 -4
- package/config.yml +11 -6
- package/content/pages/home/en.md +7 -0
- package/content/pages/home/zh-cn.md +8 -1
- package/content/pages/privacy-policy/en.md +1 -1
- package/content/pages/privacy-policy/zh-cn.md +1 -1
- package/content/pages/quick-start/en.md +3 -0
- package/content/pages/quick-start/zh-cn.md +3 -0
- package/content/pages/theme-development/en.md +23 -0
- package/content/pages/theme-development/zh-cn.md +23 -0
- package/content/posts/2026-09-23-edgepress-on-the-edge/en.md +3 -1
- package/content/posts/2026-09-23-edgepress-on-the-edge/zh-cn.md +3 -1
- 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 +19 -0
- package/content/posts/2026-09-25-edgepress-release-2026-9-5/zh-cn.md +19 -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/languages/base/en.json +2 -0
- package/languages/packs/zh-CN.json +2 -0
- package/package.json +4 -2
- package/project-compatibility.json +17 -2
- package/src/cli.js +7 -2
- package/src/content.js +33 -0
- package/src/generators.js +111 -14
- package/src/icons.js +16 -0
- package/src/maintenance.js +0 -27
- package/src/page-blocks.js +29 -8
- package/src/page-check.js +6 -1
- package/src/theme.js +13 -2
- package/static/edgepress/brand/edgepress-mark.svg +7 -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/themes/atelier/assets/style.css +48 -1
- package/themes/atelier/layouts/partials/head.html +5 -0
- package/themes/atelier/layouts/partials/header.html +1 -1
- package/themes/default/assets/style.css +49 -1
- package/themes/default/layouts/partials/head.html +3 -0
- package/themes/default/layouts/partials/header.html +1 -1
- package/themes/signal/assets/style.css +48 -1
- package/themes/signal/layouts/partials/head.html +5 -0
- package/themes/signal/layouts/partials/header.html +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"frameworkVersion": "2026.9.
|
|
3
|
+
"frameworkVersion": "2026.9.6",
|
|
4
4
|
"requirements": {
|
|
5
5
|
"nodeMajor": 22,
|
|
6
6
|
"nodeMinimumVersion": "22.12.0",
|
|
@@ -24,7 +24,10 @@
|
|
|
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
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
|
+
{ "name": "PDF-only audit output and temporary screenshot capture", "stage": "experimental", "since": "2026.9.4" },
|
|
28
|
+
{ "name": "Cloudflare one-click deployment guidance for existing and new projects", "stage": "stable", "since": "2026.9.5" },
|
|
29
|
+
{ "name": "Localized Posts navigation and responsive code copy controls", "stage": "experimental", "since": "2026.9.6" },
|
|
30
|
+
{ "name": "Linked tables of contents on Markdown post pages", "stage": "experimental", "since": "2026.9.6" }
|
|
28
31
|
],
|
|
29
32
|
"releaseHistory": [
|
|
30
33
|
{
|
|
@@ -50,6 +53,18 @@
|
|
|
50
53
|
"date": "2026-09-25",
|
|
51
54
|
"request": 4,
|
|
52
55
|
"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."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"version": "2026.9.5",
|
|
59
|
+
"date": "2026-09-25",
|
|
60
|
+
"request": 5,
|
|
61
|
+
"notes": "Clarify that the Cloudflare one-click deployment button creates a new repository copy and Worker, explain unique naming and the path for updating existing Workers, and publish the guidance in README and bilingual quick-start pages."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"version": "2026.9.6",
|
|
65
|
+
"date": "2026-09-25",
|
|
66
|
+
"request": 6,
|
|
67
|
+
"notes": "Add a localized Posts link, accessible code-copy controls, responsive command wrapping, and linked post contents; refresh the bundled theme palettes and set the canonical site URL and author metadata."
|
|
53
68
|
}
|
|
54
69
|
]
|
|
55
70
|
}
|
package/src/cli.js
CHANGED
|
@@ -322,6 +322,11 @@ async function showReport(title, report, reportPath) {
|
|
|
322
322
|
console.log('Report: ' + reportPath);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
function showStatus(title, report) {
|
|
326
|
+
console.log(title + ': ' + report.status);
|
|
327
|
+
for (const issue of report.issues) console.log('- ' + issue.severity + ': ' + issue.message);
|
|
328
|
+
}
|
|
329
|
+
|
|
325
330
|
const [command, ...args] = process.argv.slice(2);
|
|
326
331
|
try {
|
|
327
332
|
const config = command === 'new' || command === 'clean' || command === 'check' || command === 'doctor' || command === 'iterate'
|
|
@@ -350,11 +355,11 @@ try {
|
|
|
350
355
|
const pageReport = await checkPages(config);
|
|
351
356
|
const compatibilityReport = await checkCompatibility(config);
|
|
352
357
|
await showReport('Page check', pageReport, resolve(config.root, 'tools/page-check.pdf'));
|
|
353
|
-
|
|
358
|
+
showStatus('Compatibility check', compatibilityReport);
|
|
354
359
|
if (pageReport.errors || compatibilityReport.issues.some((issue) => issue.severity === 'error')) process.exitCode = 1;
|
|
355
360
|
} else if (command === 'doctor') {
|
|
356
361
|
const report = await checkCompatibility(config);
|
|
357
|
-
|
|
362
|
+
showStatus('Compatibility check', report);
|
|
358
363
|
if (report.issues.some((issue) => issue.severity === 'error')) process.exitCode = 1;
|
|
359
364
|
} else if (command === 'iterate') {
|
|
360
365
|
await buildSite(config.root);
|
package/src/content.js
CHANGED
|
@@ -47,6 +47,36 @@ function normalizeTags(value) {
|
|
|
47
47
|
return (Array.isArray(value) ? value : [value]).map(String).map((tag) => tag.trim()).filter(Boolean);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function normalizeAuthor(value, file) {
|
|
51
|
+
if (value === undefined || value === null || value === '') return '';
|
|
52
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 160) {
|
|
53
|
+
throw new Error('author must be a non-empty string of at most 160 characters in ' + file);
|
|
54
|
+
}
|
|
55
|
+
return value.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeVideo(value, file) {
|
|
59
|
+
if (value === undefined) return null;
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('video must be an object in ' + file);
|
|
61
|
+
for (const key of Object.keys(value)) {
|
|
62
|
+
if (!['src', 'captions', 'captionLanguage', 'captionLabel', 'poster', 'alt', 'title', 'description'].includes(key)) {
|
|
63
|
+
throw new Error('Unsupported video field in ' + file + ': ' + key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const localAsset = /^\/edgepress\/media\/[a-z0-9-]+\.(?:mp4|vtt|jpe?g)$/i;
|
|
67
|
+
for (const field of ['src', 'captions', 'poster']) {
|
|
68
|
+
if (typeof value[field] !== 'string' || !localAsset.test(value[field])) {
|
|
69
|
+
throw new Error('video.' + field + ' must reference a local /edgepress/media asset in ' + file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (typeof value.captionLanguage !== 'string' || !/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value.captionLanguage) ||
|
|
73
|
+
typeof value.captionLabel !== 'string' || !value.captionLabel.trim() || typeof value.alt !== 'string' || !value.alt.trim() ||
|
|
74
|
+
typeof value.title !== 'string' || !value.title.trim() || typeof value.description !== 'string' || !value.description.trim()) {
|
|
75
|
+
throw new Error('video needs a language, labels, accessible description, title, and summary in ' + file);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
function normalizeDate(value, filename, kind, file) {
|
|
51
81
|
if (value instanceof Date) {
|
|
52
82
|
if (!Number.isNaN(value.valueOf())) return value;
|
|
@@ -145,6 +175,9 @@ export async function readDocuments(config) {
|
|
|
145
175
|
if (locale !== config.i18n.defaultLocale && metadata.homepage !== true) path = locale + '/' + path;
|
|
146
176
|
return {
|
|
147
177
|
kind, file, relativePath, title, slug, date, locale,
|
|
178
|
+
author: kind === 'posts' ? normalizeAuthor(metadata.author, file) : '',
|
|
179
|
+
videoId: (kind === 'posts' ? 'post-' : 'page-') + slugify(bundlePath + '-' + locale.toLowerCase()),
|
|
180
|
+
video: kind === 'posts' ? normalizeVideo(metadata.video, file) : null,
|
|
148
181
|
tags: normalizeTags(metadata.tags),
|
|
149
182
|
description: String(metadata.description ?? ''),
|
|
150
183
|
keywords: normalizeTags(metadata.keywords),
|
package/src/generators.js
CHANGED
|
@@ -42,6 +42,51 @@ function urlPathForRoute(path) {
|
|
|
42
42
|
return '/' + value;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function decodeHeadingEntities(value) {
|
|
46
|
+
return value.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => {
|
|
47
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
|
|
48
|
+
if (entity[0] === '#') {
|
|
49
|
+
const hex = entity[1]?.toLowerCase() === 'x';
|
|
50
|
+
const point = Number.parseInt(entity.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
51
|
+
return Number.isFinite(point) && point > 0 && point <= 0x10ffff ? String.fromCodePoint(point) : match;
|
|
52
|
+
}
|
|
53
|
+
return named[entity.toLowerCase()] ?? match;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addPostContents(html, locale, config) {
|
|
58
|
+
const headings = [];
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
const content = html.replace(/<h([2-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi, (whole, levelText, attributes, inner) => {
|
|
61
|
+
const label = decodeHeadingEntities(inner.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim());
|
|
62
|
+
if (!label) return whole;
|
|
63
|
+
const base = slugify(label);
|
|
64
|
+
const count = (counts.get(base) || 0) + 1;
|
|
65
|
+
counts.set(base, count);
|
|
66
|
+
const id = count === 1 ? base : base + '-' + count;
|
|
67
|
+
headings.push({ id, label, level: Number(levelText) });
|
|
68
|
+
const safeAttributes = attributes.replace(/\s+id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
69
|
+
return '<h' + levelText + safeAttributes + ' id="' + escapeHtml(id) + '">' + inner + '</h' + levelText + '>';
|
|
70
|
+
});
|
|
71
|
+
if (!headings.length) return { html: content, toc: '' };
|
|
72
|
+
const items = headings.map((heading) => '<li class="post-toc-level-' + heading.level + '"><a href="#' +
|
|
73
|
+
escapeHtml(heading.id) + '">' + escapeHtml(heading.label) + '</a></li>').join('');
|
|
74
|
+
const contentsLabel = translate(config, locale, 'postContents');
|
|
75
|
+
const toc = '<nav class="post-toc" aria-label="' + escapeHtml(contentsLabel) + '"><h2 id="post-toc-title">' +
|
|
76
|
+
escapeHtml(contentsLabel) + '</h2><ol>' + items + '</ol></nav>';
|
|
77
|
+
return { html: content, toc };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderPostVideo(video, locale) {
|
|
81
|
+
if (!video) return '';
|
|
82
|
+
const videoTitle = escapeHtml(video.title);
|
|
83
|
+
return '<figure class="post-video"><video controls playsinline preload="metadata" poster="' + escapeHtml(video.poster) + '" aria-label="' +
|
|
84
|
+
escapeHtml(video.alt) + '"><source src="' + escapeHtml(video.src) + '" type="video/mp4"><track kind="captions" src="' +
|
|
85
|
+
escapeHtml(video.captions) + '" srclang="' + escapeHtml(video.captionLanguage) + '" label="' + escapeHtml(video.captionLabel) + '" default>' +
|
|
86
|
+
'<a href="' + escapeHtml(video.src) + '">' + (locale.toLowerCase().startsWith('zh') ? '下载 MP4 视频' : 'Download the MP4 video') +
|
|
87
|
+
'</a></video><figcaption><strong>' + videoTitle + '</strong><br>' + escapeHtml(video.description) + '</figcaption></figure>';
|
|
88
|
+
}
|
|
89
|
+
|
|
45
90
|
async function pageRoute(path, title, description, body, locale, config, extensions, metadata = {}) {
|
|
46
91
|
const urlPath = metadata.urlPath || urlPathForRoute(path);
|
|
47
92
|
const structuredData = metadata.structuredData ?? {
|
|
@@ -53,6 +98,7 @@ async function pageRoute(path, title, description, body, locale, config, extensi
|
|
|
53
98
|
inLanguage: locale,
|
|
54
99
|
isPartOf: config.site.url ? { '@type': 'WebSite', name: config.site.title, url: config.site.url } : undefined
|
|
55
100
|
};
|
|
101
|
+
body = body.replace(/<pre\b/gi, '<pre data-copyable-code');
|
|
56
102
|
let html = await renderLayout(config, extensions, {
|
|
57
103
|
title,
|
|
58
104
|
description,
|
|
@@ -69,21 +115,43 @@ async function pageRoute(path, title, description, body, locale, config, extensi
|
|
|
69
115
|
return fileRoute(path, html);
|
|
70
116
|
}
|
|
71
117
|
|
|
72
|
-
function
|
|
118
|
+
function renderPostAuthor(post, locale, config) {
|
|
119
|
+
if (!post.author) return '';
|
|
120
|
+
return '<span class="post-author">' + escapeHtml(translate(config, locale, 'postAuthor').replace('{author}', post.author)) + '</span>';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderPostCard(post, locale, config) {
|
|
73
124
|
const summary = post.description || plainText(post.markdown).slice(0, 220);
|
|
74
125
|
return '<article class="post-card"><h2><a href="' + escapeHtml(urlFor(post.path)) + '">' + escapeHtml(post.title) + '</a></h2>' +
|
|
75
|
-
'<p class="meta"><time datetime="' + post.date.toISOString() + '">' + escapeHtml(dateLabel(post.date, locale)) + '</time
|
|
126
|
+
'<p class="meta"><time datetime="' + post.date.toISOString() + '">' + escapeHtml(dateLabel(post.date, locale)) + '</time>' +
|
|
127
|
+
(post.author ? ' · ' + renderPostAuthor(post, locale, config) : '') + '</p><p>' +
|
|
76
128
|
escapeHtml(summary) + '</p></article>';
|
|
77
129
|
}
|
|
78
130
|
|
|
79
|
-
async function pageBodyForDocument(page, site, config, locale, isHomepage = false) {
|
|
80
|
-
const content = await renderBlocks(page.blocks, { config, locale, site, isHomepage });
|
|
131
|
+
async function pageBodyForDocument(page, site, config, locale, isHomepage = false, renderContext = {}) {
|
|
132
|
+
const content = await renderBlocks(page.blocks, { config, locale, site, isHomepage, ...renderContext });
|
|
81
133
|
const firstElement = page.blocks[0]?.cells?.[0]?.[0];
|
|
82
134
|
const needsTitle = !isHomepage || firstElement?.type !== 'hero';
|
|
83
135
|
const title = needsTitle ? '<header class="page-title"><h1>' + escapeHtml(page.title) + '</h1></header>' : '';
|
|
84
136
|
return '<article class="page-builder">' + title + content + '</article>';
|
|
85
137
|
}
|
|
86
138
|
|
|
139
|
+
function paginatedLatestPostsSize(page) {
|
|
140
|
+
function find(value) {
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
for (const item of value) {
|
|
143
|
+
const result = find(item);
|
|
144
|
+
if (result !== null) return result;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (!value || typeof value !== 'object') return null;
|
|
149
|
+
if (value.type === 'latest-posts' && value.paginate === true) return value.count ?? 3;
|
|
150
|
+
return find(value.cells) ?? find(value.blocks);
|
|
151
|
+
}
|
|
152
|
+
return find(page?.blocks);
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
function homepageDocument(pages, locale, defaultLocale) {
|
|
88
156
|
return pages.find((page) => page.homepage && page.locale === locale) ||
|
|
89
157
|
pages.find((page) => page.homepage && page.locale === defaultLocale) || null;
|
|
@@ -104,13 +172,24 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
104
172
|
const posts = site.posts.filter((post) => post.locale === locale).sort((a, b) => b.date - a.date);
|
|
105
173
|
const pages = site.pages.filter((page) => page.locale === locale && !page.homepage);
|
|
106
174
|
const homePage = homepageDocument(site.pages, locale, defaultLocale);
|
|
175
|
+
const latestPostsPageSize = paginatedLatestPostsSize(homePage);
|
|
176
|
+
if (latestPostsPageSize !== null && (!Number.isInteger(latestPostsPageSize) || latestPostsPageSize < 1 || latestPostsPageSize > 12)) {
|
|
177
|
+
throw new Error('A paginated latest-posts block must use a count from 1 to 12');
|
|
178
|
+
}
|
|
179
|
+
const homepagePosts = posts.length ? posts : site.posts.filter((post) => post.locale === defaultLocale).sort((a, b) => b.date - a.date);
|
|
180
|
+
const homepageArchivePages = latestPostsPageSize ? Math.ceil(homepagePosts.length / latestPostsPageSize) : 0;
|
|
107
181
|
const prefix = locale === defaultLocale ? '' : locale + '/';
|
|
108
182
|
const label = (key) => translate(config, locale, key);
|
|
109
183
|
const localeHomeUrl = localizedUrl(config, locale, '');
|
|
110
184
|
allPagePaths.add(localeHomeUrl);
|
|
111
185
|
|
|
112
186
|
if (homePage) {
|
|
113
|
-
const
|
|
187
|
+
const latestPostsPagination = latestPostsPageSize ? {
|
|
188
|
+
totalPages: homepageArchivePages,
|
|
189
|
+
archiveUrl: localizedUrl(config, locale, 'archives/'),
|
|
190
|
+
olderUrl: localizedUrl(config, locale, 'archives/page/2/')
|
|
191
|
+
} : null;
|
|
192
|
+
const body = await pageBodyForDocument(homePage, site, config, locale, true, { latestPostsPagination });
|
|
114
193
|
routes.push(await pageRoute(prefix + 'index.html', homePage.title, homePage.description || config.site.description,
|
|
115
194
|
body, locale, config, extensions, {
|
|
116
195
|
urlPath: localeHomeUrl,
|
|
@@ -131,7 +210,7 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
131
210
|
const subset = posts.slice((pageNumber - 1) * config.pagination.perPage, pageNumber * config.pagination.perPage);
|
|
132
211
|
const body = '<section class="intro"><h1>' + escapeHtml(config.site.title) + '</h1><p>' + escapeHtml(config.site.description) +
|
|
133
212
|
'</p></section><section class="post-list" aria-label="' + escapeHtml(label('latestPosts')) + '">' +
|
|
134
|
-
(subset.length ? subset.map((post) => renderPostCard(post, locale)).join('') : '<p>' + escapeHtml(label('noPosts')) + '</p>') +
|
|
213
|
+
(subset.length ? subset.map((post) => renderPostCard(post, locale, config)).join('') : '<p>' + escapeHtml(label('noPosts')) + '</p>') +
|
|
135
214
|
'</section>' + (pageNumber === 1 && pages.length ? '<section class="page-links"><h2>' + escapeHtml(label('pages')) + '</h2><ul>' +
|
|
136
215
|
pages.map((page) => '<li><a href="' + escapeHtml(urlFor(page.path)) + '">' + escapeHtml(page.title) + '</a></li>').join('') + '</ul></section>' : '') +
|
|
137
216
|
(totalPages > 1 ? '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' +
|
|
@@ -145,8 +224,10 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
145
224
|
|
|
146
225
|
for (const post of posts) allPagePaths.add(urlFor(post.path));
|
|
147
226
|
const postRoutes = await mapLimit(posts, config.concurrency, async (post) => {
|
|
227
|
+
const renderedMarkdown = addPostContents(post.html, locale, config);
|
|
148
228
|
const body = '<article class="post"><header><h1>' + escapeHtml(post.title) + '</h1><p class="meta"><time datetime="' + post.date.toISOString() + '">' +
|
|
149
|
-
escapeHtml(dateLabel(post.date, locale)) + '</time
|
|
229
|
+
escapeHtml(dateLabel(post.date, locale)) + '</time>' + (post.author ? ' · ' + renderPostAuthor(post, locale, config) : '') +
|
|
230
|
+
'</p></header>' + renderPostVideo(post.video, locale) + renderedMarkdown.toc + '<div class="post-content">' + renderedMarkdown.html + '</div>' +
|
|
150
231
|
(post.tags.length ? '<p class="tags">' + escapeHtml(label('tags')) + ': ' + post.tags.map((tag) => '<a href="' +
|
|
151
232
|
escapeHtml(localizedUrl(config, locale, 'tags/' + tagSlug(tag) + '/')) + '">' + escapeHtml(tag) + '</a>').join(' ') + '</p>' : '') + '</article>';
|
|
152
233
|
const canonicalPath = urlFor(post.path);
|
|
@@ -157,7 +238,7 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
157
238
|
'@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title,
|
|
158
239
|
description: post.description || plainText(post.markdown).slice(0, 160),
|
|
159
240
|
datePublished: post.date.toISOString(), dateModified: post.date.toISOString(),
|
|
160
|
-
author: { '@type': 'Person', name: config.site.seo.author || config.site.title },
|
|
241
|
+
author: { '@type': 'Person', name: post.author || config.site.seo.author || config.site.title },
|
|
161
242
|
mainEntityOfPage: config.site.url ? config.site.url + canonicalPath : undefined,
|
|
162
243
|
inLanguage: locale
|
|
163
244
|
}
|
|
@@ -176,9 +257,24 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
176
257
|
routes.push(...pageRoutes);
|
|
177
258
|
|
|
178
259
|
if (!homePage) allPagePaths.add(localizedUrl(config, locale, ''));
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
260
|
+
const archivePageSize = latestPostsPageSize || config.pagination.perPage;
|
|
261
|
+
const archivePageCount = Math.max(1, Math.ceil(posts.length / archivePageSize));
|
|
262
|
+
for (let pageNumber = 1; pageNumber <= archivePageCount; pageNumber += 1) {
|
|
263
|
+
const subset = posts.slice((pageNumber - 1) * archivePageSize, pageNumber * archivePageSize);
|
|
264
|
+
const archiveUrl = pageNumber === 1 ? 'archives/' : 'archives/page/' + pageNumber + '/';
|
|
265
|
+
const pagination = archivePageCount > 1
|
|
266
|
+
? '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' +
|
|
267
|
+
(pageNumber > 1 ? '<a rel="prev" href="' + escapeHtml(pageNumber === 2 ? localizedUrl(config, locale, 'archives/') :
|
|
268
|
+
localizedUrl(config, locale, 'archives/page/' + (pageNumber - 1) + '/')) + '">' + escapeHtml(label('newer')) + '</a>' : '') +
|
|
269
|
+
(pageNumber < archivePageCount ? '<a rel="next" href="' + escapeHtml(localizedUrl(config, locale, 'archives/page/' + (pageNumber + 1) + '/')) + '">' +
|
|
270
|
+
escapeHtml(label('older')) + '</a>' : '') + '</nav>' : '';
|
|
271
|
+
const archiveBody = '<section><h1>' + escapeHtml(label('archives')) + '</h1>' +
|
|
272
|
+
(subset.length ? subset.map((post) => renderPostCard(post, locale, config)).join('') : '<p>' + escapeHtml(label('noPosts')) + '</p>') + pagination + '</section>';
|
|
273
|
+
routes.push(await pageRoute(prefix + (pageNumber === 1 ? 'archives/index.html' : 'archives/page/' + pageNumber + '/index.html'),
|
|
274
|
+
pageNumber === 1 ? label('archives') : label('archives') + ' · ' + pageNumber, label('allPosts'), archiveBody, locale, config, extensions,
|
|
275
|
+
{ urlPath: localizedUrl(config, locale, archiveUrl) }));
|
|
276
|
+
allPagePaths.add(localizedUrl(config, locale, archiveUrl));
|
|
277
|
+
}
|
|
182
278
|
|
|
183
279
|
const tags = new Map();
|
|
184
280
|
for (const post of posts) for (const tag of post.tags) {
|
|
@@ -188,14 +284,14 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
188
284
|
}
|
|
189
285
|
const tagRoutes = await mapLimit([...tags.entries()], config.concurrency, async ([slug, value]) => {
|
|
190
286
|
const body = '<section><h1>' + escapeHtml(label('tag')) + ': ' + escapeHtml(value.label) + '</h1>' +
|
|
191
|
-
value.posts.map((post) => renderPostCard(post, locale)).join('') + '</section>';
|
|
287
|
+
value.posts.map((post) => renderPostCard(post, locale, config)).join('') + '</section>';
|
|
192
288
|
return pageRoute(prefix + 'tags/' + slug + '/index.html', label('tag') + ': ' + value.label,
|
|
193
289
|
label('tag') + ' ' + value.label, body, locale, config, extensions);
|
|
194
290
|
});
|
|
195
291
|
routes.push(...tagRoutes);
|
|
196
292
|
|
|
197
293
|
const searchable = [
|
|
198
|
-
...posts.map((post) => ({ title: post.title, date: post.date.toISOString(), url: urlFor(post.path), summary: post.description || plainText(post.markdown).slice(0, 220), content: plainText(post.markdown), type: 'post' })),
|
|
294
|
+
...posts.map((post) => ({ title: post.title, author: post.author, date: post.date.toISOString(), url: urlFor(post.path), summary: post.description || plainText(post.markdown).slice(0, 220), content: plainText(post.markdown), type: 'post' })),
|
|
199
295
|
...pages.map((page) => ({ title: page.title, url: urlFor(page.path), summary: page.description || plainText(page.markdown).slice(0, 220), type: 'page' }))
|
|
200
296
|
];
|
|
201
297
|
routes.push(fileRoute(prefix + 'search.json', JSON.stringify(searchable), 'application/json; charset=utf-8'));
|
|
@@ -213,7 +309,8 @@ export async function generateBuiltinRoutes(site, config, extensions) {
|
|
|
213
309
|
locale, config, extensions));
|
|
214
310
|
|
|
215
311
|
const base = config.site.url;
|
|
216
|
-
const entries = posts.slice(0, 20).map((post) => '<entry><title>' + escapeXml(post.title) + '</title
|
|
312
|
+
const entries = posts.slice(0, 20).map((post) => '<entry><title>' + escapeXml(post.title) + '</title>' +
|
|
313
|
+
(post.author ? '<author><name>' + escapeXml(post.author) + '</name></author>' : '') + '<link href="' +
|
|
217
314
|
escapeXml(base + urlFor(post.path)) + '"/><id>' + escapeXml(base + urlFor(post.path)) + '</id><updated>' + post.date.toISOString() +
|
|
218
315
|
'</updated><summary>' + escapeXml(post.description || plainText(post.markdown).slice(0, 220)) + '</summary></entry>').join('');
|
|
219
316
|
routes.push(fileRoute(prefix + 'feed.xml', '<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>' +
|
package/src/icons.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const iconNames = new Set([
|
|
2
|
+
'home', 'book-open', 'rocket', 'newspaper', 'search', 'shield-check', 'rss', 'file-text',
|
|
3
|
+
'layers', 'code', 'puzzle', 'plug', 'cloud', 'palette', 'globe', 'help-circle', 'arrow-up-right',
|
|
4
|
+
'check-circle', 'layout-grid', 'terminal', 'settings', 'sparkles'
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export function isIconName(name) {
|
|
8
|
+
return typeof name === 'string' && iconNames.has(name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderIcon(name, className = 'icon') {
|
|
12
|
+
if (!isIconName(name)) throw new Error('Unsupported EdgePress icon: ' + String(name));
|
|
13
|
+
if (!/^[a-z][a-z0-9-]*$/.test(className)) throw new Error('Icon class must be a simple CSS identifier');
|
|
14
|
+
return '<svg class="' + className + '" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="/edgepress/icons.svg#' +
|
|
15
|
+
name + '"></use></svg>';
|
|
16
|
+
}
|
package/src/maintenance.js
CHANGED
|
@@ -17,29 +17,6 @@ function versionAtLeast(current, minimum) {
|
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function markdown(report) {
|
|
21
|
-
const lines = [
|
|
22
|
-
'# EdgePress compatibility report',
|
|
23
|
-
'',
|
|
24
|
-
'Status: **' + report.status + '**',
|
|
25
|
-
'',
|
|
26
|
-
'Node.js: ' + report.runtime.node,
|
|
27
|
-
'Minimum required Node.js: ' + report.requirements.nodeMinimumVersion,
|
|
28
|
-
'Workers compatibility date: ' + report.runtime.workersDate,
|
|
29
|
-
'Minimum required date: ' + report.requirements.minimumWorkersDate,
|
|
30
|
-
'',
|
|
31
|
-
'## Findings',
|
|
32
|
-
''
|
|
33
|
-
];
|
|
34
|
-
if (!report.issues.length) lines.push('No compatibility findings.');
|
|
35
|
-
for (const issue of report.issues) lines.push('- **' + issue.severity.toUpperCase() + '** ' + issue.message);
|
|
36
|
-
lines.push('', '## Feature lifecycle', '');
|
|
37
|
-
for (const feature of report.features) {
|
|
38
|
-
lines.push('- ' + feature.name + ': ' + feature.stage + ' (since ' + feature.since + ')');
|
|
39
|
-
}
|
|
40
|
-
return lines.join('\n');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
20
|
export async function checkCompatibility(config) {
|
|
44
21
|
const root = config.root;
|
|
45
22
|
const manifest = await readJson(resolve(root, 'project-compatibility.json'));
|
|
@@ -77,10 +54,6 @@ export async function checkCompatibility(config) {
|
|
|
77
54
|
features: manifest.features,
|
|
78
55
|
issues
|
|
79
56
|
};
|
|
80
|
-
const directory = resolve(config.resolvedPaths.cache, 'reports');
|
|
81
|
-
await mkdir(directory, { recursive: true });
|
|
82
|
-
await writeFile(resolve(directory, 'compatibility.json'), JSON.stringify(report, null, 2), 'utf8');
|
|
83
|
-
await writeFile(resolve(directory, 'compatibility.md'), markdown(report), 'utf8');
|
|
84
57
|
return report;
|
|
85
58
|
}
|
|
86
59
|
|
package/src/page-blocks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { plainText } from './markdown.js';
|
|
2
2
|
import { translate } from './i18n.js';
|
|
3
|
+
import { isIconName, renderIcon } from './icons.js';
|
|
3
4
|
|
|
4
5
|
const captchaProviders = new Set(['cloudflare-turnstile', 'google-recaptcha', 'hcaptcha']);
|
|
5
6
|
|
|
@@ -65,14 +66,24 @@ function renderPrivacyServices(context) {
|
|
|
65
66
|
function renderLatestPosts(block, context) {
|
|
66
67
|
const count = block.count ?? 3;
|
|
67
68
|
if (!Number.isInteger(count) || count < 1 || count > 12) throw new Error('latest-posts.count must be an integer from 1 to 12');
|
|
69
|
+
const paginate = block.paginate ?? false;
|
|
70
|
+
if (typeof paginate !== 'boolean') throw new Error('latest-posts.paginate must be a boolean');
|
|
68
71
|
const posts = context.site.posts.filter((post) => post.locale === context.locale ||
|
|
69
|
-
(!context.site.posts.some((item) => item.locale === context.locale) && post.locale === context.config.i18n.defaultLocale))
|
|
72
|
+
(!context.site.posts.some((item) => item.locale === context.locale) && post.locale === context.config.i18n.defaultLocale))
|
|
73
|
+
.sort((a, b) => b.date - a.date);
|
|
70
74
|
const title = escapeHtml(text(block.title, 'latest-posts.title', 200));
|
|
71
|
-
const
|
|
75
|
+
const authorLabel = (post) => post.author ? '<span class="post-author">' + escapeHtml(translate(context.config, context.locale, 'postAuthor')
|
|
76
|
+
.replace('{author}', post.author)) + '</span>' : '';
|
|
77
|
+
const cards = posts.slice(0, count).map((post) => '<article class="post-card"><h3><a href="' + escapeHtml('/' + post.path.split('/').filter(Boolean).join('/') + (post.path.endsWith('/') ? '/' : '')) + '">' +
|
|
72
78
|
escapeHtml(post.title) + '</a></h3><p class="meta"><time datetime="' + post.date.toISOString() + '">' + escapeHtml(dateLabel(post.date, context.locale)) +
|
|
73
|
-
'</time
|
|
79
|
+
'</time>' + (post.author ? ' · ' + authorLabel(post) : '') + '</p><p>' + escapeHtml(post.description || plainText(post.markdown).slice(0, 220)) + '</p></article>').join('');
|
|
80
|
+
const pagination = paginate && context.latestPostsPagination?.totalPages > 1
|
|
81
|
+
? '<nav class="pagination latest-posts-pagination" aria-label="' + escapeHtml(translate(context.config, context.locale, 'pagination')) + '">' +
|
|
82
|
+
'<a rel="next" href="' + escapeHtml(context.latestPostsPagination.olderUrl) + '">' + escapeHtml(translate(context.config, context.locale, 'older')) + '</a>' +
|
|
83
|
+
'<a href="' + escapeHtml(context.latestPostsPagination.archiveUrl) + '">' + escapeHtml(translate(context.config, context.locale, 'allPosts')) + '</a></nav>'
|
|
84
|
+
: '';
|
|
74
85
|
return '<section class="latest-posts"><h2>' + title + '</h2>' +
|
|
75
|
-
(cards ? '<div class="post-list">' + cards + '</div>' : '<p>' + escapeHtml(translate(context.config, context.locale, 'noPosts')) + '</p>') + '</section>';
|
|
86
|
+
(cards ? '<div class="post-list">' + cards + '</div>' : '<p>' + escapeHtml(translate(context.config, context.locale, 'noPosts')) + '</p>') + pagination + '</section>';
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
function renderMediaText(block) {
|
|
@@ -88,12 +99,13 @@ function renderMediaText(block) {
|
|
|
88
99
|
media = '<img src="' + src + '" alt="' + alt + '" loading="lazy" decoding="async">';
|
|
89
100
|
} else {
|
|
90
101
|
if (!alt.trim()) throw new Error('media-text.alt must describe the video');
|
|
102
|
+
const videoSrc = escapeHtml(safeUrl(block.src, 'media-text.src'));
|
|
91
103
|
const captions = escapeHtml(safeUrl(block.captions, 'media-text.captions'));
|
|
92
104
|
const language = escapeHtml(text(block.captionLanguage, 'media-text.captionLanguage', 24));
|
|
93
105
|
if (!/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(language)) throw new Error('media-text.captionLanguage must be a language tag');
|
|
94
106
|
const trackLabel = escapeHtml(text(block.captionLabel, 'media-text.captionLabel', 120));
|
|
95
107
|
const poster = block.poster ? ' poster="' + escapeHtml(safeUrl(block.poster, 'media-text.poster')) + '"' : '';
|
|
96
|
-
media = '<video controls preload="
|
|
108
|
+
media = '<video controls playsinline preload="metadata" aria-label="' + alt + '"' + poster + '><source src="' + videoSrc + '" type="video/mp4"><track kind="captions" src="' + captions + '" srclang="' + language + '" label="' + trackLabel + '" default></video>';
|
|
97
109
|
}
|
|
98
110
|
const title = text(block.title, 'media-text.title', 240, true);
|
|
99
111
|
const content = text(block.text, 'media-text.text', 2000);
|
|
@@ -117,6 +129,9 @@ async function renderBlock(block, context, depth, index) {
|
|
|
117
129
|
const level = context.isHomepage && index === 0 ? 'h1' : 'h2';
|
|
118
130
|
const eyebrow = text(block.eyebrow, 'hero.eyebrow', 120, true);
|
|
119
131
|
const description = text(block.text, 'hero.text', 1000, true);
|
|
132
|
+
const mascotSrc = block.mascotSrc === undefined ? '' : safeUrl(block.mascotSrc, 'hero.mascotSrc');
|
|
133
|
+
const mascotAlt = block.mascotAlt === undefined ? '' : text(block.mascotAlt, 'hero.mascotAlt', 500);
|
|
134
|
+
if (Boolean(mascotSrc) !== Boolean(mascotAlt)) throw new Error('hero.mascotSrc and hero.mascotAlt must be provided together');
|
|
120
135
|
let action = '';
|
|
121
136
|
if (block.cta) {
|
|
122
137
|
const label = escapeHtml(text(block.cta.label, 'hero.cta.label', 120));
|
|
@@ -126,9 +141,12 @@ async function renderBlock(block, context, depth, index) {
|
|
|
126
141
|
const highlights = block.highlights === undefined ? [] : list(block.highlights, 'hero.highlights', 1, 4);
|
|
127
142
|
const highlightPanel = highlights.length ? '<ul class="hero-highlights">' + highlights.map((item) => '<li>' +
|
|
128
143
|
escapeHtml(text(item, 'hero highlight', 180)) + '</li>').join('') + '</ul>' : '';
|
|
129
|
-
|
|
144
|
+
const mascot = mascotSrc ? '<img class="hero-mascot" src="' + escapeHtml(mascotSrc) + '" alt="' +
|
|
145
|
+
escapeHtml(mascotAlt) + '" width="1222" height="1287" decoding="async">' : '';
|
|
146
|
+
const aside = mascot || highlightPanel ? '<div class="hero-aside">' + mascot + highlightPanel + '</div>' : '';
|
|
147
|
+
return '<section class="hero-block' + (aside ? ' hero-block--split' : '') + '"><div class="hero-main">' +
|
|
130
148
|
(eyebrow ? '<p class="eyebrow">' + escapeHtml(eyebrow) + '</p>' : '') + '<' + level + '>' + escapeHtml(heading) + '</' + level + '>' +
|
|
131
|
-
(description ? '<p class="hero-copy">' + escapeHtml(description) + '</p>' : '') + action + '</div>' +
|
|
149
|
+
(description ? '<p class="hero-copy">' + escapeHtml(description) + '</p>' : '') + action + '</div>' + aside + '</section>';
|
|
132
150
|
}
|
|
133
151
|
case 'section': {
|
|
134
152
|
const heading = text(block.title, 'section.title', 200, true);
|
|
@@ -269,7 +287,10 @@ async function renderBlock(block, context, depth, index) {
|
|
|
269
287
|
const title = escapeHtml(text(item?.title, 'feature title', 200));
|
|
270
288
|
const description = escapeHtml(text(item?.text, 'feature text', 1000, true));
|
|
271
289
|
const href = item?.url ? safeUrl(item.url, 'feature.url') : '';
|
|
272
|
-
|
|
290
|
+
const iconName = item?.icon === undefined ? '' : text(item.icon, 'feature icon', 40);
|
|
291
|
+
if (iconName && !isIconName(iconName)) throw new Error('Unsupported feature icon: ' + iconName);
|
|
292
|
+
const icon = iconName ? renderIcon(iconName, 'feature-icon') : '';
|
|
293
|
+
return '<article class="feature-card">' + icon + '<h3>' + (href ? '<a href="' + escapeHtml(href) + '">' + title + '</a>' : title) + '</h3>' +
|
|
273
294
|
(description ? '<p>' + description + '</p>' : '') + '</article>';
|
|
274
295
|
}).join('');
|
|
275
296
|
return '<section class="feature-section"><h2>' + escapeHtml(heading) + '</h2><div class="feature-grid">' + cells + '</div></section>';
|
package/src/page-check.js
CHANGED
|
@@ -119,7 +119,12 @@ async function resolveLocalLink(output, pageFile, href) {
|
|
|
119
119
|
else candidates.push(resolve(output, clean + '.html'), resolve(output, clean, 'index.html'));
|
|
120
120
|
for (const candidate of candidates) {
|
|
121
121
|
if (!candidate.startsWith(output + sep)) continue;
|
|
122
|
-
if (await exists(candidate))
|
|
122
|
+
if (await exists(candidate)) {
|
|
123
|
+
let fragment = url.hash.slice(1);
|
|
124
|
+
try { fragment = decodeURIComponent(fragment); }
|
|
125
|
+
catch { return { broken: true, target: href }; }
|
|
126
|
+
return { file: candidate, fragment };
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
129
|
return { broken: true, target: href };
|
|
125
130
|
}
|
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
|
|
@@ -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>
|