Skip to content

Conversation

@nixvy-13
Copy link
Member

@nixvy-13 nixvy-13 commented Sep 22, 2025

Open Graph dinamico, en el que cada pagina tiene una imagen y descripcion personalizadas.

Summary by CodeRabbit

  • New Features

    • Dynamic Open Graph and Twitter images are now generated per page, enhancing link previews on social platforms.
    • Site titles and descriptions are now driven by centralized metadata, ensuring consistent, accurate SEO across pages.
    • Meta tags (description, keywords, og:, twitter:) are populated automatically with correct values and image sizes.
  • Chores

    • Added a new dependency to support dynamic image generation.

- 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.
Copilot AI review requested due to automatic review settings September 22, 2025 17:04
@cloudflare-workers-and-pages
Copy link
Contributor

cloudflare-workers-and-pages bot commented Sep 22, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Added 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 satori dependency.

Changes

Cohort / File(s) Summary of Changes
Dependencies
package.json
Added runtime dependency satori@^0.18.2.
Pages registry data
src/api/pages.json
New JSON with pages array: { url, title, description } for 4 routes.
Layout meta & OG tags
src/layouts/Layout.astro
Added description prop; computed path and ogImageUrl; replaced static meta with dynamic OG/Twitter tags using title/description and generated image URL.
OG image API route
src/pages/[...og].png.ts
New non-prerendered GET route that resolves requested path, reads pages.json, generates SVG via generateOgImageSvg, sets caching, returns SVG; includes error fallback SVG.
Pages use registry data
src/pages/index.astro, src/pages/contacto.astro, src/pages/redes-sociales.astro, src/pages/sobre-nosotros.astro
Import pages.json, select current page by URL, pass dynamic title and description to Layout.
OG SVG template
src/components/OgImageTemplate.ts
New exported generateOgImageSvg({ title, description }) and OgImageTemplateProps; builds SVG string with gradients, logo, XML escaping, and text wrapping.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through tags where meta sings,
Spinning umbrellas into SVG things. ☔
Titles and whispers from pages aligned,
A path to an image, dynamically designed.
With satori’s breeze, I bound and twirl—
OG shines bright for every URL! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title includes "opengraph" and "Fix", which directly relates to the PR objective of adding dynamic Open Graph images and descriptions, so it meaningfully references the main change; however, it is styled like a branch name (slashes and an issue token) rather than a concise, readable single-sentence title, which reduces clarity when scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/YU_39/opengraph

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a 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.

}

const page = pagesData.pages.find(p => p.url === requestedPath);

Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
if (!page) {
return new Response('Page not found', {
status: 404,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=60',
},
});
}

Copilot uses AI. Check for mistakes.
const currentPath = "/sobre-nosotros";
const pageData = pagesData.pages.find(page => page.url === currentPath);
const { title, description } = pageData;
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
const { title, description } = pageData;
const title = pageData?.title ?? "Sobre nosotros";
const description = pageData?.description ?? "Conoce más sobre Yellow Umbrella.";

Copilot uses AI. Check for mistakes.
const currentPath = "/redes-sociales";
const pageData = pagesData.pages.find(page => page.url === currentPath);
const { title, description } = pageData;
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
const { title, description } = pageData;
const { title, description } = pageData || { title: "Redes sociales", description: "Enlaces a nuestras redes sociales." };

Copilot uses AI. Check for mistakes.
const currentPath = "/";
const pageData = pagesData.pages.find(page => page.url === currentPath);
const { title, description } = pageData;
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
const { title, description } = pageData;
const { title = "Default Title", description = "Default description" } = pageData || {};

Copilot uses AI. Check for mistakes.
const currentPath = "/contacto";
const pageData = pagesData.pages.find((page) => page.url === currentPath);
const { title, description } = pageData;
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
const { title, description } = pageData;
const { title, description } = pageData ?? { title: "Contacto", description: "Página de contacto" };

Copilot uses AI. Check for mistakes.
},
});
} catch (error) {
console.error('Error generando imagen OG (unified):', error);
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
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',
},
}
);

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 17
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>
Copy link

Copilot AI Sep 22, 2025

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.

Suggested change
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>

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a 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óneos

En Astro, el catch‑all suele llegar como string (no array) y la extensión .png no forma parte del parámetro. Simplifica y normaliza con decodeURIComponent.

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 memoria

Evita 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

📥 Commits

Reviewing files that changed from the base of the PR and between 991c0db and c9765d5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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 rasterization

src/pages/[...og].png.ts uses satori to produce an SVG (no sharp/resvg imports found); package.json still lists sharp (^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 sharp from runtime deps and do rasterization at build-time or in a Node environment.

Comment on lines 1 to 93
// 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 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/pages

Length 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.

Comment on lines +11 to 13
const path = Astro.url.pathname;
const ogImageUrl = new URL(`/og${path === '/' ? '' : path}.png`, Astro.url);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 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 || true

Length 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.

Comment on lines 96 to 106
// 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',
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 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.

Comment on lines 107 to 110
} catch (error) {
console.error('Error generando imagen OG (unified):', error);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
} 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.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between c9765d5 and ff66128.

📒 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': '*',
+    },
+  });

Comment on lines +2 to +5
import type { APIRoute } from 'astro';
import { generateOgImageSvg } from '../components/OgImageTemplate';
import pagesData from '../api/pages.json';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants