Skip to content

Commit 49c5486

Browse files
committed
feat: add OG images for Cron Builder, JSON Studio and MetaLens
- shared card generator (src/components/og/tool-og-image.tsx) reusing the root site's dark/grid/glow style rather than InfraLens's bespoke report-mockup card, since these tools have no score/report data to show - each card uses the tool's own accent color and its exact ToolHeader icon (Lucide path data, not the React component — safer for satori) - README: list metalens/ alongside the other tools in the structure overview
1 parent e33123d commit 49c5486

5 files changed

Lines changed: 314 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ app/
6565
infralens/ # InfraLens — native routes (analyze, compare, docs, privacy)
6666
cron-builder/ # Cron Builder — visual cron expression editor
6767
json-studio/ # JSON Studio — format, validate and explore JSON
68+
metalens/ # MetaLens — inspect page metadata, OG/Twitter cards, indexing signals
6869
rss.xml/ # RSS feed
6970
sitemap.ts, robots.ts, manifest.ts, opengraph-image.tsx
7071
@@ -75,6 +76,7 @@ src/
7576
infralens/ # InfraLens engine — checks, security/SSRF, DNS, scoring, history
7677
cron-builder/ # Cron Builder engine — parsing, validation, next-run calculation
7778
json-studio/ # JSON Studio engine — parsing, formatting, stats, storage
79+
metalens/ # MetaLens engine — HTML parsing, URL resolution, findings
7880
7981
docs/
8082
infralens/ # InfraLens developer docs, changelog, security policy, MIT license
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { renderToolOgImage } from "@/components/og/tool-og-image";
2+
import { brand } from "@/lib/brand";
3+
4+
export const runtime = "nodejs";
5+
export const alt = "Cron Builder — Randy Code";
6+
export const size = { width: 1200, height: 630 };
7+
export const contentType = "image/png";
8+
9+
// Raw node data from lucide-react's clock icon (dist/esm/icons/clock.mjs),
10+
// with Lucide's internal `key` field dropped — see tool-og-image.tsx.
11+
const CLOCK_ICON = [
12+
["circle", { cx: "12", cy: "12", r: "10" }],
13+
["path", { d: "M12 6v6l4 2" }],
14+
] as const;
15+
16+
export default async function Image() {
17+
return renderToolOgImage({
18+
title: "Cron Builder",
19+
tagline:
20+
"Build, validate, and understand cron expressions without memorizing the syntax.",
21+
color: brand.colors.green[500],
22+
iconNodes: CLOCK_ICON,
23+
});
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { renderToolOgImage } from "@/components/og/tool-og-image";
2+
import { brand } from "@/lib/brand";
3+
4+
export const runtime = "nodejs";
5+
export const alt = "JSON Studio — Randy Code";
6+
export const size = { width: 1200, height: 630 };
7+
export const contentType = "image/png";
8+
9+
// Raw node data from lucide-react's braces icon (dist/esm/icons/braces.mjs),
10+
// with Lucide's internal `key` field dropped — see tool-og-image.tsx.
11+
const BRACES_ICON = [
12+
[
13+
"path",
14+
{
15+
d: "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1",
16+
},
17+
],
18+
[
19+
"path",
20+
{
21+
d: "M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1",
22+
},
23+
],
24+
] as const;
25+
26+
export default async function Image() {
27+
return renderToolOgImage({
28+
title: "JSON Studio",
29+
tagline: "Validate, format and inspect JSON instantly.",
30+
color: brand.colors.green[500],
31+
iconNodes: BRACES_ICON,
32+
});
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { renderToolOgImage } from "@/components/og/tool-og-image";
2+
import { brand } from "@/lib/brand";
3+
4+
export const runtime = "nodejs";
5+
export const alt = "MetaLens — Randy Code";
6+
export const size = { width: 1200, height: 630 };
7+
export const contentType = "image/png";
8+
9+
// Raw node data from lucide-react's scan-search icon
10+
// (dist/esm/icons/scan-search.mjs), with Lucide's internal `key` field
11+
// dropped — see tool-og-image.tsx.
12+
const SCAN_SEARCH_ICON = [
13+
["path", { d: "M3 7V5a2 2 0 0 1 2-2h2" }],
14+
["path", { d: "M17 3h2a2 2 0 0 1 2 2v2" }],
15+
["path", { d: "M21 17v2a2 2 0 0 1-2 2h-2" }],
16+
["path", { d: "M7 21H5a2 2 0 0 1-2-2v-2" }],
17+
["circle", { cx: "12", cy: "12", r: "3" }],
18+
["path", { d: "m16 16-1.9-1.9" }],
19+
] as const;
20+
21+
export default async function Image() {
22+
return renderToolOgImage({
23+
title: "MetaLens",
24+
tagline:
25+
"Inspect metadata, social cards and indexing signals from any public web page.",
26+
color: brand.colors.green[500],
27+
iconNodes: SCAN_SEARCH_ICON,
28+
});
29+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { ImageResponse } from "next/og";
2+
import { readFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
5+
const SIZE = { width: 1200, height: 630 };
6+
7+
/** A single Lucide `__iconNode` entry, stripped of its internal `key` field — see the note in each tool's opengraph-image.tsx. */
8+
type IconNode = readonly ["circle" | "path", Record<string, string>];
9+
10+
/**
11+
* Renders a Lucide icon from its raw node data rather than the React
12+
* component — satori (next/og's renderer) works from plain SVG elements,
13+
* and Lucide's exported components are simple enough that their node data
14+
* translates directly, giving the exact same icon shown in the tool's own
15+
* `ToolHeader` instead of a separately drawn approximation.
16+
*/
17+
function Icon({
18+
nodes,
19+
color,
20+
size,
21+
}: {
22+
nodes: readonly IconNode[];
23+
color: string;
24+
size: number;
25+
}) {
26+
return (
27+
<svg
28+
width={size}
29+
height={size}
30+
viewBox="0 0 24 24"
31+
fill="none"
32+
stroke={color}
33+
strokeWidth={1.6}
34+
strokeLinecap="round"
35+
strokeLinejoin="round"
36+
style={{ display: "flex" }}
37+
>
38+
{nodes.map(([tag, attrs], index) =>
39+
tag === "circle" ? (
40+
<circle key={index} cx={attrs.cx} cy={attrs.cy} r={attrs.r} />
41+
) : (
42+
<path key={index} d={attrs.d} />
43+
),
44+
)}
45+
</svg>
46+
);
47+
}
48+
49+
/**
50+
* Shared OG card for the lightweight `/tools/*` family (Cron Builder, JSON
51+
* Studio, MetaLens, ...) — same dark/grid/glow language as the site's own
52+
* root `app/opengraph-image.tsx`, not a copy of InfraLens's bespoke
53+
* report-mockup card (that one mirrors InfraLens-specific score/category
54+
* data these tools don't have, and InfraLens keeps its own standalone
55+
* identity per the tool-family convention).
56+
*/
57+
export async function renderToolOgImage({
58+
title,
59+
tagline,
60+
color,
61+
iconNodes,
62+
}: {
63+
title: string;
64+
tagline: string;
65+
color: string;
66+
iconNodes: readonly IconNode[];
67+
}) {
68+
const [fontBold, fontRegular] = await Promise.all([
69+
readFile(join(process.cwd(), "assets/Inter-Bold.woff")),
70+
readFile(join(process.cwd(), "assets/Inter-Regular.woff")),
71+
]);
72+
73+
return new ImageResponse(
74+
<div
75+
style={{
76+
width: "100%",
77+
height: "100%",
78+
display: "flex",
79+
background: "#070B10",
80+
position: "relative",
81+
}}
82+
>
83+
{/* Grid pattern — same density as the root OG card */}
84+
<div
85+
style={{
86+
position: "absolute",
87+
inset: 0,
88+
display: "flex",
89+
backgroundImage:
90+
"linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)",
91+
backgroundSize: "60px 60px",
92+
}}
93+
/>
94+
95+
{/* Radial glow — tool's own accent color, top-left */}
96+
<div
97+
style={{
98+
position: "absolute",
99+
top: -140,
100+
left: -100,
101+
width: 560,
102+
height: 560,
103+
borderRadius: "50%",
104+
background: `radial-gradient(circle, ${color}33 0%, transparent 70%)`,
105+
display: "flex",
106+
}}
107+
/>
108+
109+
{/* Left accent bar */}
110+
<div
111+
style={{
112+
position: "absolute",
113+
left: 0,
114+
top: 0,
115+
bottom: 0,
116+
width: 5,
117+
background: `linear-gradient(180deg, ${color} 0%, transparent 100%)`,
118+
display: "flex",
119+
}}
120+
/>
121+
122+
<div
123+
style={{
124+
display: "flex",
125+
flexDirection: "column",
126+
justifyContent: "center",
127+
width: "100%",
128+
height: "100%",
129+
padding: "64px 88px",
130+
}}
131+
>
132+
{/* Icon + eyebrow */}
133+
<div
134+
style={{
135+
display: "flex",
136+
alignItems: "center",
137+
gap: 18,
138+
marginBottom: 32,
139+
}}
140+
>
141+
<div
142+
style={{
143+
display: "flex",
144+
alignItems: "center",
145+
justifyContent: "center",
146+
width: 76,
147+
height: 76,
148+
borderRadius: 18,
149+
background: `${color}1a`,
150+
border: `1px solid ${color}40`,
151+
}}
152+
>
153+
<Icon nodes={iconNodes} color={color} size={38} />
154+
</div>
155+
<span
156+
style={{
157+
display: "flex",
158+
fontSize: 16,
159+
fontFamily: "Inter",
160+
fontWeight: 700,
161+
color,
162+
letterSpacing: "0.16em",
163+
textTransform: "uppercase",
164+
}}
165+
>
166+
Developer Tool
167+
</span>
168+
</div>
169+
170+
{/* Title */}
171+
<div
172+
style={{
173+
display: "flex",
174+
fontSize: 76,
175+
fontFamily: "Inter",
176+
fontWeight: 700,
177+
color: "#F4F8FC",
178+
letterSpacing: "-0.02em",
179+
marginBottom: 22,
180+
}}
181+
>
182+
{title}
183+
</div>
184+
185+
{/* Tagline */}
186+
<div
187+
style={{
188+
display: "flex",
189+
fontSize: 26,
190+
fontFamily: "Inter",
191+
fontWeight: 400,
192+
color: "#A9B8C7",
193+
lineHeight: 1.45,
194+
maxWidth: 800,
195+
}}
196+
>
197+
{tagline}
198+
</div>
199+
</div>
200+
201+
{/* Bottom URL */}
202+
<div
203+
style={{
204+
position: "absolute",
205+
bottom: 40,
206+
right: 88,
207+
display: "flex",
208+
fontSize: 16,
209+
fontFamily: "Inter",
210+
fontWeight: 700,
211+
color: "#A9B8C7",
212+
letterSpacing: "0.05em",
213+
}}
214+
>
215+
randy-code.dev
216+
</div>
217+
</div>,
218+
{
219+
...SIZE,
220+
fonts: [
221+
{ name: "Inter", data: fontBold, weight: 700, style: "normal" },
222+
{ name: "Inter", data: fontRegular, weight: 400, style: "normal" },
223+
],
224+
},
225+
);
226+
}

0 commit comments

Comments
 (0)