nextjs-link-preview 1.0.5 → 2.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/README.md +132 -70
- package/bin/cli.mjs +86 -0
- package/dist/index.d.ts +21 -17
- package/dist/index.esm.js +35 -62
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +34 -61
- package/dist/index.js.map +1 -1
- package/dist/types/nextjs/components/LinkPreview.d.ts +20 -16
- package/dist/types/nextjs/components/LinkPreview.d.ts.map +1 -1
- package/package.json +21 -28
- package/src/nextjs/components/LinkPreview.tsx +194 -0
- package/bin/setup.js +0 -132
package/README.md
CHANGED
|
@@ -1,127 +1,189 @@
|
|
|
1
1
|
# Next.js Link Preview
|
|
2
2
|
|
|
3
|
-
A Next.js component for
|
|
3
|
+
A simple, lightweight Next.js component for displaying beautiful link preview cards.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Option 1: Install as npm package
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- ✅ Three size variants (small, medium, large)
|
|
13
|
-
- ✅ Two layouts (vertical, horizontal)
|
|
14
|
-
- ✅ TypeScript support
|
|
15
|
-
- ✅ Loading and error states
|
|
16
|
-
- ✅ Fully customizable styling
|
|
9
|
+
```bash
|
|
10
|
+
npm install nextjs-link-preview
|
|
11
|
+
```
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
```tsx
|
|
14
|
+
import { FaGithub } from "react-icons/fa";
|
|
15
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
<LinkPreview
|
|
18
|
+
url="https://github.com/vercel/next.js"
|
|
19
|
+
title="Next.js"
|
|
20
|
+
description="The React Framework for the Web"
|
|
21
|
+
preset={FaGithub}
|
|
22
|
+
/>;
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
### Option 2: Copy to your project
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx nextjs-link-preview init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This copies the component source into `src/components/LinkPreview.tsx`. You own the code and can customize it however you want.
|
|
27
32
|
|
|
28
33
|
```bash
|
|
29
|
-
|
|
34
|
+
# Custom path
|
|
35
|
+
npx nextjs-link-preview init --path src/ui/LinkPreview.tsx
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- Pure presentational component - no data fetching
|
|
41
|
+
- Preset icon support via react-icons
|
|
42
|
+
- Custom icon support (react-icons, lucide, heroicons, any React element)
|
|
43
|
+
- Customizable text colors
|
|
44
|
+
- Three size variants (small, medium, large)
|
|
45
|
+
- Two layouts (vertical, horizontal)
|
|
46
|
+
- TypeScript support
|
|
47
|
+
- Fully customizable styling
|
|
48
|
+
- CLI copy-to-source support
|
|
49
|
+
- Peer deps only: React, Next.js, react-icons
|
|
33
50
|
|
|
34
51
|
## Usage
|
|
35
52
|
|
|
53
|
+
### Basic Usage
|
|
54
|
+
|
|
36
55
|
```tsx
|
|
37
56
|
import { LinkPreview } from "nextjs-link-preview";
|
|
38
57
|
|
|
39
58
|
export default function Page() {
|
|
40
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
<LinkPreview
|
|
61
|
+
url="https://example.com"
|
|
62
|
+
title="Example Site"
|
|
63
|
+
description="This is an example website"
|
|
64
|
+
image="https://example.com/preview.png"
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
41
67
|
}
|
|
42
68
|
```
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
### With Preset Icons
|
|
45
71
|
|
|
46
|
-
|
|
72
|
+
```tsx
|
|
73
|
+
import { FaGithub, FaNpm } from "react-icons/fa";
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
// GitHub
|
|
76
|
+
<LinkPreview
|
|
77
|
+
url="https://github.com/user/repo"
|
|
78
|
+
title="My Repository"
|
|
79
|
+
description="A cool open source project"
|
|
80
|
+
preset={FaGithub}
|
|
81
|
+
/>
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
<LinkPreview
|
|
52
|
-
|
|
53
|
-
|
|
83
|
+
// npm
|
|
84
|
+
<LinkPreview
|
|
85
|
+
url="https://npmjs.com/package/my-package"
|
|
86
|
+
title="my-package"
|
|
87
|
+
description="An awesome npm package"
|
|
88
|
+
preset={FaNpm}
|
|
89
|
+
/>
|
|
54
90
|
```
|
|
55
91
|
|
|
56
|
-
###
|
|
92
|
+
### With Icons
|
|
93
|
+
|
|
94
|
+
Pass any React element as an icon. Works with react-icons, lucide, heroicons, or plain SVGs:
|
|
57
95
|
|
|
58
96
|
```tsx
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
import { FaGithub } from "react-icons/fa";
|
|
98
|
+
|
|
99
|
+
<LinkPreview
|
|
100
|
+
url="https://github.com/user/repo"
|
|
101
|
+
title="My Repository"
|
|
102
|
+
description="A cool open source project"
|
|
103
|
+
icon={<FaGithub size={48} />}
|
|
104
|
+
/>;
|
|
61
105
|
```
|
|
62
106
|
|
|
63
|
-
|
|
107
|
+
Priority order: `icon` > `image` > `preset`.
|
|
108
|
+
|
|
109
|
+
### Custom Text Colors
|
|
64
110
|
|
|
65
111
|
```tsx
|
|
66
112
|
<LinkPreview
|
|
67
|
-
url="https://
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
url="https://example.com"
|
|
114
|
+
title="Styled Preview"
|
|
115
|
+
description="With custom colors"
|
|
116
|
+
image="https://example.com/preview.png"
|
|
117
|
+
titleColor="#1a1a2e"
|
|
118
|
+
descriptionColor="#999"
|
|
70
119
|
/>
|
|
71
120
|
```
|
|
72
121
|
|
|
73
|
-
###
|
|
122
|
+
### Size Variants
|
|
74
123
|
|
|
75
124
|
```tsx
|
|
76
|
-
<LinkPreview url="
|
|
125
|
+
<LinkPreview url="..." title="..." image="..." size="small" />
|
|
126
|
+
<LinkPreview url="..." title="..." image="..." size="medium" /> {/* default */}
|
|
127
|
+
<LinkPreview url="..." title="..." image="..." size="large" />
|
|
77
128
|
```
|
|
78
129
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| Prop | Type | Default | Description |
|
|
82
|
-
| ------------- | ------------------------------------ | ---------------- | -------------------------- |
|
|
83
|
-
| `url` | `string` | **required** | URL to preview |
|
|
84
|
-
| `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | Preview card size |
|
|
85
|
-
| `layout` | `"vertical"` \| `"horizontal"` | `"vertical"` | Image position |
|
|
86
|
-
| `width` | `string` \| `number` | `"100%"` | Card width |
|
|
87
|
-
| `height` | `string` \| `number` | `"auto"` | Card height |
|
|
88
|
-
| `className` | `string` | `""` | CSS class name |
|
|
89
|
-
| `apiEndpoint` | `string` | `"/api/preview"` | Custom API route path |
|
|
90
|
-
| `onLoad` | `(data) => void` | `undefined` | Called when metadata loads |
|
|
91
|
-
| `onError` | `(error) => void` | `undefined` | Called when loading fails |
|
|
130
|
+
### Layouts
|
|
92
131
|
|
|
93
|
-
|
|
132
|
+
```tsx
|
|
133
|
+
{/* Vertical (default) - image on top */}
|
|
134
|
+
<LinkPreview url="..." title="..." image="..." layout="vertical" />
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
{/* Horizontal - image on left */}
|
|
137
|
+
<LinkPreview url="..." title="..." image="..." layout="horizontal" />
|
|
138
|
+
```
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
npm run build
|
|
140
|
+
### Custom Styling
|
|
101
141
|
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
```tsx
|
|
143
|
+
<LinkPreview
|
|
144
|
+
url="https://example.com"
|
|
145
|
+
title="Example"
|
|
146
|
+
image="..."
|
|
147
|
+
width="400px"
|
|
148
|
+
className="my-custom-class"
|
|
149
|
+
/>
|
|
104
150
|
```
|
|
105
151
|
|
|
106
|
-
##
|
|
152
|
+
## Props
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Default | Description |
|
|
155
|
+
| ------------------ | ------------------------------------ | ------------ | ------------------------------------------------------ |
|
|
156
|
+
| `url` | `string` | **required** | Link destination |
|
|
157
|
+
| `title` | `string` | **required** | Preview card title |
|
|
158
|
+
| `description` | `string` | `undefined` | Preview card description |
|
|
159
|
+
| `image` | `string` | `undefined` | Custom image URL |
|
|
160
|
+
| `preset` | `IconType` | `undefined` | Preset icon component (from react-icons) |
|
|
161
|
+
| `icon` | `React.ReactNode` | `undefined` | Custom icon element (takes priority over image/preset) |
|
|
162
|
+
| `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | Preview card size |
|
|
163
|
+
| `layout` | `"vertical"` \| `"horizontal"` | `"vertical"` | Image position |
|
|
164
|
+
| `width` | `string` \| `number` | `"100%"` | Card width |
|
|
165
|
+
| `height` | `string` \| `number` | `"auto"` | Card height |
|
|
166
|
+
| `className` | `string` | `""` | Additional CSS classes |
|
|
167
|
+
| `titleColor` | `string` | `undefined` | Custom title text color |
|
|
168
|
+
| `descriptionColor` | `string` | `"#666"` | Custom description text color |
|
|
169
|
+
|
|
170
|
+
## Preset Icons
|
|
171
|
+
|
|
172
|
+
Pass any react-icons component as the `preset` prop. This gives you access to the full react-icons catalog.
|
|
107
173
|
|
|
108
|
-
|
|
109
|
-
2. **Caching**: In-memory cache with 1-hour TTL reduces redundant requests and prevents rate limiting
|
|
110
|
-
3. **Metadata Extraction**: Cheerio parses Open Graph and meta tags
|
|
111
|
-
4. **Component**: React component displays the preview card
|
|
174
|
+
## Testing
|
|
112
175
|
|
|
113
|
-
|
|
176
|
+
To test the component locally:
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
```bash
|
|
179
|
+
npm test
|
|
180
|
+
```
|
|
116
181
|
|
|
117
|
-
|
|
118
|
-
- **Cache Type**: In-memory Map (resets on server restart)
|
|
119
|
-
- **Benefits**:
|
|
120
|
-
- Reduces server load and response time for frequently accessed URLs
|
|
121
|
-
- Prevents rate limiting from target websites
|
|
122
|
-
- Improves user experience with faster previews
|
|
182
|
+
This will:
|
|
123
183
|
|
|
124
|
-
|
|
184
|
+
1. Build the component
|
|
185
|
+
2. Start the demo at http://localhost:3000
|
|
186
|
+
3. Open your browser to see the interactive demo
|
|
125
187
|
|
|
126
188
|
## License
|
|
127
189
|
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { resolve, dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
|
|
13
|
+
if (!command || command === "--help" || command === "-h") {
|
|
14
|
+
console.log(`
|
|
15
|
+
nextjs-link-preview CLI
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
npx nextjs-link-preview init [--path <destination>]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Copy the LinkPreview component into your project
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--path Destination path (default: src/components/LinkPreview.tsx)
|
|
25
|
+
--help Show this help message
|
|
26
|
+
`);
|
|
27
|
+
process.exit(command === "--help" || command === "-h" ? 0 : 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (command !== "init") {
|
|
31
|
+
console.error(`Unknown command: ${command}`);
|
|
32
|
+
console.error('Run "npx nextjs-link-preview --help" for usage info.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse --path flag
|
|
37
|
+
let destPath = "src/components/LinkPreview.tsx";
|
|
38
|
+
const pathIndex = args.indexOf("--path");
|
|
39
|
+
if (pathIndex !== -1 && args[pathIndex + 1]) {
|
|
40
|
+
destPath = args[pathIndex + 1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sourcePath = join(__dirname, "..", "src", "nextjs", "components", "LinkPreview.tsx");
|
|
44
|
+
const targetPath = resolve(process.cwd(), destPath);
|
|
45
|
+
const targetDir = dirname(targetPath);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(sourcePath)) {
|
|
48
|
+
console.error("Error: Could not find LinkPreview.tsx source file.");
|
|
49
|
+
console.error("This may be a packaging issue. Please report it at:");
|
|
50
|
+
console.error("https://github.com/sethcarney/nextjs-link-preview/issues");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (existsSync(targetPath)) {
|
|
55
|
+
console.log(`File already exists at: ${destPath}`);
|
|
56
|
+
console.log("Overwriting with the latest version...\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!existsSync(targetDir)) {
|
|
60
|
+
mkdirSync(targetDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const source = readFileSync(sourcePath, "utf-8");
|
|
65
|
+
writeFileSync(targetPath, source, "utf-8");
|
|
66
|
+
|
|
67
|
+
const importPath = destPath.replace(/\.tsx$/, "").replace(/\\/g, "/");
|
|
68
|
+
console.log(`
|
|
69
|
+
LinkPreview component copied to: ${destPath}
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
import { LinkPreview } from './${importPath}';
|
|
73
|
+
|
|
74
|
+
<LinkPreview
|
|
75
|
+
url="https://github.com/user/repo"
|
|
76
|
+
title="My Repo"
|
|
77
|
+
description="A cool repository"
|
|
78
|
+
preset="github"
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
The component is now yours to customize!
|
|
82
|
+
`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error("Error copying component:", err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { IconType } from 'react-icons';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Simple Link Preview Component
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Usage with custom image:
|
|
8
|
+
* <LinkPreview url="..." title="..." image="https://example.com/image.png" />
|
|
8
9
|
*
|
|
9
|
-
* Usage:
|
|
10
|
-
*
|
|
10
|
+
* Usage with preset icon (react-icons component):
|
|
11
|
+
* <LinkPreview url="..." title="..." preset={FaGithub} />
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* Usage with icon (any React element, e.g. react-icons):
|
|
14
|
+
* <LinkPreview url="..." title="..." icon={<FaGithub size={48} />} />
|
|
15
|
+
*
|
|
16
|
+
* Usage with custom text colors:
|
|
17
|
+
* <LinkPreview url="..." title="..." titleColor="#333" descriptionColor="#999" />
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
|
-
interface LinkPreviewData {
|
|
16
|
-
title: string;
|
|
17
|
-
description: string;
|
|
18
|
-
image: string;
|
|
19
|
-
url: string;
|
|
20
|
-
}
|
|
21
20
|
type LinkPreviewSize = "small" | "medium" | "large";
|
|
22
21
|
type LinkPreviewLayout = "vertical" | "horizontal";
|
|
22
|
+
type LinkPreviewPreset = IconType;
|
|
23
23
|
interface LinkPreviewProps {
|
|
24
24
|
url: string;
|
|
25
|
+
title: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
image?: string;
|
|
28
|
+
preset?: LinkPreviewPreset;
|
|
29
|
+
icon?: React.ReactNode;
|
|
25
30
|
size?: LinkPreviewSize;
|
|
26
31
|
layout?: LinkPreviewLayout;
|
|
27
32
|
width?: string | number;
|
|
28
33
|
height?: string | number;
|
|
29
34
|
className?: string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
apiEndpoint?: string;
|
|
35
|
+
titleColor?: string;
|
|
36
|
+
descriptionColor?: string;
|
|
33
37
|
}
|
|
34
|
-
declare function LinkPreview({ url, size, layout, width, height, className,
|
|
38
|
+
declare function LinkPreview({ url, title, description, image, preset, icon, size, layout, width, height, className, titleColor, descriptionColor }: LinkPreviewProps): React.JSX.Element;
|
|
35
39
|
|
|
36
40
|
export { LinkPreview, LinkPreview as default };
|
|
37
|
-
export type {
|
|
41
|
+
export type { LinkPreviewLayout, LinkPreviewPreset, LinkPreviewProps, LinkPreviewSize };
|
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import React
|
|
2
|
+
import React from 'react';
|
|
3
3
|
|
|
4
4
|
const sizeConfig = {
|
|
5
5
|
small: {
|
|
@@ -27,65 +27,17 @@ const sizeConfig = {
|
|
|
27
27
|
lineClamp: 3
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
-
function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "",
|
|
31
|
-
const [data, setData] = useState(null);
|
|
32
|
-
const [loading, setLoading] = useState(true);
|
|
33
|
-
const [error, setError] = useState(null);
|
|
30
|
+
function LinkPreview({ url, title, description, image, preset, icon, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "", titleColor, descriptionColor }) {
|
|
34
31
|
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
32
|
const isHorizontal = layout === "horizontal";
|
|
33
|
+
const PresetIcon = preset;
|
|
34
|
+
const iconSize = size === "large" ? 96 : size === "medium" ? 64 : 48;
|
|
35
|
+
const iconFontSize = size === "large" ? "96px" : size === "medium" ? "64px" : "48px";
|
|
85
36
|
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
86
37
|
display: isHorizontal ? "flex" : "block",
|
|
87
38
|
flexDirection: isHorizontal ? "row" : undefined,
|
|
88
39
|
width,
|
|
40
|
+
maxWidth: isHorizontal ? undefined : "400px",
|
|
89
41
|
height,
|
|
90
42
|
textDecoration: "none",
|
|
91
43
|
color: "inherit",
|
|
@@ -98,15 +50,36 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
98
50
|
}, onMouseLeave: (e) => {
|
|
99
51
|
e.currentTarget.style.boxShadow = "none";
|
|
100
52
|
} },
|
|
101
|
-
|
|
53
|
+
icon ? (React.createElement("div", { style: {
|
|
102
54
|
width: isHorizontal ? config.imageWidth : "100%",
|
|
103
55
|
height: isHorizontal ? "100%" : config.imageHeight,
|
|
104
56
|
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
105
57
|
flexShrink: isHorizontal ? 0 : undefined,
|
|
106
|
-
|
|
58
|
+
display: "flex",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
justifyContent: "center",
|
|
61
|
+
backgroundColor: "#f6f8fa",
|
|
62
|
+
fontSize: iconFontSize
|
|
63
|
+
} }, icon)) : image ? (React.createElement("div", { style: {
|
|
64
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
65
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
66
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
67
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
68
|
+
backgroundImage: `url(${image})`,
|
|
107
69
|
backgroundSize: "cover",
|
|
108
|
-
backgroundPosition: "center"
|
|
109
|
-
|
|
70
|
+
backgroundPosition: "center",
|
|
71
|
+
backgroundRepeat: "no-repeat"
|
|
72
|
+
} })) : PresetIcon ? (React.createElement("div", { style: {
|
|
73
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
74
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
75
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
76
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
77
|
+
display: "flex",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
justifyContent: "center",
|
|
80
|
+
backgroundColor: "#f6f8fa"
|
|
81
|
+
} },
|
|
82
|
+
React.createElement(PresetIcon, { size: iconSize }))) : null,
|
|
110
83
|
React.createElement("div", { style: {
|
|
111
84
|
padding: config.padding,
|
|
112
85
|
flex: isHorizontal ? 1 : undefined,
|
|
@@ -114,16 +87,16 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
114
87
|
flexDirection: "column",
|
|
115
88
|
justifyContent: "center"
|
|
116
89
|
} },
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize, color: titleColor } }, title),
|
|
91
|
+
description && (React.createElement("p", { style: {
|
|
119
92
|
margin: 0,
|
|
120
93
|
fontSize: config.descriptionSize,
|
|
121
|
-
color: "#666",
|
|
94
|
+
color: descriptionColor || "#666",
|
|
122
95
|
display: "-webkit-box",
|
|
123
96
|
WebkitLineClamp: config.lineClamp,
|
|
124
97
|
WebkitBoxOrient: "vertical",
|
|
125
98
|
overflow: "hidden"
|
|
126
|
-
} },
|
|
99
|
+
} }, description)))));
|
|
127
100
|
}
|
|
128
101
|
|
|
129
102
|
export { LinkPreview, LinkPreview as default };
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["\"use client\";\r\n\r\n/**\r\n * Next.js Link Preview Component\r\n *\r\n * This component uses the Next.js API route to fetch metadata server-side,\r\n * avoiding CORS issues entirely.\r\n *\r\n * Usage:\r\n * import { LinkPreview } from './components/LinkPreview';\r\n *\r\n * <LinkPreview url=\"https://github.com\" size=\"medium\" />\r\n */\r\n\r\nimport React, { useEffect, useState } from \"react\";\r\n\r\nexport interface LinkPreviewData {\r\n title: string;\r\n description: string;\r\n image: string;\r\n url: string;\r\n}\r\n\r\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\r\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\r\n\r\nexport interface LinkPreviewProps {\r\n url: string;\r\n size?: LinkPreviewSize;\r\n layout?: LinkPreviewLayout;\r\n width?: string | number;\r\n height?: string | number;\r\n className?: string;\r\n onError?: (error: Error) => void;\r\n onLoad?: (data: LinkPreviewData) => void;\r\n apiEndpoint?: string; // Override the API endpoint if needed\r\n}\r\n\r\nconst sizeConfig = {\r\n small: {\r\n imageHeight: \"120px\",\r\n imageWidth: \"120px\",\r\n titleSize: \"14px\",\r\n descriptionSize: \"12px\",\r\n padding: \"8px\",\r\n lineClamp: 1\r\n },\r\n medium: {\r\n imageHeight: \"200px\",\r\n imageWidth: \"200px\",\r\n titleSize: \"16px\",\r\n descriptionSize: \"14px\",\r\n padding: \"12px\",\r\n lineClamp: 2\r\n },\r\n large: {\r\n imageHeight: \"300px\",\r\n imageWidth: \"280px\",\r\n titleSize: \"20px\",\r\n descriptionSize: \"16px\",\r\n padding: \"16px\",\r\n lineClamp: 3\r\n }\r\n};\r\n\r\nexport function LinkPreview({\r\n url,\r\n size = \"medium\",\r\n layout = \"vertical\",\r\n width = \"100%\",\r\n height = \"auto\",\r\n className = \"\",\r\n onError,\r\n onLoad,\r\n apiEndpoint = \"/api/preview\"\r\n}: LinkPreviewProps) {\r\n const [data, setData] = useState<LinkPreviewData | null>(null);\r\n const [loading, setLoading] = useState(true);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const config = sizeConfig[size];\r\n\r\n useEffect(() => {\r\n const fetchMetadata = async () => {\r\n try {\r\n setLoading(true);\r\n setError(null);\r\n\r\n const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);\r\n\r\n if (!response.ok) {\r\n const errorData = await response.json();\r\n throw new Error(errorData.error || \"Failed to fetch metadata\");\r\n }\r\n\r\n const metadata = await response.json();\r\n setData(metadata);\r\n onLoad?.(metadata);\r\n } catch (err) {\r\n const error = err instanceof Error ? err : new Error(\"Unknown error\");\r\n setError(error);\r\n onError?.(error);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n fetchMetadata();\r\n }, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops\r\n\r\n if (loading) {\r\n return (\r\n <div\r\n style={{\r\n padding: \"1rem\",\r\n textAlign: \"center\",\r\n color: \"#666\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n background: \"#f9f9f9\"\r\n }}\r\n >\r\n Loading preview...\r\n </div>\r\n );\r\n }\r\n\r\n if (error) {\r\n return (\r\n <div\r\n style={{\r\n padding: \"1rem\",\r\n background: \"#fff3f3\",\r\n border: \"1px solid #f44336\",\r\n borderRadius: \"8px\"\r\n }}\r\n >\r\n <strong style={{ color: \"#d32f2f\" }}>Error loading preview:</strong> {error.message}\r\n </div>\r\n );\r\n }\r\n\r\n if (!data) {\r\n return null;\r\n }\r\n\r\n const isHorizontal = layout === \"horizontal\";\r\n\r\n return (\r\n <a\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className={`link-preview ${className}`}\r\n style={{\r\n display: isHorizontal ? \"flex\" : \"block\",\r\n flexDirection: isHorizontal ? \"row\" : undefined,\r\n width,\r\n height,\r\n textDecoration: \"none\",\r\n color: \"inherit\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n overflow: \"hidden\",\r\n transition: \"box-shadow 0.3s\"\r\n }}\r\n onMouseEnter={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"0 4px 12px rgba(0,0,0,0.15)\";\r\n }}\r\n onMouseLeave={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"none\";\r\n }}\r\n >\r\n {data.image && (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n backgroundImage: `url(${data.image})`,\r\n backgroundSize: \"cover\",\r\n backgroundPosition: \"center\"\r\n }}\r\n />\r\n )}\r\n <div\r\n style={{\r\n padding: config.padding,\r\n flex: isHorizontal ? 1 : undefined,\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n justifyContent: \"center\"\r\n }}\r\n >\r\n {data.title && (\r\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize }}>{data.title}</h3>\r\n )}\r\n {data.description && (\r\n <p\r\n style={\r\n {\r\n margin: 0,\r\n fontSize: config.descriptionSize,\r\n color: \"#666\",\r\n display: \"-webkit-box\",\r\n WebkitLineClamp: config.lineClamp,\r\n WebkitBoxOrient: \"vertical\",\r\n overflow: \"hidden\"\r\n } as React.CSSProperties\r\n }\r\n >\r\n {data.description}\r\n </p>\r\n )}\r\n </div>\r\n </a>\r\n );\r\n}\r\n\r\nexport default LinkPreview;\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["\"use client\";\r\n\r\n/**\r\n * Simple Link Preview Component\r\n *\r\n * Usage with custom image:\r\n * <LinkPreview url=\"...\" title=\"...\" image=\"https://example.com/image.png\" />\r\n *\r\n * Usage with preset icon (react-icons component):\r\n * <LinkPreview url=\"...\" title=\"...\" preset={FaGithub} />\r\n *\r\n * Usage with icon (any React element, e.g. react-icons):\r\n * <LinkPreview url=\"...\" title=\"...\" icon={<FaGithub size={48} />} />\r\n *\r\n * Usage with custom text colors:\r\n * <LinkPreview url=\"...\" title=\"...\" titleColor=\"#333\" descriptionColor=\"#999\" />\r\n */\r\n\r\nimport React from \"react\";\r\nimport type { IconType } from \"react-icons\";\r\n\r\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\r\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\r\nexport type LinkPreviewPreset = IconType;\r\n\r\nexport interface LinkPreviewProps {\r\n url: string;\r\n title: string;\r\n description?: string;\r\n image?: string;\r\n preset?: LinkPreviewPreset;\r\n icon?: React.ReactNode;\r\n size?: LinkPreviewSize;\r\n layout?: LinkPreviewLayout;\r\n width?: string | number;\r\n height?: string | number;\r\n className?: string;\r\n titleColor?: string;\r\n descriptionColor?: string;\r\n}\r\n\r\nconst sizeConfig = {\r\n small: {\r\n imageHeight: \"120px\",\r\n imageWidth: \"120px\",\r\n titleSize: \"14px\",\r\n descriptionSize: \"12px\",\r\n padding: \"8px\",\r\n lineClamp: 1\r\n },\r\n medium: {\r\n imageHeight: \"200px\",\r\n imageWidth: \"200px\",\r\n titleSize: \"16px\",\r\n descriptionSize: \"14px\",\r\n padding: \"12px\",\r\n lineClamp: 2\r\n },\r\n large: {\r\n imageHeight: \"300px\",\r\n imageWidth: \"280px\",\r\n titleSize: \"20px\",\r\n descriptionSize: \"16px\",\r\n padding: \"16px\",\r\n lineClamp: 3\r\n }\r\n};\r\n\r\nexport function LinkPreview({\r\n url,\r\n title,\r\n description,\r\n image,\r\n preset,\r\n icon,\r\n size = \"medium\",\r\n layout = \"vertical\",\r\n width = \"100%\",\r\n height = \"auto\",\r\n className = \"\",\r\n titleColor,\r\n descriptionColor\r\n}: LinkPreviewProps) {\r\n const config = sizeConfig[size];\r\n const isHorizontal = layout === \"horizontal\";\r\n\r\n const PresetIcon = preset;\r\n const iconSize = size === \"large\" ? 96 : size === \"medium\" ? 64 : 48;\r\n const iconFontSize = size === \"large\" ? \"96px\" : size === \"medium\" ? \"64px\" : \"48px\";\r\n\r\n return (\r\n <a\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className={`link-preview ${className}`}\r\n style={{\r\n display: isHorizontal ? \"flex\" : \"block\",\r\n flexDirection: isHorizontal ? \"row\" : undefined,\r\n width,\r\n maxWidth: isHorizontal ? undefined : \"400px\",\r\n height,\r\n textDecoration: \"none\",\r\n color: \"inherit\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n overflow: \"hidden\",\r\n transition: \"box-shadow 0.3s\"\r\n }}\r\n onMouseEnter={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"0 4px 12px rgba(0,0,0,0.15)\";\r\n }}\r\n onMouseLeave={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"none\";\r\n }}\r\n >\r\n {icon ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n backgroundColor: \"#f6f8fa\",\r\n fontSize: iconFontSize\r\n }}\r\n >\r\n {icon}\r\n </div>\r\n ) : image ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n backgroundImage: `url(${image})`,\r\n backgroundSize: \"cover\",\r\n backgroundPosition: \"center\",\r\n backgroundRepeat: \"no-repeat\"\r\n }}\r\n />\r\n ) : PresetIcon ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n backgroundColor: \"#f6f8fa\"\r\n }}\r\n >\r\n <PresetIcon size={iconSize} />\r\n </div>\r\n ) : null}\r\n <div\r\n style={{\r\n padding: config.padding,\r\n flex: isHorizontal ? 1 : undefined,\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n justifyContent: \"center\"\r\n }}\r\n >\r\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize, color: titleColor }}>{title}</h3>\r\n {description && (\r\n <p\r\n style={\r\n {\r\n margin: 0,\r\n fontSize: config.descriptionSize,\r\n color: descriptionColor || \"#666\",\r\n display: \"-webkit-box\",\r\n WebkitLineClamp: config.lineClamp,\r\n WebkitBoxOrient: \"vertical\",\r\n overflow: \"hidden\"\r\n } as React.CSSProperties\r\n }\r\n >\r\n {description}\r\n </p>\r\n )}\r\n </div>\r\n </a>\r\n );\r\n}\r\n\r\nexport default LinkPreview;\r\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -31,65 +31,17 @@ const sizeConfig = {
|
|
|
31
31
|
lineClamp: 3
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "",
|
|
35
|
-
const [data, setData] = React.useState(null);
|
|
36
|
-
const [loading, setLoading] = React.useState(true);
|
|
37
|
-
const [error, setError] = React.useState(null);
|
|
34
|
+
function LinkPreview({ url, title, description, image, preset, icon, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "", titleColor, descriptionColor }) {
|
|
38
35
|
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
36
|
const isHorizontal = layout === "horizontal";
|
|
37
|
+
const PresetIcon = preset;
|
|
38
|
+
const iconSize = size === "large" ? 96 : size === "medium" ? 64 : 48;
|
|
39
|
+
const iconFontSize = size === "large" ? "96px" : size === "medium" ? "64px" : "48px";
|
|
89
40
|
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
90
41
|
display: isHorizontal ? "flex" : "block",
|
|
91
42
|
flexDirection: isHorizontal ? "row" : undefined,
|
|
92
43
|
width,
|
|
44
|
+
maxWidth: isHorizontal ? undefined : "400px",
|
|
93
45
|
height,
|
|
94
46
|
textDecoration: "none",
|
|
95
47
|
color: "inherit",
|
|
@@ -102,15 +54,36 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
102
54
|
}, onMouseLeave: (e) => {
|
|
103
55
|
e.currentTarget.style.boxShadow = "none";
|
|
104
56
|
} },
|
|
105
|
-
|
|
57
|
+
icon ? (React.createElement("div", { style: {
|
|
106
58
|
width: isHorizontal ? config.imageWidth : "100%",
|
|
107
59
|
height: isHorizontal ? "100%" : config.imageHeight,
|
|
108
60
|
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
109
61
|
flexShrink: isHorizontal ? 0 : undefined,
|
|
110
|
-
|
|
62
|
+
display: "flex",
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
backgroundColor: "#f6f8fa",
|
|
66
|
+
fontSize: iconFontSize
|
|
67
|
+
} }, icon)) : image ? (React.createElement("div", { style: {
|
|
68
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
69
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
70
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
71
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
72
|
+
backgroundImage: `url(${image})`,
|
|
111
73
|
backgroundSize: "cover",
|
|
112
|
-
backgroundPosition: "center"
|
|
113
|
-
|
|
74
|
+
backgroundPosition: "center",
|
|
75
|
+
backgroundRepeat: "no-repeat"
|
|
76
|
+
} })) : PresetIcon ? (React.createElement("div", { style: {
|
|
77
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
78
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
79
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
80
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
81
|
+
display: "flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
backgroundColor: "#f6f8fa"
|
|
85
|
+
} },
|
|
86
|
+
React.createElement(PresetIcon, { size: iconSize }))) : null,
|
|
114
87
|
React.createElement("div", { style: {
|
|
115
88
|
padding: config.padding,
|
|
116
89
|
flex: isHorizontal ? 1 : undefined,
|
|
@@ -118,16 +91,16 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
118
91
|
flexDirection: "column",
|
|
119
92
|
justifyContent: "center"
|
|
120
93
|
} },
|
|
121
|
-
|
|
122
|
-
|
|
94
|
+
React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize, color: titleColor } }, title),
|
|
95
|
+
description && (React.createElement("p", { style: {
|
|
123
96
|
margin: 0,
|
|
124
97
|
fontSize: config.descriptionSize,
|
|
125
|
-
color: "#666",
|
|
98
|
+
color: descriptionColor || "#666",
|
|
126
99
|
display: "-webkit-box",
|
|
127
100
|
WebkitLineClamp: config.lineClamp,
|
|
128
101
|
WebkitBoxOrient: "vertical",
|
|
129
102
|
overflow: "hidden"
|
|
130
|
-
} },
|
|
103
|
+
} }, description)))));
|
|
131
104
|
}
|
|
132
105
|
|
|
133
106
|
exports.LinkPreview = LinkPreview;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["\"use client\";\r\n\r\n/**\r\n * Next.js Link Preview Component\r\n *\r\n * This component uses the Next.js API route to fetch metadata server-side,\r\n * avoiding CORS issues entirely.\r\n *\r\n * Usage:\r\n * import { LinkPreview } from './components/LinkPreview';\r\n *\r\n * <LinkPreview url=\"https://github.com\" size=\"medium\" />\r\n */\r\n\r\nimport React, { useEffect, useState } from \"react\";\r\n\r\nexport interface LinkPreviewData {\r\n title: string;\r\n description: string;\r\n image: string;\r\n url: string;\r\n}\r\n\r\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\r\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\r\n\r\nexport interface LinkPreviewProps {\r\n url: string;\r\n size?: LinkPreviewSize;\r\n layout?: LinkPreviewLayout;\r\n width?: string | number;\r\n height?: string | number;\r\n className?: string;\r\n onError?: (error: Error) => void;\r\n onLoad?: (data: LinkPreviewData) => void;\r\n apiEndpoint?: string; // Override the API endpoint if needed\r\n}\r\n\r\nconst sizeConfig = {\r\n small: {\r\n imageHeight: \"120px\",\r\n imageWidth: \"120px\",\r\n titleSize: \"14px\",\r\n descriptionSize: \"12px\",\r\n padding: \"8px\",\r\n lineClamp: 1\r\n },\r\n medium: {\r\n imageHeight: \"200px\",\r\n imageWidth: \"200px\",\r\n titleSize: \"16px\",\r\n descriptionSize: \"14px\",\r\n padding: \"12px\",\r\n lineClamp: 2\r\n },\r\n large: {\r\n imageHeight: \"300px\",\r\n imageWidth: \"280px\",\r\n titleSize: \"20px\",\r\n descriptionSize: \"16px\",\r\n padding: \"16px\",\r\n lineClamp: 3\r\n }\r\n};\r\n\r\nexport function LinkPreview({\r\n url,\r\n size = \"medium\",\r\n layout = \"vertical\",\r\n width = \"100%\",\r\n height = \"auto\",\r\n className = \"\",\r\n onError,\r\n onLoad,\r\n apiEndpoint = \"/api/preview\"\r\n}: LinkPreviewProps) {\r\n const [data, setData] = useState<LinkPreviewData | null>(null);\r\n const [loading, setLoading] = useState(true);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const config = sizeConfig[size];\r\n\r\n useEffect(() => {\r\n const fetchMetadata = async () => {\r\n try {\r\n setLoading(true);\r\n setError(null);\r\n\r\n const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`);\r\n\r\n if (!response.ok) {\r\n const errorData = await response.json();\r\n throw new Error(errorData.error || \"Failed to fetch metadata\");\r\n }\r\n\r\n const metadata = await response.json();\r\n setData(metadata);\r\n onLoad?.(metadata);\r\n } catch (err) {\r\n const error = err instanceof Error ? err : new Error(\"Unknown error\");\r\n setError(error);\r\n onError?.(error);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n fetchMetadata();\r\n }, [url, apiEndpoint]); // Don't include callbacks in dependencies to avoid infinite loops\r\n\r\n if (loading) {\r\n return (\r\n <div\r\n style={{\r\n padding: \"1rem\",\r\n textAlign: \"center\",\r\n color: \"#666\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n background: \"#f9f9f9\"\r\n }}\r\n >\r\n Loading preview...\r\n </div>\r\n );\r\n }\r\n\r\n if (error) {\r\n return (\r\n <div\r\n style={{\r\n padding: \"1rem\",\r\n background: \"#fff3f3\",\r\n border: \"1px solid #f44336\",\r\n borderRadius: \"8px\"\r\n }}\r\n >\r\n <strong style={{ color: \"#d32f2f\" }}>Error loading preview:</strong> {error.message}\r\n </div>\r\n );\r\n }\r\n\r\n if (!data) {\r\n return null;\r\n }\r\n\r\n const isHorizontal = layout === \"horizontal\";\r\n\r\n return (\r\n <a\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className={`link-preview ${className}`}\r\n style={{\r\n display: isHorizontal ? \"flex\" : \"block\",\r\n flexDirection: isHorizontal ? \"row\" : undefined,\r\n width,\r\n height,\r\n textDecoration: \"none\",\r\n color: \"inherit\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n overflow: \"hidden\",\r\n transition: \"box-shadow 0.3s\"\r\n }}\r\n onMouseEnter={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"0 4px 12px rgba(0,0,0,0.15)\";\r\n }}\r\n onMouseLeave={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"none\";\r\n }}\r\n >\r\n {data.image && (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n backgroundImage: `url(${data.image})`,\r\n backgroundSize: \"cover\",\r\n backgroundPosition: \"center\"\r\n }}\r\n />\r\n )}\r\n <div\r\n style={{\r\n padding: config.padding,\r\n flex: isHorizontal ? 1 : undefined,\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n justifyContent: \"center\"\r\n }}\r\n >\r\n {data.title && (\r\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize }}>{data.title}</h3>\r\n )}\r\n {data.description && (\r\n <p\r\n style={\r\n {\r\n margin: 0,\r\n fontSize: config.descriptionSize,\r\n color: \"#666\",\r\n display: \"-webkit-box\",\r\n WebkitLineClamp: config.lineClamp,\r\n WebkitBoxOrient: \"vertical\",\r\n overflow: \"hidden\"\r\n } as React.CSSProperties\r\n }\r\n >\r\n {data.description}\r\n </p>\r\n )}\r\n </div>\r\n </a>\r\n );\r\n}\r\n\r\nexport default LinkPreview;\r\n"],"names":["useState","useEffect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/nextjs/components/LinkPreview.tsx"],"sourcesContent":["\"use client\";\r\n\r\n/**\r\n * Simple Link Preview Component\r\n *\r\n * Usage with custom image:\r\n * <LinkPreview url=\"...\" title=\"...\" image=\"https://example.com/image.png\" />\r\n *\r\n * Usage with preset icon (react-icons component):\r\n * <LinkPreview url=\"...\" title=\"...\" preset={FaGithub} />\r\n *\r\n * Usage with icon (any React element, e.g. react-icons):\r\n * <LinkPreview url=\"...\" title=\"...\" icon={<FaGithub size={48} />} />\r\n *\r\n * Usage with custom text colors:\r\n * <LinkPreview url=\"...\" title=\"...\" titleColor=\"#333\" descriptionColor=\"#999\" />\r\n */\r\n\r\nimport React from \"react\";\r\nimport type { IconType } from \"react-icons\";\r\n\r\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\r\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\r\nexport type LinkPreviewPreset = IconType;\r\n\r\nexport interface LinkPreviewProps {\r\n url: string;\r\n title: string;\r\n description?: string;\r\n image?: string;\r\n preset?: LinkPreviewPreset;\r\n icon?: React.ReactNode;\r\n size?: LinkPreviewSize;\r\n layout?: LinkPreviewLayout;\r\n width?: string | number;\r\n height?: string | number;\r\n className?: string;\r\n titleColor?: string;\r\n descriptionColor?: string;\r\n}\r\n\r\nconst sizeConfig = {\r\n small: {\r\n imageHeight: \"120px\",\r\n imageWidth: \"120px\",\r\n titleSize: \"14px\",\r\n descriptionSize: \"12px\",\r\n padding: \"8px\",\r\n lineClamp: 1\r\n },\r\n medium: {\r\n imageHeight: \"200px\",\r\n imageWidth: \"200px\",\r\n titleSize: \"16px\",\r\n descriptionSize: \"14px\",\r\n padding: \"12px\",\r\n lineClamp: 2\r\n },\r\n large: {\r\n imageHeight: \"300px\",\r\n imageWidth: \"280px\",\r\n titleSize: \"20px\",\r\n descriptionSize: \"16px\",\r\n padding: \"16px\",\r\n lineClamp: 3\r\n }\r\n};\r\n\r\nexport function LinkPreview({\r\n url,\r\n title,\r\n description,\r\n image,\r\n preset,\r\n icon,\r\n size = \"medium\",\r\n layout = \"vertical\",\r\n width = \"100%\",\r\n height = \"auto\",\r\n className = \"\",\r\n titleColor,\r\n descriptionColor\r\n}: LinkPreviewProps) {\r\n const config = sizeConfig[size];\r\n const isHorizontal = layout === \"horizontal\";\r\n\r\n const PresetIcon = preset;\r\n const iconSize = size === \"large\" ? 96 : size === \"medium\" ? 64 : 48;\r\n const iconFontSize = size === \"large\" ? \"96px\" : size === \"medium\" ? \"64px\" : \"48px\";\r\n\r\n return (\r\n <a\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className={`link-preview ${className}`}\r\n style={{\r\n display: isHorizontal ? \"flex\" : \"block\",\r\n flexDirection: isHorizontal ? \"row\" : undefined,\r\n width,\r\n maxWidth: isHorizontal ? undefined : \"400px\",\r\n height,\r\n textDecoration: \"none\",\r\n color: \"inherit\",\r\n border: \"1px solid #e0e0e0\",\r\n borderRadius: \"8px\",\r\n overflow: \"hidden\",\r\n transition: \"box-shadow 0.3s\"\r\n }}\r\n onMouseEnter={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"0 4px 12px rgba(0,0,0,0.15)\";\r\n }}\r\n onMouseLeave={(e) => {\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"none\";\r\n }}\r\n >\r\n {icon ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n backgroundColor: \"#f6f8fa\",\r\n fontSize: iconFontSize\r\n }}\r\n >\r\n {icon}\r\n </div>\r\n ) : image ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n backgroundImage: `url(${image})`,\r\n backgroundSize: \"cover\",\r\n backgroundPosition: \"center\",\r\n backgroundRepeat: \"no-repeat\"\r\n }}\r\n />\r\n ) : PresetIcon ? (\r\n <div\r\n style={{\r\n width: isHorizontal ? config.imageWidth : \"100%\",\r\n height: isHorizontal ? \"100%\" : config.imageHeight,\r\n minHeight: isHorizontal ? config.imageHeight : undefined,\r\n flexShrink: isHorizontal ? 0 : undefined,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n backgroundColor: \"#f6f8fa\"\r\n }}\r\n >\r\n <PresetIcon size={iconSize} />\r\n </div>\r\n ) : null}\r\n <div\r\n style={{\r\n padding: config.padding,\r\n flex: isHorizontal ? 1 : undefined,\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n justifyContent: \"center\"\r\n }}\r\n >\r\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize, color: titleColor }}>{title}</h3>\r\n {description && (\r\n <p\r\n style={\r\n {\r\n margin: 0,\r\n fontSize: config.descriptionSize,\r\n color: descriptionColor || \"#666\",\r\n display: \"-webkit-box\",\r\n WebkitLineClamp: config.lineClamp,\r\n WebkitBoxOrient: \"vertical\",\r\n overflow: \"hidden\"\r\n } as React.CSSProperties\r\n }\r\n >\r\n {description}\r\n </p>\r\n )}\r\n </div>\r\n </a>\r\n );\r\n}\r\n\r\nexport default LinkPreview;\r\n"],"names":[],"mappings":"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"}
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Simple Link Preview Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Usage with custom image:
|
|
5
|
+
* <LinkPreview url="..." title="..." image="https://example.com/image.png" />
|
|
6
6
|
*
|
|
7
|
-
* Usage:
|
|
8
|
-
*
|
|
7
|
+
* Usage with preset icon (react-icons component):
|
|
8
|
+
* <LinkPreview url="..." title="..." preset={FaGithub} />
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Usage with icon (any React element, e.g. react-icons):
|
|
11
|
+
* <LinkPreview url="..." title="..." icon={<FaGithub size={48} />} />
|
|
12
|
+
*
|
|
13
|
+
* Usage with custom text colors:
|
|
14
|
+
* <LinkPreview url="..." title="..." titleColor="#333" descriptionColor="#999" />
|
|
11
15
|
*/
|
|
12
16
|
import React from "react";
|
|
13
|
-
|
|
14
|
-
title: string;
|
|
15
|
-
description: string;
|
|
16
|
-
image: string;
|
|
17
|
-
url: string;
|
|
18
|
-
}
|
|
17
|
+
import type { IconType } from "react-icons";
|
|
19
18
|
export type LinkPreviewSize = "small" | "medium" | "large";
|
|
20
19
|
export type LinkPreviewLayout = "vertical" | "horizontal";
|
|
20
|
+
export type LinkPreviewPreset = IconType;
|
|
21
21
|
export interface LinkPreviewProps {
|
|
22
22
|
url: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
image?: string;
|
|
26
|
+
preset?: LinkPreviewPreset;
|
|
27
|
+
icon?: React.ReactNode;
|
|
23
28
|
size?: LinkPreviewSize;
|
|
24
29
|
layout?: LinkPreviewLayout;
|
|
25
30
|
width?: string | number;
|
|
26
31
|
height?: string | number;
|
|
27
32
|
className?: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
apiEndpoint?: string;
|
|
33
|
+
titleColor?: string;
|
|
34
|
+
descriptionColor?: string;
|
|
31
35
|
}
|
|
32
|
-
export declare function LinkPreview({ url, size, layout, width, height, className,
|
|
36
|
+
export declare function LinkPreview({ url, title, description, image, preset, icon, size, layout, width, height, className, titleColor, descriptionColor }: LinkPreviewProps): React.JSX.Element;
|
|
33
37
|
export default LinkPreview;
|
|
34
38
|
//# sourceMappingURL=LinkPreview.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LinkPreview.d.ts","sourceRoot":"","sources":["../../../../src/nextjs/components/LinkPreview.tsx"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"LinkPreview.d.ts","sourceRoot":"","sources":["../../../../src/nextjs/components/LinkPreview.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC3D,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,YAAY,CAAC;AAC1D,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,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,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA6BD,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,KAAK,EACL,WAAW,EACX,KAAK,EACL,MAAM,EACN,IAAI,EACJ,IAAe,EACf,MAAmB,EACnB,KAAc,EACd,MAAe,EACf,SAAc,EACd,UAAU,EACV,gBAAgB,EACjB,EAAE,gBAAgB,qBA6GlB;AAED,eAAe,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextjs-link-preview",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A Next.js component for
|
|
5
|
+
"description": "A simple, lightweight Next.js component for displaying beautiful link preview cards with preset image support",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"nextjs",
|
|
8
8
|
"next",
|
|
9
9
|
"react",
|
|
10
10
|
"link-preview",
|
|
11
11
|
"preview",
|
|
12
|
-
"metadata",
|
|
13
|
-
"opengraph",
|
|
14
|
-
"og",
|
|
15
12
|
"card",
|
|
16
13
|
"component",
|
|
17
14
|
"typescript",
|
|
18
|
-
"
|
|
15
|
+
"presentational",
|
|
16
|
+
"github",
|
|
17
|
+
"npm",
|
|
18
|
+
"preset"
|
|
19
19
|
],
|
|
20
20
|
"author": "Seth Carney",
|
|
21
21
|
"license": "MIT",
|
|
@@ -34,45 +34,38 @@
|
|
|
34
34
|
"main": "dist/index.js",
|
|
35
35
|
"module": "dist/index.esm.js",
|
|
36
36
|
"types": "dist/index.d.ts",
|
|
37
|
+
"bin": {
|
|
38
|
+
"nextjs-link-preview": "bin/cli.mjs"
|
|
39
|
+
},
|
|
37
40
|
"files": [
|
|
38
41
|
"dist",
|
|
39
42
|
"bin",
|
|
43
|
+
"src/nextjs/components/LinkPreview.tsx",
|
|
40
44
|
"README.md",
|
|
41
45
|
"LICENSE"
|
|
42
46
|
],
|
|
43
|
-
"bin": {
|
|
44
|
-
"nextjs-link-preview": "bin/setup.js"
|
|
45
|
-
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"build": "rollup -c",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"demo:install": "cd nextjs-demo && npm install",
|
|
52
|
-
"demo:build": "cd nextjs-demo && npm run build",
|
|
53
|
-
"dev:link": "npm link && cd nextjs-demo && npm link nextjs-link-preview && cd ..",
|
|
54
|
-
"dev:unlink": "cd nextjs-demo && npm unlink nextjs-link-preview && npm install nextjs-link-preview@latest && cd ..",
|
|
55
|
-
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
|
|
56
|
-
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\""
|
|
49
|
+
"dev": "node bin/cli.mjs init --path nextjs-demo/src/components/LinkPreview.tsx && cd nextjs-demo && npm run dev",
|
|
50
|
+
"test": "node bin/cli.mjs init --path nextjs-demo/src/components/LinkPreview.tsx && cd nextjs-demo && npm run dev",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
57
52
|
},
|
|
58
53
|
"peerDependencies": {
|
|
59
54
|
"next": ">=14.0.0",
|
|
60
55
|
"react": ">=18.0.0",
|
|
61
|
-
"react-dom": ">=18.0.0"
|
|
62
|
-
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"axios": "^1.6.0",
|
|
65
|
-
"cheerio": "^1.0.0-rc.12"
|
|
56
|
+
"react-dom": ">=18.0.0",
|
|
57
|
+
"react-icons": ">=4.0.0"
|
|
66
58
|
},
|
|
67
59
|
"devDependencies": {
|
|
68
60
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
69
61
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
70
62
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
71
|
-
"@types/react": "^19.2.
|
|
72
|
-
"@types/react-dom": "^19.2.
|
|
73
|
-
"prettier": "^3.
|
|
74
|
-
"
|
|
75
|
-
"rollup
|
|
63
|
+
"@types/react": "^19.2.13",
|
|
64
|
+
"@types/react-dom": "^19.2.3",
|
|
65
|
+
"prettier": "^3.8.1",
|
|
66
|
+
"react-icons": "^5.5.0",
|
|
67
|
+
"rollup": "^4.57.1",
|
|
68
|
+
"rollup-plugin-dts": "^6.3.0",
|
|
76
69
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
77
70
|
"tslib": "^2.8.1",
|
|
78
71
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple Link Preview Component
|
|
5
|
+
*
|
|
6
|
+
* Usage with custom image:
|
|
7
|
+
* <LinkPreview url="..." title="..." image="https://example.com/image.png" />
|
|
8
|
+
*
|
|
9
|
+
* Usage with preset icon (react-icons component):
|
|
10
|
+
* <LinkPreview url="..." title="..." preset={FaGithub} />
|
|
11
|
+
*
|
|
12
|
+
* Usage with icon (any React element, e.g. react-icons):
|
|
13
|
+
* <LinkPreview url="..." title="..." icon={<FaGithub size={48} />} />
|
|
14
|
+
*
|
|
15
|
+
* Usage with custom text colors:
|
|
16
|
+
* <LinkPreview url="..." title="..." titleColor="#333" descriptionColor="#999" />
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React from "react";
|
|
20
|
+
import type { IconType } from "react-icons";
|
|
21
|
+
|
|
22
|
+
export type LinkPreviewSize = "small" | "medium" | "large";
|
|
23
|
+
export type LinkPreviewLayout = "vertical" | "horizontal";
|
|
24
|
+
export type LinkPreviewPreset = IconType;
|
|
25
|
+
|
|
26
|
+
export interface LinkPreviewProps {
|
|
27
|
+
url: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
image?: string;
|
|
31
|
+
preset?: LinkPreviewPreset;
|
|
32
|
+
icon?: React.ReactNode;
|
|
33
|
+
size?: LinkPreviewSize;
|
|
34
|
+
layout?: LinkPreviewLayout;
|
|
35
|
+
width?: string | number;
|
|
36
|
+
height?: string | number;
|
|
37
|
+
className?: string;
|
|
38
|
+
titleColor?: string;
|
|
39
|
+
descriptionColor?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sizeConfig = {
|
|
43
|
+
small: {
|
|
44
|
+
imageHeight: "120px",
|
|
45
|
+
imageWidth: "120px",
|
|
46
|
+
titleSize: "14px",
|
|
47
|
+
descriptionSize: "12px",
|
|
48
|
+
padding: "8px",
|
|
49
|
+
lineClamp: 1
|
|
50
|
+
},
|
|
51
|
+
medium: {
|
|
52
|
+
imageHeight: "200px",
|
|
53
|
+
imageWidth: "200px",
|
|
54
|
+
titleSize: "16px",
|
|
55
|
+
descriptionSize: "14px",
|
|
56
|
+
padding: "12px",
|
|
57
|
+
lineClamp: 2
|
|
58
|
+
},
|
|
59
|
+
large: {
|
|
60
|
+
imageHeight: "300px",
|
|
61
|
+
imageWidth: "280px",
|
|
62
|
+
titleSize: "20px",
|
|
63
|
+
descriptionSize: "16px",
|
|
64
|
+
padding: "16px",
|
|
65
|
+
lineClamp: 3
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function LinkPreview({
|
|
70
|
+
url,
|
|
71
|
+
title,
|
|
72
|
+
description,
|
|
73
|
+
image,
|
|
74
|
+
preset,
|
|
75
|
+
icon,
|
|
76
|
+
size = "medium",
|
|
77
|
+
layout = "vertical",
|
|
78
|
+
width = "100%",
|
|
79
|
+
height = "auto",
|
|
80
|
+
className = "",
|
|
81
|
+
titleColor,
|
|
82
|
+
descriptionColor
|
|
83
|
+
}: LinkPreviewProps) {
|
|
84
|
+
const config = sizeConfig[size];
|
|
85
|
+
const isHorizontal = layout === "horizontal";
|
|
86
|
+
|
|
87
|
+
const PresetIcon = preset;
|
|
88
|
+
const iconSize = size === "large" ? 96 : size === "medium" ? 64 : 48;
|
|
89
|
+
const iconFontSize = size === "large" ? "96px" : size === "medium" ? "64px" : "48px";
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<a
|
|
93
|
+
href={url}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
className={`link-preview ${className}`}
|
|
97
|
+
style={{
|
|
98
|
+
display: isHorizontal ? "flex" : "block",
|
|
99
|
+
flexDirection: isHorizontal ? "row" : undefined,
|
|
100
|
+
width,
|
|
101
|
+
maxWidth: isHorizontal ? undefined : "400px",
|
|
102
|
+
height,
|
|
103
|
+
textDecoration: "none",
|
|
104
|
+
color: "inherit",
|
|
105
|
+
border: "1px solid #e0e0e0",
|
|
106
|
+
borderRadius: "8px",
|
|
107
|
+
overflow: "hidden",
|
|
108
|
+
transition: "box-shadow 0.3s"
|
|
109
|
+
}}
|
|
110
|
+
onMouseEnter={(e) => {
|
|
111
|
+
(e.currentTarget as HTMLElement).style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
|
112
|
+
}}
|
|
113
|
+
onMouseLeave={(e) => {
|
|
114
|
+
(e.currentTarget as HTMLElement).style.boxShadow = "none";
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{icon ? (
|
|
118
|
+
<div
|
|
119
|
+
style={{
|
|
120
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
121
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
122
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
123
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
124
|
+
display: "flex",
|
|
125
|
+
alignItems: "center",
|
|
126
|
+
justifyContent: "center",
|
|
127
|
+
backgroundColor: "#f6f8fa",
|
|
128
|
+
fontSize: iconFontSize
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{icon}
|
|
132
|
+
</div>
|
|
133
|
+
) : image ? (
|
|
134
|
+
<div
|
|
135
|
+
style={{
|
|
136
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
137
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
138
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
139
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
140
|
+
backgroundImage: `url(${image})`,
|
|
141
|
+
backgroundSize: "cover",
|
|
142
|
+
backgroundPosition: "center",
|
|
143
|
+
backgroundRepeat: "no-repeat"
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
) : PresetIcon ? (
|
|
147
|
+
<div
|
|
148
|
+
style={{
|
|
149
|
+
width: isHorizontal ? config.imageWidth : "100%",
|
|
150
|
+
height: isHorizontal ? "100%" : config.imageHeight,
|
|
151
|
+
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
152
|
+
flexShrink: isHorizontal ? 0 : undefined,
|
|
153
|
+
display: "flex",
|
|
154
|
+
alignItems: "center",
|
|
155
|
+
justifyContent: "center",
|
|
156
|
+
backgroundColor: "#f6f8fa"
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<PresetIcon size={iconSize} />
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
<div
|
|
163
|
+
style={{
|
|
164
|
+
padding: config.padding,
|
|
165
|
+
flex: isHorizontal ? 1 : undefined,
|
|
166
|
+
display: "flex",
|
|
167
|
+
flexDirection: "column",
|
|
168
|
+
justifyContent: "center"
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<h3 style={{ margin: "0 0 8px 0", fontSize: config.titleSize, color: titleColor }}>{title}</h3>
|
|
172
|
+
{description && (
|
|
173
|
+
<p
|
|
174
|
+
style={
|
|
175
|
+
{
|
|
176
|
+
margin: 0,
|
|
177
|
+
fontSize: config.descriptionSize,
|
|
178
|
+
color: descriptionColor || "#666",
|
|
179
|
+
display: "-webkit-box",
|
|
180
|
+
WebkitLineClamp: config.lineClamp,
|
|
181
|
+
WebkitBoxOrient: "vertical",
|
|
182
|
+
overflow: "hidden"
|
|
183
|
+
} as React.CSSProperties
|
|
184
|
+
}
|
|
185
|
+
>
|
|
186
|
+
{description}
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</a>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default LinkPreview;
|
package/bin/setup.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
const API_ROUTE_CONTENT = `import { NextRequest, NextResponse } from "next/server";
|
|
10
|
-
import axios from "axios";
|
|
11
|
-
import * as cheerio from "cheerio";
|
|
12
|
-
|
|
13
|
-
// Simple in-memory cache with 1 hour TTL
|
|
14
|
-
const cache = new Map<string, { data: any; timestamp: number }>();
|
|
15
|
-
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
16
|
-
|
|
17
|
-
export async function GET(request: NextRequest) {
|
|
18
|
-
const searchParams = request.nextUrl.searchParams;
|
|
19
|
-
const targetUrl = searchParams.get("url");
|
|
20
|
-
|
|
21
|
-
if (!targetUrl) {
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{ error: "URL parameter is required" },
|
|
24
|
-
{ status: 400 }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Check cache first
|
|
29
|
-
const cached = cache.get(targetUrl);
|
|
30
|
-
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
31
|
-
return NextResponse.json(cached.data);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const response = await axios.get(targetUrl, {
|
|
36
|
-
headers: {
|
|
37
|
-
"User-Agent": "Mozilla/5.0 (compatible; LinkPreviewBot/1.0)",
|
|
38
|
-
},
|
|
39
|
-
timeout: 10000,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const $ = cheerio.load(response.data);
|
|
43
|
-
|
|
44
|
-
const metadata = {
|
|
45
|
-
title:
|
|
46
|
-
$('meta[property="og:title"]').attr("content") ||
|
|
47
|
-
$('meta[name="twitter:title"]').attr("content") ||
|
|
48
|
-
$("title").text() ||
|
|
49
|
-
"",
|
|
50
|
-
description:
|
|
51
|
-
$('meta[property="og:description"]').attr("content") ||
|
|
52
|
-
$('meta[name="twitter:description"]').attr("content") ||
|
|
53
|
-
$('meta[name="description"]').attr("content") ||
|
|
54
|
-
"",
|
|
55
|
-
image:
|
|
56
|
-
$('meta[property="og:image"]').attr("content") ||
|
|
57
|
-
$('meta[name="twitter:image"]').attr("content") ||
|
|
58
|
-
"",
|
|
59
|
-
url: targetUrl,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Store in cache
|
|
63
|
-
cache.set(targetUrl, { data: metadata, timestamp: Date.now() });
|
|
64
|
-
|
|
65
|
-
return NextResponse.json(metadata);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error("Error fetching preview:", error);
|
|
68
|
-
return NextResponse.json(
|
|
69
|
-
{ error: "Failed to fetch preview" },
|
|
70
|
-
{ status: 500 }
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
`;
|
|
75
|
-
|
|
76
|
-
function setupApiRoute() {
|
|
77
|
-
const cwd = process.cwd();
|
|
78
|
-
|
|
79
|
-
// Check if we're in a Next.js project
|
|
80
|
-
const packageJsonPath = path.join(cwd, "package.json");
|
|
81
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
82
|
-
console.error(
|
|
83
|
-
"❌ Error: package.json not found. Make sure you're in a Next.js project directory."
|
|
84
|
-
);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
89
|
-
if (!packageJson.dependencies?.next && !packageJson.devDependencies?.next) {
|
|
90
|
-
console.error(
|
|
91
|
-
"❌ Error: Next.js not found in dependencies. Make sure this is a Next.js project."
|
|
92
|
-
);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Detect if project uses src/app or app directory structure
|
|
97
|
-
const hasSrcApp = fs.existsSync(path.join(cwd, "src", "app"));
|
|
98
|
-
const appDir = hasSrcApp ? path.join(cwd, "src", "app") : path.join(cwd, "app");
|
|
99
|
-
const relativePath = hasSrcApp ? "src/app/api/preview/route.ts" : "app/api/preview/route.ts";
|
|
100
|
-
|
|
101
|
-
// Create the API route directory structure
|
|
102
|
-
const apiRoutePath = path.join(appDir, "api", "preview");
|
|
103
|
-
const routeFilePath = path.join(apiRoutePath, "route.ts");
|
|
104
|
-
|
|
105
|
-
// Check if route already exists
|
|
106
|
-
if (fs.existsSync(routeFilePath)) {
|
|
107
|
-
console.log(`⚠️ API route already exists at ${relativePath}`);
|
|
108
|
-
console.log("To reinstall, delete the existing file and run this command again.");
|
|
109
|
-
process.exit(0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Create directories
|
|
113
|
-
fs.mkdirSync(apiRoutePath, { recursive: true });
|
|
114
|
-
|
|
115
|
-
// Write the route file
|
|
116
|
-
fs.writeFileSync(routeFilePath, API_ROUTE_CONTENT);
|
|
117
|
-
|
|
118
|
-
console.log(`✅ Successfully created API route at ${relativePath}`);
|
|
119
|
-
console.log("");
|
|
120
|
-
console.log("📦 Make sure you have the required dependencies:");
|
|
121
|
-
console.log(" npm install axios cheerio");
|
|
122
|
-
console.log("");
|
|
123
|
-
console.log("🎉 Setup complete! You can now use the LinkPreview component:");
|
|
124
|
-
console.log("");
|
|
125
|
-
console.log(' import { LinkPreview } from "nextjs-link-preview";');
|
|
126
|
-
console.log("");
|
|
127
|
-
console.log(" export default function Page() {");
|
|
128
|
-
console.log(' return <LinkPreview url="https://github.com" />;');
|
|
129
|
-
console.log(" }");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
setupApiRoute();
|