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 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":";;;AAsCA,MAAM,UAAU,GAAG;AACjB,IAAA,KAAK,EAAE;AACL,QAAA,WAAW,EAAE,OAAO;AACpB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,SAAS,EAAE,MAAM;AACjB,QAAA,eAAe,EAAE,MAAM;AACvB,QAAA,OAAO,EAAE,KAAK;AACd,QAAA,SAAS,EAAE;AACZ,KAAA;AACD,IAAA,MAAM,EAAE;AACN,QAAA,WAAW,EAAE,OAAO;AACpB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,SAAS,EAAE,MAAM;AACjB,QAAA,eAAe,EAAE,MAAM;AACvB,QAAA,OAAO,EAAE,MAAM;AACf,QAAA,SAAS,EAAE;AACZ,KAAA;AACD,IAAA,KAAK,EAAE;AACL,QAAA,WAAW,EAAE,OAAO;AACpB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,SAAS,EAAE,MAAM;AACjB,QAAA,eAAe,EAAE,MAAM;AACvB,QAAA,OAAO,EAAE,MAAM;AACf,QAAA,SAAS,EAAE;AACZ;CACF;AAEK,SAAU,WAAW,CAAC,EAC1B,GAAG,EACH,IAAI,GAAG,QAAQ,EACf,MAAM,GAAG,UAAU,EACnB,KAAK,GAAG,MAAM,EACd,MAAM,GAAG,MAAM,EACf,SAAS,GAAG,EAAE,EACd,OAAO,EACP,MAAM,EACN,WAAW,GAAG,cAAc,EACX,EAAA;IACjB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAyB,IAAI,CAAC;IAC9D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC5C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC;AAEtD,IAAA,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC;IAE/B,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,aAAa,GAAG,YAAW;AAC/B,YAAA,IAAI;gBACF,UAAU,CAAC,IAAI,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC;AAEd,gBAAA,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,CAAA,EAAG,WAAW,CAAA,KAAA,EAAQ,kBAAkB,CAAC,GAAG,CAAC,CAAA,CAAE,CAAC;AAE7E,gBAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;AAChB,oBAAA,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE;oBACvC,MAAM,IAAI,KAAK,CAAC,SAAS,CAAC,KAAK,IAAI,0BAA0B,CAAC;gBAChE;AAEA,gBAAA,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE;gBACtC,OAAO,CAAC,QAAQ,CAAC;AACjB,gBAAA,MAAM,GAAG,QAAQ,CAAC;YACpB;YAAE,OAAO,GAAG,EAAE;AACZ,gBAAA,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,GAAG,GAAG,GAAG,IAAI,KAAK,CAAC,eAAe,CAAC;gBACrE,QAAQ,CAAC,KAAK,CAAC;AACf,gBAAA,OAAO,GAAG,KAAK,CAAC;YAClB;oBAAU;gBACR,UAAU,CAAC,KAAK,CAAC;YACnB;AACF,QAAA,CAAC;AAED,QAAA,aAAa,EAAE;IACjB,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;IAEvB,IAAI,OAAO,EAAE;QACX,QACE,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAE;AACL,gBAAA,OAAO,EAAE,MAAM;AACf,gBAAA,SAAS,EAAE,QAAQ;AACnB,gBAAA,KAAK,EAAE,MAAM;AACb,gBAAA,MAAM,EAAE,mBAAmB;AAC3B,gBAAA,YAAY,EAAE,KAAK;AACnB,gBAAA,UAAU,EAAE;AACb,aAAA,EAAA,EAAA,oBAAA,CAGG;IAEV;IAEA,IAAI,KAAK,EAAE;QACT,QACE,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAE;AACL,gBAAA,OAAO,EAAE,MAAM;AACf,gBAAA,UAAU,EAAE,SAAS;AACrB,gBAAA,MAAM,EAAE,mBAAmB;AAC3B,gBAAA,YAAY,EAAE;AACf,aAAA,EAAA;AAED,YAAA,KAAA,CAAA,aAAA,CAAA,QAAA,EAAA,EAAQ,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAA,EAAA,wBAAA,CAAiC;YAAC,GAAG;AACvE,YAAA,KAAK,CAAC,OAAO,CACV;IAEV;IAEA,IAAI,CAAC,IAAI,EAAE;AACT,QAAA,OAAO,IAAI;IACb;AAEA,IAAA,MAAM,YAAY,GAAG,MAAM,KAAK,YAAY;IAE5C,QACE,2BACE,IAAI,EAAE,GAAG,EACT,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,SAAS,EAAE,gBAAgB,SAAS,CAAA,CAAE,EACtC,KAAK,EAAE;YACL,OAAO,EAAE,YAAY,GAAG,MAAM,GAAG,OAAO;YACxC,aAAa,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS;YAC/C,KAAK;YACL,MAAM;AACN,YAAA,cAAc,EAAE,MAAM;AACtB,YAAA,KAAK,EAAE,SAAS;AAChB,YAAA,MAAM,EAAE,mBAAmB;AAC3B,YAAA,YAAY,EAAE,KAAK;AACnB,YAAA,QAAQ,EAAE,QAAQ;AAClB,YAAA,UAAU,EAAE;AACb,SAAA,EACD,YAAY,EAAE,CAAC,CAAC,KAAI;YACjB,CAAC,CAAC,aAA6B,CAAC,KAAK,CAAC,SAAS,GAAG,6BAA6B;AAClF,QAAA,CAAC,EACD,YAAY,EAAE,CAAC,CAAC,KAAI;YACjB,CAAC,CAAC,aAA6B,CAAC,KAAK,CAAC,SAAS,GAAG,MAAM;QAC3D,CAAC,EAAA;AAEA,QAAA,IAAI,CAAC,KAAK,KACT,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAE;gBACL,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM;gBAChD,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC,WAAW;gBAClD,SAAS,EAAE,YAAY,GAAG,MAAM,CAAC,WAAW,GAAG,SAAS;gBACxD,UAAU,EAAE,YAAY,GAAG,CAAC,GAAG,SAAS;AACxC,gBAAA,eAAe,EAAE,CAAA,IAAA,EAAO,IAAI,CAAC,KAAK,CAAA,CAAA,CAAG;AACrC,gBAAA,cAAc,EAAE,OAAO;AACvB,gBAAA,kBAAkB,EAAE;AACrB,aAAA,EAAA,CACD,CACH;AACD,QAAA,KAAA,CAAA,aAAA,CAAA,KAAA,EAAA,EAAK,KAAK,EAAE;gBACV,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EAAE,YAAY,GAAG,CAAC,GAAG,SAAS;AAClC,gBAAA,OAAO,EAAE,MAAM;AACf,gBAAA,aAAa,EAAE,QAAQ;AACvB,gBAAA,cAAc,EAAE;AACjB,aAAA,EAAA;YACE,IAAI,CAAC,KAAK,KACT,4BAAI,KAAK,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,IAC3D,IAAI,CAAC,KAAK,CACR,CACN;AACA,YAAA,IAAI,CAAC,WAAW,KACf,KAAA,CAAA,aAAA,CAAA,GAAA,EAAA,EACE,KAAK,EAAE;AACL,oBAAA,MAAM,EAAE,CAAC;oBACT,QAAQ,EAAE,MAAM,CAAC,eAAe;AAChC,oBAAA,KAAK,EAAE,MAAM;AACb,oBAAA,OAAO,EAAE,aAAa;oBACtB,eAAe,EAAE,MAAM,CAAC,SAAS;AACjC,oBAAA,eAAe,EAAE,UAAU;AAC3B,oBAAA,QAAQ,EAAE;iBACY,EAAA,EAEvB,IAAI,CAAC,WAAW,CACf,CACL,CACG,CACJ;AAER;;;;"}
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
+ }