diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25fec33333..7ddf71e7ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable ClawSweeper changes are tracked here.
This file was reconstructed from first-parent git history. Generated dashboard,
checkpoint, and status-only commits are intentionally omitted.
+## Unreleased
+
+### Added
+
+- Added a generated 1200x630 social preview card plus large-image Open Graph and
+ Twitter metadata for the docs site.
+
## 0.2.0 - 2026-05-03
### Added
diff --git a/scripts/build-docs-site.mjs b/scripts/build-docs-site.mjs
index 084b679c49..0da2cc9e43 100644
--- a/scripts/build-docs-site.mjs
+++ b/scripts/build-docs-site.mjs
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
+import { socialCardPng } from "./social-card.mjs";
const root = process.cwd();
const docsDir = path.join(root, "docs");
@@ -80,6 +81,7 @@ for (const page of pages) {
fs.writeFileSync(path.join(outDir, "clawsweeper.svg"), clawSvg(), "utf8");
fs.writeFileSync(path.join(outDir, "favicon.svg"), faviconSvg(), "utf8");
+fs.writeFileSync(path.join(outDir, "social-card.png"), socialCardPng());
fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
fs.writeFileSync(path.join(outDir, "CNAME"), `${customDomain}\n`, "utf8");
fs.writeFileSync(
@@ -325,7 +327,13 @@ function layout({ page, html, toc, prev, next, sectionName }) {
-
+
+
+
+
+
+
+
diff --git a/scripts/social-card.mjs b/scripts/social-card.mjs
new file mode 100644
index 0000000000..e9b3d1c4c2
--- /dev/null
+++ b/scripts/social-card.mjs
@@ -0,0 +1,293 @@
+import zlib from "node:zlib";
+
+const WIDTH = 1200;
+const HEIGHT = 630;
+
+const COLORS = {
+ ink: "#06181c",
+ paper: "#fdf6e9",
+ shell: "#f4ead7",
+ reef: "#0b3a3f",
+ tide: "#0a6a72",
+ kelp: "#13848e",
+ coral: "#ec5b3c",
+ crab: "#d9472b",
+ crabDark: "#7d2613",
+ sun: "#f4a93a",
+ sand: "#e9d7b1",
+};
+
+const FONT = {
+ A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
+ B: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
+ C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"],
+ D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
+ E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
+ F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
+ G: ["01111", "10000", "10000", "10111", "10001", "10001", "01111"],
+ H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
+ I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"],
+ J: ["00111", "00010", "00010", "00010", "00010", "10010", "01100"],
+ K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
+ L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
+ M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
+ N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
+ O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
+ P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
+ Q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"],
+ R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
+ S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
+ T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
+ U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
+ V: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"],
+ W: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"],
+ X: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
+ Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
+ Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
+ 0: ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
+ 1: ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
+ 2: ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
+ 3: ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
+ 4: ["10010", "10010", "10010", "11111", "00010", "00010", "00010"],
+ 5: ["11111", "10000", "10000", "11110", "00001", "00001", "11110"],
+ 6: ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
+ 7: ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
+ 8: ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
+ 9: ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
+ ".": ["00000", "00000", "00000", "00000", "00000", "01100", "01100"],
+ " ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
+ "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
+};
+
+export function socialCardPng() {
+ const card = new Raster(WIDTH, HEIGHT);
+ card.background();
+
+ card.roundRect(68, 66, 680, 498, 30, rgba(COLORS.paper, 0.96));
+ card.roundRect(82, 80, 652, 470, 22, rgba("#f7ecd3", 0.94));
+ card.rect(82, 532, 652, 16, rgba(COLORS.coral, 1));
+ card.rect(82, 516, 424, 16, rgba(COLORS.sun, 1));
+ card.rect(506, 516, 228, 16, rgba(COLORS.kelp, 1));
+
+ card.text("OPENCLAW MAINTENANCE BOT", 112, 112, 4, COLORS.coral, 1);
+ card.text("CLAW", 110, 166, 22, COLORS.ink, 1);
+ card.text("SWEEPER", 112, 330, 14, COLORS.reef, 1);
+ card.text("REVIEW APPLY REPAIR COMMIT", 112, 478, 4, COLORS.tide, 0);
+
+ card.roundRect(804, 96, 310, 88, 24, rgba(COLORS.paper, 0.16));
+ card.text("FOUR", 838, 128, 6, COLORS.paper, 1);
+ card.text("LANES", 1002, 128, 6, COLORS.sun, 1);
+
+ card.roundRect(790, 220, 352, 260, 32, rgba(COLORS.paper, 0.1));
+ card.lobster(966, 332, 1.02);
+
+ card.roundRect(806, 506, 318, 52, 18, rgba(COLORS.ink, 0.36));
+ card.text("CLAW SWEEP", 846, 522, 5, COLORS.paper, 1);
+
+ card.text("CLAWSWEEPER.BOT", 812, 574, 4, COLORS.sand, 1);
+ return encodePng(card.width, card.height, card.data);
+}
+
+class Raster {
+ constructor(width, height) {
+ this.width = width;
+ this.height = height;
+ this.data = Buffer.alloc(width * height * 4);
+ }
+
+ background() {
+ const top = rgb(COLORS.reef);
+ const bottom = rgb(COLORS.ink);
+ for (let y = 0; y < this.height; y++) {
+ const t = y / (this.height - 1);
+ const row = mix(top, bottom, t);
+ for (let x = 0; x < this.width; x++) {
+ const wave = Math.sin((x + y * 0.8) / 34) * 4 + Math.sin((x - y * 1.4) / 91) * 6;
+ const grain = ((x * 17 + y * 31) % 23) - 11;
+ this.raw(x, y, row[0] + wave + grain, row[1] + wave * 0.8, row[2] + wave * 0.55, 255);
+ }
+ }
+ for (let y = 0; y < this.height; y += 38)
+ this.line(0, y, this.width, y + 110, rgba("#ffffff", 0.025), 2);
+ this.circle(1088, 86, 132, rgba(COLORS.coral, 0.13));
+ this.circle(1016, 594, 220, rgba(COLORS.tide, 0.2));
+ this.circle(760, -40, 180, rgba(COLORS.sun, 0.12));
+ }
+
+ lobster(cx, cy, scale) {
+ const s = scale;
+ const crab = rgba(COLORS.crab, 1);
+ const dark = rgba(COLORS.crabDark, 1);
+ const ink = rgba(COLORS.ink, 1);
+ this.line(cx - 76 * s, cy + 28 * s, cx - 154 * s, cy + 82 * s, dark, 12 * s);
+ this.line(cx + 76 * s, cy + 28 * s, cx + 154 * s, cy + 82 * s, dark, 12 * s);
+ this.circle(cx - 170 * s, cy + 88 * s, 34 * s, crab);
+ this.circle(cx + 170 * s, cy + 88 * s, 34 * s, crab);
+ this.circle(cx - 182 * s, cy + 76 * s, 20 * s, crab);
+ this.circle(cx + 182 * s, cy + 76 * s, 20 * s, crab);
+ this.circle(cx, cy + 38 * s, 88 * s, crab);
+ this.ellipse(cx, cy + 30 * s, 98 * s, 64 * s, crab);
+ this.line(cx - 54 * s, cy - 22 * s, cx - 54 * s, cy - 58 * s, ink, 4 * s);
+ this.line(cx + 54 * s, cy - 22 * s, cx + 54 * s, cy - 58 * s, ink, 4 * s);
+ this.circle(cx - 54 * s, cy - 66 * s, 12 * s, rgba(COLORS.paper, 1));
+ this.circle(cx + 54 * s, cy - 66 * s, 12 * s, rgba(COLORS.paper, 1));
+ this.circle(cx - 50 * s, cy - 66 * s, 4 * s, ink);
+ this.circle(cx + 58 * s, cy - 66 * s, 4 * s, ink);
+ this.line(cx - 48 * s, cy + 72 * s, cx + 48 * s, cy + 72 * s, rgba(COLORS.paper, 0.45), 4 * s);
+ this.line(cx - 24 * s, cy + 95 * s, cx + 24 * s, cy + 95 * s, ink, 4 * s);
+
+ for (const dir of [-1, 1]) {
+ this.line(cx + dir * 62 * s, cy + 85 * s, cx + dir * 120 * s, cy + 150 * s, ink, 6 * s);
+ this.line(cx + dir * 28 * s, cy + 96 * s, cx + dir * 46 * s, cy + 170 * s, ink, 6 * s);
+ }
+ this.line(cx + 168 * s, cy + 104 * s, cx + 216 * s, cy + 186 * s, rgba("#8a5a2c", 1), 7 * s);
+ this.roundRect(cx + 196 * s, cy + 180 * s, 42 * s, 48 * s, 8 * s, rgba(COLORS.sun, 1));
+ for (let i = 0; i < 5; i++) {
+ this.line(
+ cx + (202 + i * 8) * s,
+ cy + 188 * s,
+ cx + (194 + i * 12) * s,
+ cy + 226 * s,
+ rgba("#8a5a2c", 1),
+ 2 * s,
+ );
+ }
+ }
+
+ text(text, x, y, scale, color, gap = 1) {
+ let cursor = x;
+ for (const char of text.toUpperCase()) {
+ const glyph = FONT[char] || FONT[" "];
+ for (let gy = 0; gy < glyph.length; gy++) {
+ for (let gx = 0; gx < glyph[gy].length; gx++) {
+ if (glyph[gy][gx] === "1")
+ this.rect(cursor + gx * scale, y + gy * scale, scale, scale, rgba(color, 1));
+ }
+ }
+ cursor += 5 * scale + gap * scale;
+ }
+ }
+
+ raw(x, y, r, g, b, a) {
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
+ const i = (Math.floor(y) * this.width + Math.floor(x)) * 4;
+ this.data[i] = clamp(r);
+ this.data[i + 1] = clamp(g);
+ this.data[i + 2] = clamp(b);
+ this.data[i + 3] = clamp(a);
+ }
+
+ pixel(x, y, color) {
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
+ const i = (Math.floor(y) * this.width + Math.floor(x)) * 4;
+ const a = color[3] / 255;
+ this.data[i] = clamp(color[0] * a + this.data[i] * (1 - a));
+ this.data[i + 1] = clamp(color[1] * a + this.data[i + 1] * (1 - a));
+ this.data[i + 2] = clamp(color[2] * a + this.data[i + 2] * (1 - a));
+ this.data[i + 3] = 255;
+ }
+
+ rect(x, y, w, h, color) {
+ const x0 = Math.max(0, Math.floor(x));
+ const y0 = Math.max(0, Math.floor(y));
+ const x1 = Math.min(this.width, Math.ceil(x + w));
+ const y1 = Math.min(this.height, Math.ceil(y + h));
+ for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) this.pixel(xx, yy, color);
+ }
+
+ roundRect(x, y, w, h, r, color) {
+ this.rect(x + r, y, w - 2 * r, h, color);
+ this.rect(x, y + r, w, h - 2 * r, color);
+ this.circle(x + r, y + r, r, color);
+ this.circle(x + w - r, y + r, r, color);
+ this.circle(x + r, y + h - r, r, color);
+ this.circle(x + w - r, y + h - r, r, color);
+ }
+
+ circle(cx, cy, r, color) {
+ this.ellipse(cx, cy, r, r, color);
+ }
+
+ ellipse(cx, cy, rx, ry, color) {
+ const x0 = Math.floor(cx - rx);
+ const y0 = Math.floor(cy - ry);
+ const x1 = Math.ceil(cx + rx);
+ const y1 = Math.ceil(cy + ry);
+ for (let y = y0; y <= y1; y++) {
+ for (let x = x0; x <= x1; x++) {
+ const dx = (x - cx) / rx;
+ const dy = (y - cy) / ry;
+ if (dx * dx + dy * dy <= 1) this.pixel(x, y, color);
+ }
+ }
+ }
+
+ line(x0, y0, x1, y1, color, width = 1) {
+ const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
+ for (let i = 0; i <= steps; i++) {
+ const t = steps === 0 ? 0 : i / steps;
+ this.circle(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, width / 2, color);
+ }
+ }
+}
+
+function encodePng(width, height, rgbaData) {
+ const stride = width * 4 + 1;
+ const raw = Buffer.alloc(stride * height);
+ for (let y = 0; y < height; y++) {
+ raw[y * stride] = 0;
+ rgbaData.copy(raw, y * stride + 1, y * width * 4, (y + 1) * width * 4);
+ }
+ return Buffer.concat([
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
+ chunk("IHDR", Buffer.concat([u32(width), u32(height), Buffer.from([8, 6, 0, 0, 0])])),
+ chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
+ chunk("IEND", Buffer.alloc(0)),
+ ]);
+}
+
+function chunk(type, data) {
+ const name = Buffer.from(type, "ascii");
+ return Buffer.concat([
+ u32(data.length),
+ name,
+ data,
+ u32(crc32(Buffer.concat([name, data])) >>> 0),
+ ]);
+}
+
+function u32(value) {
+ const out = Buffer.alloc(4);
+ out.writeUInt32BE(value >>> 0);
+ return out;
+}
+
+function crc32(buffer) {
+ let crc = 0xffffffff;
+ for (const byte of buffer) {
+ crc ^= byte;
+ for (let i = 0; i < 8; i++) crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
+ }
+ return crc ^ 0xffffffff;
+}
+
+function rgb(hex) {
+ const value = hex.replace("#", "");
+ return [
+ parseInt(value.slice(0, 2), 16),
+ parseInt(value.slice(2, 4), 16),
+ parseInt(value.slice(4, 6), 16),
+ ];
+}
+
+function rgba(hex, alpha) {
+ return [...rgb(hex), Math.round(alpha * 255)];
+}
+
+function mix(a, b, t) {
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
+}
+
+function clamp(value) {
+ return Math.max(0, Math.min(255, Math.round(value)));
+}