docs: add social preview card
This commit is contained in:
parent
4bc090e812
commit
a48a724573
@ -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
|
||||
|
||||
@ -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 }) {
|
||||
<meta property="og:description" content="${escapeAttr(description)}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://${customDomain}/${page.outRel === "index.html" ? "" : page.outRel}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta property="og:image" content="https://${customDomain}/social-card.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="ClawSweeper: conservative OpenClaw maintenance bot">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="https://${customDomain}/social-card.png">
|
||||
<meta name="twitter:image:alt" content="ClawSweeper: conservative OpenClaw maintenance bot">
|
||||
<link rel="icon" type="image/svg+xml" href="${rootPrefix}favicon.svg">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
293
scripts/social-card.mjs
Normal file
293
scripts/social-card.mjs
Normal file
@ -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)));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user