Skip to content

Commit e712fc0

Browse files
feat: base og design update (#252)
* feat: update og base image * refactor: simplify OG image generation with safe gradients and reduced content Replace server-side gradient system with hardcoded linear gradients that work reliably in Satori. Remove navigation footer items, usage instructions, and MCP icon from index OG image to create cleaner, more focused design. Fix SyneLogo SVG color inheritance by explicitly deriving fill color from style prop instead of relying on currentColor propagation, which doesn't work in Satori/OG contexts. Reduce tagline font size from * chore: add trailing newlines to OpenSpec agent instructions and JSON files Add missing trailing newlines to AGENTS.md, CLAUDE.md, licenses.json, and package.json files. Add blank lines after HTML comments in agent instruction files for improved readability and consistency with project formatting standards.
1 parent c43228f commit e712fc0

File tree

11 files changed

+1160
-72
lines changed

11 files changed

+1160
-72
lines changed

.vscode/terminals.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"autorun": true,
3-
"autokill": true,
4-
"restore": false,
5-
"terminals": [
6-
{
7-
"name": "app",
8-
"icon": "code",
9-
"color": "terminal.ansiCyan",
10-
"commands": ["cd web", "nvm use", "npm i", "npm run dev"],
11-
"open": true,
12-
"focus": true
13-
}
14-
]
2+
"autorun": true,
3+
"autokill": true,
4+
"restore": false,
5+
"terminals": [
6+
{
7+
"name": "app",
8+
"icon": "code",
9+
"color": "terminal.ansiCyan",
10+
"commands": ["cd web", "nvm use", "npm i --include=dev", "npm run dev"],
11+
"open": true,
12+
"focus": true
13+
}
14+
]
1515
}

web/AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
<!-- OPENSPEC:START -->
2+
23
# OpenSpec Instructions
34

45
These instructions are for AI assistants working in this project.
56

67
Always open `@/openspec/AGENTS.md` when the request:
8+
79
- Mentions planning or proposals (words like proposal, spec, change, plan)
810
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
911
- Sounds ambiguous and you need the authoritative spec before coding
1012

1113
Use `@/openspec/AGENTS.md` to learn:
14+
1215
- How to create and apply change proposals
1316
- Spec format and conventions
1417
- Project structure and guidelines
1518

1619
Keep this managed block so 'openspec update' can refresh the instructions.
1720

18-
<!-- OPENSPEC:END -->
21+
<!-- OPENSPEC:END -->

web/CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
<!-- OPENSPEC:START -->
2+
23
# OpenSpec Instructions
34

45
These instructions are for AI assistants working in this project.
56

67
Always open `@/openspec/AGENTS.md` when the request:
8+
79
- Mentions planning or proposals (words like proposal, spec, change, plan)
810
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
911
- Sounds ambiguous and you need the authoritative spec before coding
1012

1113
Use `@/openspec/AGENTS.md` to learn:
14+
1215
- How to create and apply change proposals
1316
- Spec format and conventions
1417
- Project structure and guidelines
1518

1619
Keep this managed block so 'openspec update' can refresh the instructions.
1720

18-
<!-- OPENSPEC:END -->
21+
<!-- OPENSPEC:END -->

web/app/licenses.json

Lines changed: 1016 additions & 1 deletion
Large diffs are not rendered by default.

web/app/modules/mcp-icon.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
export function McpIcon() {
1+
import { SVGProps } from "react";
2+
3+
export function McpIcon(props: SVGProps<SVGSVGElement>) {
24
return (
35
<svg
46
fill="currentColor"
57
fillRule="evenodd"
68
height="1em"
7-
style={{ flex: "none", lineHeight: 1 }}
89
viewBox="0 0 24 24"
910
width="1em"
1011
xmlns="http://www.w3.org/2000/svg"
12+
{...props}
13+
style={{ flex: "none", lineHeight: 1, ...props.style }}
1114
>
1215
<title>ModelContextProtocol</title>
1316
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path>

web/app/modules/meta-og-image.server.tsx

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,19 @@ import { Resvg } from "@resvg/resvg-js";
44
import { CSSProperties, ReactNode } from "react";
55
import type { SatoriOptions } from "satori";
66
import satori from "satori";
7-
import { SyneLogo } from "./svg";
87
import { randomInt } from "es-toolkit";
8+
import { SyneLogo } from "./svg";
99

1010
const OG_IMAGE_HEIGHT = 630;
1111
const OG_IMAGE_WIDTH = 1200;
1212

13-
const OG_DEFAULT_GRADIENTS = [
14-
"linear-gradient(to top, #f3e7e9 0%, #e3eeff 99%, #e3eeff 100%)",
15-
"linear-gradient(to top, #fbc2eb 0%, #a6c1ee 100%)",
16-
"linear-gradient(to top, #fad0c4 0%, #ffd1ff 100%)",
17-
"linear-gradient(-20deg, #ddd6f3 0%, #faaca8 50%, #faaca8 100%)",
18-
"linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 50%, #c2e9fb 100%)",
19-
"linear-gradient(120deg, #84fab0 0%, #8fd3f4 50%, #8fd3f4 100%)",
20-
"linear-gradient(45deg, #93a5cf 0%, #e4efe9 50%, #e4efe9 100%)",
21-
"linear-gradient(to top, #c1dfc4 0%, #deecdd 50%, #deecdd 100%)",
22-
"linear-gradient(to top, #dbdcd7 0%, #dddcd7 24%, #e2c9cc 30%, #e7627d 46%, #b8235a 59%, #801357 71%, #3d1635 84%, #1c1a27 100%)",
23-
"linear-gradient(to top, #a8edea 0%, #fed6e3 50%, #fed6e3 100%)",
24-
"linear-gradient(to top, #fbc2eb 0%, #a6c1ee 50%, #a6c1ee 100%)",
25-
"linear-gradient(to right, #ffecd2 0%, #fcb69f 50%, #fcb69f 100%)",
26-
"linear-gradient(to right, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)",
27-
"linear-gradient(to right, #f6d365 0%, #fda085 50%, #fda085 100%)",
28-
"linear-gradient(to right, #a1c4fd 0%, #c2e9fb 50%, #c2e9fb 100%)",
29-
"linear-gradient(to right, #84fab0 0%, #8fd3f4 50%, #8fd3f4 100%)",
30-
"linear-gradient(to right, #cfd9df 0%, #e2ebf0 50%, #e2ebf0 100%)",
31-
"linear-gradient(to right, #e0c3fc 0%, #8ec5fc 50%, #8ec5fc 100%)",
32-
"linear-gradient(to right, #4facfe 0%, #00f2fe 50%, #00f2fe 100%)",
33-
"linear-gradient(to top, #d5d4d0 0%, #d5d4d0 1%, #eeeeec 31%, #efeeec 75%, #e9e9e7 100%)",
34-
"linear-gradient(to top, #accbee 0%, #e7f0fd 100%)",
35-
"linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%)",
36-
"linear-gradient(to top, #feada6 0%, #f5efef 100%)",
37-
"linear-gradient(to top, #fff1eb 0%, #ace0f9 100%)",
38-
"linear-gradient(135deg, #fdfcfb 0%, #e2d1c3 100%)",
39-
"linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%)",
13+
// Satori has limited gradient support; use simple linear gradients that match the OG design palette.
14+
const SAFE_LINEAR_GRADIENTS = [
15+
"linear-gradient(135deg, #5a1f0e 0%, #0b0b0b 70%, #000 100%)",
16+
"linear-gradient(135deg, #6c6400 0%, #0b0b0b 70%, #000 100%)",
17+
"linear-gradient(135deg, #0c5a54 0%, #0b0b0b 70%, #000 100%)",
4018
];
4119

42-
function getRegularSans(baseUrl: string) {
43-
return fetch(new URL(`${baseUrl}/fonts/Inter-Regular.ttf`)).then(
44-
async (res) => {
45-
return res.arrayBuffer();
46-
},
47-
);
48-
}
49-
5020
async function getBaseOptions(
5121
requestUrl: string,
5222
partial: Partial<SatoriOptions> = {},
@@ -68,6 +38,14 @@ async function getBaseOptions(
6838
};
6939
}
7040

41+
function getRegularSans(baseUrl: string) {
42+
return fetch(new URL(`${baseUrl}/fonts/Inter-Regular.ttf`)).then(
43+
async (res) => {
44+
return res.arrayBuffer();
45+
},
46+
);
47+
}
48+
7149
function OGImage({
7250
children,
7351
style,
@@ -115,31 +93,99 @@ function CategoryPill({ children }: { children: ReactNode }) {
11593
);
11694
}
11795

118-
export async function composeIndexOGImage(params: { requestUrl: string }) {
119-
const { requestUrl } = params;
96+
export async function composeIndexOGImage(params: {
97+
requestUrl: string;
98+
packageCount?: string;
99+
authorCount?: string;
100+
version?: string;
101+
}) {
102+
const {
103+
requestUrl,
104+
packageCount = "22,266",
105+
authorCount = "34,363",
106+
version = "3.0.0",
107+
} = params;
120108

121-
const backgroundImage =
122-
OG_DEFAULT_GRADIENTS[randomInt(0, OG_DEFAULT_GRADIENTS.length)];
109+
const gradient =
110+
SAFE_LINEAR_GRADIENTS[randomInt(0, SAFE_LINEAR_GRADIENTS.length)];
123111

124112
const svg = await satori(
125113
<div
126114
style={{
127115
display: "flex",
128116
position: "relative",
129117
flexDirection: "column",
130-
alignItems: "center",
131-
justifyContent: "center",
132-
gap: 30,
133118
height: "100%",
134119
width: "100%",
135-
paddingTop: 40,
136-
backgroundImage,
120+
backgroundColor: "#000",
121+
color: "white",
122+
fontFamily: "Inter",
137123
}}
138124
>
139-
<SyneLogo style={{ transform: "scale(1.5)" }} />
140-
<p style={{ fontSize: 50, color: "#000" }}>
141-
The R Packages & Authors Search Engine
142-
</p>
125+
{/* Background Gradient */}
126+
<div
127+
style={{
128+
position: "absolute",
129+
top: 0,
130+
left: 0,
131+
right: 0,
132+
height: "80%",
133+
background: gradient,
134+
opacity: 0.5,
135+
}}
136+
/>
137+
{/* Gradient Overlay for dark bottom */}
138+
<div
139+
style={{
140+
position: "absolute",
141+
top: 0,
142+
left: 0,
143+
right: 0,
144+
height: "100%",
145+
backgroundImage: "linear-gradient(to bottom, transparent, #000 80%)",
146+
}}
147+
/>
148+
149+
{/* Content */}
150+
<div
151+
style={{
152+
display: "flex",
153+
flexDirection: "column",
154+
alignItems: "center",
155+
justifyContent: "center",
156+
height: "100%",
157+
width: "100%",
158+
color: "white",
159+
}}
160+
>
161+
<SyneLogo style={{ width: 600, color: "white" }} />
162+
<p
163+
style={{
164+
fontSize: 26,
165+
color: "#d1d5db", // gray-300
166+
marginTop: 40,
167+
textAlign: "center",
168+
}}
169+
>
170+
Search for {packageCount} R packages and {authorCount} authors
171+
</p>
172+
</div>
173+
174+
{/* Footer */}
175+
<div
176+
style={{
177+
position: "absolute",
178+
bottom: 40,
179+
width: "100%",
180+
display: "flex",
181+
justifyContent: "center",
182+
gap: 30,
183+
fontSize: 16,
184+
color: "#6b7280", // gray-500
185+
}}
186+
>
187+
<span style={{ color: "#4b5563" }}>v{version}</span>
188+
</div>
143189
</div>,
144190
await getBaseOptions(requestUrl),
145191
);

web/app/modules/svg.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@ type Props = {
66
};
77

88
export function SyneLogo({ className, style }: Props) {
9+
// Explicitly derive the fill color so it works in Satori/OG contexts where `color`
10+
// on the SVG style may not propagate to path fills.
11+
const { color, ...restStyle } = style || {};
12+
const fillColor = color || "currentColor";
13+
914
return (
1015
<svg
1116
xmlns="http://www.w3.org/2000/svg"
1217
viewBox="0 0 665.001 100.7"
1318
className={className}
14-
style={style}
19+
style={restStyle}
1520
>
1621
<path
17-
stroke="currentColor"
22+
stroke="none"
1823
strokeLinecap="round"
1924
strokeWidth=".945"
25+
fill={fillColor}
2026
d="M651.001 55.1h-62v8h76v19h-101v-64h100.8v19h-75.8v8h62v10Zm-254.2-37 75.3 44.4-9.3 5.3V18.1h25v64h-25l-75.3-43.6 9.3-5.4v49h-25v-64h25Zm-303.3 37.3h26a24.852 24.852 0 0 1-2.197 9.219 23.166 23.166 0 0 1-4.703 6.681q-5.05 5.05-13.867 8.108a63.331 63.331 0 0 1-4.983 1.492q-9.046 2.325-21.549 2.961a195.288 195.288 0 0 1-9.901.239 232.668 232.668 0 0 1-13.343-.364q-6.307-.363-11.913-1.088a140.637 140.637 0 0 1-.744-.098q-10.245-1.381-17.791-4.508a48.429 48.429 0 0 1-1.809-.792 32.82 32.82 0 0 1-7.053-4.358A26.48 26.48 0 0 1 4.301 67a24.838 24.838 0 0 1-3.448-8.257q-.746-3.268-.839-7.051A44.302 44.302 0 0 1 .001 50.6a37.684 37.684 0 0 1 .696-7.44q.953-4.734 3.212-8.482a23.423 23.423 0 0 1 .392-.628 28.364 28.364 0 0 1 8.365-8.311 36.139 36.139 0 0 1 4.035-2.289 54.773 54.773 0 0 1 8.341-3.209q5.149-1.538 11.259-2.441 11.5-1.7 26-1.7a173.2 173.2 0 0 1 12.816.446q10.676.794 18.684 3.004 9.167 2.53 15.054 6.809a28.342 28.342 0 0 1 3.846 3.341q6.4 6.7 6.9 16.3h-26a11.741 11.741 0 0 0-2.437-3.877 15.634 15.634 0 0 0-1.963-1.773 15.049 15.049 0 0 0-2.578-1.56q-2.822-1.371-7.022-2.29a48.856 48.856 0 0 0-4.44-.738q-5.01-.618-11.874-.659a164.976 164.976 0 0 0-.986-.003q-8.4 0-14.733.668a71.391 71.391 0 0 0-6.067.882 40.628 40.628 0 0 0-4.371 1.083q-4.728 1.476-7.329 3.867a11.273 11.273 0 0 0-3.627 7.463 15.641 15.641 0 0 0-.073 1.537 11.41 11.41 0 0 0 .759 4.232 10.395 10.395 0 0 0 2.941 4.068 15.069 15.069 0 0 0 3.173 2.06q3.153 1.565 7.869 2.512a52.199 52.199 0 0 0 .658.128q8 1.5 20.8 1.5a149.227 149.227 0 0 0 6.368-.127q6.083-.261 10.292-1.057a40.166 40.166 0 0 0 .59-.116 43.413 43.413 0 0 0 3.64-.899q3.564-1.058 5.742-2.535a12.064 12.064 0 0 0 .168-.116 17.499 17.499 0 0 0 2.041-1.662q1.59-1.522 2.359-3.188Zm64.1 26.7h-25v-64h77.3q9.1 0 16.35 2.05a27.434 27.434 0 0 1 5.954 2.422A21.085 21.085 0 0 1 237.751 27a16.13 16.13 0 0 1 3.479 6.667q.771 2.899.771 6.433a24.954 24.954 0 0 1-.282 3.858q-.322 2.057-1.013 3.763a13.421 13.421 0 0 1-.705 1.479q-2 3.6-5.5 5.6-3.5 2-8 2.9a59.693 59.693 0 0 1-8.182 1.039 66.264 66.264 0 0 1-1.218.061l-7.3-1.4q11.9.1 18.35 1a34.089 34.089 0 0 1 3.151.58q3.122.741 4.947 1.965a7.434 7.434 0 0 1 .902.705q2.477 2.283 2.548 6.499a14.884 14.884 0 0 1 .002.251v13.7h-25V71.4q0-1.879-.452-3.19a4.995 4.995 0 0 0-.698-1.36q-.971-1.309-3.867-1.94a19.215 19.215 0 0 0-1.133-.21q-2.937-.458-8.287-.566a176.155 176.155 0 0 0-3.563-.034h-39.1v18Zm0-44v9.3h52.3a14.976 14.976 0 0 0 2.198-.153q1.602-.238 2.852-.847a3.457 3.457 0 0 0 1.921-2.508 6.041 6.041 0 0 0 .129-1.292 5.073 5.073 0 0 0-.172-1.365 3.289 3.289 0 0 0-1.878-2.185q-1.778-.824-4.27-.933a17.789 17.789 0 0 0-.78-.017h-52.3Zm119.7 44h-27.5l42.9-64h27.8l43.3 64h-27.5l-37.1-57.3h14.9l-36.8 57.3Zm222.5 12.1 34.3-94.2 17.9 6.5-34.3 94.2-17.9-6.5Zm-159.2-21.8h-68.4v-15h68.4v15Z"
2127
fontSize="12"
2228
style={{
23-
stroke: "currentColor",
2429
strokeWidth: ".25mm",
25-
fill: "currentColor",
2630
}}
2731
vectorEffect="non-scaling-stroke"
2832
/>

web/app/routes/og._index.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,26 @@ import { composeIndexOGImage } from "../modules/meta-og-image.server";
22
import { ENV } from "../data/env";
33
import { minutesToSeconds } from "date-fns";
44
import { LoaderFunctionArgs } from "react-router";
5+
import { PackageService } from "../data/package.service";
6+
import { AuthorService } from "../data/author.service";
57

68
export const loader = async ({ request }: LoaderFunctionArgs) => {
79
const { origin } = new URL(request.url);
810

11+
const [packageRes, authorRes] = await Promise.allSettled([
12+
PackageService.getTotalPackagesCount(),
13+
AuthorService.getTotalAuthorsCount(),
14+
]);
15+
16+
const packageCount = packageRes.status === "fulfilled" ? packageRes.value : 0;
17+
const authorCount = authorRes.status === "fulfilled" ? authorRes.value : 0;
18+
const version = ENV.npm_package_version;
19+
920
const png = await composeIndexOGImage({
1021
requestUrl: origin,
22+
packageCount: Intl.NumberFormat().format(packageCount),
23+
authorCount: Intl.NumberFormat().format(authorCount),
24+
version,
1125
});
1226

1327
// Respond with the PNG buffer

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,4 @@
104104
"engines": {
105105
"node": ">=20.0.0"
106106
}
107-
}
107+
}

web/public/images/we/lukas_v2.webp

44.8 KB
Loading

0 commit comments

Comments
 (0)