-
Notifications
You must be signed in to change notification settings - Fork 0
Fix/yu 39/opengraph #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Introduced `OgImageTemplate` component for rendering OG images with dynamic content. - Integrated `satori` library for SVG rendering of OG images. - Created unified route handler for OG images at `/og/[...route].png`. - Updated layout to include dynamic description and OG image metadata. - Added new JSON files for page content and metadata. - Implemented error handling for OG image generation. - Added utility functions for font loading and page data management.
…o en un json y limpiado el codigo de debug
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
yellowumbrella-web | ff66128 | Commit Preview URL Branch Preview URL |
Sep 23 2025, 02:40 PM |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdded dynamic Open Graph metadata and image generation. Introduced a pages registry JSON, updated multiple pages to read title/description from it, enhanced the layout to compute meta and OG/Twitter tags, added an OG image API route generating SVG via a new template module, and added the Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User Agent
participant P as Page (*.astro)
participant L as Layout.astro
participant R as OG Route [...og].png.ts
participant D as pages.json
participant T as OgImageTemplate.ts
U->>P: Request page
P->>D: Import/read pages data
P->>L: Render with { title, description }
L->>L: Compute ogImageUrl for current path
note right of L: Meta tags set: OG/Twitter<br/>title, description, image
U->>R: Request og image URL
R->>R: Normalize path from params
R->>D: Load page data for path
R->>T: generateOgImageSvg(title, description)
T-->>R: SVG string
R-->>U: 200 image/svg+xml (cache headers)
alt Error
R-->>U: 200 fallback SVG
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements dynamic Open Graph (OG) metadata functionality, allowing each page to have personalized images and descriptions instead of using static values. The implementation centralizes page data in a JSON configuration file and generates dynamic OG images using React components.
- Centralized page metadata configuration in
pages.json - Dynamic OG image generation using Satori for all pages
- Updated page components to use dynamic titles and descriptions from configuration
Reviewed Changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/sobre-nosotros.astro | Updated to use dynamic title and description from pages.json |
| src/pages/redes-sociales.astro | Updated to use dynamic title and description from pages.json |
| src/pages/index.astro | Updated to use dynamic title and description from pages.json |
| src/pages/contacto.astro | Updated to use dynamic title and description from pages.json |
| src/pages/[...og].png.ts | New unified OG image generator handling all page routes |
| src/layouts/Layout.astro | Updated to accept dynamic description and generate dynamic OG image URLs |
| src/components/OgImageTemplate.tsx | New React component template for generating OG images |
| src/api/pages.json | New configuration file containing page metadata |
| package.json | Added satori dependency for OG image generation |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
src/pages/[...og].png.ts
Outdated
| } | ||
|
|
||
| const page = pagesData.pages.find(p => p.url === requestedPath); | ||
|
|
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error. The page variable could be undefined if no matching page is found in the JSON data. This will cause a destructuring error when trying to extract title and description.
| if (!page) { | |
| return new Response('Page not found', { | |
| status: 404, | |
| headers: { | |
| 'Content-Type': 'text/plain; charset=utf-8', | |
| 'Cache-Control': 'public, max-age=60', | |
| }, | |
| }); | |
| } |
| const currentPath = "/sobre-nosotros"; | ||
| const pageData = pagesData.pages.find(page => page.url === currentPath); | ||
| const { title, description } = pageData; |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error. The pageData variable could be undefined if no matching page is found, which will cause a destructuring error when trying to extract title and description.
| const { title, description } = pageData; | |
| const title = pageData?.title ?? "Sobre nosotros"; | |
| const description = pageData?.description ?? "Conoce más sobre Yellow Umbrella."; |
| const currentPath = "/redes-sociales"; | ||
| const pageData = pagesData.pages.find(page => page.url === currentPath); | ||
| const { title, description } = pageData; |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error. The pageData variable could be undefined if no matching page is found, which will cause a destructuring error when trying to extract title and description.
| const { title, description } = pageData; | |
| const { title, description } = pageData || { title: "Redes sociales", description: "Enlaces a nuestras redes sociales." }; |
| const currentPath = "/"; | ||
| const pageData = pagesData.pages.find(page => page.url === currentPath); | ||
| const { title, description } = pageData; |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error. The pageData variable could be undefined if no matching page is found, which will cause a destructuring error when trying to extract title and description.
| const { title, description } = pageData; | |
| const { title = "Default Title", description = "Default description" } = pageData || {}; |
| const currentPath = "/contacto"; | ||
| const pageData = pagesData.pages.find((page) => page.url === currentPath); | ||
| const { title, description } = pageData; |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference error. The pageData variable could be undefined if no matching page is found, which will cause a destructuring error when trying to extract title and description.
| const { title, description } = pageData; | |
| const { title, description } = pageData ?? { title: "Contacto", description: "Página de contacto" }; |
src/pages/[...og].png.ts
Outdated
| }, | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error generando imagen OG (unified):', error); |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch block logs the error but doesn't return a response. This will result in an undefined return value, causing the API route to fail. Consider returning a fallback response or re-throwing the error.
| console.error('Error generando imagen OG (unified):', error); | |
| console.error('Error generando imagen OG (unified):', error); | |
| return new Response( | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630"><rect width="100%" height="100%" fill="#fff"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="48" fill="#d00">Error generando imagen OG</text></svg>', | |
| { | |
| status: 500, | |
| headers: { | |
| 'Content-Type': 'image/svg+xml; charset=utf-8', | |
| 'Cache-Control': 'no-store', | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET', | |
| 'Access-Control-Allow-Headers': 'Content-Type', | |
| }, | |
| } | |
| ); |
src/components/OgImageTemplate.tsx
Outdated
| const umbrellaLogoBase64 = "data:image/svg+xml;base64," + Buffer.from(` | ||
| <svg height="120" width="120" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000"> | ||
| <g> | ||
| <g> | ||
| <path style="fill:#3b280d;" d="M266.96,10.96v321.893h-21.92V10.96C245.04,4.915,249.955,0,256,0 C262.045,0,266.96,4.915,266.96,10.96z"/> | ||
| <path style="fill:#e9ca01;" d="M212.054,411.836c-15.689,0-23.169-8.327-26.682-15.313c-5.538-11.012-4.564-25.726,2.315-34.987 c3.613-4.863,2.599-11.735-2.264-15.347c-4.861-3.612-11.734-2.599-15.346,2.264c-11.925,16.053-13.655,39.331-4.304,57.925 c8.755,17.41,25.624,27.395,46.282,27.395c22.561,0,38.267-9.842,46.68-29.253c7.237-16.696,8.235-38.783,8.235-60.164v-11.525 H245.03v11.525C245.03,395.439,237.018,411.836,212.054,411.836z"/> | ||
| <path style="fill:#ffeb0a;" d="M253.808,25.307c-1.461,0.931-2.856,1.86-4.317,2.857c-1.528,0.997-2.989,1.993-4.45,3.056 c-1.329,0.863-2.657,1.794-3.919,2.789c-2.059,1.395-4.052,2.856-5.978,4.384c-6.044,4.584-11.89,9.366-17.536,14.481 c-3.654,3.255-7.174,6.642-10.695,10.096c-9.565,9.565-18.466,19.862-26.636,30.821c-1.528,2.06-3.056,4.185-4.517,6.31 c-3.919,5.514-7.572,11.16-11.026,17.005c-1.063,1.727-2.059,3.454-3.056,5.248c-2.989,5.181-5.779,10.428-8.303,15.808 c-0.996,1.86-1.86,3.787-2.723,5.713c-0.133,0.2-0.266,0.399-0.266,0.598c-1.794,3.719-3.388,7.572-4.916,11.426 c-0.531,1.129-0.996,2.324-1.395,3.52c-0.531,1.196-0.996,2.458-1.395,3.653c-1.129,2.99-2.192,6.046-3.188,9.101 c-1.196,3.587-2.325,7.24-3.388,10.959c-0.864,2.99-1.661,5.979-2.391,9.035c-0.332,1.062-0.531,2.125-0.797,3.188 c-0.133,0.531-0.266,1.062-0.399,1.661c-0.598,2.656-1.196,5.314-1.727,8.037c-0.066,0.199-0.066,0.465-0.133,0.665 c-0.398,1.992-0.73,4.051-1.063,6.044c-0.465,2.259-0.797,4.516-1.13,6.775c-0.398,2.656-0.731,5.248-1.063,7.904 c-12.953-19.794-36.666-33.079-63.834-33.079c-23.98,0-45.302,10.363-58.852,26.37c-1.727,1.993-3.255,4.053-4.716,6.244 c0.532-3.123,1.063-6.178,1.727-9.233C23.714,110.863,123.616,30.157,245.04,25.573C247.963,25.374,250.885,25.307,253.808,25.307 z"/> | ||
| <path style="fill:#e9ca01;" d="M512,225.976c-13.019-19.529-36.6-32.615-63.568-32.615c-27.301,0-51.147,13.418-64.033,33.412 c-0.398-2.657-0.797-5.248-1.328-7.904c-0.465-2.658-0.93-5.248-1.528-7.838c-1.129-5.646-2.524-11.226-3.986-16.739 c-0.598-2.125-1.196-4.185-1.86-6.311c-1.328-4.516-2.79-8.9-4.384-13.285c-0.531-1.528-1.063-2.989-1.594-4.45 c-0.598-1.528-1.129-2.989-1.727-4.45c-2.59-6.444-5.38-12.754-8.369-18.932c-0.797-1.727-1.661-3.454-2.591-5.115 c-0.664-1.395-1.395-2.723-2.126-4.118c-1.196-2.326-2.458-4.584-3.72-6.775c-1.262-2.259-2.591-4.451-3.919-6.642 c-1.328-2.193-2.723-4.384-4.118-6.576c-1.395-2.126-2.79-4.252-4.251-6.377c-15.942-23.315-35.404-44.04-57.59-61.443 c-4.65-3.653-9.432-7.174-14.348-10.495c-3.521-2.524-7.174-4.849-10.827-7.174c-0.066,0-0.133-0.067-0.133-0.067 c-2.192-1.328-4.317-2.656-6.509-3.919c1.461-0.997,2.856-1.926,4.317-2.857H256c3.653,0,7.373,0.066,10.96,0.266 c59.915,2.193,114.582,23.049,157.16,56.261c16.54,12.82,31.22,27.501,43.707,43.708C490.478,154.769,505.955,188.978,512,225.976 z"/> | ||
| <path style="fill:#ffeb0a;" d="M256,32.042v194.889c12.853-20.088,36.765-33.592,64.146-33.592 c27.38,0,51.293,13.504,64.146,33.592c0.033-0.051,0.068-0.1,0.1-0.151C372.105,144.558,323.778,74.118,256,32.042z"/> | ||
| <path style="fill:#e9ca01;" d="M249.508,28.133c-35.497,23.619-65.251,55.184-86.714,92.161 c-15.139,26.081-26.15,54.854-32.151,85.423c-1.339,6.821-2.434,13.73-3.264,20.72c0.108,0.166,0.222,0.328,0.328,0.495 c6.651-10.396,16.27-19.02,27.793-24.93c10.744-5.51,23.142-8.661,36.353-8.661c5.586,0,11.024,0.571,16.257,1.636 c20.417,4.156,37.659,15.966,47.889,31.955V32.042C253.855,30.711,251.692,29.407,249.508,28.133z"/> | ||
| <path style="fill:#fffdb8;" d="M252.2,25.307v1.827c-0.377,0.236-0.748,0.478-1.118,0.713c-0.053,0.032-0.106,0.07-0.159,0.102 c-0.06,0.038-0.119,0.076-0.179,0.115c-1.046,0.669-2.085,1.344-3.117,2.031c-0.384,0.255-0.774,0.509-1.158,0.771 c-0.152,0.096-0.298,0.197-0.45,0.299c-0.152,0.102-0.311,0.204-0.463,0.306c-114.89,6.776-209.132,79.498-233.764,175.31 c-3.481,3.267-6.578,6.852-9.232,10.698C20.467,111.784,122.075,30,246.469,25.466C248.375,25.39,250.287,25.339,252.2,25.307z"/> | ||
| </g> | ||
| </g> |
Copilot
AI
Sep 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Buffer is a Node.js API that may not be available in all runtime environments where this component might be executed. Consider using a pre-encoded base64 string or a different encoding method for better compatibility.
| const umbrellaLogoBase64 = "data:image/svg+xml;base64," + Buffer.from(` | |
| <svg height="120" width="120" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000"> | |
| <g> | |
| <g> | |
| <path style="fill:#3b280d;" d="M266.96,10.96v321.893h-21.92V10.96C245.04,4.915,249.955,0,256,0 C262.045,0,266.96,4.915,266.96,10.96z"/> | |
| <path style="fill:#e9ca01;" d="M212.054,411.836c-15.689,0-23.169-8.327-26.682-15.313c-5.538-11.012-4.564-25.726,2.315-34.987 c3.613-4.863,2.599-11.735-2.264-15.347c-4.861-3.612-11.734-2.599-15.346,2.264c-11.925,16.053-13.655,39.331-4.304,57.925 c8.755,17.41,25.624,27.395,46.282,27.395c22.561,0,38.267-9.842,46.68-29.253c7.237-16.696,8.235-38.783,8.235-60.164v-11.525 H245.03v11.525C245.03,395.439,237.018,411.836,212.054,411.836z"/> | |
| <path style="fill:#ffeb0a;" d="M253.808,25.307c-1.461,0.931-2.856,1.86-4.317,2.857c-1.528,0.997-2.989,1.993-4.45,3.056 c-1.329,0.863-2.657,1.794-3.919,2.789c-2.059,1.395-4.052,2.856-5.978,4.384c-6.044,4.584-11.89,9.366-17.536,14.481 c-3.654,3.255-7.174,6.642-10.695,10.096c-9.565,9.565-18.466,19.862-26.636,30.821c-1.528,2.06-3.056,4.185-4.517,6.31 c-3.919,5.514-7.572,11.16-11.026,17.005c-1.063,1.727-2.059,3.454-3.056,5.248c-2.989,5.181-5.779,10.428-8.303,15.808 c-0.996,1.86-1.86,3.787-2.723,5.713c-0.133,0.2-0.266,0.399-0.266,0.598c-1.794,3.719-3.388,7.572-4.916,11.426 c-0.531,1.129-0.996,2.324-1.395,3.52c-0.531,1.196-0.996,2.458-1.395,3.653c-1.129,2.99-2.192,6.046-3.188,9.101 c-1.196,3.587-2.325,7.24-3.388,10.959c-0.864,2.99-1.661,5.979-2.391,9.035c-0.332,1.062-0.531,2.125-0.797,3.188 c-0.133,0.531-0.266,1.062-0.399,1.661c-0.598,2.656-1.196,5.314-1.727,8.037c-0.066,0.199-0.066,0.465-0.133,0.665 c-0.398,1.992-0.73,4.051-1.063,6.044c-0.465,2.259-0.797,4.516-1.13,6.775c-0.398,2.656-0.731,5.248-1.063,7.904 c-12.953-19.794-36.666-33.079-63.834-33.079c-23.98,0-45.302,10.363-58.852,26.37c-1.727,1.993-3.255,4.053-4.716,6.244 c0.532-3.123,1.063-6.178,1.727-9.233C23.714,110.863,123.616,30.157,245.04,25.573C247.963,25.374,250.885,25.307,253.808,25.307 z"/> | |
| <path style="fill:#e9ca01;" d="M512,225.976c-13.019-19.529-36.6-32.615-63.568-32.615c-27.301,0-51.147,13.418-64.033,33.412 c-0.398-2.657-0.797-5.248-1.328-7.904c-0.465-2.658-0.93-5.248-1.528-7.838c-1.129-5.646-2.524-11.226-3.986-16.739 c-0.598-2.125-1.196-4.185-1.86-6.311c-1.328-4.516-2.79-8.9-4.384-13.285c-0.531-1.528-1.063-2.989-1.594-4.45 c-0.598-1.528-1.129-2.989-1.727-4.45c-2.59-6.444-5.38-12.754-8.369-18.932c-0.797-1.727-1.661-3.454-2.591-5.115 c-0.664-1.395-1.395-2.723-2.126-4.118c-1.196-2.326-2.458-4.584-3.72-6.775c-1.262-2.259-2.591-4.451-3.919-6.642 c-1.328-2.193-2.723-4.384-4.118-6.576c-1.395-2.126-2.79-4.252-4.251-6.377c-15.942-23.315-35.404-44.04-57.59-61.443 c-4.65-3.653-9.432-7.174-14.348-10.495c-3.521-2.524-7.174-4.849-10.827-7.174c-0.066,0-0.133-0.067-0.133-0.067 c-2.192-1.328-4.317-2.656-6.509-3.919c1.461-0.997,2.856-1.926,4.317-2.857H256c3.653,0,7.373,0.066,10.96,0.266 c59.915,2.193,114.582,23.049,157.16,56.261c16.54,12.82,31.22,27.501,43.707,43.708C490.478,154.769,505.955,188.978,512,225.976 z"/> | |
| <path style="fill:#ffeb0a;" d="M256,32.042v194.889c12.853-20.088,36.765-33.592,64.146-33.592 c27.38,0,51.293,13.504,64.146,33.592c0.033-0.051,0.068-0.1,0.1-0.151C372.105,144.558,323.778,74.118,256,32.042z"/> | |
| <path style="fill:#e9ca01;" d="M249.508,28.133c-35.497,23.619-65.251,55.184-86.714,92.161 c-15.139,26.081-26.15,54.854-32.151,85.423c-1.339,6.821-2.434,13.73-3.264,20.72c0.108,0.166,0.222,0.328,0.328,0.495 c6.651-10.396,16.27-19.02,27.793-24.93c10.744-5.51,23.142-8.661,36.353-8.661c5.586,0,11.024,0.571,16.257,1.636 c20.417,4.156,37.659,15.966,47.889,31.955V32.042C253.855,30.711,251.692,29.407,249.508,28.133z"/> | |
| <path style="fill:#fffdb8;" d="M252.2,25.307v1.827c-0.377,0.236-0.748,0.478-1.118,0.713c-0.053,0.032-0.106,0.07-0.159,0.102 c-0.06,0.038-0.119,0.076-0.179,0.115c-1.046,0.669-2.085,1.344-3.117,2.031c-0.384,0.255-0.774,0.509-1.158,0.771 c-0.152,0.096-0.298,0.197-0.45,0.299c-0.152,0.102-0.311,0.204-0.463,0.306c-114.89,6.776-209.132,79.498-233.764,175.31 c-3.481,3.267-6.578,6.852-9.232,10.698C20.467,111.784,122.075,30,246.469,25.466C248.375,25.39,250.287,25.339,252.2,25.307z"/> | |
| </g> | |
| </g> | |
| const umbrellaLogoBase64 = "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyMCIgd2lkdGg9IjEyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgZmlsbD0iIzAwMDAwMCI+PGc+PGc+PHBhdGggc3R5bGU9ImZpbGw6IzNiMjgwZDsiIGQ9Ik0yNjYuOTYsMTAuOTZ2MzIxLjg5M2gtMjEuOTJW MTAuOTZD245LjA0LDUuOTE1LDI0OS45NTUsMCwyNTYsMCBDMjYyLjA0NSwwLDI2Ni45Niw0LjkxNSwyNjYuOTYsMTAuOTZ6Ii8+PHBhdGggc3R5bGU9ImZpbGw6I2U5Y2EwMTsiIGQ9Ik0yMTIuMDU0LDQxMS44MzZjLTE1LjY4OSwwLTIzLjE2OS04LjMyNy0yNi42ODItMTUuMzEzYy01LjUzOC0xMS4wMTItNC41NjQtMjUuNzI2LDIuMzE1LTM0Ljk4NyBjMy42MTMtNC44NjMsMi41OTktMTEuNzM1LTIuMjY0LTE1LjM0N2MtNC44NjEtMy42MTItMTEuNzM0LTIuNTk5LTE1LjM0NiwyLjI2NGMtMTEuOTI1LDE2LjA1My0xMy42NTUsMzkuMzMxLTQuMzA0LDU3LjkyNSBjOC43NTUsMTcuNDEsMjUuNjI0LDI3LjM5NSw0Ni4yODIsMjcuMzk1YzIyLjU2MSwwLDM4LjI2Ny05Ljg0Miw0Ni42OC0yOS4yNTNjNy4yMzctMTYuNjk2LDguMjM1LTM4Ljc4Myw4LjIzNS02MC4xNjR2LTExLjUyNSBIMjQ1LjAzdjExLjUyNUMyNDUuMDMsMzk1LjQzOSwyMzcuMDE4LDQxMS44MzYsMjEyLjA1NCw0MTEuODM2eiIvPjxwYXRoIHN0eWxlPSJmaWxsOiNmZmViMGE7IiBkPSJNMjUzLjgwOCwyNS4zMDdjLTEuNDYxLDAuOTMxLTIuODU2LDEuODYtNC4zMTcsMi44NTdjLTEuNTI4LDAuOTk3LTIuOTg5LDEuOTkzLTQuNDUsMy4wNTYgYy0xLjMyOSwwLjg2My0yLjY1NywxLjc5NC0zLjkxOSwyLjc4OWMtMi4wNTksMS4zOTUtNC4wNTIsMi44NTYtNS45NzgsNC4zODRjLTYuMDQ0LDQuNTg0LTExLjg5LDkuMzY2LTE3LjUzNiwxNC40ODEgYy0zLjY1NCwzLjI1NS03LjE3NCw2LjY0Mi0xMC42OTUsMTAuMDk2Yy05LjU2NSw5LjU2NS0xOC40NjYsMTkuODYyLTI2LjYzNiwzMC44MjFjLTEuNTI4LDIuMDYtMy4wNTYsNC4xODUtNC41MTcsNi4zMSBjLTMuOTE5LDUuNTE0LTcuNTcyLDExLjE2LTExLjAyNiwxNy4wMDVjLTEuMDYzLDEuNzI3LTIuMDU5LDMuNDU0LTMuMDU2LDUuMjQ4Yy0yLjk4OSw1LjE4MS01Ljc3OSwxMC40MjgtOC4zMDMsMTUuODA4IGMtMC45OTYsMS44Ni0xLjg2LDMuNzg3LTIuNzIzLDUuNzEzYy0wLjEzMywwLjItMC4yNjYsMC4zOTktMC4yNjYsMC41OThjLTEuNzk0LDMuNzE5LTMuMzg4LDcuNTcyLTQuOTE2LDExLjQyNiBjLTAuNTMxLDEuMTI5LTAuOTk2LDIuMzI0LTEuMzk1LDMuNTJjLTAuNTMxLDEuMTk2LTAuOTk2LDIuNDU4LTEuMzk1LDMuNjUzYy0xLjEyOSwyLjk5LTIuMTkyLDYuMDQ2LTMuMTg4LDkuMTAxIGMtMS4xOTYsMy41ODctMi4zMjUsNy4yNC0zLjM4OCwxMC45NTljLTAuODY0LDIuOTktMS42NjEsNS45NzktMi4zOTEsOS4wMzUgYy0wLjMzMiwxLjA2Mi0wLjUzMSwyLjEyNS0wLjc5NywzLjE4OGMtMC4xMzMsMC41MzEtMC4yNjYsMS4wNjItMC4zOTksMS42NjFjLTAuNTk4LDIuNjU2LTEuMTk2LDUuMzE0LTEuNzI3LDguMDM3IGMtMC4wNjYsMC4xOTktMC4wNjYsMC40NjUtMC4xMzMsMC42NjVjLTAuMzk4LDEuOTkyLTAuNzMsNC4wNTEtMS4wNjMsNi4wNDQgYy0wLjQ2NSwyLjI1OS0wLjc5Nyw0LjUxNi0xLjEzLDYuNzc1Yy0wLjM5OCwyLjY1Ni0wLjczMSw1LjI0OC0xLjA2Myw3LjkwNCAtMTIuOTUzLTE5Ljc5NC0zNi42NjYtMzMuMDc5LTYzLjgzNC0zMy4wNzl jLTIzLjk4LDAtNDUuMzAyLDEwLjM2My01OC44NTIsMjYuMzcwYy0xLjcyNywxLjk5My0zLjI1NSw0LjA1My00LjcxNiw2LjI0NCBjMC41MzItMy4xMjMsMS4wNjMtNi4xNzgsMS43MjctOS4yMzNDMjMuNzE0LDExMC44NjMsMTIzLjYxNiwzMC4xNTcsMjQ1LjA0LDM1LjU3M0MyNDcuOTYzLDI1LjM3NCwyNTAuODg1LDI1LjMwNywyNTMuODA4LDI1LjMwNyB6Ii8+PHBhdGggc3R5bGU9ImZpbGw6I2U5Y2EwMTsiIGQ9Ik01MTIsMjI1Ljk3NmMtMTMuMDE5LTE5LjUyOS0zNi42LTMyLjYxNS02My41NjgtMzIuNjE1Yy0yNy4zMDEsMC01MS4xNDcsMTMuNDE4LTY0LjAzMywzMy40MTIgYy0wLjM5OC0yLjY1Ny0wLjc5Ny01LjI0OC0xLjMyOC03LjkwNGMtMC40NjUtMi42NTgtMC45My01LjI0OC0xLjUyOC03LjgzOCBjLTEuMTI5LTUuNjQ2LTIuNTI0LTExLjIyNi0zLjk4Ni0xNi43MzljLTAuNTk4LTIuMTI1LTEuMTk2LTQuMTg1LTEuODYtNi4zMTFjLTEuMzI4LTQuNTE2LTIuNzktOC45LTEuMzg0LTEzLjI4NWMtMC41MzEtMS41MjgtMS4wNjMtMi45ODktMS41OTQtNC40NSBjLTAuNTk4LTEuNTI4LTEuMTI5LTIuOTg5LTEuNzI3LTQuNDUwYy0yLjU5LTYuNDQ0LTUuMzgtMTIuNzU0LTguMzY5LTE4LjkyMiBjLTAuNzk3LTEuNzI3LTEuNjYxLTMuNDU0LTIuNTkxLTUuMTE1Yy0wLjY2NC0xLjM5NS0xLjM5NS0yLjcyMy0yLjEyNi00LjExOCBjLTEuMTk2LTIuMzI2LTIuNDU4LTQuNTg0LTMuNzItNi43NzVjLTEuMjYyLTIuMjU5LTIuNTkxLTQuNDUxLTMuOTE5LTYuNjQyIGMtMS4zMjgtMi4xOTMtMi43MjMtNC4zODQtNC4xMTgtNi41NzZjLTEuMzk1LTIuMTI2LTIuNzktNC4yNTItNC4yNTEtNi4zNzdjLTE1Ljk0Mi0yMy4zMTUtMzUuNDA0LTQ0LjA0LTU3LjU5LTYxLjQ0MyBjLTQuNjUtMy42NTMtOS40MzItNy4xNzQtMTQuMzQ4LTEwLjQ5NWMtMy41MjEtMi41MjQtNy4xNzQtNC44NDktMTAuODI3LTcuMTc0Yy0wLjA2NiwwLTAuMTMzLTAuMDY3LTAuMTMzLTAuMDY3IGMtMi4xOTItMS4zMjgtNC4zMTctMi42NTYtNi41MDktMy45MTljMS40NjEtMC45OTcsMi44NTYtMS45MjYsNC4zMTctMi44NTdIMjU2YzMuNjUzLDAsNy4zNzMsMC4wNjYsMTAuOTYsMC4yNjYgYzU5LjkxNSwyLjE5MywxMTQuNTgyLDIzLjA0OSwxNTcuMTYsNTYuMjYxYzE2LjU0LDEyLjgyLDMxLjIyLDI3LjUwMSw0My43MDcsNDMuNzA4QzQ5MC40NzgsMTU0Ljc2OSw1MDUuOTU1LDE4OC45NzgsNTEyLDIyNS45NzZ6Ii8+PHBhdGggc3R5bGU9ImZpbGw6I2ZmZWIwYTsiIGQ9Ik0yNTYsMzIuMDQydjE5NC44ODljMTIuODUzLTIwLjA4OCwzNi43NjUtMzMuNTkyLDY0LjE0Ni0zMy41OTIgYzI3LjM4LDAsNTEuMjkzLDEzLjUwNCw2NC4xNDYsMzMuNTkyYzAuMDMzLTAuMDUxLDAuMDY4LTAuMSwwLjEtMC4xNTFDMzcyLjEwNSwxNDQuNTU4LDMyMy43NzgsNzQuMTE4LDI1NiwzMi4wNDJ6Ii8+PHBhdGggc3R5bGU9ImZpbGw6I2U5Y2EwMTsiIGQ9Ik0yNDkuNTA4LDI4LjEzM2MtMzUuNDk3LDIzLjYxOS02NS4yNTEsNTUuMTg0LTg2LjcxNCw5Mi4xNjEgYy0xNS4xMzksMjYuMDgxLTI2LjE1LDU0Ljg1NC0zMi4xNTEsODUuNDIzYy0xLjMzOSw2LjgyMS0yLjQzNCwxMy43My0zLjI2NCwyMC43MiBjMC4xMDgsMC4xNjYsMC4yMjIsMC4zMjgsMC4zMjgsMC40OTUgYzYuNjUxLTEwLjM5NiwxNi4yNy0xOS4wMiwyNy43OTMtMjQuOTMgYzEwLjc0NC01LjUxLDIzLjE0Mi04LjY2MSwzNi4zNTMtOC42NjFjNS41ODYsMCwxMS4wMjQsMC41NzEsMTYuMjU3LDEuNjM2IGMyMC40MTcsNC4xNTYsMzcuNjU5LDE1Ljk2Niw0Ny44ODksMzEuOTU1VjMyLjA0MkMyNTMuODU1LDMwLjcxMSwyNTEuNjkyLDI5LjQwNywyNDkuNTA4LDI4LjEzM3oiLz48cGF0aCBzdHlsZT0iZmlsbDojZmZmZGI4OyIgZD0iTTI1Mi4yLDI1LjMwN3YxLjgyN2MtMC4zNzcsMC4yMzYtMC43NDgsMC40NzgtMS4xMTgsMC43MTNjLTAuMDUzLDAuMDMyLTAuMTA2LDAuMDctMC4xNTksMC4xMDIgYy0wLjA2LDAuMDM4LTAuMTE5LDAuMDc2LTAuMTc5LDAuMTE1Yy0xLjA0NiwwLjY2OS0yLjA4NSwxLjM0NC0zLjExNywyLjAzMSBjLTAuMzg0LDAuMjU1LTAuNzc0LDAuNTA5LTEuMTU4LDAuNzcxYy0wLjE1MiwwLjA5Ni0wLjI5OCwwLjE5Ny0wLjQ1LDAuMjk5Yy0wLjE1MiwwLjEwMi0wLjMxMSwwLjIwNC0wLjQ2MywwLjMwNiBjLTExNC44OSw2Ljc3Ni0yMDkuMTMyLDc5LjQ5OC0yMzMuNzY0LDE3NS4zMTAgYy0zLjQ4MSwzLjI2Ny02LjU3OCw2Ljg1Mi05LjIzMiwxMC42OThDMjAuNDY3LDExMS43ODQsMTIyLjA3NSwzMCwyNDYuNDY5LDI1LjQ2NkMyNDguMzc1LDI1LjM5LDM1MC4yODcsMjUuMzM5LDI1Mi4yLDI1LjMwN3oiLz48L2c+PC9nPjwvc3ZnPg=="; | |
| </g> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (12)
src/pages/index.astro (1)
6-8: Guard against missing page data and use the real path.Avoid a crash if the URL isn’t found and remove hard‑coded path.
Apply:
-const currentPath = "/"; -const pageData = pagesData.pages.find(page => page.url === currentPath); -const { title, description } = pageData; +const currentPath = Astro.url.pathname; +const pageData = pagesData.pages.find((page) => page.url === currentPath); +const { title, description } = pageData ?? { title: "Yellow Umbrella", description: "" };src/pages/sobre-nosotros.astro (1)
6-8: Same guard + dynamic path here.-const currentPath = "/sobre-nosotros"; -const pageData = pagesData.pages.find(page => page.url === currentPath); -const { title, description } = pageData; +const currentPath = Astro.url.pathname; +const pageData = pagesData.pages.find((page) => page.url === currentPath); +const { title, description } = pageData ?? { title: "Sobre nosotros", description: "" };src/pages/redes-sociales.astro (1)
18-22: SSR flag consistency + guard + dynamic path.
- Keep prerender strategy consistent across pages (all SSR or all prerender), unless there’s a specific need here.
- Add null guard and use real path.
-const currentPath = "/redes-sociales"; -const pageData = pagesData.pages.find(page => page.url === currentPath); -const { title, description } = pageData; +const currentPath = Astro.url.pathname; +const pageData = pagesData.pages.find((page) => page.url === currentPath); +const { title, description } = pageData ?? { title: "Redes sociales", description: "" };src/pages/contacto.astro (1)
7-9: Same guard + dynamic path.-const currentPath = "/contacto"; -const pageData = pagesData.pages.find((page) => page.url === currentPath); -const { title, description } = pageData; +const currentPath = Astro.url.pathname; +const pageData = pagesData.pages.find((page) => page.url === currentPath); +const { title, description } = pageData ?? { title: "Contacto", description: "" };src/api/pages.json (1)
14-16: Minor copy nit: add accent.“Síguenos” lleva tilde.
- "description": "Siguenos en nuestras redes sociales" + "description": "Síguenos en nuestras redes sociales"src/layouts/Layout.astro (3)
38-44: Make Twitter/OG URL/title fully dynamic.Use the current URL and dynamic title for Twitter too.
- <meta property="og:url" content="https://yellowumbrella.dev/" /> + <meta property="og:url" content={Astro.url.toString()} /> - <meta property="twitter:url" content="https://yellowumbrella.dev/"/> - <meta name="twitter:title" content="Yellow Umbrella"/> + <meta name="twitter:url" content={Astro.url.toString()}/> + <meta name="twitter:title" content={title}/>
29-31: Optional: drop keywords meta.Search engines ignore it; reduce noise.
- <meta name="keywords" content="YellowUmbrella, yellow, umbrella"/>
34-37: Add alt text for og:image.Helpful for accessibility parsers/tools.
<meta property="og:image" content={ogImageUrl.toString()} /> + <meta property="og:image:alt" content={title} />src/components/OgImageTemplate.tsx (2)
52-59: Update src to new var and add alt.- <img - src={umbrellaLogoBase64} + <img + src={umbrellaLogoDataUrl} + alt="Yellow Umbrella logo"
27-41: Tiny style DRY (optional).fontFamily is repeated; extract a const styles object to reduce duplication.
src/pages/[...og].png.ts (2)
30-66: Parsing de params innecesariamente complejo y con supuestos erróneosEn Astro, el catch‑all suele llegar como string (no array) y la extensión
.pngno forma parte del parámetro. Simplifica y normaliza condecodeURIComponent.Apply this diff:
- const ogParam = params.og || ''; - - // Manejar la ruta correctamente para todas las rutas OG unificadas - let requestedPath: string | undefined; - - if (!ogParam || ogParam === '' || ogParam === 'og.png') { - // Para /og.png (página principal) - requestedPath = '/'; - } else { - // Para rutas como /og/contacto.png - let cleanRoute = ''; - - if (Array.isArray(ogParam)) { - // Para rutas como og/contacto.png -> ["og", "contacto.png"] - if (ogParam.length >= 2) { - cleanRoute = ogParam[1]; // Tomar "contacto.png" - } else if (ogParam.length === 1 && ogParam[0] !== 'og') { - cleanRoute = ogParam[0]; // Caso edge donde solo hay un segmento - } - } else { - // Si es string, podría ser "og" (para /og.png) o "og/contacto.png" - if (ogParam === 'og') { - requestedPath = '/'; - } else { - cleanRoute = ogParam.replace(/^og\//, ''); // Remover prefijo "og/" - } - } - - if (cleanRoute) { - // Removemos la extensión .png si existe - cleanRoute = cleanRoute.replace(/\.png$/, ''); - // Construimos la ruta final - requestedPath = `/${cleanRoute}`; - } else if (!requestedPath) { - requestedPath = '/'; // Fallback a página principal - } - } + const ogParamRaw = params.og ?? ''; + const ogParamStr = Array.isArray(ogParamRaw) ? ogParamRaw.join('/') : ogParamRaw; // En Astro suele ser string + const cleaned = ogParamStr.replace(/\.png$/i, ''); + const segments = cleaned.split('/').filter(Boolean); + const slug = segments[0] === 'og' ? segments.slice(1).join('/') : segments.join('/'); + const requestedPath = slug ? `/${decodeURIComponent(slug)}` : '/';
12-28: Carga de fuente en cada request — cachea en memoriaEvita fetch por petición. Cachea el ArrayBuffer y reutilízalo.
Apply this diff:
export const prerender = false; -export const GET: APIRoute = async ({ params, request }) => { +let fontCache: ArrayBuffer | null = null; +async function loadMonaspace(origin: string): Promise<ArrayBuffer | null> { + if (fontCache) return fontCache; + try { + const fontUrl = new URL('/fonts/MonaspaceKrypton-Regular.woff', origin); + const res = await fetch(fontUrl.href); + if (!res.ok) throw new Error(`MonaspaceKrypton font error: ${res.status}`); + fontCache = await res.arrayBuffer(); + return fontCache; + } catch (e) { + console.error('Error cargando MonaspaceKrypton en unified og:', e); + return null; + } +} + +export const GET: APIRoute = async ({ params, request }) => { @@ - try { - // Construir URL absoluta para la fuente local - const url = new URL(request.url); - const fontUrl = new URL('/fonts/MonaspaceKrypton-Regular.woff', url.origin); - - const fontResponse = await fetch(fontUrl.href); - if (!fontResponse.ok) { - throw new Error(`MonaspaceKrypton font error: ${fontResponse.status}`); - } - fontData = await fontResponse.arrayBuffer(); - } catch (localError) { - console.error('Error cargando MonaspaceKrypton en unified og:', localError); - } + const url = new URL(request.url); + const fontData = await loadMonaspace(url.origin); + const fontName = 'MonaspaceKrypton';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
package.json(1 hunks)src/api/pages.json(1 hunks)src/components/OgImageTemplate.tsx(1 hunks)src/layouts/Layout.astro(2 hunks)src/pages/[...og].png.ts(1 hunks)src/pages/contacto.astro(1 hunks)src/pages/index.astro(1 hunks)src/pages/redes-sociales.astro(2 hunks)src/pages/sobre-nosotros.astro(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/[...og].png.ts (1)
src/components/OgImageTemplate.tsx (1)
OgImageTemplate(27-93)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Workers Builds: yellowumbrella-web
🔇 Additional comments (3)
src/pages/index.astro (1)
12-16: Potential duplicate head/meta.If Head.astro injects meta tags, it may conflict with Layout’s dynamic tags. Confirm it only renders in-body UI.
src/components/OgImageTemplate.tsx (1)
1-3: ESM import of React is fine; ensure TS config includes DOM lib.Satori renders in a non-DOM env; keep TS lib settings aligned to avoid TS complaining about JSX.IntrinsicElements.
package.json (1)
29-31: Avoid sharp in Cloudflare Workers — add @resvg/resvg-wasm if you need PNG rasterizationsrc/pages/[...og].png.ts uses satori to produce an SVG (no
sharp/resvgimports found); package.json still listssharp(^0.34.3). Node-native sharp won't run in Workers — either:
- If you need .png at runtime in the Worker: add @resvg/resvg-wasm and rasterize the satori SVG there.
- If not required at runtime: move/remove
sharpfrom runtime deps and do rasterization at build-time or in a Node environment.
src/components/OgImageTemplate.tsx
Outdated
| // src/components/OgImageTemplate.tsx | ||
| import React from 'react'; | ||
|
|
||
| // SVG del umbrella como string base64 - compatible con Satori | ||
| const umbrellaLogoBase64 = "data:image/svg+xml;base64," + Buffer.from(` | ||
| <svg height="120" width="120" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000"> | ||
| <g> | ||
| <g> | ||
| <path style="fill:#3b280d;" d="M266.96,10.96v321.893h-21.92V10.96C245.04,4.915,249.955,0,256,0 C262.045,0,266.96,4.915,266.96,10.96z"/> | ||
| <path style="fill:#e9ca01;" d="M212.054,411.836c-15.689,0-23.169-8.327-26.682-15.313c-5.538-11.012-4.564-25.726,2.315-34.987 c3.613-4.863,2.599-11.735-2.264-15.347c-4.861-3.612-11.734-2.599-15.346,2.264c-11.925,16.053-13.655,39.331-4.304,57.925 c8.755,17.41,25.624,27.395,46.282,27.395c22.561,0,38.267-9.842,46.68-29.253c7.237-16.696,8.235-38.783,8.235-60.164v-11.525 H245.03v11.525C245.03,395.439,237.018,411.836,212.054,411.836z"/> | ||
| <path style="fill:#ffeb0a;" d="M253.808,25.307c-1.461,0.931-2.856,1.86-4.317,2.857c-1.528,0.997-2.989,1.993-4.45,3.056 c-1.329,0.863-2.657,1.794-3.919,2.789c-2.059,1.395-4.052,2.856-5.978,4.384c-6.044,4.584-11.89,9.366-17.536,14.481 c-3.654,3.255-7.174,6.642-10.695,10.096c-9.565,9.565-18.466,19.862-26.636,30.821c-1.528,2.06-3.056,4.185-4.517,6.31 c-3.919,5.514-7.572,11.16-11.026,17.005c-1.063,1.727-2.059,3.454-3.056,5.248c-2.989,5.181-5.779,10.428-8.303,15.808 c-0.996,1.86-1.86,3.787-2.723,5.713c-0.133,0.2-0.266,0.399-0.266,0.598c-1.794,3.719-3.388,7.572-4.916,11.426 c-0.531,1.129-0.996,2.324-1.395,3.52c-0.531,1.196-0.996,2.458-1.395,3.653c-1.129,2.99-2.192,6.046-3.188,9.101 c-1.196,3.587-2.325,7.24-3.388,10.959c-0.864,2.99-1.661,5.979-2.391,9.035c-0.332,1.062-0.531,2.125-0.797,3.188 c-0.133,0.531-0.266,1.062-0.399,1.661c-0.598,2.656-1.196,5.314-1.727,8.037c-0.066,0.199-0.066,0.465-0.133,0.665 c-0.398,1.992-0.73,4.051-1.063,6.044c-0.465,2.259-0.797,4.516-1.13,6.775c-0.398,2.656-0.731,5.248-1.063,7.904 c-12.953-19.794-36.666-33.079-63.834-33.079c-23.98,0-45.302,10.363-58.852,26.37c-1.727,1.993-3.255,4.053-4.716,6.244 c0.532-3.123,1.063-6.178,1.727-9.233C23.714,110.863,123.616,30.157,245.04,25.573C247.963,25.374,250.885,25.307,253.808,25.307 z"/> | ||
| <path style="fill:#e9ca01;" d="M512,225.976c-13.019-19.529-36.6-32.615-63.568-32.615c-27.301,0-51.147,13.418-64.033,33.412 c-0.398-2.657-0.797-5.248-1.328-7.904c-0.465-2.658-0.93-5.248-1.528-7.838c-1.129-5.646-2.524-11.226-3.986-16.739 c-0.598-2.125-1.196-4.185-1.86-6.311c-1.328-4.516-2.79-8.9-4.384-13.285c-0.531-1.528-1.063-2.989-1.594-4.45 c-0.598-1.528-1.129-2.989-1.727-4.45c-2.59-6.444-5.38-12.754-8.369-18.932c-0.797-1.727-1.661-3.454-2.591-5.115 c-0.664-1.395-1.395-2.723-2.126-4.118c-1.196-2.326-2.458-4.584-3.72-6.775c-1.262-2.259-2.591-4.451-3.919-6.642 c-1.328-2.193-2.723-4.384-4.118-6.576c-1.395-2.126-2.79-4.252-4.251-6.377c-15.942-23.315-35.404-44.04-57.59-61.443 c-4.65-3.653-9.432-7.174-14.348-10.495c-3.521-2.524-7.174-4.849-10.827-7.174c-0.066,0-0.133-0.067-0.133-0.067 c-2.192-1.328-4.317-2.656-6.509-3.919c1.461-0.997,2.856-1.926,4.317-2.857H256c3.653,0,7.373,0.066,10.96,0.266 c59.915,2.193,114.582,23.049,157.16,56.261c16.54,12.82,31.22,27.501,43.707,43.708C490.478,154.769,505.955,188.978,512,225.976 z"/> | ||
| <path style="fill:#ffeb0a;" d="M256,32.042v194.889c12.853-20.088,36.765-33.592,64.146-33.592 c27.38,0,51.293,13.504,64.146,33.592c0.033-0.051,0.068-0.1,0.1-0.151C372.105,144.558,323.778,74.118,256,32.042z"/> | ||
| <path style="fill:#e9ca01;" d="M249.508,28.133c-35.497,23.619-65.251,55.184-86.714,92.161 c-15.139,26.081-26.15,54.854-32.151,85.423c-1.339,6.821-2.434,13.73-3.264,20.72c0.108,0.166,0.222,0.328,0.328,0.495 c6.651-10.396,16.27-19.02,27.793-24.93c10.744-5.51,23.142-8.661,36.353-8.661c5.586,0,11.024,0.571,16.257,1.636 c20.417,4.156,37.659,15.966,47.889,31.955V32.042C253.855,30.711,251.692,29.407,249.508,28.133z"/> | ||
| <path style="fill:#fffdb8;" d="M252.2,25.307v1.827c-0.377,0.236-0.748,0.478-1.118,0.713c-0.053,0.032-0.106,0.07-0.159,0.102 c-0.06,0.038-0.119,0.076-0.179,0.115c-1.046,0.669-2.085,1.344-3.117,2.031c-0.384,0.255-0.774,0.509-1.158,0.771 c-0.152,0.096-0.298,0.197-0.45,0.299c-0.152,0.102-0.311,0.204-0.463,0.306c-114.89,6.776-209.132,79.498-233.764,175.31 c-3.481,3.267-6.578,6.852-9.232,10.698C20.467,111.784,122.075,30,246.469,25.466C248.375,25.39,250.287,25.339,252.2,25.307z"/> | ||
| </g> | ||
| </g> | ||
| </svg> | ||
| `).toString('base64'); | ||
|
|
||
| interface OgImageTemplateProps { | ||
| title: string; | ||
| description: string; | ||
| fontFamily?: string; | ||
| } | ||
|
|
||
| export function OgImageTemplate({ title, description, fontFamily = 'Arial, sans-serif' }: OgImageTemplateProps) { | ||
| return ( | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| height: '630px', | ||
| width: '1200px', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| backgroundColor: '#1a1a1a', | ||
| padding: '40px', | ||
| fontFamily: fontFamily, | ||
| boxSizing: 'border-box', | ||
| }} | ||
| > | ||
| <div style={{ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| textAlign: 'center', | ||
| width: '100%', | ||
| height: '100%', | ||
| justifyContent: 'center', | ||
| }}> | ||
| {/* Logo Yellow Umbrella - Usando imagen base64 como recomienda Satori */} | ||
| <img | ||
| src={umbrellaLogoBase64} | ||
| width={120} | ||
| height={120} | ||
| style={{ | ||
| marginBottom: '30px', | ||
| }} | ||
| /> | ||
|
|
||
| {/* Título */} | ||
| <h1 | ||
| style={{ | ||
| color: '#ffd300', | ||
| fontSize: '60px', | ||
| margin: '0 0 20px 0', | ||
| fontFamily: fontFamily, | ||
| fontWeight: 'bold', | ||
| textAlign: 'center', | ||
| lineHeight: '1.1', | ||
| }} | ||
| > | ||
| {title} | ||
| </h1> | ||
|
|
||
| {/* Descripción */} | ||
| <p | ||
| style={{ | ||
| color: '#fff', | ||
| fontSize: '30px', | ||
| margin: '0', | ||
| fontFamily: fontFamily, | ||
| maxWidth: '800px', | ||
| textAlign: 'center', | ||
| lineHeight: '1.2', | ||
| }} | ||
| > | ||
| {description} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
End-to-end OG generation verification.
Confirm the route returns Content-Type: image/png and not SVG mislabeled as .png.
🏁 Script executed:
#!/bin/bash
# Find the OG endpoint(s) and inspect handler
fd -a ".png.ts" src/pages
rg -nP "Content-Type|content-type|setHeader|PNG|SVG|satori" -S src/pagesLength of output: 1311
OG endpoint returns SVG instead of PNG — fix or rename
src/pages/[...og].png.ts sets 'Content-Type': 'image/svg+xml; charset=utf-8' and returns an SVG generated by satori (see src/pages/[...og].png.ts, header at line ~100). Either rasterize the SVG and return a PNG with Content-Type: image/png, or rename the endpoint to .svg.ts to match the returned content.
🤖 Prompt for AI Agents
In src/components/OgImageTemplate.tsx lines 1-93: the review notes the OG
endpoint currently returns SVG while the route filename implies PNG; fix by
either (A) rasterizing the Satori-generated SVG to PNG and updating the response
header to "Content-Type: image/png" in src/pages/[...og].png.ts (use a
rasterizer like @resvg-js/resvg or sharp to convert the SVG buffer to PNG and
return that buffer), or (B) if you want to keep returning SVG, rename the
endpoint file to [...og].svg.ts and keep the "Content-Type: image/svg+xml;
charset=utf-8" header; update any consumers/links to the new URI accordingly.
| const path = Astro.url.pathname; | ||
| const ogImageUrl = new URL(`/og${path === '/' ? '' : path}.png`, Astro.url); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify OG route path won’t collide.
ogImageUrl builds /og{path}.png. Ensure your route file is under pages/og/[...slug].png.ts (or equivalent) to avoid catching unrelated .png requests.
🏁 Script executed:
#!/bin/bash
fd -a "og" src/pages || true
fd -a "[...og].png.ts" src/pages || trueLength of output: 43
Fix PNG route collision — restrict OG route to /og/
src/pages/[...og].png.ts is a root-level catch-all for .png; move it to src/pages/og/[...slug].png.ts (or change the route to only match /og/*) so Layout.astro's /og{path}.png doesn't capture unrelated .png requests.
🤖 Prompt for AI Agents
In src/layouts/Layout.astro around lines 11-13, the OG image URL construction
(/og{path}.png) collides with a root-level catch-all PNG route; move the
catch-all handler into an /og/ subdirectory or restrict its route pattern so it
only matches /og/*: rename src/pages/[...og].png.ts to
src/pages/og/[...slug].png.ts (or change its filename/pattern to
og/[...og].png.ts) so the server only serves PNGs under /og/, and update any
imports/exports or references accordingly to ensure the Layout-generated
/og{path}.png URLs map to that route only.
src/pages/[...og].png.ts
Outdated
| // Devolver SVG con headers apropiados para mejor compatibilidad | ||
| // Muchas plataformas de redes sociales aceptan SVG para Open Graph | ||
| return new Response(svg, { | ||
| headers: { | ||
| 'Content-Type': 'image/svg+xml; charset=utf-8', | ||
| 'Cache-Control': 'public, max-age=3600', // Cache por 1 hora | ||
| 'Access-Control-Allow-Origin': '*', | ||
| 'Access-Control-Allow-Methods': 'GET', | ||
| 'Access-Control-Allow-Headers': 'Content-Type', | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruta .png devuelve SVG — alto riesgo de que OG no lo renderice
Devolver image/svg+xml en una ruta .png rompe a la mayoría de scrapers (FB, X/Twitter, LinkedIn no aceptan SVG para OG). Rasteriza el SVG a PNG y ajusta headers.
Apply this diff:
-import satori from 'satori';
+import satori from 'satori';
+import { Resvg } from '@resvg/resvg-js';
@@
- const svg = await satori(element, satoriConfig);
-
- // Devolver SVG con headers apropiados para mejor compatibilidad
- // Muchas plataformas de redes sociales aceptan SVG para Open Graph
- return new Response(svg, {
- headers: {
- 'Content-Type': 'image/svg+xml; charset=utf-8',
- 'Cache-Control': 'public, max-age=3600', // Cache por 1 hora
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET',
- 'Access-Control-Allow-Headers': 'Content-Type',
- },
- });
+ const svg = await satori(element, satoriConfig);
+ // Rasterizar a PNG para máxima compatibilidad OG
+ const resvg = new Resvg(svg, {
+ fitTo: { mode: 'width', value: 1200 },
+ background: 'transparent',
+ });
+ const png = resvg.render().asPng();
+ return new Response(png, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
+ 'Content-Length': String(png.length),
+ },
+ });Nota: añade @resvg/resvg-js a dependencias.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Devolver SVG con headers apropiados para mejor compatibilidad | |
| // Muchas plataformas de redes sociales aceptan SVG para Open Graph | |
| return new Response(svg, { | |
| headers: { | |
| 'Content-Type': 'image/svg+xml; charset=utf-8', | |
| 'Cache-Control': 'public, max-age=3600', // Cache por 1 hora | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET', | |
| 'Access-Control-Allow-Headers': 'Content-Type', | |
| }, | |
| }); | |
| import satori from 'satori'; | |
| import { Resvg } from '@resvg/resvg-js'; | |
| const svg = await satori(element, satoriConfig); | |
| // Rasterizar a PNG para máxima compatibilidad OG | |
| const resvg = new Resvg(svg, { | |
| fitTo: { mode: 'width', value: 1200 }, | |
| background: 'transparent', | |
| }); | |
| const png = resvg.render().asPng(); | |
| return new Response(png, { | |
| headers: { | |
| 'Content-Type': 'image/png', | |
| 'Cache-Control': 'public, max-age=3600, s-maxage=86400', | |
| 'Content-Length': String(png.length), | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In src/pages/[...og].png.ts around lines 96 to 106, the handler currently
returns SVG with 'image/svg+xml' on a .png route which breaks OG scrapers;
instead rasterize the generated SVG to PNG using the @resvg/resvg-js library
(add it to package.json), produce PNG bytes (e.g., resvg.render(svg).asPng()),
and return a Response with Content-Type 'image/png' plus the existing
Cache-Control and CORS headers; ensure errors from rasterization are caught and
return a 500 when rendering fails.
| } catch (error) { | ||
| console.error('Error generando imagen OG (unified):', error); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sin respuesta en caso de error — devuelve 500 explícito
El catch solo loguea. Responde con 500 para evitar timeouts y facilitar el debug.
Apply this diff:
} catch (error) {
console.error('Error generando imagen OG (unified):', error);
- }
+ return new Response('Error generando imagen OG', {
+ status: 500,
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
+ });
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| console.error('Error generando imagen OG (unified):', error); | |
| } | |
| }; | |
| } catch (error) { | |
| console.error('Error generando imagen OG (unified):', error); | |
| return new Response('Error generando imagen OG', { | |
| status: 500, | |
| headers: { 'Content-Type': 'text/plain; charset=utf-8' }, | |
| }); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/pages/[...og].png.ts around lines 107 to 110, the catch block only logs
the error and never returns an HTTP response; change it so the handler returns
an explicit 500 response when an exception occurs. Capture the caught error (or
ensure it is available), call console.error as before, and then return a new
Response with a clear error message or empty body and status 500 (and
appropriate headers like content-type if needed) so the caller receives an
immediate failure instead of timing out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/pages/[...og].png.ts (1)
42-45: Fallback de página más robusto.Si pages.json cambia de estructura o viene vacío, este destructuring puede fallar. Añade defaults seguros.
-const page = pagesData.pages.find(p => p.url === requestedPath) || pagesData.pages[0]; -const { title, description } = page; +const page = + Array.isArray(pagesData?.pages) + ? (pagesData.pages.find((p) => p.url === requestedPath) ?? pagesData.pages[0]) + : undefined; +const { title, description } = page ?? { title: 'Yellow Umbrella', description: '' };src/components/OgImageTemplate.ts (2)
26-33: Pequeño off‑by‑one en el ajuste de líneas.La condición no cuenta el espacio entre palabras, pudiendo sobrepasar maxLength.
-words.forEach(word => { - if ((currentLine + word).length <= maxLength) { - currentLine += (currentLine ? ' ' : '') + word; - } else { +words.forEach((word) => { + const projected = (currentLine ? currentLine.length + 1 : 0) + word.length; + if (projected <= maxLength) { + currentLine += (currentLine ? ' ' : '') + word; + } else { if (currentLine) lines.push(currentLine); currentLine = word; } });
65-67: Compatibilidad SVG: usa flood-opacity en lugar de rgba en flood-color.Algunos renderers (p.ej., resvg) son más estrictos con flood-color.
-<feDropShadow dx="2" dy="2" stdDeviation="3" flood-color="rgba(0,0,0,0.3)" /> +<feDropShadow dx="2" dy="2" stdDeviation="3" flood-color="#000000" flood-opacity="0.3" />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/components/OgImageTemplate.ts(1 hunks)src/pages/[...og].png.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/[...og].png.ts (1)
src/components/OgImageTemplate.ts (1)
generateOgImageSvg(7-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Workers Builds: yellowumbrella-web
🔇 Additional comments (1)
src/pages/[...og].png.ts (1)
57-76: El catch devuelve 200 con un error (falta status 500 y CORS en fallback).Devuelve 500 y no caches el error para facilitar depuración y evitar que se sirvan fallos desde CDN.
Si no aplicas la rasterización aún, al menos ajusta el fallback:
- return new Response(fallbackSvg, { - headers: { - 'Content-Type': 'image/svg+xml; charset=utf-8', - 'Cache-Control': 'public, max-age=300', - }, - }); + return new Response(fallbackSvg, { + status: 500, + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': 'no-store', + 'Access-Control-Allow-Origin': '*', + }, + });
| import type { APIRoute } from 'astro'; | ||
| import { generateOgImageSvg } from '../components/OgImageTemplate'; | ||
| import pagesData from '../api/pages.json'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Devolver PNG en una ruta .png (rasteriza el SVG con @resvg/resvg-js).
Los scrapers OG (X/Twitter, Facebook, LinkedIn, WhatsApp) no aceptan SVG. Además, la extensión de la ruta es .png. Rasteriza a PNG y ajusta headers; en errores, devuelve 500 y no caches el fallo.
Aplica este diff:
import type { APIRoute } from 'astro';
import { generateOgImageSvg } from '../components/OgImageTemplate';
import pagesData from '../api/pages.json';
+
+import { Resvg } from '@resvg/resvg-js';
@@
// Generar el SVG
const svgContent = generateOgImageSvg({ title, description });
-
- return new Response(svgContent, {
- headers: {
- 'Content-Type': 'image/svg+xml; charset=utf-8',
- 'Cache-Control': 'public, max-age=3600',
- 'Access-Control-Allow-Origin': '*',
- },
- });
+ // Rasterizar a PNG para máxima compatibilidad OG
+ const resvg = new Resvg(svgContent, {
+ fitTo: { mode: 'width', value: 1200 },
+ background: 'transparent',
+ });
+ const png = resvg.render().asPng();
+ return new Response(png, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
+ 'Content-Length': String(png.length),
+ 'Access-Control-Allow-Origin': '*',
+ },
+ });
@@
} catch (error) {
console.error('Error generando imagen OG:', error);
const fallbackSvg = `
@@
- return new Response(fallbackSvg, {
- headers: {
- 'Content-Type': 'image/svg+xml; charset=utf-8',
- 'Cache-Control': 'public, max-age=300',
- },
- });
+ const resvg = new Resvg(fallbackSvg, {
+ fitTo: { mode: 'width', value: 1200 },
+ background: 'transparent',
+ });
+ const png = resvg.render().asPng();
+ return new Response(png, {
+ status: 500,
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': 'no-store',
+ 'Content-Length': String(png.length),
+ 'Access-Control-Allow-Origin': '*',
+ },
+ });Nota: añade @resvg/resvg-js a dependencias.
Also applies to: 46-55, 57-76
🤖 Prompt for AI Agents
In src/pages/[...og].png.ts around lines 2-5 (and also apply same changes to
lines 46-55 and 57-76), the route currently returns an SVG but the endpoint is
.png and OG scrapers do not accept SVG; update the handler to import and use
@resvg/resvg-js to rasterize the SVG output of generateOgImageSvg into a PNG
Buffer, then return a Response with that PNG body and headers: Content-Type:
image/png, Cache-Control with a suitable caching policy for successful responses
(e.g., public, max-age) and appropriate Open Graph-compatible headers; on any
error catch it, log or include minimal error context, return a 500 Response with
a plain text or JSON error body, and set Cache-Control: no-store, no-cache to
avoid caching failures; ensure @resvg/resvg-js is added to package.json
dependencies.
Open Graph dinamico, en el que cada pagina tiene una imagen y descripcion personalizadas.
Summary by CodeRabbit
New Features
Chores