docs(landing): host blog images in the community repo (#98)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-28 01:49:23 +08:00
committed by GitHub
parent 71669b2086
commit 0951dbfb8a
13 changed files with 134 additions and 66 deletions
+4
View File
@@ -30,6 +30,10 @@ cache/
packages/web/test-results/
packages/web/playwright-report/
# blog images staged locally by the landing capture/render scripts, then uploaded to
# the community repo's blog-assets/ — never committed here (see lib/links.ts)
packages/landing/.blog-assets/
# local design-doc symlink and agent instructions
/design
/AGENTS.md
@@ -1,48 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 368" width="920" height="368" role="img" aria-label="Benchmark: PenguinHarness vs Claude Code vs OpenAI Codex on two suites — comparable accuracy at a small fraction of the cost">
<text x="148" y="36" font-size="12" font-weight="400" fill="#898781" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Accuracy · suite total · higher is better</text>
<text x="596" y="36" font-size="12" font-weight="400" fill="#898781" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Total cost (USD) · lower is better</text>
<text x="24" y="68" font-size="13" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Data analysis — 15 tasks, single run</text>
<rect x="147" y="78" width="1" height="92" fill="#c3c2b7"/>
<text x="136" y="94.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">PenguinHarness</text>
<path d="M149 82 h172.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-172.0 z" fill="#2a78d6"/>
<text x="337" y="94.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">66.67%</text>
<text x="136" y="124.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Claude Code</text>
<path d="M149 112 h136.8 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-136.8 z" fill="#898781"/>
<text x="301.7841607919604" y="124.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">53.33%</text>
<text x="136" y="154.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">OpenAI Codex</text>
<path d="M149 142 h136.8 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-136.8 z" fill="#898781"/>
<text x="301.7841607919604" y="154.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">53.33%</text>
<rect x="595" y="78" width="1" height="92" fill="#c3c2b7"/>
<text x="584" y="94.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">PenguinHarness</text>
<path d="M597 82 h10.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-10.0 z" fill="#2a78d6"/>
<text x="623" y="94.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$0.55</text>
<text x="584" y="124.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Claude Code</text>
<path d="M597 112 h172.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-172.0 z" fill="#898781"/>
<text x="785" y="124.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$38.48</text>
<text x="584" y="154.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">OpenAI Codex</text>
<path d="M597 142 h84.8 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-84.8 z" fill="#898781"/>
<text x="697.7945961503353" y="154.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$19.41</text>
<text x="24" y="210" font-size="13" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Coding — 40 tasks × 2 runs</text>
<rect x="147" y="220" width="1" height="92" fill="#c3c2b7"/>
<text x="136" y="236.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">PenguinHarness</text>
<path d="M149 224 h141.4 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-141.4 z" fill="#2a78d6"/>
<text x="306.3913043478261" y="236.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">71.25%</text>
<text x="136" y="266.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Claude Code</text>
<path d="M149 254 h172.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-172.0 z" fill="#898781"/>
<text x="337" y="266.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">86.25%</text>
<text x="136" y="296.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">OpenAI Codex</text>
<path d="M149 284 h141.4 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-141.4 z" fill="#898781"/>
<text x="306.3913043478261" y="296.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">71.25%</text>
<rect x="595" y="220" width="1" height="92" fill="#c3c2b7"/>
<text x="584" y="236.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">PenguinHarness</text>
<path d="M597 224 h10.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-10.0 z" fill="#2a78d6"/>
<text x="623" y="236.5" font-size="12" font-weight="600" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$3.81</text>
<text x="584" y="266.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Claude Code</text>
<path d="M597 254 h113.5 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-113.5 z" fill="#898781"/>
<text x="726.5332606324973" y="266.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$146.97</text>
<text x="584" y="296.5" font-size="12" font-weight="400" fill="#52514e" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">OpenAI Codex</text>
<path d="M597 284 h172.0 a4 4 0 0 1 4 4 v8 a4 4 0 0 1 -4 4 h-172.0 z" fill="#898781"/>
<text x="785" y="296.5" font-size="12" font-weight="400" fill="#1f2328" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">$220.08</text>
<text x="24" y="338" font-size="12" font-weight="400" fill="#898781" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">Each harness runs the model it is normally paired with: PenguinHarness on DeepSeek V4 Pro, Claude Code on Claude Opus 4.8,</text>
<text x="24" y="356" font-size="12" font-weight="400" fill="#898781" text-anchor="start" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">OpenAI Codex on GPT-5.5. Accuracy, Tokens and cost are suite totals at official pricing.</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

@@ -10,9 +10,15 @@
* dist -> provision one demo user per UI language (password rotated once so the
* initial-password banner never shows) -> open /models, scroll the OpenRouter group
* to the top of the viewport (Claude Opus 5 sits on its first card row, the free
* rows a few rows below) -> screenshot, light theme, zh + en, into
* public/blog-assets/ as free-models-page-<lang>-light.webp (re-encoded to WebP
* inside Chromium, same as capture-shots.mjs).
* rows a few rows below) -> screenshot, light theme, zh + en, as
* free-models-page-<lang>-light.webp (re-encoded to WebP inside Chromium, same as
* capture-shots.mjs).
*
* Output is a two-step flow, because blog images are not committed to this repo:
* 1. this script writes into the gitignored staging dir packages/landing/.blog-assets/;
* 2. upload the files it produced to the `blog-assets/` directory of the sibling
* `Prism-Shadow/penguin-harness-community` repo, which is what the posts load from
* (the renderer resolves /blog-assets/<name> there — see src/lib/links.ts).
*
* Prereqs: `pnpm --filter @prismshadow/penguin-{skills,core,server,web} build` and
* Playwright's chromium. Run: `node scripts/capture-blog-shots.mjs` (or
@@ -27,7 +33,8 @@ import { chromium } from "@playwright/test";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "../../..");
const OUT_DIR = path.resolve(HERE, "../public/blog-assets");
// Gitignored staging dir: these images are hosted in the community repo, not committed here.
const OUT_DIR = path.resolve(HERE, "../.blog-assets");
const SRV_PORT = 8944; // Distinct from capture-shots.mjs (8940/8941) so both can run.
// On loopback binds the App is canonically served on `localhost`; the 127.0.0.1
// counterpart is the Workspace-preview host, where /api deliberately answers 401
@@ -2,9 +2,15 @@
* Renders the README / blog benchmark chart from the same data the landing page uses
* (src/lib/benchmark-data.ts), so the static SVGs cannot drift from the site when the
* numbers are refreshed. Emits:
* assets/readme/benchmark-light.svg
* assets/readme/benchmark-dark.svg
* packages/landing/public/blog-assets/benchmark-light.svg
* assets/readme/benchmark-light.svg (committed here, used by the README)
* assets/readme/benchmark-dark.svg (committed here, used by the README)
* packages/landing/.blog-assets/benchmark-light.svg
*
* That last one is a two-step flow: blog images are not committed to this repo, so the file
* lands in the gitignored staging dir packages/landing/.blog-assets/ and is then uploaded to
* the `blog-assets/` directory of the sibling `Prism-Shadow/penguin-harness-community` repo,
* which is what the posts load from (the renderer resolves /blog-assets/<name> there — see
* packages/landing/src/lib/links.ts).
*
* Two panels per suite (accuracy, cost), horizontal bars scaled linearly from zero — the
* cost spread is ~70x, so the PenguinHarness bar is by far the shortest. That is the
@@ -15,7 +21,7 @@
*
* Run: node packages/landing/scripts/render-benchmark-svg.mjs
*/
import { readFileSync, writeFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -171,9 +177,11 @@ const bench = loadBench();
const targets = [
["light", path.join(REPO, "assets/readme/benchmark-light.svg")],
["dark", path.join(REPO, "assets/readme/benchmark-dark.svg")],
["light", path.join(LANDING, "public/blog-assets/benchmark-light.svg")],
["light", path.join(LANDING, ".blog-assets/benchmark-light.svg")],
];
for (const [theme, file] of targets) {
// The blog staging dir is gitignored, so it does not exist in a fresh clone.
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, render(theme, bench));
console.log(`wrote ${path.relative(REPO, file)}`);
}
+28 -8
View File
@@ -25,19 +25,39 @@ export const INSTALL_CMD = "curl -fsSL https://penguin.ooo/install.sh | sh";
export const INSTALL_CMD_WINDOWS = "irm https://penguin.ooo/install.ps1 | iex";
/**
* Demo videos live in the sibling `penguin-harness-community` repo rather than in this
* one: they are ~9 MB each, and this repo's whole history is ~17 MB, so committing them
* here would triple what every contributor clones for assets only the marketing site
* shows. Served by raw.githubusercontent with `accept-ranges: bytes` (seeking works),
* `access-control-allow-origin: *` and a 5-minute cache. The `application/octet-stream`
* content type does not block playback — `nosniff` is only enforced for scripts and
* styles, and `<video>` sniffs the container itself (verified in Chromium).
* Pair every embed with a poster and `preload="none"`: nothing is fetched until play.
* Heavy marketing media lives in the sibling `penguin-harness-community` repo rather than
* in this one, so that assets only the landing site ever renders stay out of the clone of
* everyone who builds the product. Served by raw.githubusercontent with
* `access-control-allow-origin: *` and a 5-minute cache; a GitHub outage degrades the site
* to missing media, which is the accepted trade for not carrying the bytes here.
*/
const COMMUNITY_RAW =
"https://github.com/Prism-Shadow/penguin-harness-community/raw/refs/heads/main";
/**
* Demo videos: ~9 MB each, against a whole repo history of ~17 MB — committing them here
* would triple what every contributor clones. raw.githubusercontent sends
* `accept-ranges: bytes`, so seeking works. The `application/octet-stream` content type
* does not block playback — `nosniff` is only enforced for scripts and styles, and
* `<video>` sniffs the container itself (verified in Chromium).
* Pair every embed with a poster and `preload="none"`: nothing is fetched until play.
*/
export const demoVideoUrl = (name: string): string => `${COMMUNITY_RAW}/videos/${name}.mp4`;
/**
* Blog images: a few hundred KB per post, forever, since a published post's screenshots
* are never deleted — the one asset class whose growth is unbounded in the number of posts
* written. Hosting them in the community repo keeps that growth out of this history.
*
* The post Markdown deliberately does *not* spell these URLs out. Bodies keep writing the
* portable `/blog-assets/<name>` path (both `![alt](…)` and the raw `<img src="…">` tags
* some posts use), and the renderer resolves it here at render time — see the `img`
* adapter in src/pages/blog-post.tsx. One source of truth for where the images are hosted,
* Markdown that stays readable and diffable, and moving the host again is a one-line
* change instead of a sweep over every post.
*/
export const blogAssetUrl = (name: string): string => `${COMMUNITY_RAW}/blog-assets/${name}`;
/** API key consoles (same URLs the in-app Models page links to). */
export const DEEPSEEK_KEYS_URL = "https://platform.deepseek.com/api_keys";
export const OPENROUTER_KEYS_URL = "https://openrouter.ai/workspaces/default/keys";
+28
View File
@@ -12,6 +12,7 @@ import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import { Link, useParams } from "react-router";
import { S } from "../lib/strings";
import { blogAssetUrl } from "../lib/links";
import { useLocale } from "../state/locale";
import { formatAuthors, formatPostDate, getPost } from "../lib/blog";
import { extractToc, slugifyHeading } from "../lib/toc";
@@ -50,6 +51,32 @@ export function MdLink({
);
}
/** The path prefix post bodies use for images this repo does not carry. */
const BLOG_ASSET_PREFIX = "/blog-assets/";
/**
* Image adapter: rewrites `/blog-assets/<name>` sources to the community repo that actually hosts
* them (see blogAssetUrl in lib/links.ts). Every other src — absolute URLs, GitHub user-attachment
* uploads, anything else a post embeds — is forwarded untouched.
*
* The rewrite lives here rather than in the Markdown so there is one source of truth for the
* hosting location: post bodies keep the stable, portable relative path, they stay readable and
* diffable, and the tests that assert on those paths keep asserting on the paths. Applies equally
* to `![alt](…)` and to the raw `<img src="…">` tags posts use for theme-swapped screenshots,
* because rehype-raw turns those into ordinary `img` nodes before this map is consulted.
*/
export function MdImage({
node: _node,
src,
...imgProps
}: ComponentPropsWithoutRef<"img"> & ExtraProps) {
const resolved =
typeof src === "string" && src.startsWith(BLOG_ASSET_PREFIX)
? blogAssetUrl(src.slice(BLOG_ASSET_PREFIX.length))
: src;
return <img {...imgProps} src={resolved} />;
}
/**
* Built once at module scope rather than inline per render: react-markdown uses each entry as the
* element **type**, so a fresh arrow every render is a new type on every commit and React remounts
@@ -68,6 +95,7 @@ const MD_COMPONENTS: Components = {
</h3>
),
a: MdLink,
img: MdImage,
};
/**
+50 -1
View File
@@ -11,7 +11,8 @@ import { describe, expect, it } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import Markdown from "react-markdown";
import { REHYPE_PLUGINS, REMARK_PLUGINS } from "../src/pages/blog-post";
import { MdImage, REHYPE_PLUGINS, REMARK_PLUGINS } from "../src/pages/blog-post";
import { blogAssetUrl } from "../src/lib/links";
const render = (markdown: string) =>
renderToStaticMarkup(
@@ -22,6 +23,20 @@ const render = (markdown: string) =>
),
);
/** Same pipeline plus the image adapter the page installs. */
const renderWithImages = (markdown: string) =>
renderToStaticMarkup(
createElement(
Markdown,
{
remarkPlugins: REMARK_PLUGINS,
rehypePlugins: REHYPE_PLUGINS,
components: { img: MdImage },
},
markdown,
),
);
const DETAILS = `<details>
<summary><strong>Expand: the layout</strong></summary>
@@ -58,3 +73,37 @@ describe("blog post raw HTML", () => {
expect(html).toContain("<code>snapshots/v&lt;version&gt;.tar.gz</code>");
});
});
/**
* Blog images are not committed to this repo — they are hosted in the community repo, and post
* bodies keep writing the portable `/blog-assets/<name>` path. What has to hold is therefore not
* that a local file exists, but that the renderer resolves that path to the hosted URL; anything
* else a post embeds must be left alone.
*/
describe("blog post images", () => {
const HOSTED = blogAssetUrl("rag-app-en-light.webp");
it("resolves a Markdown /blog-assets/ image to the community repo", () => {
const html = renderWithImages("![The generated RAG app](/blog-assets/rag-app-en-light.webp)");
expect(html).toContain(`src="${HOSTED}"`);
expect(html).not.toContain('src="/blog-assets/');
expect(html).toContain('alt="The generated RAG app"');
});
it("resolves raw <img> tags too, keeping their attributes", () => {
const html = renderWithImages(
'<img class="dark:hidden" src="/blog-assets/goal-mode-en-light.webp" alt="Goal mode" width="1920" height="1350" />',
);
expect(html).toContain(`src="${blogAssetUrl("goal-mode-en-light.webp")}"`);
expect(html).toContain('class="dark:hidden"');
expect(html).toContain('width="1920"');
expect(html).toContain('height="1350"');
});
it("leaves every other image source untouched", () => {
const external = "https://github.com/user-attachments/assets/a0d866e9";
const html = renderWithImages(`![Local config](${external})\n\n![Site asset](/og-cover.png)`);
expect(html).toContain(`src="${external}"`);
expect(html).toContain('src="/og-cover.png"');
});
});