docs(landing): host blog images in the community repo (#98)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@@ -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 |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
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)}`);
|
||||
}
|
||||
|
||||
@@ -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 `` 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";
|
||||
|
||||
@@ -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 `` 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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<version>.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("");
|
||||
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(`\n\n`);
|
||||
expect(html).toContain(`src="${external}"`);
|
||||
expect(html).toContain('src="/og-cover.png"');
|
||||
});
|
||||
});
|
||||
|
||||