Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion .vercelignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
# Git and version control
.git
.github

# Build cache
.next/cache
.cache

# Reports and testing
reports
.next/cache
coverage
__tests__

# Documentation (except README and data files)
docs/
*.md
!README.md
!src/data/**/*.md
!src/data/**/*.mdx

# 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
308 changes: 308 additions & 0 deletions URL_PREVIEW_CARD_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
# 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"
}
}
```

#### 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

#### `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 (
<URLPreviewCard url="https://vetswhocode.io" />
);
}
```

### 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 (
<div>
<h1>Check out this link:</h1>
<URLPreviewCard url="https://github.com/Vets-Who-Code" />
</div>
);
}
```

### 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 (
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 tw-gap-4">
{urls.map((url) => (
<URLPreviewCard key={url} url={url} />
))}
</div>
);
}
```

### With Custom Styling
```tsx
import URLPreviewCard from '@/components/url-preview-card';

export default function StyledCard() {
return (
<URLPreviewCard
url="https://vetswhocode.io"
className="tw-max-w-md tw-mx-auto tw-shadow-2xl"
/>
);
}
```

### 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<URLMetadata | null>(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 <div>Loading...</div>;

return (
<div>
<h2>{metadata.title}</h2>
<p>{metadata.description}</p>
<img src={metadata.image} alt={metadata.title} />
</div>
);
}
```

## 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 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, a unique SVG is generated client-side based on the hostname

## 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 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`:
- 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

- `node-html-parser`: Lightweight HTML parsing library for Node.js
- **No additional dependencies** for image generation - uses inline SVG generation

## 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
23 changes: 22 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const nextConfig = {

experimental: {},

// Ensure MDX and data files are included for all pages
outputFileTracingIncludes: {
'/**': ['src/data/**/*'],
},

webpack(config, { isServer }) {
config.module.rules.push({
test: /\.svg$/,
Expand All @@ -38,12 +43,28 @@ const nextConfig = {
};
}

// Optimize for serverless functions
if (isServer) {
// Enable tree-shaking for server bundles
config.optimization = {
...config.optimization,
usedExports: true,
sideEffects: false,
};
}

return config;
},

images: {
domains: [],
remotePatterns: [],
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
],
unoptimized: false,
},
};

Expand Down
Loading