Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/assets/readme-hero.json
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
{
"en": "confirmed up to here Model says \"I'll rerun the failing test.\" Runs a command Bash · npm test Asks permission leaves the sandbox You approve written to the log Gets the result exit 1 · pruned, kept Edits a file resume.ts Turn ends run completed one turn · seven RuntimeEvents · append-only State(t) = Project(Log[0…t])",
"zh-CN": "到这里已确认 模型说 「我重新跑一下失败的测试。」 执行命令 Bash · npm test 请求权限 超出沙箱 你批准了 已写进日志 拿到结果 exit 1 · 裁剪展示,全量保留 编辑文件 resume.ts 本轮结束 运行完成 一轮交互 · 7 条运行时事件 · 只追加写入 State(t) = Project(Log[0…t])"
"zh-CN": "到这里已确认 模型说 「我重新跑一下失败的测试。」 执行命令 Bash · npm test 请求权限 超出沙箱 你批准了 已写进日志 拿到结果 exit 1 · 裁剪展示,全量保留 编辑文件 resume.ts 本轮结束 运行完成 一轮交互 · 7 条运行时事件 · 只追加写入 State(t) = Project(Log[0…t])",
"headline": {
"en": "A high-performance agent workspace that keeps a complete record of everything it did.",
"zh-CN": "一个高性能的 Agent 工作台, 并完整记录 它做过的每一件事。"
}
}
23 changes: 16 additions & 7 deletions website/scripts/hero-text.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,15 +34,24 @@ const entities = {
' ': ' ',
};

export function heroText(html) {
const start = html.indexOf('<header class="hero">');
if (start === -1) throw new Error('no hero header in the page');
const end = html.indexOf('</header>', start);
return html
.slice(start, end)
.replace(hidden, ' ')
// The social preview image keeps the headline above the scene, so it has
// this on top of what the README hero has.
export function headlineText(html) {
const [, headline] = html.match(/<h1 class="display">(.*?)<\/h1>/su) ?? [];
if (!headline) throw new Error('no hero headline in the page');
return text(headline);
}

const text = (html) =>
html
.replace(/<[^>]+>/gu, ' ')
.replace(/&[a-z#0-9]+;/gu, (entity) => entities[entity] ?? entity)
.replace(/\s+/gu, ' ')
.trim();

export function heroText(html) {
const start = html.indexOf('<header class="hero">');
if (start === -1) throw new Error('no hero header in the page');
const end = html.indexOf('</header>', start);
return text(html.slice(start, end).replace(hidden, ' '));
}
52 changes: 46 additions & 6 deletions website/scripts/readme-hero.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,10 +18,12 @@
*/

/**
* Renders the README hero images from the built site, so the README shows
* the same headline and RuntimeEvents scene as maka.apache.org. Run
* Renders the README hero images and the social preview images from the
* built site, so the README and every link card show 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/`.
* copy or styles and commit the PNGs it writes to `.github/assets/` and
* `website/src/assets/`.
*/
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
Expand All@@ -31,10 +33,11 @@ import { fileURLToPath } from 'node:url';

import { chromium } from '@playwright/test';

import { heroText } from './hero-text.mjs';
import { headlineText, 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 social = fileURLToPath(new URL('../src/assets/', import.meta.url));
const types = {
'.html': 'text/html',
'.css': 'text/css',
Expand DownExpand Up@@ -67,6 +70,25 @@ const readmeOnly = `
.scene { margin-top: 0 !important; }
`;

// The social preview: the brand, the headline and the scene, centred in the
// 1200×630 frame link previews are cut to. Light only, since a preview shows
// on the sharing site's own background, and the sections below the hero are
// dropped so the frame ends where the scene does.
const socialOnly = `
.lede, .cta, .fine, .hero ~ *, footer { display: none !important; }
.nav > :not(.brand) { display: none !important; }
html, body { overflow: hidden !important; }
.hero {
box-sizing: border-box !important;
min-height: 570px !important;
padding: 0 0 12px !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
}
.scene { margin-top: 28px !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 = (() => {
Expand All@@ -87,10 +109,13 @@ if (!executable || !existsSync(executable)) {
}

const manifest = {};
const headlines = {};
const browser = await chromium.launch();
try {
for (const locale of ['en', 'zh-CN']) {
manifest[locale] = heroText(readFileSync(join(dist, locale, 'index.html'), 'utf8'));
const html = readFileSync(join(dist, locale, 'index.html'), 'utf8');
manifest[locale] = heroText(html);
headlines[locale] = headlineText(html);
for (const colorScheme of ['light', 'dark']) {
const page = await browser.newPage({
viewport: { width: 1600, height: 1000 },
Expand All@@ -106,9 +131,24 @@ try {
console.log(path);
await page.close();
}
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 2,
colorScheme: 'light',
reducedMotion: 'reduce',
});
await page.goto(`${origin}/${locale}/`);
await page.addStyleTag({ content: socialOnly });
await page.evaluate(() => document.fonts.ready);
const path = join(social, `social.${locale}.png`);
await page.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.
// pages have moved on and the committed images have not. The README heroes
// show the scene; the social previews show the headline above it.
manifest.headline = headlines;
const path = join(assets, 'readme-hero.json');
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(path);
Expand Down
Binary file addedwebsite/src/assets/social.en.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file addedwebsite/src/assets/social.zh-CN.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions website/src/components/Preview.astro
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
/*
* 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.
*/
// The tags a link preview on X, Slack and the like is built from; nothing
// else on the page reaches the card. The image is a render of the homepage
// hero in the page's language, made by scripts/readme-hero.mjs, and the
// crawlers need its URL to be absolute.
import type { ImageMetadata } from 'astro';
import socialEn from '../assets/social.en.png';
import socialZhCN from '../assets/social.zh-CN.png';
import type { Copy, Locale } from '../copy/types';

const images: Record<Locale, ImageMetadata> = { en: socialEn, 'zh-CN': socialZhCN };
const ogLocales: Record<Locale, string> = { en: 'en_US', 'zh-CN': 'zh_CN' };

interface Props {
copy: Copy;
url: URL;
title: string;
description: string;
}

const { copy, url, title, description } = Astro.props;
const image = images[copy.locale];
const src = new URL(image.src, Astro.site);
// The card's alt: what the site says it is, then what the picture shows.
const alt = `${copy.positioning} ${copy.sceneAlt}`;
---

<meta property="og:type" content="website" />
<meta property="og:site_name" content={copy.siteName} />
<meta property="og:url" content={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:locale" content={ogLocales[copy.locale]} />
<meta property="og:image" content={src} />
<meta property="og:image:width" content={String(image.width)} />
<meta property="og:image:height" content={String(image.height)} />
<meta property="og:image:alt" content={alt} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={src} />
<meta name="twitter:image:alt" content={alt} />
2 changes: 2 additions & 0 deletions website/src/copy/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,8 @@ export const en: Copy = {
positioning:
'Apache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.',
theme: { toDark: 'Switch to dark mode', toLight: 'Switch to light mode' },
sceneAlt:
'One turn of RuntimeEvents: the model speaks, runs a command, asks permission, you approve, it gets the result, edits a file, the turn ends.',
nav: {
docs: 'Docs',
downloads: 'Downloads',
Expand Down
3 changes: 3 additions & 0 deletions website/src/copy/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,9 @@ export interface Copy {
siteName: string;
positioning: string;
theme: { toDark: string; toLight: string };
// The README hero's alt text, which describes the same scene the social
// preview shows; the site test holds it to README.md's `<img alt>`.
sceneAlt: string;
nav: {
docs: string;
downloads: string;
Expand Down
2 changes: 2 additions & 0 deletions website/src/copy/zh-CN.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,8 @@ export const zhCN: Copy = {
siteName: 'Apache Maka (Incubating)',
positioning: 'Apache Maka(孵化中)是一个高性能的 Agent 工作台,并完整记录它做过的每一件事。',
theme: { toDark: '切换到深色模式', toLight: '切换到浅色模式' },
sceneAlt:
'一轮交互的运行时事件:模型说、执行命令、请求权限、你批准了、拿到结果、编辑文件、本轮结束。',
nav: {
docs: '文档',
downloads: '下载',
Expand Down
2 changes: 2 additions & 0 deletions website/src/layouts/Site.astro
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import '../styles/site.css';
import { Image } from 'astro:assets';
import logo from '../../../apps/desktop/assets/app-icons/sky.png';
import incubator from '../assets/incubator.png';
import Preview from '../components/Preview.astro';
import { links } from '../copy/links';
import type { Copy, Locale } from '../copy/types';

Expand DownExpand Up@@ -70,6 +71,7 @@ const asf = [
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<Preview copy={copy} url={canonical} title={title} description={description} />
{locales.map((locale) => <link rel="alternate" hreflang={locale} href={new URL(href(locale), Astro.site)} />)}
<link rel="alternate" hreflang="x-default" href={new URL(href('en'), Astro.site)} />
<link rel="icon" type="image/png" href={logo.src} />
Expand Down
14 changes: 11 additions & 3 deletions website/src/pages/index.astro
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,12 @@
*/
// The root sends visitors to the English homepage at once. Astro's own
// redirect page waits two seconds first, so it is written out by hand.
// Link-preview crawlers stop here rather than follow the refresh, so the
// page carries the English homepage's preview tags itself.
import Preview from '../components/Preview.astro';
import { en } from '../copy/en';

const home = new URL('/en/', Astro.site);
---

<!doctype html>
Expand All@@ -27,11 +33,13 @@
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0;url=/en/" />
<meta name="robots" content="noindex" />
<link rel="canonical" href="https://maka.apache.org/en/" />
<title>Apache Maka (Incubating)</title>
<link rel="canonical" href={home} />
<title>{en.siteName}</title>
<meta name="description" content={en.positioning} />
<Preview copy={en} url={home} title={en.siteName} description={en.positioning} />
<script is:inline>
location.replace('/en/');
</script>
</head>
<body><a href="/en/">Apache Maka (Incubating)</a></body>
<body><a href="/en/">{en.siteName}</a></body>
</html>
61 changes: 53 additions & 8 deletions website/test/site.test.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,12 +22,13 @@
* the HTML that will be published rather than on the source that produced it.
*/
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, statSync } from 'node:fs';
import test from 'node:test';

import { heroText } from '../scripts/hero-text.mjs';
import { headlineText, heroText } from '../scripts/hero-text.mjs';

const dist = new URL('../dist/', import.meta.url);
const repo = new URL('../../', import.meta.url);
const page = (path) => readFileSync(new URL(path, dist), 'utf8');
const locales = ['en', 'zh-CN'];
const pages = ['index.html', 'downloads/index.html'];
Expand DownExpand Up@@ -66,6 +67,51 @@ test('the root redirects to the English homepage without a delay', () => {
assert.match(page('index.html'), /content="0;url=\/en\/"/u);
});

const readmeAlt = (locale) =>
readFileSync(new URL(locale === 'en' ? 'README.md' : 'README.zh-CN.md', repo), 'utf8').match(
/<img alt="([^"]+)" src="\.\/\.github\/assets\/readme-hero\./u,
)[1];

const meta = (html, key) =>
[...html.matchAll(/<meta (?:property|name)="([^"]+)" content="([^"]*)"/gu)].find(
([, name]) => name === key,
)?.[2];

// Link previews on X, Slack and the like are built from these tags alone, so
// every page carries them, the image URL is absolute and the image ships. The
// root page too: crawlers read it as-is rather than follow the meta refresh.
test('every page carries a complete link preview', () => {
for (const path of ['index.html', ...locales.flatMap((l) => pages.map((p) => `${l}/${p}`))]) {
const html = page(path);
const locale = path.startsWith('zh-CN/') ? 'zh-CN' : 'en';
for (const key of ['og:title', 'og:description', 'og:url']) {
assert.ok(meta(html, key), `${path} ${key}`);
}
// The alt is the language's positioning line plus the scene description the
// README hero already carries, so the two never drift apart.
const alt = meta(html, 'og:image:alt');
assert.equal(meta(html, 'twitter:image:alt'), alt, path);
assert.equal(
alt,
`${meta(page(`${locale}/index.html`), 'description')} ${readmeAlt(locale)}`,
path,
);
assert.equal(meta(html, 'twitter:card'), 'summary_large_image', path);
assert.equal(meta(html, 'og:locale'), locale === 'en' ? 'en_US' : 'zh_CN', path);
assert.match(meta(html, 'og:url'), /^https:\/\/maka\.apache\.org\//u, path);
const image = meta(html, 'og:image');
assert.match(image, /^https:\/\/maka\.apache\.org\/_astro\/social\.[^/]+\.png$/u, path);
assert.equal(meta(html, 'twitter:image'), image, path);
assert.ok(image.includes(`/social.${locale}.`), `${path} shows the ${locale} hero`);
const [width, height] = ['og:image:width', 'og:image:height'].map((k) => Number(meta(html, k)));
assert.equal(width / height, 1200 / 630, path);
assert.ok(
statSync(new URL(image.slice('https://maka.apache.org/'.length), dist)).size > 0,
image,
);
}
});

test('every copy button on the downloads page has its own accessible name', () => {
for (const locale of locales) {
const names = [
Expand DownExpand Up@@ -128,16 +174,15 @@ test('the READMEs and the repository description open with the same sentence', (
// 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', () => {
test('the committed README heroes and social previews were rendered from the current hero copy', () => {
const manifest = JSON.parse(
readFileSync(new URL('../../.github/assets/readme-hero.json', import.meta.url), 'utf8'),
);
const rerender = 'run `npm --workspace @maka/website run readme-hero` and commit the images';
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`,
);
const html = page(`${locale}/index.html`);
assert.equal(heroText(html), manifest[locale], `${locale}: ${rerender}`);
assert.equal(headlineText(html), manifest.headline[locale], `${locale} headline: ${rerender}`);
}
});

Expand Down