From e34dd835460fd7767274a553702263b9bf44e21a Mon Sep 17 00:00:00 2001 From: jeromehardaway Date: Thu, 27 Nov 2025 01:52:25 -0500 Subject: [PATCH 1/5] complete og cards --- URL_PREVIEW_CARD_README.md | 310 +++++++++++ package-lock.json | 481 +++++++++++++++++- package.json | 2 + ...J.js => fallback-2D9G9u8xCedCZFyw-yHC1.js} | 0 src/components/url-preview-card/index.tsx | 130 +++++ src/pages/api/og/fetch.ts | 93 ++++ src/pages/api/og/generate.tsx | 104 ++++ src/pages/url-preview-demo.tsx | 103 ++++ src/types/url-metadata.ts | 15 + yarn.lock | 265 +++++++++- 10 files changed, 1470 insertions(+), 33 deletions(-) create mode 100644 URL_PREVIEW_CARD_README.md rename public/{fallback-VgpzxPwKvXAx3Q4748PfJ.js => fallback-2D9G9u8xCedCZFyw-yHC1.js} (100%) create mode 100644 src/components/url-preview-card/index.tsx create mode 100644 src/pages/api/og/fetch.ts create mode 100644 src/pages/api/og/generate.tsx create mode 100644 src/pages/url-preview-demo.tsx create mode 100644 src/types/url-metadata.ts diff --git a/URL_PREVIEW_CARD_README.md b/URL_PREVIEW_CARD_README.md new file mode 100644 index 000000000..9b6c0cbf1 --- /dev/null +++ b/URL_PREVIEW_CARD_README.md @@ -0,0 +1,310 @@ +# URL Preview Card Feature + +This feature allows you to create beautiful preview cards for any URL shared in your application. It automatically fetches Open Graph metadata (title, description, image) from the URL and displays it in a card format. + +## Features + +- **Automatic metadata extraction**: Fetches Open Graph tags, Twitter Card data, and fallback metadata from any URL +- **Autogenerated images**: Creates a fallback image for URLs that don't have Open Graph images +- **Loading states**: Shows a skeleton loader while fetching metadata +- **Error handling**: Gracefully handles failed requests with error messages +- **Responsive design**: Works on all screen sizes +- **Hover effects**: Interactive animations on hover +- **External link safety**: Opens links in new tabs with `rel="noopener noreferrer"` + +## Files Created + +### 1. Type Definitions +`/src/types/url-metadata.ts` +- `URLMetadata`: Interface for URL metadata (title, description, image, etc.) +- `OGFetchResponse`: API response interface + +### 2. API Routes + +#### `/api/og/fetch` +Fetches metadata from a given URL by parsing HTML and extracting Open Graph tags. + +**Request:** +```typescript +POST /api/og/fetch +Content-Type: application/json + +{ + "url": "https://example.com" +} +``` + +**Response:** +```typescript +{ + "success": true, + "data": { + "url": "https://example.com", + "title": "Example Domain", + "description": "Example description", + "image": "https://example.com/og-image.jpg", + "siteName": "Example", + "favicon": "https://example.com/favicon.ico", + "type": "website" + } +} +``` + +#### `/api/og/generate` +Generates a fallback Open Graph image for URLs that don't have their own OG image. + +**Request:** +``` +GET /api/og/generate?url=https://example.com +``` + +**Response:** +Returns a 1200x630 PNG image with the URL's hostname and a link icon. + +### 3. Components + +#### `URLPreviewCard` +`/src/components/url-preview-card/index.tsx` + +A React component that displays a preview card for a URL. + +**Props:** +- `url` (string, required): The URL to preview +- `className` (string, optional): Additional CSS classes + +**Usage:** +```tsx +import URLPreviewCard from '@/components/url-preview-card'; + +function MyComponent() { + return ( + + ); +} +``` + +### 4. Demo Page +`/src/pages/url-preview-demo.tsx` + +A demo page that showcases the URL preview card functionality. Visit `/url-preview-demo` to test it. + +## Usage Examples + +### Basic Usage +```tsx +import URLPreviewCard from '@/components/url-preview-card'; + +export default function MyPage() { + return ( +
+

Check out this link:

+ +
+ ); +} +``` + +### Multiple Cards +```tsx +import URLPreviewCard from '@/components/url-preview-card'; + +export default function LinkCollection() { + const urls = [ + 'https://vetswhocode.io', + 'https://github.com/Vets-Who-Code', + 'https://www.npmjs.com/package/next', + ]; + + return ( +
+ {urls.map((url) => ( + + ))} +
+ ); +} +``` + +### With Custom Styling +```tsx +import URLPreviewCard from '@/components/url-preview-card'; + +export default function StyledCard() { + return ( + + ); +} +``` + +### Using the API Directly +If you need just the metadata without the card component: + +```tsx +import { useState, useEffect } from 'react'; +import type { URLMetadata } from '@/types/url-metadata'; + +export default function CustomCard() { + const [metadata, setMetadata] = useState(null); + + useEffect(() => { + fetch('/api/og/fetch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: 'https://vetswhocode.io' }), + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + setMetadata(data.data); + } + }); + }, []); + + if (!metadata) return
Loading...
; + + return ( +
+

{metadata.title}

+

{metadata.description}

+ {metadata.title} +
+ ); +} +``` + +## How It Works + +1. **User provides a URL**: The URL is passed to the `URLPreviewCard` component +2. **Fetch metadata**: The component calls `/api/og/fetch` with the URL +3. **Parse HTML**: The API route fetches the URL and parses the HTML with Cheerio +4. **Extract metadata**: Open Graph tags, Twitter Card data, and fallback metadata are extracted +5. **Return data**: The metadata is returned to the component +6. **Display card**: The component renders a card with the title, description, and image +7. **Fallback image**: If no image is found, `/api/og/generate` creates one on the fly + +## Customization + +### Styling +The component uses Tailwind CSS with the `tw-` prefix. You can customize the appearance by: +- Modifying the component in `/src/components/url-preview-card/index.tsx` +- Passing custom classes via the `className` prop +- Updating the Tailwind config + +### Image Generation +Customize the fallback OG image appearance in `/src/pages/api/og/generate.tsx`: +- Change colors, gradients, typography +- Add your logo or branding +- Modify the layout + +### Metadata Extraction +Enhance metadata extraction in `/src/pages/api/og/fetch.ts`: +- Add more metadata sources (Schema.org, JSON-LD, etc.) +- Implement caching to reduce API calls +- Add support for additional metadata fields + +## Performance Optimization + +### Caching (Recommended) +To avoid fetching the same URL multiple times, consider implementing caching: + +1. **Client-side caching**: Use React Query or SWR +2. **Server-side caching**: Store metadata in Redis or your database +3. **CDN caching**: Cache the API responses at the edge + +Example with Prisma (database caching): +```typescript +// Add to your Prisma schema +model URLMetadata { + id String @id @default(cuid()) + url String @unique + title String? + description String? + image String? + siteName String? + favicon String? + type String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +## Testing + +Visit `/url-preview-demo` to test the feature: +1. Enter any URL +2. Click "Preview" +3. See the generated card + +Try these example URLs: +- https://vetswhocode.io +- https://github.com/Vets-Who-Code +- https://www.npmjs.com/package/next + +## Dependencies + +- `cheerio`: HTML parsing library (server-side jQuery) +- `@vercel/og`: OG image generation library + +## Browser Support + +Works in all modern browsers that support: +- ES6+ +- Fetch API +- Next.js Image component + +## Security Considerations + +- **SSRF Protection**: The API validates URLs before fetching +- **Timeout**: Requests timeout after 10 seconds to prevent hanging +- **User-Agent**: A custom user-agent is set to identify the bot +- **Content-Type validation**: Only HTML content is parsed +- **XSS Protection**: Metadata is sanitized before rendering + +## Troubleshooting + +### No image appears +- Check if the URL has Open Graph meta tags +- Verify the image URL is accessible +- Check browser console for CORS errors +- The fallback image generator should handle this automatically + +### Slow loading +- The URL might be slow to respond +- Consider implementing caching +- Check your network connection + +### CORS errors +- OG image fetching happens server-side to avoid CORS issues +- If you see CORS errors, ensure you're using the API route + +### Build errors +- Ensure all dependencies are installed: `npm install` +- Check TypeScript errors: `npm run typecheck` +- Clear Next.js cache: `rm -rf .next` + +## Future Enhancements + +Potential improvements: +- [ ] Add database caching for fetched metadata +- [ ] Implement rate limiting to prevent abuse +- [ ] Add support for video OG tags +- [ ] Create multiple card layout variants +- [ ] Add copy-to-clipboard functionality for URLs +- [ ] Implement a URL shortener integration +- [ ] Add social share buttons to cards +- [ ] Support for rich embeds (YouTube, Twitter, etc.) + +## Contributing + +To contribute to this feature: +1. Create a new branch from `og_cards` +2. Make your changes +3. Test thoroughly +4. Submit a pull request + +--- + +Built with ❤️ for Vets Who Code diff --git a/package-lock.json b/package-lock.json index df62c7ee2..f6c8cfa78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,9 +24,11 @@ "@shopify/shopify-api": "^12.1.2", "@types/express": "^4.17.17", "@vercel/analytics": "^1.4.1", + "@vercel/og": "^0.8.5", "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", + "cheerio": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "complexity-report": "^2.0.0-alpha", @@ -5620,6 +5622,14 @@ } } }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5766,6 +5776,21 @@ "@shopify/graphql-client": "^1.4.1" } }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6942,6 +6967,18 @@ } } }, + "node_modules/@vercel/og": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.5.tgz", + "integrity": "sha512-fHqnxfBYcwkamlEgcIzaZqL8IHT09hR7FZL7UdMTdGJyoaBzM/dY6ulO5Swi4ig30FrBJI9I2C+GLV9sb9vexA==", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.16.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@vercel/oidc": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", @@ -8010,6 +8047,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -8081,8 +8126,7 @@ "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -8283,6 +8327,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001689", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", @@ -8381,6 +8433,185 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-4.3.0.tgz", "integrity": "sha512-Bio+lel1H6bZILbZNx+htTc13ir2AfgHIitBZHz3YV7XzMKOjyjV5ttwrqx9+MpI2J1b1c5cGZ1yebcWuudciQ==" }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/cheerio/node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -8957,11 +9188,36 @@ "urix": "^0.1.0" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "engines": { + "node": ">=16" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -8977,7 +9233,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -8991,7 +9246,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -9003,7 +9257,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -9018,7 +9271,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -9028,6 +9280,16 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -9045,7 +9307,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, "engines": { "node": ">= 6" }, @@ -9704,6 +9965,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -9720,6 +9989,40 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -9736,7 +10039,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, "engines": { "node": ">=0.12" }, @@ -11138,6 +11440,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" + }, "node_modules/figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", @@ -11987,6 +12294,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -14299,6 +14617,15 @@ "node": ">=10" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -16032,7 +16359,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "dependencies": { "boolbase": "^1.0.0" }, @@ -16339,6 +16665,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16351,6 +16682,15 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -16393,17 +16733,75 @@ } }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "dev": true, + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -18556,6 +18954,27 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/satori": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz", + "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -19178,6 +19597,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -20162,6 +20586,11 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -20691,6 +21120,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -20732,6 +21169,15 @@ "node": ">=4" } }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -22019,6 +22465,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==" + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 0cda9b898..cf9213707 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,11 @@ "@shopify/shopify-api": "^12.1.2", "@types/express": "^4.17.17", "@vercel/analytics": "^1.4.1", + "@vercel/og": "^0.8.5", "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", + "cheerio": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "complexity-report": "^2.0.0-alpha", diff --git a/public/fallback-VgpzxPwKvXAx3Q4748PfJ.js b/public/fallback-2D9G9u8xCedCZFyw-yHC1.js similarity index 100% rename from public/fallback-VgpzxPwKvXAx3Q4748PfJ.js rename to public/fallback-2D9G9u8xCedCZFyw-yHC1.js diff --git a/src/components/url-preview-card/index.tsx b/src/components/url-preview-card/index.tsx new file mode 100644 index 000000000..70f607bbe --- /dev/null +++ b/src/components/url-preview-card/index.tsx @@ -0,0 +1,130 @@ +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import type { URLMetadata } from '@/types/url-metadata'; + +interface URLPreviewCardProps { + url: string; + className?: string; +} + +export default function URLPreviewCard({ url, className = '' }: URLPreviewCardProps) { + const [metadata, setMetadata] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchMetadata = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/og/fetch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url }), + }); + + const data = await response.json(); + + if (data.success && data.data) { + setMetadata(data.data); + } else { + setError(data.error || 'Failed to fetch URL metadata'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + if (url) { + fetchMetadata(); + } + }, [url]); + + if (loading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (error || !metadata) { + return ( +
+

+ {error || 'Failed to load preview'} +

+ + {url} + +
+ ); + } + + const hostname = new URL(metadata.url).hostname; + const displayImage = metadata.image || `/api/og/generate?url=${encodeURIComponent(url)}`; + + return ( + +
+ {metadata.title +
+
+ {metadata.siteName && ( +
+ {metadata.favicon && ( + + )} + + {metadata.siteName} + +
+ )} +

+ {metadata.title} +

+ {metadata.description && ( +

+ {metadata.description} +

+ )} +

+ {hostname} +

+
+
+ ); +} diff --git a/src/pages/api/og/fetch.ts b/src/pages/api/og/fetch.ts new file mode 100644 index 000000000..ae95acd6a --- /dev/null +++ b/src/pages/api/og/fetch.ts @@ -0,0 +1,93 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import * as cheerio from 'cheerio'; +import type { OGFetchResponse, URLMetadata } from '@/types/url-metadata'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST' && req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + const url = req.method === 'POST' ? req.body.url : req.query.url; + + if (!url || typeof url !== 'string') { + return res.status(400).json({ success: false, error: 'URL is required' }); + } + + try { + // Validate URL format + const urlObj = new URL(url); + + // Fetch the URL content + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; VetsWhoCodeBot/1.0; +https://vetswhocode.io)', + }, + // Set timeout to 10 seconds + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + return res.status(400).json({ + success: false, + error: `Failed to fetch URL: ${response.statusText}` + }); + } + + const html = await response.text(); + const $ = cheerio.load(html); + + // Extract Open Graph metadata + const metadata: URLMetadata = { + url: url, + title: + $('meta[property="og:title"]').attr('content') || + $('meta[name="twitter:title"]').attr('content') || + $('title').text() || + urlObj.hostname, + description: + $('meta[property="og:description"]').attr('content') || + $('meta[name="twitter:description"]').attr('content') || + $('meta[name="description"]').attr('content') || + undefined, + image: + $('meta[property="og:image"]').attr('content') || + $('meta[name="twitter:image"]').attr('content') || + $('meta[property="og:image:url"]').attr('content') || + undefined, + siteName: + $('meta[property="og:site_name"]').attr('content') || + urlObj.hostname, + favicon: + $('link[rel="icon"]').attr('href') || + $('link[rel="shortcut icon"]').attr('href') || + `${urlObj.origin}/favicon.ico`, + type: + $('meta[property="og:type"]').attr('content') || + 'website', + }; + + // Convert relative URLs to absolute + if (metadata.image && !metadata.image.startsWith('http')) { + metadata.image = new URL(metadata.image, urlObj.origin).toString(); + } + if (metadata.favicon && !metadata.favicon.startsWith('http')) { + metadata.favicon = new URL(metadata.favicon, urlObj.origin).toString(); + } + + return res.status(200).json({ success: true, data: metadata }); + } catch (error) { + console.error('Error fetching URL metadata:', error); + + if (error instanceof TypeError && error.message.includes('Invalid URL')) { + return res.status(400).json({ success: false, error: 'Invalid URL format' }); + } + + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch URL metadata' + }); + } +} diff --git a/src/pages/api/og/generate.tsx b/src/pages/api/og/generate.tsx new file mode 100644 index 000000000..4cd81375f --- /dev/null +++ b/src/pages/api/og/generate.tsx @@ -0,0 +1,104 @@ +import { ImageResponse } from '@vercel/og'; +import type { NextRequest } from 'next/server'; + +export const config = { + runtime: 'edge', +}; + +export default async function handler(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const url = searchParams.get('url'); + + if (!url) { + return new Response('URL parameter is required', { status: 400 }); + } + + const urlObj = new URL(url); + const hostname = urlObj.hostname; + + return new ImageResponse( + ( +
+
+
+ + + +
+
+ {hostname} +
+
+ Shared Link +
+
+
+ ), + { + width: 1200, + height: 630, + } + ); + } catch (error) { + console.error('Error generating OG image:', error); + return new Response('Failed to generate image', { status: 500 }); + } +} diff --git a/src/pages/url-preview-demo.tsx b/src/pages/url-preview-demo.tsx new file mode 100644 index 000000000..5fef59393 --- /dev/null +++ b/src/pages/url-preview-demo.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import URLPreviewCard from '@/components/url-preview-card'; + +export default function URLPreviewDemo() { + const [url, setUrl] = useState(''); + const [submittedUrl, setSubmittedUrl] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setSubmittedUrl(url); + }; + + const exampleUrls = [ + 'https://vetswhocode.io', + 'https://github.com/Vets-Who-Code', + 'https://www.npmjs.com/package/next', + ]; + + return ( +
+
+

+ URL Preview Card Demo +

+ +
+
+
+ +
+ setUrl(e.target.value)} + placeholder="https://example.com" + className="tw-flex-1 tw-px-4 tw-py-2 tw-border tw-border-gray-300 tw-rounded-md focus:tw-ring-2 focus:tw-ring-blue-500 focus:tw-border-transparent" + required + /> + +
+
+
+ +
+

Try these examples:

+
+ {exampleUrls.map((exampleUrl) => ( + + ))} +
+
+
+ + {submittedUrl && ( +
+
+

+ Preview Card +

+ +
+ +
+

+ Usage Example +

+
+
+                  {`import URLPreviewCard from '@/components/url-preview-card';
+
+function MyComponent() {
+  return (
+    
+  );
+}`}
+                
+
+
+
+ )} +
+
+ ); +} diff --git a/src/types/url-metadata.ts b/src/types/url-metadata.ts new file mode 100644 index 000000000..e51d3921f --- /dev/null +++ b/src/types/url-metadata.ts @@ -0,0 +1,15 @@ +export interface URLMetadata { + url: string; + title?: string; + description?: string; + image?: string; + siteName?: string; + favicon?: string; + type?: string; +} + +export interface OGFetchResponse { + success: boolean; + data?: URLMetadata; + error?: string; +} diff --git a/yarn.lock b/yarn.lock index 7e7ad9501..7683c2853 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2356,6 +2356,11 @@ "@radix-ui/react-visually-hidden" "^1.0.3" classnames "^2.3.2" +"@resvg/resvg-wasm@2.4.0": + version "2.4.0" + resolved "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz" + integrity sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg== + "@rollup/plugin-babel@^5.2.0": version "5.3.1" resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" @@ -2436,6 +2441,14 @@ dependencies: "@shopify/graphql-client" "^1.4.1" +"@shuding/opentype.js@1.4.0-beta.0": + version "1.4.0-beta.0" + resolved "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz" + integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA== + dependencies: + fflate "^0.7.3" + string.prototype.codepointat "^0.2.1" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -3129,6 +3142,14 @@ resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.4.1.tgz" integrity sha512-ekpL4ReX2TH3LnrRZTUKjHHNpNy9S1I7QmS+g/RQXoSUQ8ienzosuX7T9djZ/s8zPhBx1mpHP/Rw5875N+zQIQ== +"@vercel/og@^0.8.5": + version "0.8.5" + resolved "https://registry.npmjs.org/@vercel/og/-/og-0.8.5.tgz" + integrity sha512-fHqnxfBYcwkamlEgcIzaZqL8IHT09hR7FZL7UdMTdGJyoaBzM/dY6ulO5Swi4ig30FrBJI9I2C+GLV9sb9vexA== + dependencies: + "@resvg/resvg-wasm" "2.4.0" + satori "0.16.0" + "@vercel/oidc@3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz" @@ -3937,6 +3958,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz" + integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" @@ -4113,6 +4139,11 @@ camelcase@^6.2.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: version "1.0.30001689" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz" @@ -4212,6 +4243,35 @@ check-types@^5.1.0: resolved "https://registry.npmjs.org/check-types/-/check-types-5.1.0.tgz" integrity sha512-avyYsSECJeYxowzVMGxzwXz9gc+LAEQ8l8nDVRJqbJilfwHDBPxpjTZC0mb1gr8AAARDc/I7P4+IG6SxokWWmw== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz" + integrity sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.2.2" + encoding-sniffer "^0.2.1" + htmlparser2 "^10.0.0" + parse5 "^7.3.0" + parse5-htmlparser2-tree-adapter "^7.1.0" + parse5-parser-stream "^7.1.2" + undici "^7.12.0" + whatwg-mimetype "^4.0.0" + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" @@ -4381,7 +4441,7 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@^1.0.0, color-name@~1.1.4: +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -4578,6 +4638,26 @@ crypto-random-string@^2.0.0: resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-background-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz" + integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA== + +css-box-shadow@1.0.0-3: + version "1.0.0-3" + resolved "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz" + integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg== + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-gradient-parser@^0.0.16: + version "0.0.16" + resolved "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz" + integrity sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA== + css-select@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" @@ -4589,6 +4669,15 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + css-tree@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" @@ -4988,9 +5077,18 @@ domhandler@2.3: domelementtype "1" domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + version "3.2.2" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +domutils@^3.2.1, domutils@^3.2.2: + version "3.2.2" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -5061,6 +5159,11 @@ emittery@^0.13.1: resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== +emoji-regex-xs@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz" + integrity sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -5091,6 +5194,14 @@ encodeurl@~2.0.0: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +encoding-sniffer@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz" + integrity sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" @@ -5109,6 +5220,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + entities@1.0: version "1.0.0" resolved "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz" @@ -5360,7 +5476,7 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -6035,6 +6151,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fflate@^0.7.3: + version "0.7.4" + resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz" + integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== + figures@^1.3.5: version "1.7.0" resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz" @@ -6621,6 +6742,11 @@ hast-util-whitespace@^3.0.0: dependencies: "@types/hast" "^3.0.0" +hex-rgb@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz" + integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== + html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" @@ -6633,6 +6759,16 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +htmlparser2@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz" + integrity sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.2.1" + entities "^6.0.0" + htmlparser2@3.8.x: version "3.8.3" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz" @@ -6691,6 +6827,13 @@ husky@^7.0.4: resolved "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +iconv-lite@^0.6.3, iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -6698,13 +6841,6 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - idb@^7.0.1: version "7.1.1" resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" @@ -7947,6 +8083,14 @@ lilconfig@2.0.5: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz" integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== +linebreak@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz" + integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== + dependencies: + base64-js "0.0.8" + unicode-trie "^2.0.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" @@ -9181,6 +9325,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -9188,6 +9337,14 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-css-color@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz" + integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg== + dependencies: + color-name "^1.1.4" + hex-rgb "^4.1.0" + parse-entities@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz" @@ -9211,12 +9368,27 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@^7.0.0, parse5@^7.1.1: - version "7.2.1" - resolved "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz" - integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== +parse5-htmlparser2-tree-adapter@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== dependencies: - entities "^4.5.0" + domhandler "^5.0.3" + parse5 "^7.0.0" + +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + +parse5@^7.0.0, parse5@^7.1.1, parse5@^7.3.0: + version "7.3.0" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" parseurl@~1.3.3: version "1.3.3" @@ -9452,7 +9624,7 @@ postcss-selector-parser@^6.1.2: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -10234,6 +10406,23 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, "safer-buffer@>= 2.1.2 < 3", "safer-bu resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +satori@0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz" + integrity sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ== + dependencies: + "@shuding/opentype.js" "1.4.0-beta.0" + css-background-parser "^0.1.0" + css-box-shadow "1.0.0-3" + css-gradient-parser "^0.0.16" + css-to-react-native "^3.0.0" + emoji-regex-xs "^2.0.1" + escape-html "^1.0.3" + linebreak "^1.1.0" + parse-css-color "^0.2.1" + postcss-value-parser "^4.2.0" + yoga-layout "^3.2.1" + sax@>=0.6.0: version "1.4.1" resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" @@ -10744,6 +10933,11 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.codepointat@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + string.prototype.includes@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz" @@ -11173,6 +11367,11 @@ through@^2.3.6, through@^2.3.8: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -11514,6 +11713,11 @@ undici-types@~6.21.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici@^7.12.0: + version "7.16.0" + resolved "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz" + integrity sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" @@ -11537,6 +11741,14 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + unified@^11.0.0: version "11.0.5" resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" @@ -11860,11 +12072,23 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + whatwg-url@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" @@ -12264,6 +12488,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoga-layout@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz" + integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ== + "zod@^3.25.76 || ^4.1.8": version "3.25.76" resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz" From d3e07d40cb94ae9542709610787fa4e574b2728c Mon Sep 17 00:00:00 2001 From: jeromehardaway Date: Thu, 27 Nov 2025 02:11:12 -0500 Subject: [PATCH 2/5] fix errors --- URL_PREVIEW_CARD_README.md | 2 +- next.config.js | 8 +- package-lock.json | 290 ++-------------------- package.json | 2 +- src/components/url-preview-card/index.tsx | 10 + src/pages/api/og/fetch.ts | 49 ++-- src/pages/api/og/generate.tsx | 130 +++++++--- src/pages/url-preview-demo.tsx | 7 +- yarn.lock | 123 ++------- 9 files changed, 201 insertions(+), 420 deletions(-) diff --git a/URL_PREVIEW_CARD_README.md b/URL_PREVIEW_CARD_README.md index 9b6c0cbf1..ebc3cc1a2 100644 --- a/URL_PREVIEW_CARD_README.md +++ b/URL_PREVIEW_CARD_README.md @@ -245,7 +245,7 @@ Try these example URLs: ## Dependencies -- `cheerio`: HTML parsing library (server-side jQuery) +- `node-html-parser`: Lightweight HTML parsing library for Node.js - `@vercel/og`: OG image generation library ## Browser Support diff --git a/next.config.js b/next.config.js index c1e4835b6..c5d335fb9 100644 --- a/next.config.js +++ b/next.config.js @@ -43,7 +43,13 @@ const nextConfig = { images: { domains: [], - remotePatterns: [], + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + unoptimized: false, }, }; diff --git a/package-lock.json b/package-lock.json index f6c8cfa78..b4a16e33d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", - "cheerio": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "complexity-report": "^2.0.0-alpha", @@ -51,6 +50,7 @@ "next-react-svg": "^1.1.3", "next-seo": "^5.5.0", "next-sitemap": "^3.1.55", + "node-html-parser": "^7.0.1", "plato": "^1.7.0", "react": "^18.3.1", "react-ace": "^11.0.1", @@ -8433,185 +8433,6 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-4.3.0.tgz", "integrity": "sha512-Bio+lel1H6bZILbZNx+htTc13ir2AfgHIitBZHz3YV7XzMKOjyjV5ttwrqx9+MpI2J1b1c5cGZ1yebcWuudciQ==" }, - "node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cheerio-select/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/cheerio-select/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/cheerio-select/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/cheerio-select/node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/cheerio/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" - } - }, - "node_modules/cheerio/node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/cheerio/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "engines": { - "node": ">=18" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -9989,40 +9810,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -12294,6 +12081,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, "node_modules/hex-rgb": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", @@ -16315,6 +16110,15 @@ "tslib": "^2.0.3" } }, + "node_modules/node-html-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", + "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -16736,6 +16540,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, "dependencies": { "entities": "^6.0.0" }, @@ -16743,58 +16548,11 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, "engines": { "node": ">=0.12" }, @@ -21120,14 +20878,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index cf9213707..f98d1daed 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", - "cheerio": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "complexity-report": "^2.0.0-alpha", @@ -63,6 +62,7 @@ "next-react-svg": "^1.1.3", "next-seo": "^5.5.0", "next-sitemap": "^3.1.55", + "node-html-parser": "^7.0.1", "plato": "^1.7.0", "react": "^18.3.1", "react-ace": "^11.0.1", diff --git a/src/components/url-preview-card/index.tsx b/src/components/url-preview-card/index.tsx index 70f607bbe..312cfc4c4 100644 --- a/src/components/url-preview-card/index.tsx +++ b/src/components/url-preview-card/index.tsx @@ -26,6 +26,15 @@ export default function URLPreviewCard({ url, className = '' }: URLPreviewCardPr body: JSON.stringify({ url }), }); + // Check if response is JSON + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + const text = await response.text(); + console.error('Non-JSON response:', text); + setError('Server returned an error. Check console for details.'); + return; + } + const data = await response.json(); if (data.success && data.data) { @@ -34,6 +43,7 @@ export default function URLPreviewCard({ url, className = '' }: URLPreviewCardPr setError(data.error || 'Failed to fetch URL metadata'); } } catch (err) { + console.error('Fetch error:', err); setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); diff --git a/src/pages/api/og/fetch.ts b/src/pages/api/og/fetch.ts index ae95acd6a..8c6f85cb3 100644 --- a/src/pages/api/og/fetch.ts +++ b/src/pages/api/og/fetch.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import * as cheerio from 'cheerio'; +import { parse } from 'node-html-parser'; import type { OGFetchResponse, URLMetadata } from '@/types/url-metadata'; export default async function handler( @@ -20,15 +20,19 @@ export default async function handler( // Validate URL format const urlObj = new URL(url); - // Fetch the URL content + // Fetch the URL content with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; VetsWhoCodeBot/1.0; +https://vetswhocode.io)', }, - // Set timeout to 10 seconds - signal: AbortSignal.timeout(10000), + signal: controller.signal, }); + clearTimeout(timeoutId); + if (!response.ok) { return res.status(400).json({ success: false, @@ -37,35 +41,40 @@ export default async function handler( } const html = await response.text(); - const $ = cheerio.load(html); + const root = parse(html); + + // Helper function to get meta content + const getMeta = (selector: string): string | undefined => { + return root.querySelector(selector)?.getAttribute('content') || undefined; + }; // Extract Open Graph metadata const metadata: URLMetadata = { url: url, title: - $('meta[property="og:title"]').attr('content') || - $('meta[name="twitter:title"]').attr('content') || - $('title').text() || + getMeta('meta[property="og:title"]') || + getMeta('meta[name="twitter:title"]') || + root.querySelector('title')?.text || urlObj.hostname, description: - $('meta[property="og:description"]').attr('content') || - $('meta[name="twitter:description"]').attr('content') || - $('meta[name="description"]').attr('content') || + getMeta('meta[property="og:description"]') || + getMeta('meta[name="twitter:description"]') || + getMeta('meta[name="description"]') || undefined, image: - $('meta[property="og:image"]').attr('content') || - $('meta[name="twitter:image"]').attr('content') || - $('meta[property="og:image:url"]').attr('content') || + getMeta('meta[property="og:image"]') || + getMeta('meta[name="twitter:image"]') || + getMeta('meta[property="og:image:url"]') || undefined, siteName: - $('meta[property="og:site_name"]').attr('content') || + getMeta('meta[property="og:site_name"]') || urlObj.hostname, favicon: - $('link[rel="icon"]').attr('href') || - $('link[rel="shortcut icon"]').attr('href') || + root.querySelector('link[rel="icon"]')?.getAttribute('href') || + root.querySelector('link[rel="shortcut icon"]')?.getAttribute('href') || `${urlObj.origin}/favicon.ico`, type: - $('meta[property="og:type"]').attr('content') || + getMeta('meta[property="og:type"]') || 'website', }; @@ -85,6 +94,10 @@ export default async function handler( return res.status(400).json({ success: false, error: 'Invalid URL format' }); } + if (error instanceof Error && error.name === 'AbortError') { + return res.status(408).json({ success: false, error: 'Request timeout' }); + } + return res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Failed to fetch URL metadata' diff --git a/src/pages/api/og/generate.tsx b/src/pages/api/og/generate.tsx index 4cd81375f..4b35f375e 100644 --- a/src/pages/api/og/generate.tsx +++ b/src/pages/api/og/generate.tsx @@ -17,6 +17,25 @@ export default async function handler(req: NextRequest) { const urlObj = new URL(url); const hostname = urlObj.hostname; + // Generate unique colors based on hostname + const hashCode = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; + }; + + const hash = Math.abs(hashCode(hostname)); + const hue1 = hash % 360; + const hue2 = (hash + 60) % 360; + + const gradient1 = `hsl(${hue1}, 70%, 50%)`; + const gradient2 = `hsl(${hue2}, 70%, 35%)`; + + // Get first letter of hostname for the icon + const firstLetter = hostname.replace('www.', '').charAt(0).toUpperCase(); + return new ImageResponse( (
+ {/* Decorative circles in background */} +
+
+ + {/* Main content */}
+ {/* Letter icon */}
- - - + {firstLetter} +
+ + {/* Hostname */}
{hostname}
+ + {/* Subtitle */}
- Shared Link + + + + + Shared from Vets Who Code
diff --git a/src/pages/url-preview-demo.tsx b/src/pages/url-preview-demo.tsx index 5fef59393..1befa3498 100644 --- a/src/pages/url-preview-demo.tsx +++ b/src/pages/url-preview-demo.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import URLPreviewCard from '@/components/url-preview-card'; -export default function URLPreviewDemo() { +const URLPreviewDemo = () => { const [url, setUrl] = useState(''); const [submittedUrl, setSubmittedUrl] = useState(''); @@ -55,6 +55,7 @@ export default function URLPreviewDemo() { {exampleUrls.map((exampleUrl) => (
); -} +}; + +export default URLPreviewDemo; diff --git a/yarn.lock b/yarn.lock index 7683c2853..d6603f945 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4243,35 +4243,6 @@ check-types@^5.1.0: resolved "https://registry.npmjs.org/check-types/-/check-types-5.1.0.tgz" integrity sha512-avyYsSECJeYxowzVMGxzwXz9gc+LAEQ8l8nDVRJqbJilfwHDBPxpjTZC0mb1gr8AAARDc/I7P4+IG6SxokWWmw== -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz" - integrity sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.2.2" - encoding-sniffer "^0.2.1" - htmlparser2 "^10.0.0" - parse5 "^7.3.0" - parse5-htmlparser2-tree-adapter "^7.1.0" - parse5-parser-stream "^7.1.2" - undici "^7.12.0" - whatwg-mimetype "^4.0.0" - chokidar@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" @@ -5077,18 +5048,9 @@ domhandler@2.3: domelementtype "1" domutils@^3.0.1: - version "3.2.2" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" - integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -domutils@^3.2.1, domutils@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" - integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + version "3.1.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -5194,14 +5156,6 @@ encodeurl@~2.0.0: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== -encoding-sniffer@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz" - integrity sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw== - dependencies: - iconv-lite "^0.6.3" - whatwg-encoding "^3.1.1" - enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" @@ -6742,6 +6696,11 @@ hast-util-whitespace@^3.0.0: dependencies: "@types/hast" "^3.0.0" +he@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hex-rgb@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz" @@ -6759,16 +6718,6 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz" - integrity sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.2.1" - entities "^6.0.0" - htmlparser2@3.8.x: version "3.8.3" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz" @@ -6827,13 +6776,6 @@ husky@^7.0.4: resolved "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== -iconv-lite@^0.6.3, iconv-lite@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -6841,6 +6783,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + idb@^7.0.1: version "7.1.1" resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" @@ -9077,6 +9026,14 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-html-parser@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz" + integrity sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA== + dependencies: + css-select "^5.1.0" + he "1.2.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -9368,22 +9325,7 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5-htmlparser2-tree-adapter@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz" - integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== - dependencies: - domhandler "^5.0.3" - parse5 "^7.0.0" - -parse5-parser-stream@^7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz" - integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== - dependencies: - parse5 "^7.0.0" - -parse5@^7.0.0, parse5@^7.1.1, parse5@^7.3.0: +parse5@^7.0.0, parse5@^7.1.1: version "7.3.0" resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== @@ -11713,11 +11655,6 @@ undici-types@~6.21.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -undici@^7.12.0: - version "7.16.0" - resolved "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz" - integrity sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g== - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" @@ -12072,23 +12009,11 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" -whatwg-encoding@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz" - integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== - dependencies: - iconv-lite "0.6.3" - whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== -whatwg-mimetype@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz" - integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== - whatwg-url@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" From d40762f47c3111aab4aa63716d88afe870dec13d Mon Sep 17 00:00:00 2001 From: jeromehardaway Date: Thu, 27 Nov 2025 02:44:10 -0500 Subject: [PATCH 3/5] prevent uneeded files from being uploaded to vercel --- .vercelignore | 38 ++++++++++++++++++- next.config.js | 22 +++++++++++ ...1.js => fallback-Y2rXFGIRBCnDbTEoOAhES.js} | 0 3 files changed, 59 insertions(+), 1 deletion(-) rename public/{fallback-2D9G9u8xCedCZFyw-yHC1.js => fallback-Y2rXFGIRBCnDbTEoOAhES.js} (100%) diff --git a/.vercelignore b/.vercelignore index 9d404684c..a1fdca39b 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1,3 +1,39 @@ +# Git and version control .git +.github + +# Build cache +.next/cache +.cache + +# Reports and testing reports -.next/cache \ No newline at end of file +coverage +__tests__ + +# Documentation (except README) +docs/ +*.md +!README.md + +# Development files +.vscode +.idea + +# Logs +*.log +logs + +# Temporary files +tmp +temp + +# Test files +*.test.ts +*.test.tsx +*.test.js +*.test.jsx +*.spec.ts +*.spec.tsx +*.spec.js +*.spec.jsx \ No newline at end of file diff --git a/next.config.js b/next.config.js index c5d335fb9..ee354d9bc 100644 --- a/next.config.js +++ b/next.config.js @@ -26,6 +26,18 @@ const nextConfig = { experimental: {}, + // Exclude heavy dependencies from functions that don't need them + outputFileTracingExcludes: { + // Exclude @vercel/og from all API routes except /api/og/generate + '/api/!(og)/**': ['node_modules/@vercel/og/**/*'], + '/api/og/fetch': ['node_modules/@vercel/og/**/*'], + }, + + // Include only necessary files for specific routes + outputFileTracingIncludes: { + '/api/og/generate': [], + }, + webpack(config, { isServer }) { config.module.rules.push({ test: /\.svg$/, @@ -38,6 +50,16 @@ const nextConfig = { }; } + // Optimize for serverless functions + if (isServer) { + // Enable tree-shaking for server bundles + config.optimization = { + ...config.optimization, + usedExports: true, + sideEffects: false, + }; + } + return config; }, diff --git a/public/fallback-2D9G9u8xCedCZFyw-yHC1.js b/public/fallback-Y2rXFGIRBCnDbTEoOAhES.js similarity index 100% rename from public/fallback-2D9G9u8xCedCZFyw-yHC1.js rename to public/fallback-Y2rXFGIRBCnDbTEoOAhES.js From edd213f567d4ef156b9b226fd6563e7257b318a4 Mon Sep 17 00:00:00 2001 From: jeromehardaway Date: Thu, 27 Nov 2025 13:38:08 -0500 Subject: [PATCH 4/5] solves issue for mdx pages --- .vercelignore | 4 +++- next.config.js | 21 +++++++++++++++---- ...S.js => fallback-aLBVMOQteZJ8XzoliNFpJ.js} | 0 3 files changed, 20 insertions(+), 5 deletions(-) rename public/{fallback-Y2rXFGIRBCnDbTEoOAhES.js => fallback-aLBVMOQteZJ8XzoliNFpJ.js} (100%) diff --git a/.vercelignore b/.vercelignore index a1fdca39b..2dae74440 100644 --- a/.vercelignore +++ b/.vercelignore @@ -11,10 +11,12 @@ reports coverage __tests__ -# Documentation (except README) +# Documentation (except README and data files) docs/ *.md !README.md +!src/data/**/*.md +!src/data/**/*.mdx # Development files .vscode diff --git a/next.config.js b/next.config.js index ee354d9bc..61c738935 100644 --- a/next.config.js +++ b/next.config.js @@ -28,14 +28,27 @@ const nextConfig = { // Exclude heavy dependencies from functions that don't need them outputFileTracingExcludes: { - // Exclude @vercel/og from all API routes except /api/og/generate - '/api/!(og)/**': ['node_modules/@vercel/og/**/*'], + // Exclude @vercel/og from specific API routes that don't use it + '/api/ai/**': ['node_modules/@vercel/og/**/*'], + '/api/auth/**': ['node_modules/@vercel/og/**/*'], + '/api/certificates/**': ['node_modules/@vercel/og/**/*'], + '/api/contact': ['node_modules/@vercel/og/**/*'], + '/api/courses/**': ['node_modules/@vercel/og/**/*'], + '/api/enrollment/**': ['node_modules/@vercel/og/**/*'], + '/api/lms/**': ['node_modules/@vercel/og/**/*'], + '/api/mentee': ['node_modules/@vercel/og/**/*'], + '/api/mentor': ['node_modules/@vercel/og/**/*'], + '/api/military-resume/**': ['node_modules/@vercel/og/**/*'], + '/api/newsletter': ['node_modules/@vercel/og/**/*'], '/api/og/fetch': ['node_modules/@vercel/og/**/*'], + '/api/progress': ['node_modules/@vercel/og/**/*'], + '/api/shopify/**': ['node_modules/@vercel/og/**/*'], + '/api/user/**': ['node_modules/@vercel/og/**/*'], }, - // Include only necessary files for specific routes + // Ensure MDX and data files are included for all pages outputFileTracingIncludes: { - '/api/og/generate': [], + '/**': ['src/data/**/*'], }, webpack(config, { isServer }) { diff --git a/public/fallback-Y2rXFGIRBCnDbTEoOAhES.js b/public/fallback-aLBVMOQteZJ8XzoliNFpJ.js similarity index 100% rename from public/fallback-Y2rXFGIRBCnDbTEoOAhES.js rename to public/fallback-aLBVMOQteZJ8XzoliNFpJ.js From 58033db1525a790f5db75c115e927af0be1c30d8 Mon Sep 17 00:00:00 2001 From: jeromehardaway Date: Thu, 27 Nov 2025 16:08:27 -0500 Subject: [PATCH 5/5] update to solve 250 MB error --- URL_PREVIEW_CARD_README.md | 32 ++-- next.config.js | 20 -- package-lock.json | 180 ------------------ package.json | 1 - ...J.js => fallback-ig51mFqkghFuP2RH4aGuS.js} | 0 src/components/url-preview-card/index.tsx | 48 ++++- src/pages/api/og/generate.tsx | 178 ----------------- yarn.lock | 142 +------------- 8 files changed, 63 insertions(+), 538 deletions(-) rename public/{fallback-aLBVMOQteZJ8XzoliNFpJ.js => fallback-ig51mFqkghFuP2RH4aGuS.js} (100%) delete mode 100644 src/pages/api/og/generate.tsx diff --git a/URL_PREVIEW_CARD_README.md b/URL_PREVIEW_CARD_README.md index ebc3cc1a2..8a50ed3a6 100644 --- a/URL_PREVIEW_CARD_README.md +++ b/URL_PREVIEW_CARD_README.md @@ -50,16 +50,13 @@ Content-Type: application/json } ``` -#### `/api/og/generate` -Generates a fallback Open Graph image for URLs that don't have their own OG image. - -**Request:** -``` -GET /api/og/generate?url=https://example.com -``` - -**Response:** -Returns a 1200x630 PNG image with the URL's hostname and a link icon. +#### Fallback Image Generation +For URLs without OG images, the component automatically generates a unique SVG image client-side using the `generateFallbackImage()` utility function. This approach: +- Creates unique colors based on hostname (same site = same colors) +- Shows the first letter of the domain +- Includes VWC branding +- **No API calls required** - fully client-side generation +- **Zero bundle size impact** - just inline SVG generation ### 3. Components @@ -179,11 +176,11 @@ export default function CustomCard() { 1. **User provides a URL**: The URL is passed to the `URLPreviewCard` component 2. **Fetch metadata**: The component calls `/api/og/fetch` with the URL -3. **Parse HTML**: The API route fetches the URL and parses the HTML with Cheerio +3. **Parse HTML**: The API route fetches the URL and parses the HTML with node-html-parser 4. **Extract metadata**: Open Graph tags, Twitter Card data, and fallback metadata are extracted 5. **Return data**: The metadata is returned to the component 6. **Display card**: The component renders a card with the title, description, and image -7. **Fallback image**: If no image is found, `/api/og/generate` creates one on the fly +7. **Fallback image**: If no image is found, a unique SVG is generated client-side based on the hostname ## Customization @@ -194,10 +191,11 @@ The component uses Tailwind CSS with the `tw-` prefix. You can customize the app - Updating the Tailwind config ### Image Generation -Customize the fallback OG image appearance in `/src/pages/api/og/generate.tsx`: -- Change colors, gradients, typography -- Add your logo or branding -- Modify the layout +Customize the fallback OG image appearance in the `generateFallbackImage()` function in `/src/components/url-preview-card/index.tsx`: +- Change colors, gradients, typography (lines 22-42) +- Modify the SVG layout and design +- Add your logo or different branding +- Adjust the hash function for different color schemes ### Metadata Extraction Enhance metadata extraction in `/src/pages/api/og/fetch.ts`: @@ -246,7 +244,7 @@ Try these example URLs: ## Dependencies - `node-html-parser`: Lightweight HTML parsing library for Node.js -- `@vercel/og`: OG image generation library +- **No additional dependencies** for image generation - uses inline SVG generation ## Browser Support diff --git a/next.config.js b/next.config.js index 61c738935..56e38924e 100644 --- a/next.config.js +++ b/next.config.js @@ -26,26 +26,6 @@ const nextConfig = { experimental: {}, - // Exclude heavy dependencies from functions that don't need them - outputFileTracingExcludes: { - // Exclude @vercel/og from specific API routes that don't use it - '/api/ai/**': ['node_modules/@vercel/og/**/*'], - '/api/auth/**': ['node_modules/@vercel/og/**/*'], - '/api/certificates/**': ['node_modules/@vercel/og/**/*'], - '/api/contact': ['node_modules/@vercel/og/**/*'], - '/api/courses/**': ['node_modules/@vercel/og/**/*'], - '/api/enrollment/**': ['node_modules/@vercel/og/**/*'], - '/api/lms/**': ['node_modules/@vercel/og/**/*'], - '/api/mentee': ['node_modules/@vercel/og/**/*'], - '/api/mentor': ['node_modules/@vercel/og/**/*'], - '/api/military-resume/**': ['node_modules/@vercel/og/**/*'], - '/api/newsletter': ['node_modules/@vercel/og/**/*'], - '/api/og/fetch': ['node_modules/@vercel/og/**/*'], - '/api/progress': ['node_modules/@vercel/og/**/*'], - '/api/shopify/**': ['node_modules/@vercel/og/**/*'], - '/api/user/**': ['node_modules/@vercel/og/**/*'], - }, - // Ensure MDX and data files are included for all pages outputFileTracingIncludes: { '/**': ['src/data/**/*'], diff --git a/package-lock.json b/package-lock.json index b4a16e33d..7edd8bf4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@shopify/shopify-api": "^12.1.2", "@types/express": "^4.17.17", "@vercel/analytics": "^1.4.1", - "@vercel/og": "^0.8.5", "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", @@ -5622,14 +5621,6 @@ } } }, - "node_modules/@resvg/resvg-wasm": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", - "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", - "engines": { - "node": ">= 10" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5776,21 +5767,6 @@ "@shopify/graphql-client": "^1.4.1" } }, - "node_modules/@shuding/opentype.js": { - "version": "1.4.0-beta.0", - "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", - "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", - "dependencies": { - "fflate": "^0.7.3", - "string.prototype.codepointat": "^0.2.1" - }, - "bin": { - "ot": "bin/ot" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6967,18 +6943,6 @@ } } }, - "node_modules/@vercel/og": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.5.tgz", - "integrity": "sha512-fHqnxfBYcwkamlEgcIzaZqL8IHT09hR7FZL7UdMTdGJyoaBzM/dY6ulO5Swi4ig30FrBJI9I2C+GLV9sb9vexA==", - "dependencies": { - "@resvg/resvg-wasm": "2.4.0", - "satori": "0.16.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@vercel/oidc": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", @@ -8047,14 +8011,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -8327,14 +8283,6 @@ "node": ">= 6" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001689", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", @@ -9009,32 +8957,6 @@ "urix": "^0.1.0" } }, - "node_modules/css-background-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", - "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==" - }, - "node_modules/css-box-shadow": { - "version": "1.0.0-3", - "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", - "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-gradient-parser": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", - "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", - "engines": { - "node": ">=16" - } - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -9101,16 +9023,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -9786,14 +9698,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, - "node_modules/emoji-regex-xs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", - "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -11227,11 +11131,6 @@ "bser": "2.1.1" } }, - "node_modules/fflate": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" - }, "node_modules/figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", @@ -12089,17 +11988,6 @@ "he": "bin/he" } }, - "node_modules/hex-rgb": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", - "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -14412,15 +14300,6 @@ "node": ">=10" } }, - "node_modules/linebreak": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", - "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", - "dependencies": { - "base64-js": "0.0.8", - "unicode-trie": "^2.0.0" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -16469,11 +16348,6 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, - "node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16486,15 +16360,6 @@ "node": ">=6" } }, - "node_modules/parse-css-color": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", - "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", - "dependencies": { - "color-name": "^1.1.4", - "hex-rgb": "^4.1.0" - } - }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -18712,27 +18577,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/satori": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz", - "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", - "dependencies": { - "@shuding/opentype.js": "1.4.0-beta.0", - "css-background-parser": "^0.1.0", - "css-box-shadow": "1.0.0-3", - "css-gradient-parser": "^0.0.16", - "css-to-react-native": "^3.0.0", - "emoji-regex-xs": "^2.0.1", - "escape-html": "^1.0.3", - "linebreak": "^1.1.0", - "parse-css-color": "^0.2.1", - "postcss-value-parser": "^4.2.0", - "yoga-layout": "^3.2.1" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -19355,11 +19199,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/string.prototype.codepointat": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", - "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -20344,11 +20183,6 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", - "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -20919,15 +20753,6 @@ "node": ">=4" } }, - "node_modules/unicode-trie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", - "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", - "dependencies": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" - } - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -22215,11 +22040,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", - "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==" - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index f98d1daed..0cf2c797c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@shopify/shopify-api": "^12.1.2", "@types/express": "^4.17.17", "@vercel/analytics": "^1.4.1", - "@vercel/og": "^0.8.5", "ace-builds": "^1.33.1", "ai": "^5.0.93", "axios": "^1.4.0", diff --git a/public/fallback-aLBVMOQteZJ8XzoliNFpJ.js b/public/fallback-ig51mFqkghFuP2RH4aGuS.js similarity index 100% rename from public/fallback-aLBVMOQteZJ8XzoliNFpJ.js rename to public/fallback-ig51mFqkghFuP2RH4aGuS.js diff --git a/src/components/url-preview-card/index.tsx b/src/components/url-preview-card/index.tsx index 312cfc4c4..cd6778c1e 100644 --- a/src/components/url-preview-card/index.tsx +++ b/src/components/url-preview-card/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import Image from 'next/image'; import type { URLMetadata } from '@/types/url-metadata'; @@ -7,6 +7,45 @@ interface URLPreviewCardProps { className?: string; } +// Generate a unique fallback image as an SVG data URL +function generateFallbackImage(hostname: string): string { + // Hash function to generate consistent colors + const hashCode = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; + }; + + const hash = Math.abs(hashCode(hostname)); + const hue1 = hash % 360; + const hue2 = (hash + 60) % 360; + + const firstLetter = hostname.replace('www.', '').charAt(0).toUpperCase(); + + const svg = ` + + + + + + + + + + + + ${firstLetter} + ${hostname} + Shared from Vets Who Code + + `; + + // Use btoa for browser compatibility (or encodeURIComponent for simpler approach) + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} + export default function URLPreviewCard({ url, className = '' }: URLPreviewCardProps) { const [metadata, setMetadata] = useState(null); const [loading, setLoading] = useState(true); @@ -87,7 +126,10 @@ export default function URLPreviewCard({ url, className = '' }: URLPreviewCardPr } const hostname = new URL(metadata.url).hostname; - const displayImage = metadata.image || `/api/og/generate?url=${encodeURIComponent(url)}`; + + // Generate fallback image using useMemo to avoid regenerating on every render + const fallbackImage = useMemo(() => generateFallbackImage(hostname), [hostname]); + const displayImage = metadata.image || fallbackImage; return (
diff --git a/src/pages/api/og/generate.tsx b/src/pages/api/og/generate.tsx deleted file mode 100644 index 4b35f375e..000000000 --- a/src/pages/api/og/generate.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { ImageResponse } from '@vercel/og'; -import type { NextRequest } from 'next/server'; - -export const config = { - runtime: 'edge', -}; - -export default async function handler(req: NextRequest) { - try { - const { searchParams } = new URL(req.url); - const url = searchParams.get('url'); - - if (!url) { - return new Response('URL parameter is required', { status: 400 }); - } - - const urlObj = new URL(url); - const hostname = urlObj.hostname; - - // Generate unique colors based on hostname - const hashCode = (str: string) => { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - return hash; - }; - - const hash = Math.abs(hashCode(hostname)); - const hue1 = hash % 360; - const hue2 = (hash + 60) % 360; - - const gradient1 = `hsl(${hue1}, 70%, 50%)`; - const gradient2 = `hsl(${hue2}, 70%, 35%)`; - - // Get first letter of hostname for the icon - const firstLetter = hostname.replace('www.', '').charAt(0).toUpperCase(); - - return new ImageResponse( - ( -
- {/* Decorative circles in background */} -
-
- - {/* Main content */} -
- {/* Letter icon */} -
-
- {firstLetter} -
-
- - {/* Hostname */} -
- {hostname} -
- - {/* Subtitle */} -
- - - - - Shared from Vets Who Code -
-
-
- ), - { - width: 1200, - height: 630, - } - ); - } catch (error) { - console.error('Error generating OG image:', error); - return new Response('Failed to generate image', { status: 500 }); - } -} diff --git a/yarn.lock b/yarn.lock index d6603f945..f192426ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2356,11 +2356,6 @@ "@radix-ui/react-visually-hidden" "^1.0.3" classnames "^2.3.2" -"@resvg/resvg-wasm@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz" - integrity sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg== - "@rollup/plugin-babel@^5.2.0": version "5.3.1" resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" @@ -2441,14 +2436,6 @@ dependencies: "@shopify/graphql-client" "^1.4.1" -"@shuding/opentype.js@1.4.0-beta.0": - version "1.4.0-beta.0" - resolved "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz" - integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA== - dependencies: - fflate "^0.7.3" - string.prototype.codepointat "^0.2.1" - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -3142,14 +3129,6 @@ resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.4.1.tgz" integrity sha512-ekpL4ReX2TH3LnrRZTUKjHHNpNy9S1I7QmS+g/RQXoSUQ8ienzosuX7T9djZ/s8zPhBx1mpHP/Rw5875N+zQIQ== -"@vercel/og@^0.8.5": - version "0.8.5" - resolved "https://registry.npmjs.org/@vercel/og/-/og-0.8.5.tgz" - integrity sha512-fHqnxfBYcwkamlEgcIzaZqL8IHT09hR7FZL7UdMTdGJyoaBzM/dY6ulO5Swi4ig30FrBJI9I2C+GLV9sb9vexA== - dependencies: - "@resvg/resvg-wasm" "2.4.0" - satori "0.16.0" - "@vercel/oidc@3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz" @@ -3958,11 +3937,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz" - integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== - bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" @@ -4139,11 +4113,6 @@ camelcase@^6.2.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -camelize@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" - integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== - caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: version "1.0.30001689" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz" @@ -4412,7 +4381,7 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -4609,26 +4578,6 @@ crypto-random-string@^2.0.0: resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-background-parser@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz" - integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA== - -css-box-shadow@1.0.0-3: - version "1.0.0-3" - resolved "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz" - integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg== - -css-color-keywords@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" - integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== - -css-gradient-parser@^0.0.16: - version "0.0.16" - resolved "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz" - integrity sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA== - css-select@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" @@ -4640,15 +4589,6 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-to-react-native@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz" - integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== - dependencies: - camelize "^1.0.0" - css-color-keywords "^1.0.0" - postcss-value-parser "^4.0.2" - css-tree@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" @@ -5121,11 +5061,6 @@ emittery@^0.13.1: resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== -emoji-regex-xs@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz" - integrity sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -5430,7 +5365,7 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@^1.0.3, escape-html@~1.0.3: +escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -6105,11 +6040,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fflate@^0.7.3: - version "0.7.4" - resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz" - integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== - figures@^1.3.5: version "1.7.0" resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz" @@ -6701,11 +6631,6 @@ he@1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hex-rgb@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz" - integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== - html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" @@ -8032,14 +7957,6 @@ lilconfig@2.0.5: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz" integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== -linebreak@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz" - integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== - dependencies: - base64-js "0.0.8" - unicode-trie "^2.0.0" - lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" @@ -9282,11 +9199,6 @@ package-json-from-dist@^1.0.0: resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== -pako@^0.2.5: - version "0.2.9" - resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz" - integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -9294,14 +9206,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-css-color@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz" - integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg== - dependencies: - color-name "^1.1.4" - hex-rgb "^4.1.0" - parse-entities@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz" @@ -9566,7 +9470,7 @@ postcss-selector-parser@^6.1.2: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -10348,23 +10252,6 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, "safer-buffer@>= 2.1.2 < 3", "safer-bu resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -satori@0.16.0: - version "0.16.0" - resolved "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz" - integrity sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ== - dependencies: - "@shuding/opentype.js" "1.4.0-beta.0" - css-background-parser "^0.1.0" - css-box-shadow "1.0.0-3" - css-gradient-parser "^0.0.16" - css-to-react-native "^3.0.0" - emoji-regex-xs "^2.0.1" - escape-html "^1.0.3" - linebreak "^1.1.0" - parse-css-color "^0.2.1" - postcss-value-parser "^4.2.0" - yoga-layout "^3.2.1" - sax@>=0.6.0: version "1.4.1" resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" @@ -10875,11 +10762,6 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.codepointat@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz" - integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== - string.prototype.includes@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz" @@ -11309,11 +11191,6 @@ through@^2.3.6, through@^2.3.8: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tiny-inflate@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz" - integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== - tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -11678,14 +11555,6 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unicode-trie@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz" - integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== - dependencies: - pako "^0.2.5" - tiny-inflate "^1.0.0" - unified@^11.0.0: version "11.0.5" resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" @@ -12413,11 +12282,6 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yoga-layout@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz" - integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ== - "zod@^3.25.76 || ^4.1.8": version "3.25.76" resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz"