.*?<\/div>|
.*?<\/div>/gsu;
+
+const entities = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ ''': "'",
+ ' ': ' ',
+};
+
+export function heroText(html) {
+ const start = html.indexOf('');
+ if (start === -1) throw new Error('no hero header in the page');
+ const end = html.indexOf('', start);
+ return html
+ .slice(start, end)
+ .replace(hidden, ' ')
+ .replace(/<[^>]+>/gu, ' ')
+ .replace(/&[a-z#0-9]+;/gu, (entity) => entities[entity] ?? entity)
+ .replace(/\s+/gu, ' ')
+ .trim();
+}
diff --git a/website/scripts/readme-hero.mjs b/website/scripts/readme-hero.mjs
new file mode 100644
index 0000000000..5407fbe5b6
--- /dev/null
+++ b/website/scripts/readme-hero.mjs
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Renders the README hero images from the built site, so the README shows
+ * the same headline and RuntimeEvents scene as maka.apache.org. Run
+ * `npm --workspace @maka/website run readme-hero` after changing the hero
+ * copy or styles and commit the PNGs it writes to `.github/assets/`.
+ */
+import { execFileSync } from 'node:child_process';
+import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
+import http from 'node:http';
+import { extname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { chromium } from '@playwright/test';
+
+import { heroText } from './hero-text.mjs';
+
+const dist = fileURLToPath(new URL('../dist/', import.meta.url));
+const assets = fileURLToPath(new URL('../../.github/assets/', import.meta.url));
+const types = {
+ '.html': 'text/html',
+ '.css': 'text/css',
+ '.js': 'text/javascript',
+ '.woff2': 'font/woff2',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml',
+};
+
+// The built pages reference /_astro/... absolutely, so serve dist over HTTP.
+const server = http.createServer((request, response) => {
+ let path = join(dist, decodeURIComponent(new URL(request.url, 'http://x').pathname));
+ if (existsSync(path) && statSync(path).isDirectory()) path = join(path, 'index.html');
+ if (!existsSync(path)) {
+ response.writeHead(404);
+ response.end();
+ return;
+ }
+ response.writeHead(200, { 'content-type': types[extname(path)] ?? 'application/octet-stream' });
+ response.end(readFileSync(path));
+});
+await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
+const origin = `http://127.0.0.1:${server.address().port}`;
+
+// The scene only: the README carries the headline, the lede and the links as
+// its own text, and reduced motion shows every event of the turn at once.
+const readmeOnly = `
+ .display, .cta, .fine, .lede { display: none !important; }
+ .hero { padding-top: 32px !important; padding-bottom: 32px !important; }
+ .scene { margin-top: 0 !important; }
+`;
+
+// npm ci installs the Playwright package but not a browser, so a clean
+// checkout has to be able to fetch one before this command can run.
+const executable = (() => {
+ try {
+ return chromium.executablePath();
+ } catch {
+ return undefined;
+ }
+})();
+if (!executable || !existsSync(executable)) {
+ execFileSync(
+ process.platform === 'win32' ? 'npx.cmd' : 'npx',
+ ['playwright', 'install', 'chromium'],
+ {
+ stdio: 'inherit',
+ },
+ );
+}
+
+const manifest = {};
+const browser = await chromium.launch();
+try {
+ for (const locale of ['en', 'zh-CN']) {
+ manifest[locale] = heroText(readFileSync(join(dist, locale, 'index.html'), 'utf8'));
+ for (const colorScheme of ['light', 'dark']) {
+ const page = await browser.newPage({
+ viewport: { width: 1600, height: 1000 },
+ deviceScaleFactor: 2,
+ colorScheme,
+ reducedMotion: 'reduce',
+ });
+ await page.goto(`${origin}/${locale}/`);
+ await page.addStyleTag({ content: readmeOnly });
+ await page.evaluate(() => document.fonts.ready);
+ const path = join(assets, `readme-hero.${locale}.${colorScheme}.png`);
+ await page.locator('.hero').screenshot({ path });
+ console.log(path);
+ await page.close();
+ }
+ }
+ // The copy these images were made from, so the site test can tell when the
+ // pages have moved on and the committed images have not.
+ const path = join(assets, 'readme-hero.json');
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
+ console.log(path);
+} finally {
+ await browser.close();
+ server.close();
+}
diff --git a/website/src/copy/en.ts b/website/src/copy/en.ts
index d017a11cb3..aa5d17b508 100644
--- a/website/src/copy/en.ts
+++ b/website/src/copy/en.ts
@@ -19,10 +19,11 @@
import type { Copy } from './types';
-// The Incubator disclaimer is quoted verbatim from the Incubator branding
-// guide and appears in English on every page, whatever the page language.
+// The disclaimer is the first paragraph of DISCLAIMER-WIP word for word, so
+// the site, the READMEs and the release file all state it the same way. It
+// appears in English on every page, whatever the page language.
export const incubatorDisclaimer =
- 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.';
+ 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.';
export const en: Copy = {
locale: 'en',
@@ -117,7 +118,7 @@ export const en: Copy = {
p: 'Three paths, kept separate on purpose.',
nightly: {
title: 'Try Desktop Nightly',
- body: 'Daily builds from main for developers and testers, published on GitHub Releases. Apple Silicon Macs now; Windows is an unsigned preview.',
+ body: 'Daily builds from main for developers and testers, published on GitHub Releases. macOS on Apple Silicon and Intel; Windows and Linux are unsigned previews.',
note: 'NOT AN ASF RELEASE · MAY BE UNSTABLE',
},
source: {
@@ -188,7 +189,8 @@ export const en: Copy = {
},
nightly: {
label: 'Desktop Nightly',
- value: 'Daily from main, Apple Silicon Macs, Windows unsigned preview.',
+ value:
+ 'Daily from main. macOS arm64 and x64; Windows x64 and Linux x64 and arm64 as unsigned previews.',
note: 'NOT AN ASF RELEASE',
},
source: {
@@ -213,8 +215,8 @@ export const en: Copy = {
nightly: {
h2: 'Desktop Nightly',
note: 'NOT AN ASF RELEASE',
- p: 'Desktop Nightly is built daily from main for developers and testers and published as a GitHub prerelease. Choose the newest Maka Desktop Nightly; after installation the app updates itself on the Nightly channel. It is not an ASF release and is not intended for production use. It targets Apple Silicon Macs.',
- windows: 'Windows is an unsigned preview, not a supported release tier.',
+ p: 'Desktop Nightly is built daily from main for developers and testers and published as a GitHub prerelease. Choose the newest Maka Desktop Nightly; after installation the app updates itself on the Nightly channel. It is not an ASF release and is not intended for production use. It ships for macOS on Apple Silicon and Intel, Windows x64, and Linux x64 and arm64.',
+ windows: 'The Windows and Linux builds are unsigned previews, not a supported release tier.',
},
source: {
h2: 'Build from source',
diff --git a/website/src/copy/zh-CN.ts b/website/src/copy/zh-CN.ts
index 9c1cbc54b6..6773ce82db 100644
--- a/website/src/copy/zh-CN.ts
+++ b/website/src/copy/zh-CN.ts
@@ -98,7 +98,7 @@ export const zhCN: Copy = {
p: '三条路径,边界分明。',
nightly: {
title: '体验 Desktop Nightly',
- body: '每天基于 main 构建,面向开发者和测试者,发布在 GitHub Releases。目前支持 Apple Silicon Mac;Windows 是未签名预览。',
+ body: '每天基于 main 构建,面向开发者和测试者,发布在 GitHub Releases。macOS 覆盖 Apple Silicon 与 Intel;Windows 和 Linux 是未签名预览。',
note: '不是 ASF RELEASE · 可能不稳定',
},
source: {
@@ -168,7 +168,8 @@ export const zhCN: Copy = {
},
nightly: {
label: 'Desktop Nightly',
- value: '每天基于 main 构建,支持 Apple Silicon Mac,Windows 为未签名预览。',
+ value:
+ '每天基于 main 构建。macOS arm64 与 x64;Windows x64、Linux x64 与 arm64 为未签名预览。',
note: '不是 ASF RELEASE',
},
source: {
@@ -193,8 +194,8 @@ export const zhCN: Copy = {
nightly: {
h2: 'Desktop Nightly',
note: '不是 ASF RELEASE',
- p: 'Desktop Nightly 每天基于 main 构建,面向开发者和测试者,以 GitHub prerelease 形式发布。选择最新的 Maka Desktop Nightly;安装后应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产环境。目前仅支持 Apple Silicon Mac。',
- windows: 'Windows 是未签名预览,不属于受支持的发布层级。',
+ p: 'Desktop Nightly 每天基于 main 构建,面向开发者和测试者,以 GitHub prerelease 形式发布。选择最新的 Maka Desktop Nightly;安装后应用会在 Nightly 渠道自动更新。它不是 ASF release,不适合生产环境。提供 macOS 的 Apple Silicon 与 Intel、Windows x64、Linux x64 与 arm64 构建。',
+ windows: 'Windows 和 Linux 构建是未签名预览,不属于受支持的发布层级。',
},
source: {
h2: '从源码构建',
diff --git a/website/test/site.test.mjs b/website/test/site.test.mjs
index 210b16e472..b9c6a8bd26 100644
--- a/website/test/site.test.mjs
+++ b/website/test/site.test.mjs
@@ -25,6 +25,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
+import { heroText } from '../scripts/hero-text.mjs';
+
const dist = new URL('../dist/', import.meta.url);
const page = (path) => readFileSync(new URL(path, dist), 'utf8');
const locales = ['en', 'zh-CN'];
@@ -32,8 +34,12 @@ const pages = ['index.html', 'downloads/index.html'];
const positioning =
'Apache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.';
-const disclaimer =
- 'Apache Maka is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator.';
+// DISCLAIMER-WIP is the project's copy of the Incubator disclaimer, so the
+// site has to carry that paragraph rather than a wording of its own.
+const disclaimer = readFileSync(new URL('../../DISCLAIMER-WIP', import.meta.url), 'utf8')
+ .split('\n\n')[0]
+ .replace(/\s+/gu, ' ')
+ .trim();
// The copyright and trademark line stays in English on every page, like the disclaimer.
const trademark =
'Copyright © 2026 The Apache Software Foundation, licensed under the Apache License, Version 2.0. Apache Maka, Apache Incubator, Apache and the Apache feather logo are trademarks of The Apache Software Foundation.';
@@ -103,6 +109,38 @@ test('the English homepage uses the positioning sentence unchanged', () => {
assert.ok(page('en/index.html').includes(positioning));
});
+// #4307 settled one sentence for the homepage, the READMEs and the repository
+// description. The description is a folded YAML scalar, so compare it unfolded.
+test('the READMEs and the repository description open with the same sentence', () => {
+ const root = new URL('../../', import.meta.url);
+ const read = (path) => readFileSync(new URL(path, root), 'utf8');
+ assert.ok(read('README.md').includes(positioning), 'README.md');
+ assert.ok(
+ read('README.zh-CN.md').includes(
+ 'Apache Maka(孵化中)是一个高性能的 Agent 工作台,并完整记录它做过的每一件事。',
+ ),
+ 'README.zh-CN.md',
+ );
+ const [, description] = read('.asf.yaml').match(/description: >-\n((?: {4}.*\n)+)/u);
+ assert.equal(description.replace(/\s+/gu, ' ').trim(), positioning);
+});
+
+// The README heroes are screenshots of these pages, so the copy the render
+// baked in has to be the copy the pages carry now. Compare through the
+// manifest the render writes, which needs no browser and no pixels.
+test('the committed README heroes were rendered from the current hero copy', () => {
+ const manifest = JSON.parse(
+ readFileSync(new URL('../../.github/assets/readme-hero.json', import.meta.url), 'utf8'),
+ );
+ for (const locale of locales) {
+ assert.equal(
+ heroText(page(`${locale}/index.html`)),
+ manifest[locale],
+ `${locale}: run \`npm --workspace @maka/website run readme-hero\` and commit the images`,
+ );
+ }
+});
+
test('both languages link the same documents', () => {
for (const path of pages) {
const [en, zh] = locales.map((locale) =>