web-next: OG images for profiles and articles#269
Conversation
Add the first Satori-based Open Graph renderer for profile cards and expose it through Account.ogImageUrl. The renderer stores regenerated PNGs under the og/v2 cache namespace and vendors the official Pubnyan asset used as the quiet brand seal. The GraphQL test covers initial rendering and cache reuse. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Extend the Satori Open Graph renderer with the article card template and expose ArticleContent.ogImageUrl. Article content now renders and caches per-language OG images under the existing og/v2 namespace. The GraphQL test covers language-specific rendering and cache reuse. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Add profile and article Open Graph image endpoints that redirect through the new GraphQL-backed renderers. Set profile and article meta tags to the new web-next endpoint URLs, including language-specific article image variants. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Keep the previous Open Graph image on disk until the database record has been updated to point at the newly rendered image. This prevents a failed metadata update from leaving the stored key pointing at a file that was already deleted. Extend the GraphQL tests to cover helper ownership of deletion and stale cache cleanup through the profile and article resolvers. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Resolve the default article language before querying the rendered Open Graph image. Requests without an explicit language now render one article content instead of selecting ogImageUrl for every translation. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Include the article source identity in the hashed Open Graph image input so distinct articles cannot produce the same article_content.og_image_key when their visible preview text is otherwise identical. Add a regression test that renders two matching article previews for the same author and verifies they receive distinct image URLs instead of hitting the unique cache key constraint. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Assign a high Pothos complexity to Account.ogImageUrl so single-account profile OG lookups still work, while unpaginated account list selections exceed the anonymous and signed query limits before rendering can fan out. Add a regression test for the blocked bulk query and normalize article OG test comparisons by language so translation ordering does not make the suite flaky. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Assign a high Pothos complexity to ArticleContent.ogImageUrl and keep single-language article OG lookups below the limit by lowering the contents multiplier when a language is requested. Add regression coverage for article list queries with and without a language argument so bulk selections cannot fan out into OG rendering. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Replace the split footer copy and Pubnyan label with the official dark Hackers' Pub logo asset on a plain black footer bar for both profile and article OG images. Bump the OG render version so existing cached images are regenerated with the updated visual treatment. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Increase the line-height budget for OG title, excerpt, profile name, and bio text so Satori and Resvg do not clip descenders at the bottom of wrapped lines. Remove the hard overflow clipping and tighten truncation lengths so long profile and article text still stays within the preview layout. hackers-pub#265 Assisted-by: Codex:gpt-5.5
Shorten the article OG excerpt and reduce its rendered height so wrapped article previews keep clear space above the brand footer. Bump the OG image cache version so existing article previews regenerate with the adjusted layout. hackers-pub#265 Assisted-by: Codex:gpt-5.5
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughAdds deterministic OG image generation and caching: a new renderer module, GraphQL fields on Account and ArticleContent that compute/persist ogImageKey, web redirect routes for profile/article OG images, page meta tag wiring, and tests covering rendering, caching, and query-complexity guardrails. Changes
Sequence DiagramsequenceDiagram
participant Client
participant GraphQL_Resolver
participant OG_Renderer
participant Database
participant Disk
Client->>GraphQL_Resolver: Query Account.ogImageUrl / ArticleContent.ogImageUrl
GraphQL_Resolver->>Database: read existing ogImageKey
GraphQL_Resolver->>OG_Renderer: put...OgImage(disk, existingKey, input)
OG_Renderer->>OG_Renderer: canonicalize input -> sha256 hash -> candidate key
alt key matches existingKey
OG_Renderer->>GraphQL_Resolver: return existing key
else new key
OG_Renderer->>OG_Renderer: render Satori -> SVG
OG_Renderer->>OG_Renderer: convert SVG -> PNG (Resvg)
OG_Renderer->>Disk: upload og/v2/<hash>.png
OG_Renderer->>Disk: (if existingKey) delete existingKey
OG_Renderer->>GraphQL_Resolver: return new key
end
GraphQL_Resolver->>Database: update ogImageKey (if changed)
GraphQL_Resolver->>Client: return ogImageUrl (disk URL)
sequenceDiagram
participant Browser
participant Web_Route
participant Relay_GraphQL
participant Disk
Browser->>Web_Route: GET /@handle/og or /.../ogimage?l=...
Web_Route->>Relay_GraphQL: fetch account/article contents -> ogImageUrl
Relay_GraphQL->>Web_Route: ogImageUrl
alt ogImageUrl present
Web_Route->>Browser: 302 redirect -> ogImageUrl
Browser->>Disk: GET ogImageUrl
Disk->>Browser: PNG bytes
else missing
Web_Route->>Browser: 404
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 28 minutes and 57 seconds.Comment |
|
@codex review |
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic Open Graph (OG) image generation for user profiles and articles, utilizing Satori and Resvg for rendering and Flydrive for storage. The implementation includes new GraphQL fields, caching logic, and frontend meta tag integration. Feedback focuses on ensuring external images are converted to data URIs for proper rendering, utilizing standard library functions for more efficient Base64 encoding, and refining the cache key generation logic to handle transient URLs effectively.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@graphql/og.ts`:
- Around line 139-143: The truncateText function currently uses slice which can
split UTF-16 surrogate pairs or grapheme clusters (e.g., emoji or combining
marks); update truncateText to perform grapheme-safe truncation by first
compacting the text as now, then iterate grapheme clusters (using Intl.Segmenter
if available, falling back to Array.from([...text]) as a backup) to accumulate
clusters until adding the next cluster would exceed maxLength, join those
clusters, trimEnd and append the ellipsis character if truncated; keep the
function name truncateText and preserve the compact/trim behavior and returned
ellipsis.
In `@web-next/src/routes/`(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx:
- Around line 21-24: Normalize the query param retrieval so an empty `?l=` is
treated as "no language supplied": when reading the language use the result of
requestUrl.searchParams.get("l") (requestedLanguage) but treat "" as
null/undefined before falling back to await getDefaultLanguage(handle, idOrYear,
slug); update the logic that sets language (the requestedLanguage ?? await
getDefaultLanguage(...)) to explicitly check for an empty string (e.g.,
requestedLanguage && requestedLanguage.length > 0) or coerce "" to null so the
default lookup runs and you never forward an empty locale into the GraphQL call.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4d9cc436-4678-41b4-9e56-b753be4478c9
⛔ Files ignored due to path filters (1)
graphql/assets/pubnyan-normal-transparent.svgis excluded by!**/*.svg
📒 Files selected for processing (11)
graphql/account.test.tsgraphql/account.tsgraphql/assets/README.mdgraphql/og.tsgraphql/post.more.test.tsgraphql/post.tsgraphql/schema.graphqlweb-next/src/routes/(root)/[handle]/(profile)/index.tsxweb-next/src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsxweb-next/src/routes/(root)/[handle]/[idOrYear]/[slug]/ogimage.tsxweb-next/src/routes/(root)/[handle]/og.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38b9c77006
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Use grapheme-aware truncation for OG image text so emoji sequences and combining marks are not split before the ellipsis is appended. Add a focused test that covers emoji ZWJ sequences and combining marks. hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
Convert external avatar URLs to data URIs before handing them to Satori so profile and article previews render avatars consistently. Use Deno's standard base64 encoder for both the brand logo and fetched image bytes, and add coverage for data URI passthrough and remote image embedding. hackers-pub#269 (comment) hackers-pub#269 (comment) hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
Treat blank article OG image language parameters as missing so malformed `?l=` requests fall back to the article's default content language. hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements dynamic Open Graph (OG) image generation for user profiles and articles using Satori and Resvg. It introduces new ogImageUrl fields to the GraphQL schema, implements caching logic with complexity limits, and adds API routes for image redirection. Feedback focuses on a potential Denial of Service vulnerability in remote image fetching due to missing timeouts and size limits. Additionally, the reviewer noted architectural concerns regarding tight coupling between packages caused by hardcoded relative paths for font and logo assets.
Keep avatar URLs out of the OG cache hash and use a stable avatar identity instead, so signed or otherwise transient image URLs do not force repeated preview generation for unchanged avatars. Add profile and article coverage proving that changing only the rendered avatar URL does not change the cached OG image key. hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
web-next/src/routes/(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx (1)
28-49: ⚡ Quick winAdd structured handling around the two GraphQL lookups.
If either
fetchQuery(...).toPromise()rejects, this route currently falls through as an unstructured 500. Catch those failures, log them withhandle,idOrYear,slug, andrequestedLanguage, and return a controlled 5xx instead. As per coding guidelines "Use structured logging via LogTape for error handling" and "Include context in error details when logging errors".Also applies to: 64-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-next/src/routes/`(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx around lines 28 - 49, Wrap both fetchQuery(...).toPromise() GraphQL calls (the one using ogimageQuery and the second lookup around lines 64-85) in try/catch blocks; on catch, log the error using the LogTape logger (e.g., LogTape.error or processLogger equivalent) with structured fields { handle, idOrYear, slug, requestedLanguage: language } and the caught error, and return a controlled 5xx Response (e.g., new Response("Internal Server Error", { status: 500 })) instead of allowing an unstructured throw; keep createEnvironment(), fetchQuery, and the query names (ogimageQuery / the second query) intact while only adding the try/catch, logging, and the 5xx return.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@graphql/og.ts`:
- Around line 437-456: Compute the canonical input, digest and resulting key up
front in putOgImage (using canonicalize, OG_VERSION, OG_SIZE,
crypto.subtle.digest, and encodeHex/OG_NAMESPACE) and return early if
existingKey === key so you avoid calling renderPng, asset embedding, or disk.put
on cache hits; apply the same lazy-change to the other similar function/block
around the 459-482 region so both code paths skip asset fetching and Satori
element construction when the derived key matches.
- Around line 124-135: The loadImageDataUri function can hang or OOM when
fetching remote images; update loadImageDataUri to use an AbortController with a
short timeout (e.g., a few seconds) and to enforce a max-size limit by streaming
response.body instead of calling arrayBuffer() directly: create an
AbortController, pass controller.signal to fetch(imageUrl), set a timeout to
call controller.abort(), then read response.body.getReader() and accumulate
chunks into a Uint8Array while tracking total bytes and aborting/returning
FALLBACK_IMAGE_DATA_URI if the total exceeds the configured MAX_IMAGE_BYTES;
keep the existing content-type handling, convert the accumulated bytes to base64
only if under the limit, and ensure any fetch/stream errors or aborts return
FALLBACK_IMAGE_DATA_URI.
- Around line 51-58: The loadFont function currently resolves fonts from the
legacy web tree; vendor the font files into a renderer-owned/shared assets
directory and update loadFont(filename: string) to read from that renderer
assets location (e.g., reference a new RENDERER_ASSETS_DIR or renderer assets
path used by the renderer), replacing the join(import.meta.dirname!, "..",
"web", "fonts", filename) resolution; ensure the new directory contains the same
font files and that loadFont still returns Promise<ArrayBuffer> for the renamed
path so OG generation no longer depends on the legacy web/ tree.
---
Nitpick comments:
In `@web-next/src/routes/`(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx:
- Around line 28-49: Wrap both fetchQuery(...).toPromise() GraphQL calls (the
one using ogimageQuery and the second lookup around lines 64-85) in try/catch
blocks; on catch, log the error using the LogTape logger (e.g., LogTape.error or
processLogger equivalent) with structured fields { handle, idOrYear, slug,
requestedLanguage: language } and the caught error, and return a controlled 5xx
Response (e.g., new Response("Internal Server Error", { status: 500 })) instead
of allowing an unstructured throw; keep createEnvironment(), fetchQuery, and the
query names (ogimageQuery / the second query) intact while only adding the
try/catch, logging, and the 5xx return.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 037cb3c9-e53d-4b1d-8a3f-3c8dc48fda93
📒 Files selected for processing (3)
graphql/og.test.tsgraphql/og.tsweb-next/src/routes/(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx
Add timeout and size checks when fetching remote images for OG rendering so slow or oversized avatar responses fall back instead of tying up Satori generation. Cover both Content-Length rejection and fetch timeout fallback. hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic Open Graph (OG) image generation for user profiles and articles, utilizing Satori and Resvg to render images that are then cached on disk. The changes include new GraphQL fields, complexity limits for bulk queries, and frontend integration for social sharing. Key feedback highlights a security risk regarding SSRF and potential memory issues when fetching remote avatars, suggests optimizing the caching layer to prevent unnecessary processing, and recommends preserving newlines during text truncation to maintain intended formatting.
Read remote OG images as bounded streams so responses without Content-Length cannot be fully buffered past the image limit. Defer Satori element construction until after the cache key check so cache hits skip avatar and logo loading. hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces Open Graph (OG) image generation for user profiles and article content, utilizing Satori and Resvg to render dynamic images. It adds new GraphQL fields for retrieving these images and implements a caching mechanism on the disk to store and reuse generated images. The review feedback highlighted a security improvement for the image fetching process and raised concerns regarding the use of side effects within GraphQL query resolvers.
Restrict remote OG image embedding to HTTP(S) URLs and require image content types before reading the response body. Cover non-image responses and unsupported URL schemes in the OG image tests. hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements dynamic Open Graph (OG) image generation for user profiles and articles using Satori and Resvg. It introduces a new og.ts utility for rendering images, adds ogImageUrl fields to the GraphQL schema for Account and ArticleContent, and updates the frontend to include the necessary meta tags. Feedback focuses on adhering to the repository's import style guide, improving the robustness of remote image fetching, and optimizing Relay environment initialization in server-side routes.
There was a problem hiding this comment.
Code Review
This pull request implements dynamic Open Graph (OG) image generation for user profiles and articles using Satori and Resvg. It introduces a new ogImageUrl field to the GraphQL schema for both Account and ArticleContent types, along with a caching mechanism that stores generated PNGs on disk. The feedback highlights potential race conditions due to database updates and disk deletions occurring within GraphQL query resolvers, suggesting that cache invalidation or generation should be moved to mutations or background tasks. Additionally, there is a suggestion to improve font selection for CJK characters in profile bios by explicitly setting the lang attribute.
|
Codex Review: Didn't find any major issues. 🚀 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Treat image responses with a missing or empty body as failed remote avatar loads so OG rendering uses the fallback image instead of producing an invalid empty data URI. hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
Move external GraphQL imports before internal model imports and keep the internal model imports in alphabetical order. hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
Create the Relay environment once per OG image request and pass it into the default-language lookup instead of initializing a second environment. hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
Stop deleting previously stored OG images from query resolvers after a cache key update. This avoids races where concurrent requests can still be using the old object while another resolver refreshes the cache. hackers-pub#269 (comment) hackers-pub#269 (comment) Assisted-by: Codex:gpt-5.5
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements dynamic Open Graph (OG) image generation for user profiles and articles. It introduces a new og.ts utility that uses Satori and Resvg to render PNG images, which are then cached on disk. The GraphQL schema is updated to include ogImageUrl fields for Account and ArticleContent types, with complexity limits to prevent abuse. Corresponding frontend changes in the SolidStart stack add the necessary meta tags and API routes to serve these images. Extensive tests were added to verify image generation, caching behavior, and complexity constraints. I have no feedback to provide.
|
Codex Review: Didn't find any major issues. Bravo. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Closes #265.
What changed
This PR implements
og:imagesupport for web-next profile and article pages.It adds Satori/Resvg-based image rendering in graphql/og.ts, exposes cached
ogImageUrlfields from graphql/account.ts and graphql/post.ts, and wires those fields into the web-next profile and article routes under web-next/src/routes/(root)/[handle]/og.tsx and web-next/src/routes/(root)/[handle]/[idOrYear]/[slug]/ogimage.tsx.The image design follows DESIGN.md: black and white first, quiet typography, Pubnyan plus the Hackers' Pub logotype in the footer, and no extra marketing copy inside the image.
Notes
The generated images are cached through the configured drive and keyed by a stable hash of the render input. Article cache keys include article/content identity so two articles with the same title and text do not collide.
The expensive
ogImageUrlfields also carry high GraphQL complexity so public bulk queries cannot cheaply trigger many Satori/Resvg renders at once.Article
/ogimagerequests now resolve a single language instead of asking GraphQL for every translation when no?l=parameter is present.