|
| 1 | +# URL Preview Card Feature |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Automatic metadata extraction**: Fetches Open Graph tags, Twitter Card data, and fallback metadata from any URL |
| 8 | +- **Autogenerated images**: Creates a fallback image for URLs that don't have Open Graph images |
| 9 | +- **Loading states**: Shows a skeleton loader while fetching metadata |
| 10 | +- **Error handling**: Gracefully handles failed requests with error messages |
| 11 | +- **Responsive design**: Works on all screen sizes |
| 12 | +- **Hover effects**: Interactive animations on hover |
| 13 | +- **External link safety**: Opens links in new tabs with `rel="noopener noreferrer"` |
| 14 | + |
| 15 | +## Files Created |
| 16 | + |
| 17 | +### 1. Type Definitions |
| 18 | +`/src/types/url-metadata.ts` |
| 19 | +- `URLMetadata`: Interface for URL metadata (title, description, image, etc.) |
| 20 | +- `OGFetchResponse`: API response interface |
| 21 | + |
| 22 | +### 2. API Routes |
| 23 | + |
| 24 | +#### `/api/og/fetch` |
| 25 | +Fetches metadata from a given URL by parsing HTML and extracting Open Graph tags. |
| 26 | + |
| 27 | +**Request:** |
| 28 | +```typescript |
| 29 | +POST /api/og/fetch |
| 30 | +Content-Type: application/json |
| 31 | + |
| 32 | +{ |
| 33 | + "url": "https://example.com" |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +**Response:** |
| 38 | +```typescript |
| 39 | +{ |
| 40 | + "success": true, |
| 41 | + "data": { |
| 42 | + "url": "https://example.com", |
| 43 | + "title": "Example Domain", |
| 44 | + "description": "Example description", |
| 45 | + "image": "https://example.com/og-image.jpg", |
| 46 | + "siteName": "Example", |
| 47 | + "favicon": "https://example.com/favicon.ico", |
| 48 | + "type": "website" |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +#### Fallback Image Generation |
| 54 | +For URLs without OG images, the component automatically generates a unique SVG image client-side using the `generateFallbackImage()` utility function. This approach: |
| 55 | +- Creates unique colors based on hostname (same site = same colors) |
| 56 | +- Shows the first letter of the domain |
| 57 | +- Includes VWC branding |
| 58 | +- **No API calls required** - fully client-side generation |
| 59 | +- **Zero bundle size impact** - just inline SVG generation |
| 60 | + |
| 61 | +### 3. Components |
| 62 | + |
| 63 | +#### `URLPreviewCard` |
| 64 | +`/src/components/url-preview-card/index.tsx` |
| 65 | + |
| 66 | +A React component that displays a preview card for a URL. |
| 67 | + |
| 68 | +**Props:** |
| 69 | +- `url` (string, required): The URL to preview |
| 70 | +- `className` (string, optional): Additional CSS classes |
| 71 | + |
| 72 | +**Usage:** |
| 73 | +```tsx |
| 74 | +import URLPreviewCard from '@/components/url-preview-card'; |
| 75 | + |
| 76 | +function MyComponent() { |
| 77 | + return ( |
| 78 | + <URLPreviewCard url="https://vetswhocode.io" /> |
| 79 | + ); |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### 4. Demo Page |
| 84 | +`/src/pages/url-preview-demo.tsx` |
| 85 | + |
| 86 | +A demo page that showcases the URL preview card functionality. Visit `/url-preview-demo` to test it. |
| 87 | + |
| 88 | +## Usage Examples |
| 89 | + |
| 90 | +### Basic Usage |
| 91 | +```tsx |
| 92 | +import URLPreviewCard from '@/components/url-preview-card'; |
| 93 | + |
| 94 | +export default function MyPage() { |
| 95 | + return ( |
| 96 | + <div> |
| 97 | + <h1>Check out this link:</h1> |
| 98 | + <URLPreviewCard url="https://github.com/Vets-Who-Code" /> |
| 99 | + </div> |
| 100 | + ); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +### Multiple Cards |
| 105 | +```tsx |
| 106 | +import URLPreviewCard from '@/components/url-preview-card'; |
| 107 | + |
| 108 | +export default function LinkCollection() { |
| 109 | + const urls = [ |
| 110 | + 'https://vetswhocode.io', |
| 111 | + 'https://github.com/Vets-Who-Code', |
| 112 | + 'https://www.npmjs.com/package/next', |
| 113 | + ]; |
| 114 | + |
| 115 | + return ( |
| 116 | + <div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 tw-gap-4"> |
| 117 | + {urls.map((url) => ( |
| 118 | + <URLPreviewCard key={url} url={url} /> |
| 119 | + ))} |
| 120 | + </div> |
| 121 | + ); |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +### With Custom Styling |
| 126 | +```tsx |
| 127 | +import URLPreviewCard from '@/components/url-preview-card'; |
| 128 | + |
| 129 | +export default function StyledCard() { |
| 130 | + return ( |
| 131 | + <URLPreviewCard |
| 132 | + url="https://vetswhocode.io" |
| 133 | + className="tw-max-w-md tw-mx-auto tw-shadow-2xl" |
| 134 | + /> |
| 135 | + ); |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +### Using the API Directly |
| 140 | +If you need just the metadata without the card component: |
| 141 | + |
| 142 | +```tsx |
| 143 | +import { useState, useEffect } from 'react'; |
| 144 | +import type { URLMetadata } from '@/types/url-metadata'; |
| 145 | + |
| 146 | +export default function CustomCard() { |
| 147 | + const [metadata, setMetadata] = useState<URLMetadata | null>(null); |
| 148 | + |
| 149 | + useEffect(() => { |
| 150 | + fetch('/api/og/fetch', { |
| 151 | + method: 'POST', |
| 152 | + headers: { 'Content-Type': 'application/json' }, |
| 153 | + body: JSON.stringify({ url: 'https://vetswhocode.io' }), |
| 154 | + }) |
| 155 | + .then(res => res.json()) |
| 156 | + .then(data => { |
| 157 | + if (data.success) { |
| 158 | + setMetadata(data.data); |
| 159 | + } |
| 160 | + }); |
| 161 | + }, []); |
| 162 | + |
| 163 | + if (!metadata) return <div>Loading...</div>; |
| 164 | + |
| 165 | + return ( |
| 166 | + <div> |
| 167 | + <h2>{metadata.title}</h2> |
| 168 | + <p>{metadata.description}</p> |
| 169 | + <img src={metadata.image} alt={metadata.title} /> |
| 170 | + </div> |
| 171 | + ); |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +## How It Works |
| 176 | + |
| 177 | +1. **User provides a URL**: The URL is passed to the `URLPreviewCard` component |
| 178 | +2. **Fetch metadata**: The component calls `/api/og/fetch` with the URL |
| 179 | +3. **Parse HTML**: The API route fetches the URL and parses the HTML with node-html-parser |
| 180 | +4. **Extract metadata**: Open Graph tags, Twitter Card data, and fallback metadata are extracted |
| 181 | +5. **Return data**: The metadata is returned to the component |
| 182 | +6. **Display card**: The component renders a card with the title, description, and image |
| 183 | +7. **Fallback image**: If no image is found, a unique SVG is generated client-side based on the hostname |
| 184 | + |
| 185 | +## Customization |
| 186 | + |
| 187 | +### Styling |
| 188 | +The component uses Tailwind CSS with the `tw-` prefix. You can customize the appearance by: |
| 189 | +- Modifying the component in `/src/components/url-preview-card/index.tsx` |
| 190 | +- Passing custom classes via the `className` prop |
| 191 | +- Updating the Tailwind config |
| 192 | + |
| 193 | +### Image Generation |
| 194 | +Customize the fallback OG image appearance in the `generateFallbackImage()` function in `/src/components/url-preview-card/index.tsx`: |
| 195 | +- Change colors, gradients, typography (lines 22-42) |
| 196 | +- Modify the SVG layout and design |
| 197 | +- Add your logo or different branding |
| 198 | +- Adjust the hash function for different color schemes |
| 199 | + |
| 200 | +### Metadata Extraction |
| 201 | +Enhance metadata extraction in `/src/pages/api/og/fetch.ts`: |
| 202 | +- Add more metadata sources (Schema.org, JSON-LD, etc.) |
| 203 | +- Implement caching to reduce API calls |
| 204 | +- Add support for additional metadata fields |
| 205 | + |
| 206 | +## Performance Optimization |
| 207 | + |
| 208 | +### Caching (Recommended) |
| 209 | +To avoid fetching the same URL multiple times, consider implementing caching: |
| 210 | + |
| 211 | +1. **Client-side caching**: Use React Query or SWR |
| 212 | +2. **Server-side caching**: Store metadata in Redis or your database |
| 213 | +3. **CDN caching**: Cache the API responses at the edge |
| 214 | + |
| 215 | +Example with Prisma (database caching): |
| 216 | +```typescript |
| 217 | +// Add to your Prisma schema |
| 218 | +model URLMetadata { |
| 219 | + id String @id @default(cuid()) |
| 220 | + url String @unique |
| 221 | + title String? |
| 222 | + description String? |
| 223 | + image String? |
| 224 | + siteName String? |
| 225 | + favicon String? |
| 226 | + type String? |
| 227 | + createdAt DateTime @default(now()) |
| 228 | + updatedAt DateTime @updatedAt |
| 229 | +} |
| 230 | +``` |
| 231 | +
|
| 232 | +## Testing |
| 233 | +
|
| 234 | +Visit `/url-preview-demo` to test the feature: |
| 235 | +1. Enter any URL |
| 236 | +2. Click "Preview" |
| 237 | +3. See the generated card |
| 238 | +
|
| 239 | +Try these example URLs: |
| 240 | +- https://vetswhocode.io |
| 241 | +- https://github.com/Vets-Who-Code |
| 242 | +- https://www.npmjs.com/package/next |
| 243 | +
|
| 244 | +## Dependencies |
| 245 | +
|
| 246 | +- `node-html-parser`: Lightweight HTML parsing library for Node.js |
| 247 | +- **No additional dependencies** for image generation - uses inline SVG generation |
| 248 | +
|
| 249 | +## Browser Support |
| 250 | +
|
| 251 | +Works in all modern browsers that support: |
| 252 | +- ES6+ |
| 253 | +- Fetch API |
| 254 | +- Next.js Image component |
| 255 | +
|
| 256 | +## Security Considerations |
| 257 | +
|
| 258 | +- **SSRF Protection**: The API validates URLs before fetching |
| 259 | +- **Timeout**: Requests timeout after 10 seconds to prevent hanging |
| 260 | +- **User-Agent**: A custom user-agent is set to identify the bot |
| 261 | +- **Content-Type validation**: Only HTML content is parsed |
| 262 | +- **XSS Protection**: Metadata is sanitized before rendering |
| 263 | +
|
| 264 | +## Troubleshooting |
| 265 | +
|
| 266 | +### No image appears |
| 267 | +- Check if the URL has Open Graph meta tags |
| 268 | +- Verify the image URL is accessible |
| 269 | +- Check browser console for CORS errors |
| 270 | +- The fallback image generator should handle this automatically |
| 271 | +
|
| 272 | +### Slow loading |
| 273 | +- The URL might be slow to respond |
| 274 | +- Consider implementing caching |
| 275 | +- Check your network connection |
| 276 | +
|
| 277 | +### CORS errors |
| 278 | +- OG image fetching happens server-side to avoid CORS issues |
| 279 | +- If you see CORS errors, ensure you're using the API route |
| 280 | +
|
| 281 | +### Build errors |
| 282 | +- Ensure all dependencies are installed: `npm install` |
| 283 | +- Check TypeScript errors: `npm run typecheck` |
| 284 | +- Clear Next.js cache: `rm -rf .next` |
| 285 | +
|
| 286 | +## Future Enhancements |
| 287 | +
|
| 288 | +Potential improvements: |
| 289 | +- [ ] Add database caching for fetched metadata |
| 290 | +- [ ] Implement rate limiting to prevent abuse |
| 291 | +- [ ] Add support for video OG tags |
| 292 | +- [ ] Create multiple card layout variants |
| 293 | +- [ ] Add copy-to-clipboard functionality for URLs |
| 294 | +- [ ] Implement a URL shortener integration |
| 295 | +- [ ] Add social share buttons to cards |
| 296 | +- [ ] Support for rich embeds (YouTube, Twitter, etc.) |
| 297 | +
|
| 298 | +## Contributing |
| 299 | +
|
| 300 | +To contribute to this feature: |
| 301 | +1. Create a new branch from `og_cards` |
| 302 | +2. Make your changes |
| 303 | +3. Test thoroughly |
| 304 | +4. Submit a pull request |
| 305 | +
|
| 306 | +--- |
| 307 | +
|
| 308 | +Built with ❤️ for Vets Who Code |
0 commit comments