nextjs-link-preview 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/bin/setup.js +107 -0
- package/dist/index.esm.js +130 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/nextjs/components/LinkPreview.d.ts +34 -0
- package/dist/nextjs/components/LinkPreview.d.ts.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Seth Carney
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Next.js Link Preview
|
|
2
|
+
|
|
3
|
+
A customizable Next.js component for generating beautiful link preview cards with images, titles, and descriptions extracted from URL metadata.
|
|
4
|
+
|
|
5
|
+
## Why Next.js?
|
|
6
|
+
|
|
7
|
+
**Browser CORS limitations** prevent client-side link preview components from fetching metadata from most websites. This Next.js implementation solves that problem by:
|
|
8
|
+
|
|
9
|
+
- ✅ **Server-side fetching**: API route fetches metadata on the server
|
|
10
|
+
- ✅ **No CORS issues**: Works with GitHub, Twitter, Reddit, and any public URL
|
|
11
|
+
- ✅ **Production-ready**: Deploy to Vercel, Netlify, or any Node.js platform
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Automatically extracts Open Graph and meta tags from URLs
|
|
16
|
+
- Three size variants: small, medium, and large
|
|
17
|
+
- Two layout options: vertical (image top) and horizontal (image left)
|
|
18
|
+
- Fully customizable styling
|
|
19
|
+
- TypeScript support
|
|
20
|
+
- Loading and error states
|
|
21
|
+
- Callback functions for load and error events
|
|
22
|
+
- Responsive design
|
|
23
|
+
- **No CORS issues** - works with any public URL!
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Install the Package
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install nextjs-link-preview
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Set Up the API Route
|
|
34
|
+
|
|
35
|
+
Run the setup command to automatically create the API route in your Next.js project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx nextjs-link-preview
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will create `app/api/preview/route.ts` in your project.
|
|
42
|
+
|
|
43
|
+
### 3. Install Peer Dependencies
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install axios cheerio
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. Use the Component
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
53
|
+
|
|
54
|
+
export default function Page() {
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<LinkPreview url="https://github.com" size="medium" />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That's it! No CORS issues, works with any URL.
|
|
64
|
+
|
|
65
|
+
## Usage Examples
|
|
66
|
+
|
|
67
|
+
### Basic Example
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
71
|
+
|
|
72
|
+
export default function Page() {
|
|
73
|
+
return <LinkPreview url="https://github.com" />;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### With Size Variants
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
81
|
+
|
|
82
|
+
export default function Page() {
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
{/* Small preview - compact view with 1 line description */}
|
|
86
|
+
<LinkPreview url="https://github.com" size="small" />
|
|
87
|
+
|
|
88
|
+
{/* Medium preview (default) - balanced view with 2 line description */}
|
|
89
|
+
<LinkPreview url="https://github.com" size="medium" />
|
|
90
|
+
|
|
91
|
+
{/* Large preview - detailed view with 3 line description */}
|
|
92
|
+
<LinkPreview url="https://github.com" size="large" />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### With Horizontal Layout
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { LinkPreview } from "@/components/LinkPreview";
|
|
102
|
+
|
|
103
|
+
export default function Page() {
|
|
104
|
+
return (
|
|
105
|
+
<div>
|
|
106
|
+
{/* Horizontal layout - image on left, text on right */}
|
|
107
|
+
<LinkPreview url="https://github.com" layout="horizontal" size="medium" />
|
|
108
|
+
|
|
109
|
+
{/* Vertical layout (default) - image on top, text below */}
|
|
110
|
+
<LinkPreview url="https://github.com" layout="vertical" size="medium" />
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### With Custom Styling
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<LinkPreview url="https://github.com" width="400px" className="my-custom-class" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### With Callbacks
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<LinkPreview
|
|
126
|
+
url="https://github.com"
|
|
127
|
+
onLoad={(data) => console.log("Loaded:", data)}
|
|
128
|
+
onError={(error) => console.error("Error:", error)}
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## API
|
|
133
|
+
|
|
134
|
+
### LinkPreview Props
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Default | Description |
|
|
137
|
+
| ------------- | ------------------------------------ | ---------------- | ------------------------------------------------------------------------------- |
|
|
138
|
+
| `url` | `string` | **required** | The URL to generate a preview for |
|
|
139
|
+
| `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | Size variant of the preview card |
|
|
140
|
+
| `layout` | `"vertical"` \| `"horizontal"` | `"vertical"` | Layout: vertical (image top, text below) or horizontal (image left, text right) |
|
|
141
|
+
| `width` | `string` \| `number` | `"100%"` | Width of the preview card |
|
|
142
|
+
| `height` | `string` \| `number` | `"auto"` | Height of the preview card |
|
|
143
|
+
| `className` | `string` | `""` | Additional CSS class name |
|
|
144
|
+
| `apiEndpoint` | `string` | `"/api/preview"` | Custom API endpoint (if you moved the route) |
|
|
145
|
+
| `onLoad` | `(data: LinkPreviewData) => void` | `undefined` | Callback when metadata is loaded |
|
|
146
|
+
| `onError` | `(error: Error) => void` | `undefined` | Callback when loading fails |
|
|
147
|
+
|
|
148
|
+
### Size Variants
|
|
149
|
+
|
|
150
|
+
| Size | Image Height | Title Size | Description Lines | Padding |
|
|
151
|
+
| -------- | ------------ | ---------- | ----------------- | ------- |
|
|
152
|
+
| `small` | 120px | 14px | 1 line | 8px |
|
|
153
|
+
| `medium` | 200px | 16px | 2 lines | 12px |
|
|
154
|
+
| `large` | 300px | 20px | 3 lines | 16px |
|
|
155
|
+
|
|
156
|
+
### LinkPreviewData Type
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
interface LinkPreviewData {
|
|
160
|
+
title: string; // Page title (from og:title or <title>)
|
|
161
|
+
description: string; // Page description (from og:description or meta description)
|
|
162
|
+
image: string; // Preview image URL (from og:image or twitter:image)
|
|
163
|
+
url: string; // Original URL
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Testing & Demo
|
|
168
|
+
|
|
169
|
+
This project includes an interactive test suite built with Next.js to help you experiment with different URLs and size variants.
|
|
170
|
+
|
|
171
|
+
### Run the Demo
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Install demo dependencies (first time only)
|
|
175
|
+
npm run demo:install
|
|
176
|
+
|
|
177
|
+
# Start the demo server
|
|
178
|
+
npm run demo
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The demo application will open in your browser at http://localhost:3000 with:
|
|
182
|
+
|
|
183
|
+
- Interactive URL input for testing any link
|
|
184
|
+
- Pre-configured example URLs for popular sites
|
|
185
|
+
- Toggle between small, medium, and large sizes
|
|
186
|
+
- View extracted metadata
|
|
187
|
+
- Side-by-side comparison of all size variants
|
|
188
|
+
- **No CORS issues** - test GitHub, Twitter, Reddit, and more!
|
|
189
|
+
|
|
190
|
+
## How It Works
|
|
191
|
+
|
|
192
|
+
### 1. API Route (`app/api/preview/route.ts`)
|
|
193
|
+
|
|
194
|
+
The Next.js API route runs on the server and:
|
|
195
|
+
|
|
196
|
+
- Receives the URL as a query parameter
|
|
197
|
+
- Fetches the HTML using axios
|
|
198
|
+
- Parses metadata using Cheerio
|
|
199
|
+
- Returns JSON response
|
|
200
|
+
|
|
201
|
+
This bypasses CORS because it's a server-to-server request.
|
|
202
|
+
|
|
203
|
+
### 2. Client Component (`components/LinkPreview.tsx`)
|
|
204
|
+
|
|
205
|
+
The React component:
|
|
206
|
+
|
|
207
|
+
- Calls the API route (no CORS!)
|
|
208
|
+
- Handles loading and error states
|
|
209
|
+
- Renders the preview card with metadata
|
|
210
|
+
|
|
211
|
+
## Deployment
|
|
212
|
+
|
|
213
|
+
### Vercel (Recommended)
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run build
|
|
217
|
+
vercel deploy
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Other Platforms
|
|
221
|
+
|
|
222
|
+
This works on any platform that supports Next.js:
|
|
223
|
+
|
|
224
|
+
- Netlify
|
|
225
|
+
- AWS Amplify
|
|
226
|
+
- Railway
|
|
227
|
+
- Any Node.js hosting
|
|
228
|
+
|
|
229
|
+
## Project Structure
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
nextjs-link-preview/
|
|
233
|
+
├── src/
|
|
234
|
+
│ └── nextjs/
|
|
235
|
+
│ ├── app/
|
|
236
|
+
│ │ └── api/
|
|
237
|
+
│ │ └── preview/
|
|
238
|
+
│ │ └── route.ts # Server-side API route
|
|
239
|
+
│ └── components/
|
|
240
|
+
│ └── LinkPreview.tsx # Client component
|
|
241
|
+
├── nextjs-demo/ # Interactive demo app
|
|
242
|
+
│ ├── src/
|
|
243
|
+
│ │ ├── app/
|
|
244
|
+
│ │ │ ├── api/preview/
|
|
245
|
+
│ │ │ ├── page.tsx # Demo interface
|
|
246
|
+
│ │ │ └── layout.tsx
|
|
247
|
+
│ │ └── components/
|
|
248
|
+
│ │ └── LinkPreview.tsx
|
|
249
|
+
│ └── package.json
|
|
250
|
+
└── package.json
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Dependencies
|
|
254
|
+
|
|
255
|
+
### Required
|
|
256
|
+
|
|
257
|
+
- **Next.js** >= 14.0.0
|
|
258
|
+
- **React** >= 18.0.0
|
|
259
|
+
- **axios** - For HTTP requests
|
|
260
|
+
- **cheerio** - For HTML parsing and metadata extraction
|
|
261
|
+
|
|
262
|
+
### Dev Dependencies
|
|
263
|
+
|
|
264
|
+
- TypeScript
|
|
265
|
+
- Node types
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
270
|
+
|
|
271
|
+
## Contributing
|
|
272
|
+
|
|
273
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
274
|
+
|
|
275
|
+
## Troubleshooting
|
|
276
|
+
|
|
277
|
+
### "Module not found" errors
|
|
278
|
+
|
|
279
|
+
Make sure you've installed all dependencies:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npm install axios cheerio
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### API route not found
|
|
286
|
+
|
|
287
|
+
Ensure the API route is at `app/api/preview/route.ts` in your Next.js project.
|
|
288
|
+
|
|
289
|
+
### Still getting CORS errors
|
|
290
|
+
|
|
291
|
+
Make sure you're using the Next.js component, not the old React-only version. The component should be calling `/api/preview`, not fetching URLs directly.
|
|
292
|
+
|
|
293
|
+
## Need Help?
|
|
294
|
+
|
|
295
|
+
- Check the [demo README](nextjs-demo/README.md) for more details
|
|
296
|
+
- Review the source code in `src/nextjs/`
|
|
297
|
+
- Open an issue on GitHub
|
package/bin/setup.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const API_ROUTE_CONTENT = `import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
import axios from "axios";
|
|
8
|
+
import * as cheerio from "cheerio";
|
|
9
|
+
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
const searchParams = request.nextUrl.searchParams;
|
|
12
|
+
const targetUrl = searchParams.get("url");
|
|
13
|
+
|
|
14
|
+
if (!targetUrl) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: "URL parameter is required" },
|
|
17
|
+
{ status: 400 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await axios.get(targetUrl, {
|
|
23
|
+
headers: {
|
|
24
|
+
"User-Agent": "Mozilla/5.0 (compatible; LinkPreviewBot/1.0)",
|
|
25
|
+
},
|
|
26
|
+
timeout: 10000,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const $ = cheerio.load(response.data);
|
|
30
|
+
|
|
31
|
+
const metadata = {
|
|
32
|
+
title:
|
|
33
|
+
$('meta[property="og:title"]').attr("content") ||
|
|
34
|
+
$('meta[name="twitter:title"]').attr("content") ||
|
|
35
|
+
$("title").text() ||
|
|
36
|
+
"",
|
|
37
|
+
description:
|
|
38
|
+
$('meta[property="og:description"]').attr("content") ||
|
|
39
|
+
$('meta[name="twitter:description"]').attr("content") ||
|
|
40
|
+
$('meta[name="description"]').attr("content") ||
|
|
41
|
+
"",
|
|
42
|
+
image:
|
|
43
|
+
$('meta[property="og:image"]').attr("content") ||
|
|
44
|
+
$('meta[name="twitter:image"]').attr("content") ||
|
|
45
|
+
"",
|
|
46
|
+
url: targetUrl,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(metadata);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error fetching preview:", error);
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: "Failed to fetch preview" },
|
|
54
|
+
{ status: 500 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
function setupApiRoute() {
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
|
|
63
|
+
// Check if we're in a Next.js project
|
|
64
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
65
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
66
|
+
console.error("❌ Error: package.json not found. Make sure you're in a Next.js project directory.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
71
|
+
if (!packageJson.dependencies?.next && !packageJson.devDependencies?.next) {
|
|
72
|
+
console.error("❌ Error: Next.js not found in dependencies. Make sure this is a Next.js project.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create the API route directory structure
|
|
77
|
+
const apiRoutePath = path.join(cwd, "app", "api", "preview");
|
|
78
|
+
const routeFilePath = path.join(apiRoutePath, "route.ts");
|
|
79
|
+
|
|
80
|
+
// Check if route already exists
|
|
81
|
+
if (fs.existsSync(routeFilePath)) {
|
|
82
|
+
console.log("⚠️ API route already exists at app/api/preview/route.ts");
|
|
83
|
+
console.log("To reinstall, delete the existing file and run this command again.");
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create directories
|
|
88
|
+
fs.mkdirSync(apiRoutePath, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// Write the route file
|
|
91
|
+
fs.writeFileSync(routeFilePath, API_ROUTE_CONTENT);
|
|
92
|
+
|
|
93
|
+
console.log("✅ Successfully created API route at app/api/preview/route.ts");
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("📦 Make sure you have the required dependencies:");
|
|
96
|
+
console.log(" npm install axios cheerio");
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log("🎉 Setup complete! You can now use the LinkPreview component:");
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(' import { LinkPreview } from "nextjs-link-preview";');
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(" export default function Page() {");
|
|
103
|
+
console.log(' return <LinkPreview url="https://github.com" />;');
|
|
104
|
+
console.log(" }");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setupApiRoute();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
const sizeConfig = {
|
|
5
|
+
small: {
|
|
6
|
+
imageHeight: '120px',
|
|
7
|
+
imageWidth: '120px',
|
|
8
|
+
titleSize: '14px',
|
|
9
|
+
descriptionSize: '12px',
|
|
10
|
+
padding: '8px',
|
|
11
|
+
lineClamp: 1
|
|
12
|
+
},
|
|
13
|
+
medium: {
|
|
14
|
+
imageHeight: '200px',
|
|
15
|
+
imageWidth: '200px',
|
|
16
|
+
titleSize: '16px',
|
|
17
|
+
descriptionSize: '14px',
|
|
18
|
+
padding: '12px',
|
|
19
|
+
lineClamp: 2
|
|
20
|
+
},
|
|
21
|
+
large: {
|
|
22
|
+
imageHeight: '300px',
|
|
23
|
+
imageWidth: '280px',
|
|
24
|
+
titleSize: '20px',
|
|
25
|
+
descriptionSize: '16px',
|
|
26
|
+
padding: '16px',
|
|
27
|
+
lineClamp: 3
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function LinkPreview({ url, size = 'medium', layout = 'vertical', width = '100%', height = 'auto', className = '', onError, onLoad, apiEndpoint = '/api/preview' }) {
|
|
31
|
+
const [data, setData] = useState(null);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
const config = sizeConfig[size];
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const fetchMetadata = async () => {
|
|
37
|
+
try {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorData = await response.json();
|
|
43
|
+
throw new Error(errorData.error || 'Failed to fetch metadata');
|
|
44
|
+
}
|
|
45
|
+
const metadata = await response.json();
|
|
46
|
+
setData(metadata);
|
|
47
|
+
onLoad?.(metadata);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const error = err instanceof Error ? err : new Error('Unknown error');
|
|
51
|
+
setError(error);
|
|
52
|
+
onError?.(error);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
fetchMetadata();
|
|
59
|
+
}, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops
|
|
60
|
+
if (loading) {
|
|
61
|
+
return (React.createElement("div", { style: {
|
|
62
|
+
padding: '1rem',
|
|
63
|
+
textAlign: 'center',
|
|
64
|
+
color: '#666',
|
|
65
|
+
border: '1px solid #e0e0e0',
|
|
66
|
+
borderRadius: '8px',
|
|
67
|
+
background: '#f9f9f9'
|
|
68
|
+
} }, "Loading preview..."));
|
|
69
|
+
}
|
|
70
|
+
if (error) {
|
|
71
|
+
return (React.createElement("div", { style: {
|
|
72
|
+
padding: '1rem',
|
|
73
|
+
background: '#fff3f3',
|
|
74
|
+
border: '1px solid #f44336',
|
|
75
|
+
borderRadius: '8px'
|
|
76
|
+
} },
|
|
77
|
+
React.createElement("strong", { style: { color: '#d32f2f' } }, "Error loading preview:"),
|
|
78
|
+
' ',
|
|
79
|
+
error.message));
|
|
80
|
+
}
|
|
81
|
+
if (!data) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const isHorizontal = layout === 'horizontal';
|
|
85
|
+
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
86
|
+
display: isHorizontal ? 'flex' : 'block',
|
|
87
|
+
flexDirection: isHorizontal ? 'row' : undefined,
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
textDecoration: 'none',
|
|
91
|
+
color: 'inherit',
|
|
92
|
+
border: '1px solid #e0e0e0',
|
|
93
|
+
borderRadius: '8px',
|
|
94
|
+
overflow: 'hidden',
|
|
95
|
+
transition: 'box-shadow 0.3s'
|
|
96
|
+
}, onMouseEnter: (e) => {
|
|
97
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
98
|
+
}, onMouseLeave: (e) => {
|
|
99
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
100
|
+
} },
|
|
101
|
+
data.image && (React.createElement("div", { style: {
|
|
102
|
+
width: isHorizontal ? config.imageWidth : '100%',
|
|
103
|
+
height: isHorizontal ? '100%' : config.imageHeight,
|
|
104
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
105
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
106
|
+
backgroundImage: `url(${data.image})`,
|
|
107
|
+
backgroundSize: 'cover',
|
|
108
|
+
backgroundPosition: 'center'
|
|
109
|
+
} })),
|
|
110
|
+
React.createElement("div", { style: {
|
|
111
|
+
padding: config.padding,
|
|
112
|
+
flex: isHorizontal ? 1 : undefined,
|
|
113
|
+
display: 'flex',
|
|
114
|
+
flexDirection: 'column',
|
|
115
|
+
justifyContent: 'center'
|
|
116
|
+
} },
|
|
117
|
+
data.title && (React.createElement("h3", { style: { margin: '0 0 8px 0', fontSize: config.titleSize } }, data.title)),
|
|
118
|
+
data.description && (React.createElement("p", { style: {
|
|
119
|
+
margin: 0,
|
|
120
|
+
fontSize: config.descriptionSize,
|
|
121
|
+
color: '#666',
|
|
122
|
+
display: '-webkit-box',
|
|
123
|
+
WebkitLineClamp: config.lineClamp,
|
|
124
|
+
WebkitBoxOrient: 'vertical',
|
|
125
|
+
overflow: 'hidden'
|
|
126
|
+
} }, data.description)))));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { LinkPreview, LinkPreview as default };
|
|
130
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["'use client';\n\n/**\n * Next.js Link Preview Component\n *\n * This component uses the Next.js API route to fetch metadata server-side,\n * avoiding CORS issues entirely.\n *\n * Usage:\n * import { LinkPreview } from './components/LinkPreview';\n *\n * <LinkPreview url=\"https://github.com\" size=\"medium\" />\n */\n\nimport React, { useEffect, useState } from 'react';\n\nexport interface LinkPreviewData {\n title: string;\n description: string;\n image: string;\n url: string;\n}\n\nexport type LinkPreviewSize = 'small' | 'medium' | 'large';\nexport type LinkPreviewLayout = 'vertical' | 'horizontal';\n\nexport interface LinkPreviewProps {\n url: string;\n size?: LinkPreviewSize;\n layout?: LinkPreviewLayout;\n width?: string | number;\n height?: string | number;\n className?: string;\n onError?: (error: Error) => void;\n onLoad?: (data: LinkPreviewData) => void;\n apiEndpoint?: string; // Override the API endpoint if needed\n}\n\nconst sizeConfig = {\n small: {\n imageHeight: '120px',\n imageWidth: '120px',\n titleSize: '14px',\n descriptionSize: '12px',\n padding: '8px',\n lineClamp: 1\n },\n medium: {\n imageHeight: '200px',\n imageWidth: '200px',\n titleSize: '16px',\n descriptionSize: '14px',\n padding: '12px',\n lineClamp: 2\n },\n large: {\n imageHeight: '300px',\n imageWidth: '280px',\n titleSize: '20px',\n descriptionSize: '16px',\n padding: '16px',\n lineClamp: 3\n }\n};\n\nexport function LinkPreview({\n url,\n size = 'medium',\n layout = 'vertical',\n width = '100%',\n height = 'auto',\n className = '',\n onError,\n onLoad,\n apiEndpoint = '/api/preview'\n}: LinkPreviewProps) {\n const [data, setData] = useState<LinkPreviewData | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const config = sizeConfig[size];\n\n useEffect(() => {\n const fetchMetadata = async () => {\n try {\n setLoading(true);\n setError(null);\n\n const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);\n\n if (!response.ok) {\n const errorData = await response.json();\n throw new Error(errorData.error || 'Failed to fetch metadata');\n }\n\n const metadata = await response.json();\n setData(metadata);\n onLoad?.(metadata);\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Unknown error');\n setError(error);\n onError?.(error);\n } finally {\n setLoading(false);\n }\n };\n\n fetchMetadata();\n }, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops\n\n if (loading) {\n return (\n <div\n style={{\n padding: '1rem',\n textAlign: 'center',\n color: '#666',\n border: '1px solid #e0e0e0',\n borderRadius: '8px',\n background: '#f9f9f9'\n }}\n >\n Loading preview...\n </div>\n );\n }\n\n if (error) {\n return (\n <div\n style={{\n padding: '1rem',\n background: '#fff3f3',\n border: '1px solid #f44336',\n borderRadius: '8px'\n }}\n >\n <strong style={{ color: '#d32f2f' }}>Error loading preview:</strong>{' '}\n {error.message}\n </div>\n );\n }\n\n if (!data) {\n return null;\n }\n\n const isHorizontal = layout === 'horizontal';\n\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={`link-preview ${className}`}\n style={{\n display: isHorizontal ? 'flex' : 'block',\n flexDirection: isHorizontal ? 'row' : undefined,\n width,\n height,\n textDecoration: 'none',\n color: 'inherit',\n border: '1px solid #e0e0e0',\n borderRadius: '8px',\n overflow: 'hidden',\n transition: 'box-shadow 0.3s'\n }}\n onMouseEnter={(e) => {\n (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';\n }}\n onMouseLeave={(e) => {\n (e.currentTarget as HTMLElement).style.boxShadow = 'none';\n }}\n >\n {data.image && (\n <div\n style={{\n width: isHorizontal ? config.imageWidth : '100%',\n height: isHorizontal ? '100%' : config.imageHeight,\n minHeight: isHorizontal ? config.imageHeight : undefined,\n flexShrink: isHorizontal ? 0 : undefined,\n backgroundImage: `url(${data.image})`,\n backgroundSize: 'cover',\n backgroundPosition: 'center'\n }}\n />\n )}\n <div style={{\n padding: config.padding,\n flex: isHorizontal ? 1 : undefined,\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'center'\n }}>\n {data.title && (\n <h3 style={{ margin: '0 0 8px 0', fontSize: config.titleSize }}>\n {data.title}\n </h3>\n )}\n {data.description && (\n <p\n style={{\n margin: 0,\n fontSize: config.descriptionSize,\n color: '#666',\n display: '-webkit-box',\n WebkitLineClamp: config.lineClamp,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden'\n } as React.CSSProperties}\n >\n {data.description}\n </p>\n )}\n </div>\n </a>\n );\n}\n\nexport default LinkPreview;\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var React = require('react');
|
|
7
|
+
|
|
8
|
+
const sizeConfig = {
|
|
9
|
+
small: {
|
|
10
|
+
imageHeight: '120px',
|
|
11
|
+
imageWidth: '120px',
|
|
12
|
+
titleSize: '14px',
|
|
13
|
+
descriptionSize: '12px',
|
|
14
|
+
padding: '8px',
|
|
15
|
+
lineClamp: 1
|
|
16
|
+
},
|
|
17
|
+
medium: {
|
|
18
|
+
imageHeight: '200px',
|
|
19
|
+
imageWidth: '200px',
|
|
20
|
+
titleSize: '16px',
|
|
21
|
+
descriptionSize: '14px',
|
|
22
|
+
padding: '12px',
|
|
23
|
+
lineClamp: 2
|
|
24
|
+
},
|
|
25
|
+
large: {
|
|
26
|
+
imageHeight: '300px',
|
|
27
|
+
imageWidth: '280px',
|
|
28
|
+
titleSize: '20px',
|
|
29
|
+
descriptionSize: '16px',
|
|
30
|
+
padding: '16px',
|
|
31
|
+
lineClamp: 3
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function LinkPreview({ url, size = 'medium', layout = 'vertical', width = '100%', height = 'auto', className = '', onError, onLoad, apiEndpoint = '/api/preview' }) {
|
|
35
|
+
const [data, setData] = React.useState(null);
|
|
36
|
+
const [loading, setLoading] = React.useState(true);
|
|
37
|
+
const [error, setError] = React.useState(null);
|
|
38
|
+
const config = sizeConfig[size];
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
const fetchMetadata = async () => {
|
|
41
|
+
try {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
setError(null);
|
|
44
|
+
const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const errorData = await response.json();
|
|
47
|
+
throw new Error(errorData.error || 'Failed to fetch metadata');
|
|
48
|
+
}
|
|
49
|
+
const metadata = await response.json();
|
|
50
|
+
setData(metadata);
|
|
51
|
+
onLoad?.(metadata);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const error = err instanceof Error ? err : new Error('Unknown error');
|
|
55
|
+
setError(error);
|
|
56
|
+
onError?.(error);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
fetchMetadata();
|
|
63
|
+
}, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops
|
|
64
|
+
if (loading) {
|
|
65
|
+
return (React.createElement("div", { style: {
|
|
66
|
+
padding: '1rem',
|
|
67
|
+
textAlign: 'center',
|
|
68
|
+
color: '#666',
|
|
69
|
+
border: '1px solid #e0e0e0',
|
|
70
|
+
borderRadius: '8px',
|
|
71
|
+
background: '#f9f9f9'
|
|
72
|
+
} }, "Loading preview..."));
|
|
73
|
+
}
|
|
74
|
+
if (error) {
|
|
75
|
+
return (React.createElement("div", { style: {
|
|
76
|
+
padding: '1rem',
|
|
77
|
+
background: '#fff3f3',
|
|
78
|
+
border: '1px solid #f44336',
|
|
79
|
+
borderRadius: '8px'
|
|
80
|
+
} },
|
|
81
|
+
React.createElement("strong", { style: { color: '#d32f2f' } }, "Error loading preview:"),
|
|
82
|
+
' ',
|
|
83
|
+
error.message));
|
|
84
|
+
}
|
|
85
|
+
if (!data) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const isHorizontal = layout === 'horizontal';
|
|
89
|
+
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
90
|
+
display: isHorizontal ? 'flex' : 'block',
|
|
91
|
+
flexDirection: isHorizontal ? 'row' : undefined,
|
|
92
|
+
width,
|
|
93
|
+
height,
|
|
94
|
+
textDecoration: 'none',
|
|
95
|
+
color: 'inherit',
|
|
96
|
+
border: '1px solid #e0e0e0',
|
|
97
|
+
borderRadius: '8px',
|
|
98
|
+
overflow: 'hidden',
|
|
99
|
+
transition: 'box-shadow 0.3s'
|
|
100
|
+
}, onMouseEnter: (e) => {
|
|
101
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
102
|
+
}, onMouseLeave: (e) => {
|
|
103
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
104
|
+
} },
|
|
105
|
+
data.image && (React.createElement("div", { style: {
|
|
106
|
+
width: isHorizontal ? config.imageWidth : '100%',
|
|
107
|
+
height: isHorizontal ? '100%' : config.imageHeight,
|
|
108
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
109
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
110
|
+
backgroundImage: `url(${data.image})`,
|
|
111
|
+
backgroundSize: 'cover',
|
|
112
|
+
backgroundPosition: 'center'
|
|
113
|
+
} })),
|
|
114
|
+
React.createElement("div", { style: {
|
|
115
|
+
padding: config.padding,
|
|
116
|
+
flex: isHorizontal ? 1 : undefined,
|
|
117
|
+
display: 'flex',
|
|
118
|
+
flexDirection: 'column',
|
|
119
|
+
justifyContent: 'center'
|
|
120
|
+
} },
|
|
121
|
+
data.title && (React.createElement("h3", { style: { margin: '0 0 8px 0', fontSize: config.titleSize } }, data.title)),
|
|
122
|
+
data.description && (React.createElement("p", { style: {
|
|
123
|
+
margin: 0,
|
|
124
|
+
fontSize: config.descriptionSize,
|
|
125
|
+
color: '#666',
|
|
126
|
+
display: '-webkit-box',
|
|
127
|
+
WebkitLineClamp: config.lineClamp,
|
|
128
|
+
WebkitBoxOrient: 'vertical',
|
|
129
|
+
overflow: 'hidden'
|
|
130
|
+
} }, data.description)))));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exports.LinkPreview = LinkPreview;
|
|
134
|
+
exports.default = LinkPreview;
|
|
135
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["'use client';\n\n/**\n * Next.js Link Preview Component\n *\n * This component uses the Next.js API route to fetch metadata server-side,\n * avoiding CORS issues entirely.\n *\n * Usage:\n * import { LinkPreview } from './components/LinkPreview';\n *\n * <LinkPreview url=\"https://github.com\" size=\"medium\" />\n */\n\nimport React, { useEffect, useState } from 'react';\n\nexport interface LinkPreviewData {\n title: string;\n description: string;\n image: string;\n url: string;\n}\n\nexport type LinkPreviewSize = 'small' | 'medium' | 'large';\nexport type LinkPreviewLayout = 'vertical' | 'horizontal';\n\nexport interface LinkPreviewProps {\n url: string;\n size?: LinkPreviewSize;\n layout?: LinkPreviewLayout;\n width?: string | number;\n height?: string | number;\n className?: string;\n onError?: (error: Error) => void;\n onLoad?: (data: LinkPreviewData) => void;\n apiEndpoint?: string; // Override the API endpoint if needed\n}\n\nconst sizeConfig = {\n small: {\n imageHeight: '120px',\n imageWidth: '120px',\n titleSize: '14px',\n descriptionSize: '12px',\n padding: '8px',\n lineClamp: 1\n },\n medium: {\n imageHeight: '200px',\n imageWidth: '200px',\n titleSize: '16px',\n descriptionSize: '14px',\n padding: '12px',\n lineClamp: 2\n },\n large: {\n imageHeight: '300px',\n imageWidth: '280px',\n titleSize: '20px',\n descriptionSize: '16px',\n padding: '16px',\n lineClamp: 3\n }\n};\n\nexport function LinkPreview({\n url,\n size = 'medium',\n layout = 'vertical',\n width = '100%',\n height = 'auto',\n className = '',\n onError,\n onLoad,\n apiEndpoint = '/api/preview'\n}: LinkPreviewProps) {\n const [data, setData] = useState<LinkPreviewData | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const config = sizeConfig[size];\n\n useEffect(() => {\n const fetchMetadata = async () => {\n try {\n setLoading(true);\n setError(null);\n\n const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);\n\n if (!response.ok) {\n const errorData = await response.json();\n throw new Error(errorData.error || 'Failed to fetch metadata');\n }\n\n const metadata = await response.json();\n setData(metadata);\n onLoad?.(metadata);\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Unknown error');\n setError(error);\n onError?.(error);\n } finally {\n setLoading(false);\n }\n };\n\n fetchMetadata();\n }, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops\n\n if (loading) {\n return (\n <div\n style={{\n padding: '1rem',\n textAlign: 'center',\n color: '#666',\n border: '1px solid #e0e0e0',\n borderRadius: '8px',\n background: '#f9f9f9'\n }}\n >\n Loading preview...\n </div>\n );\n }\n\n if (error) {\n return (\n <div\n style={{\n padding: '1rem',\n background: '#fff3f3',\n border: '1px solid #f44336',\n borderRadius: '8px'\n }}\n >\n <strong style={{ color: '#d32f2f' }}>Error loading preview:</strong>{' '}\n {error.message}\n </div>\n );\n }\n\n if (!data) {\n return null;\n }\n\n const isHorizontal = layout === 'horizontal';\n\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={`link-preview ${className}`}\n style={{\n display: isHorizontal ? 'flex' : 'block',\n flexDirection: isHorizontal ? 'row' : undefined,\n width,\n height,\n textDecoration: 'none',\n color: 'inherit',\n border: '1px solid #e0e0e0',\n borderRadius: '8px',\n overflow: 'hidden',\n transition: 'box-shadow 0.3s'\n }}\n onMouseEnter={(e) => {\n (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';\n }}\n onMouseLeave={(e) => {\n (e.currentTarget as HTMLElement).style.boxShadow = 'none';\n }}\n >\n {data.image && (\n <div\n style={{\n width: isHorizontal ? config.imageWidth : '100%',\n height: isHorizontal ? '100%' : config.imageHeight,\n minHeight: isHorizontal ? config.imageHeight : undefined,\n flexShrink: isHorizontal ? 0 : undefined,\n backgroundImage: `url(${data.image})`,\n backgroundSize: 'cover',\n backgroundPosition: 'center'\n }}\n />\n )}\n <div style={{\n padding: config.padding,\n flex: isHorizontal ? 1 : undefined,\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'center'\n }}>\n {data.title && (\n <h3 style={{ margin: '0 0 8px 0', fontSize: config.titleSize }}>\n {data.title}\n </h3>\n )}\n {data.description && (\n <p\n style={{\n margin: 0,\n fontSize: config.descriptionSize,\n color: '#666',\n display: '-webkit-box',\n WebkitLineClamp: config.lineClamp,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden'\n } as React.CSSProperties}\n >\n {data.description}\n </p>\n )}\n </div>\n </a>\n );\n}\n\nexport default LinkPreview;\n"],"names":["useState","useEffect"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Link Preview Component
|
|
3
|
+
*
|
|
4
|
+
* This component uses the Next.js API route to fetch metadata server-side,
|
|
5
|
+
* avoiding CORS issues entirely.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { LinkPreview } from './components/LinkPreview';
|
|
9
|
+
*
|
|
10
|
+
* <LinkPreview url="https://github.com" size="medium" />
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
export interface LinkPreviewData {
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
image: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}
|
|
19
|
+
export type LinkPreviewSize = 'small' | 'medium' | 'large';
|
|
20
|
+
export type LinkPreviewLayout = 'vertical' | 'horizontal';
|
|
21
|
+
export interface LinkPreviewProps {
|
|
22
|
+
url: string;
|
|
23
|
+
size?: LinkPreviewSize;
|
|
24
|
+
layout?: LinkPreviewLayout;
|
|
25
|
+
width?: string | number;
|
|
26
|
+
height?: string | number;
|
|
27
|
+
className?: string;
|
|
28
|
+
onError?: (error: Error) => void;
|
|
29
|
+
onLoad?: (data: LinkPreviewData) => void;
|
|
30
|
+
apiEndpoint?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function LinkPreview({ url, size, layout, width, height, className, onError, onLoad, apiEndpoint }: LinkPreviewProps): React.JSX.Element | null;
|
|
33
|
+
export default LinkPreview;
|
|
34
|
+
//# sourceMappingURL=LinkPreview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkPreview.d.ts","sourceRoot":"","sources":["../../../src/nextjs/components/LinkPreview.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC3D,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA6BD,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,IAAe,EACf,MAAmB,EACnB,KAAc,EACd,MAAe,EACf,SAAc,EACd,OAAO,EACP,MAAM,EACN,WAA4B,EAC7B,EAAE,gBAAgB,4BA8IlB;AAED,eAAe,WAAW,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-link-preview",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A Next.js component for generating beautiful link preview cards with server-side metadata fetching - No CORS issues!",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nextjs",
|
|
8
|
+
"next",
|
|
9
|
+
"react",
|
|
10
|
+
"link-preview",
|
|
11
|
+
"preview",
|
|
12
|
+
"metadata",
|
|
13
|
+
"opengraph",
|
|
14
|
+
"og",
|
|
15
|
+
"card",
|
|
16
|
+
"component",
|
|
17
|
+
"typescript",
|
|
18
|
+
"server-side"
|
|
19
|
+
],
|
|
20
|
+
"author": "Seth Carney",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/sethcarney/nextjs-link-preview"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/sethcarney/nextjs-link-preview#readme",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/sethcarney/nextjs-link-preview/issues"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"module": "dist/index.esm.js",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"bin",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"bin": {
|
|
40
|
+
"nextjs-link-preview": "./bin/setup.js"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rollup -c",
|
|
44
|
+
"prepublishOnly": "npm run build",
|
|
45
|
+
"demo": "cd nextjs-demo && npm run dev",
|
|
46
|
+
"demo:install": "cd nextjs-demo && npm install",
|
|
47
|
+
"demo:build": "cd nextjs-demo && npm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"next": ">=14.0.0",
|
|
51
|
+
"react": ">=18.0.0",
|
|
52
|
+
"react-dom": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"axios": "^1.6.0",
|
|
56
|
+
"cheerio": "^1.0.0-rc.12"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
60
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
61
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
62
|
+
"@types/react": "^19.2.2",
|
|
63
|
+
"@types/react-dom": "^19.2.2",
|
|
64
|
+
"rollup": "^4.53.1",
|
|
65
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
66
|
+
"tslib": "^2.8.1",
|
|
67
|
+
"typescript": "^5.9.3"
|
|
68
|
+
}
|
|
69
|
+
}
|