nextjs-link-preview 1.0.5 → 1.0.6
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 +187 -133
- package/dist/index.d.ts +22 -18
- package/dist/index.esm.js +19 -61
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +18 -60
- package/dist/index.js.map +1 -1
- package/dist/types/nextjs/components/LinkPreview.d.ts +21 -17
- package/dist/types/nextjs/components/LinkPreview.d.ts.map +1 -1
- package/package.json +72 -80
- package/bin/setup.js +0 -132
package/README.md
CHANGED
|
@@ -1,133 +1,187 @@
|
|
|
1
|
-
# Next.js Link Preview
|
|
2
|
-
|
|
3
|
-
A Next.js component for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { LinkPreview } from "nextjs-link-preview";
|
|
38
|
-
|
|
39
|
-
export default function Page() {
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
<LinkPreview url="
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
# Next.js Link Preview
|
|
2
|
+
|
|
3
|
+
A simple, lightweight Next.js component for displaying beautiful link preview cards.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install nextjs-link-preview
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
13
|
+
|
|
14
|
+
<LinkPreview
|
|
15
|
+
url="https://github.com/vercel/next.js"
|
|
16
|
+
title="Next.js"
|
|
17
|
+
description="The React Framework for the Web"
|
|
18
|
+
preset="github"
|
|
19
|
+
/>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- ✅ Pure presentational component - no data fetching
|
|
25
|
+
- ✅ Preset image support for GitHub and npm
|
|
26
|
+
- ✅ Three size variants (small, medium, large)
|
|
27
|
+
- ✅ Two layouts (vertical, horizontal)
|
|
28
|
+
- ✅ TypeScript support
|
|
29
|
+
- ✅ Fully customizable styling
|
|
30
|
+
- ✅ Zero dependencies (only peer deps: React, Next.js)
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Basic Usage
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { LinkPreview } from "nextjs-link-preview";
|
|
38
|
+
|
|
39
|
+
export default function Page() {
|
|
40
|
+
return (
|
|
41
|
+
<LinkPreview
|
|
42
|
+
url="https://example.com"
|
|
43
|
+
title="Example Site"
|
|
44
|
+
description="This is an example website"
|
|
45
|
+
image="https://example.com/preview.png"
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### With Presets
|
|
52
|
+
|
|
53
|
+
Use built-in presets for popular platforms:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// GitHub
|
|
57
|
+
<LinkPreview
|
|
58
|
+
url="https://github.com/user/repo"
|
|
59
|
+
title="My Repository"
|
|
60
|
+
description="A cool open source project"
|
|
61
|
+
preset="github"
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
// npm
|
|
65
|
+
<LinkPreview
|
|
66
|
+
url="https://npmjs.com/package/my-package"
|
|
67
|
+
title="my-package"
|
|
68
|
+
description="An awesome npm package"
|
|
69
|
+
preset="npm"
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Size Variants
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<LinkPreview url="..." title="..." image="..." size="small" />
|
|
77
|
+
<LinkPreview url="..." title="..." image="..." size="medium" /> {/* default */}
|
|
78
|
+
<LinkPreview url="..." title="..." image="..." size="large" />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Layouts
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
{/* Vertical (default) - image on top */}
|
|
85
|
+
<LinkPreview url="..." title="..." image="..." layout="vertical" />
|
|
86
|
+
|
|
87
|
+
{/* Horizontal - image on left */}
|
|
88
|
+
<LinkPreview url="..." title="..." image="..." layout="horizontal" />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Custom Styling
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<LinkPreview
|
|
95
|
+
url="https://example.com"
|
|
96
|
+
title="Example"
|
|
97
|
+
image="..."
|
|
98
|
+
width="400px"
|
|
99
|
+
className="my-custom-class"
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Props
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
| ------------- | ------------------------------------ | ------------ | -------------------------------------- |
|
|
107
|
+
| `url` | `string` | **required** | Link destination |
|
|
108
|
+
| `title` | `string` | **required** | Preview card title |
|
|
109
|
+
| `description` | `string` | `undefined` | Preview card description (optional) |
|
|
110
|
+
| `image` | `string` | `undefined` | Custom image URL (optional) |
|
|
111
|
+
| `preset` | `"github"` \| `"npm"` | `undefined` | Use preset image (optional) |
|
|
112
|
+
| `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | Preview card size |
|
|
113
|
+
| `layout` | `"vertical"` \| `"horizontal"` | `"vertical"` | Image position |
|
|
114
|
+
| `width` | `string` \| `number` | `"100%"` | Card width |
|
|
115
|
+
| `height` | `string` \| `number` | `"auto"` | Card height |
|
|
116
|
+
| `className` | `string` | `""` | Additional CSS classes |
|
|
117
|
+
|
|
118
|
+
**Note:** Either `image` or `preset` should be provided. If both are provided, `image` takes precedence.
|
|
119
|
+
|
|
120
|
+
## Presets
|
|
121
|
+
|
|
122
|
+
Available presets:
|
|
123
|
+
|
|
124
|
+
- `github` - GitHub logo
|
|
125
|
+
- `npm` - npm logo
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
### GitHub Repository Preview
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<LinkPreview
|
|
133
|
+
url="https://github.com/vercel/next.js"
|
|
134
|
+
title="Next.js"
|
|
135
|
+
description="The React Framework for the Web"
|
|
136
|
+
preset="github"
|
|
137
|
+
size="large"
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### npm Package Preview
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<LinkPreview
|
|
145
|
+
url="https://npmjs.com/package/react"
|
|
146
|
+
title="react"
|
|
147
|
+
description="React is a JavaScript library for building user interfaces."
|
|
148
|
+
preset="npm"
|
|
149
|
+
layout="horizontal"
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Custom Article Preview
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<LinkPreview
|
|
157
|
+
url="https://example.com/article"
|
|
158
|
+
title="How to Build Amazing Web Apps"
|
|
159
|
+
description="Learn the best practices for modern web development"
|
|
160
|
+
image="https://example.com/article-cover.jpg"
|
|
161
|
+
size="medium"
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Testing
|
|
166
|
+
|
|
167
|
+
To test the component locally:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm test
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This will:
|
|
174
|
+
1. Build the component
|
|
175
|
+
2. Start the demo at http://localhost:3000
|
|
176
|
+
3. Open your browser to see the interactive demo
|
|
177
|
+
|
|
178
|
+
That's it!
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
183
|
+
|
|
184
|
+
## Links
|
|
185
|
+
|
|
186
|
+
- [GitHub](https://github.com/sethcarney/nextjs-link-preview)
|
|
187
|
+
- [npm](https://www.npmjs.com/package/nextjs-link-preview)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Simple Link Preview Component
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Usage with custom image:
|
|
7
|
+
* <LinkPreview
|
|
8
|
+
* url="https://example.com"
|
|
9
|
+
* title="Example"
|
|
10
|
+
* description="Example description"
|
|
11
|
+
* image="https://example.com/image.png"
|
|
12
|
+
* />
|
|
8
13
|
*
|
|
9
|
-
* Usage:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* Usage with preset:
|
|
15
|
+
* <LinkPreview
|
|
16
|
+
* url="https://github.com/user/repo"
|
|
17
|
+
* title="My Repo"
|
|
18
|
+
* description="A cool repository"
|
|
19
|
+
* preset="github"
|
|
20
|
+
* />
|
|
13
21
|
*/
|
|
14
22
|
|
|
15
|
-
interface LinkPreviewData {
|
|
16
|
-
title: string;
|
|
17
|
-
description: string;
|
|
18
|
-
image: string;
|
|
19
|
-
url: string;
|
|
20
|
-
}
|
|
21
23
|
type LinkPreviewSize = "small" | "medium" | "large";
|
|
22
24
|
type LinkPreviewLayout = "vertical" | "horizontal";
|
|
25
|
+
type LinkPreviewPreset = "github" | "npm";
|
|
23
26
|
interface LinkPreviewProps {
|
|
24
27
|
url: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
image?: string;
|
|
31
|
+
preset?: LinkPreviewPreset;
|
|
25
32
|
size?: LinkPreviewSize;
|
|
26
33
|
layout?: LinkPreviewLayout;
|
|
27
34
|
width?: string | number;
|
|
28
35
|
height?: string | number;
|
|
29
36
|
className?: string;
|
|
30
|
-
onError?: (error: Error) => void;
|
|
31
|
-
onLoad?: (data: LinkPreviewData) => void;
|
|
32
|
-
apiEndpoint?: string;
|
|
33
37
|
}
|
|
34
|
-
declare function LinkPreview({ url,
|
|
38
|
+
declare function LinkPreview({ url, title, description, image, preset, size, layout, width, height, className }: 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,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import React
|
|
2
|
+
import React from 'react';
|
|
3
3
|
|
|
4
|
+
// Inline SVG data URIs for preset images
|
|
5
|
+
const PRESET_IMAGES = {
|
|
6
|
+
github: `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjMjQyOTJmIi8+PC9zdmc+`,
|
|
7
|
+
npm: "https://avatars.githubusercontent.com/u/6078720?s=200&v=4"
|
|
8
|
+
};
|
|
4
9
|
const sizeConfig = {
|
|
5
10
|
small: {
|
|
6
11
|
imageHeight: "120px",
|
|
@@ -27,65 +32,16 @@ const sizeConfig = {
|
|
|
27
32
|
lineClamp: 3
|
|
28
33
|
}
|
|
29
34
|
};
|
|
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);
|
|
35
|
+
function LinkPreview({ url, title, description, image, preset, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "" }) {
|
|
34
36
|
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
37
|
const isHorizontal = layout === "horizontal";
|
|
38
|
+
// Use preset image if no custom image provided
|
|
39
|
+
const imageUrl = image || (preset ? PRESET_IMAGES[preset] : undefined);
|
|
85
40
|
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
86
41
|
display: isHorizontal ? "flex" : "block",
|
|
87
42
|
flexDirection: isHorizontal ? "row" : undefined,
|
|
88
43
|
width,
|
|
44
|
+
maxWidth: isHorizontal ? undefined : "400px",
|
|
89
45
|
height,
|
|
90
46
|
textDecoration: "none",
|
|
91
47
|
color: "inherit",
|
|
@@ -98,14 +54,16 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
98
54
|
}, onMouseLeave: (e) => {
|
|
99
55
|
e.currentTarget.style.boxShadow = "none";
|
|
100
56
|
} },
|
|
101
|
-
|
|
57
|
+
imageUrl && (React.createElement("div", { style: {
|
|
102
58
|
width: isHorizontal ? config.imageWidth : "100%",
|
|
103
59
|
height: isHorizontal ? "100%" : config.imageHeight,
|
|
104
60
|
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
105
61
|
flexShrink: isHorizontal ? 0 : undefined,
|
|
106
|
-
backgroundImage: `url(${
|
|
107
|
-
backgroundSize: "cover",
|
|
108
|
-
backgroundPosition: "center"
|
|
62
|
+
backgroundImage: `url(${imageUrl})`,
|
|
63
|
+
backgroundSize: preset ? "contain" : "cover",
|
|
64
|
+
backgroundPosition: "center",
|
|
65
|
+
backgroundRepeat: "no-repeat",
|
|
66
|
+
backgroundColor: preset ? "#f6f8fa" : "transparent"
|
|
109
67
|
} })),
|
|
110
68
|
React.createElement("div", { style: {
|
|
111
69
|
padding: config.padding,
|
|
@@ -114,8 +72,8 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
114
72
|
flexDirection: "column",
|
|
115
73
|
justifyContent: "center"
|
|
116
74
|
} },
|
|
117
|
-
|
|
118
|
-
|
|
75
|
+
React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize } }, title),
|
|
76
|
+
description && (React.createElement("p", { style: {
|
|
119
77
|
margin: 0,
|
|
120
78
|
fontSize: config.descriptionSize,
|
|
121
79
|
color: "#666",
|
|
@@ -123,7 +81,7 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
123
81
|
WebkitLineClamp: config.lineClamp,
|
|
124
82
|
WebkitBoxOrient: "vertical",
|
|
125
83
|
overflow: "hidden"
|
|
126
|
-
} },
|
|
84
|
+
} }, description)))));
|
|
127
85
|
}
|
|
128
86
|
|
|
129
87
|
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\";\n\n/**\n * Simple Link Preview Component\n *\n * Usage with custom image:\n * <LinkPreview\n * url=\"https://example.com\"\n * title=\"Example\"\n * description=\"Example description\"\n * image=\"https://example.com/image.png\"\n * />\n *\n * Usage with preset:\n * <LinkPreview\n * url=\"https://github.com/user/repo\"\n * title=\"My Repo\"\n * description=\"A cool repository\"\n * preset=\"github\"\n * />\n */\n\nimport React from \"react\";\n\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\nexport type LinkPreviewPreset = \"github\" | \"npm\";\n\n// Inline SVG data URIs for preset images\nconst PRESET_IMAGES: Record<LinkPreviewPreset, string> = {\n github: `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjMjQyOTJmIi8+PC9zdmc+`,\n npm: \"https://avatars.githubusercontent.com/u/6078720?s=200&v=4\"\n};\n\nexport interface LinkPreviewProps {\n url: string;\n title: string;\n description?: string;\n image?: string;\n preset?: LinkPreviewPreset;\n size?: LinkPreviewSize;\n layout?: LinkPreviewLayout;\n width?: string | number;\n height?: string | number;\n className?: string;\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 title,\n description,\n image,\n preset,\n size = \"medium\",\n layout = \"vertical\",\n width = \"100%\",\n height = \"auto\",\n className = \"\"\n}: LinkPreviewProps) {\n const config = sizeConfig[size];\n const isHorizontal = layout === \"horizontal\";\n\n // Use preset image if no custom image provided\n const imageUrl = image || (preset ? PRESET_IMAGES[preset] : undefined);\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 maxWidth: isHorizontal ? undefined : \"400px\",\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 {imageUrl && (\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(${imageUrl})`,\n backgroundSize: preset ? \"contain\" : \"cover\",\n backgroundPosition: \"center\",\n backgroundRepeat: \"no-repeat\",\n backgroundColor: preset ? \"#f6f8fa\" : \"transparent\"\n }}\n />\n )}\n <div\n style={{\n padding: config.padding,\n flex: isHorizontal ? 1 : undefined,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\"\n }}\n >\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize }}>{title}</h3>\n {description && (\n <p\n style={\n {\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 >\n {description}\n </p>\n )}\n </div>\n </a>\n );\n}\n\nexport default LinkPreview;\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,11 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
|
|
6
6
|
var React = require('react');
|
|
7
7
|
|
|
8
|
+
// Inline SVG data URIs for preset images
|
|
9
|
+
const PRESET_IMAGES = {
|
|
10
|
+
github: `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjMjQyOTJmIi8+PC9zdmc+`,
|
|
11
|
+
npm: "https://avatars.githubusercontent.com/u/6078720?s=200&v=4"
|
|
12
|
+
};
|
|
8
13
|
const sizeConfig = {
|
|
9
14
|
small: {
|
|
10
15
|
imageHeight: "120px",
|
|
@@ -31,65 +36,16 @@ const sizeConfig = {
|
|
|
31
36
|
lineClamp: 3
|
|
32
37
|
}
|
|
33
38
|
};
|
|
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);
|
|
39
|
+
function LinkPreview({ url, title, description, image, preset, size = "medium", layout = "vertical", width = "100%", height = "auto", className = "" }) {
|
|
38
40
|
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
41
|
const isHorizontal = layout === "horizontal";
|
|
42
|
+
// Use preset image if no custom image provided
|
|
43
|
+
const imageUrl = image || (preset ? PRESET_IMAGES[preset] : undefined);
|
|
89
44
|
return (React.createElement("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: `link-preview ${className}`, style: {
|
|
90
45
|
display: isHorizontal ? "flex" : "block",
|
|
91
46
|
flexDirection: isHorizontal ? "row" : undefined,
|
|
92
47
|
width,
|
|
48
|
+
maxWidth: isHorizontal ? undefined : "400px",
|
|
93
49
|
height,
|
|
94
50
|
textDecoration: "none",
|
|
95
51
|
color: "inherit",
|
|
@@ -102,14 +58,16 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
102
58
|
}, onMouseLeave: (e) => {
|
|
103
59
|
e.currentTarget.style.boxShadow = "none";
|
|
104
60
|
} },
|
|
105
|
-
|
|
61
|
+
imageUrl && (React.createElement("div", { style: {
|
|
106
62
|
width: isHorizontal ? config.imageWidth : "100%",
|
|
107
63
|
height: isHorizontal ? "100%" : config.imageHeight,
|
|
108
64
|
minHeight: isHorizontal ? config.imageHeight : undefined,
|
|
109
65
|
flexShrink: isHorizontal ? 0 : undefined,
|
|
110
|
-
backgroundImage: `url(${
|
|
111
|
-
backgroundSize: "cover",
|
|
112
|
-
backgroundPosition: "center"
|
|
66
|
+
backgroundImage: `url(${imageUrl})`,
|
|
67
|
+
backgroundSize: preset ? "contain" : "cover",
|
|
68
|
+
backgroundPosition: "center",
|
|
69
|
+
backgroundRepeat: "no-repeat",
|
|
70
|
+
backgroundColor: preset ? "#f6f8fa" : "transparent"
|
|
113
71
|
} })),
|
|
114
72
|
React.createElement("div", { style: {
|
|
115
73
|
padding: config.padding,
|
|
@@ -118,8 +76,8 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
118
76
|
flexDirection: "column",
|
|
119
77
|
justifyContent: "center"
|
|
120
78
|
} },
|
|
121
|
-
|
|
122
|
-
|
|
79
|
+
React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize } }, title),
|
|
80
|
+
description && (React.createElement("p", { style: {
|
|
123
81
|
margin: 0,
|
|
124
82
|
fontSize: config.descriptionSize,
|
|
125
83
|
color: "#666",
|
|
@@ -127,7 +85,7 @@ function LinkPreview({ url, size = "medium", layout = "vertical", width = "100%"
|
|
|
127
85
|
WebkitLineClamp: config.lineClamp,
|
|
128
86
|
WebkitBoxOrient: "vertical",
|
|
129
87
|
overflow: "hidden"
|
|
130
|
-
} },
|
|
88
|
+
} }, description)))));
|
|
131
89
|
}
|
|
132
90
|
|
|
133
91
|
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\";\n\n/**\n * Simple Link Preview Component\n *\n * Usage with custom image:\n * <LinkPreview\n * url=\"https://example.com\"\n * title=\"Example\"\n * description=\"Example description\"\n * image=\"https://example.com/image.png\"\n * />\n *\n * Usage with preset:\n * <LinkPreview\n * url=\"https://github.com/user/repo\"\n * title=\"My Repo\"\n * description=\"A cool repository\"\n * preset=\"github\"\n * />\n */\n\nimport React from \"react\";\n\nexport type LinkPreviewSize = \"small\" | \"medium\" | \"large\";\nexport type LinkPreviewLayout = \"vertical\" | \"horizontal\";\nexport type LinkPreviewPreset = \"github\" | \"npm\";\n\n// Inline SVG data URIs for preset images\nconst PRESET_IMAGES: Record<LinkPreviewPreset, string> = {\n github: `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjMjQyOTJmIi8+PC9zdmc+`,\n npm: \"https://avatars.githubusercontent.com/u/6078720?s=200&v=4\"\n};\n\nexport interface LinkPreviewProps {\n url: string;\n title: string;\n description?: string;\n image?: string;\n preset?: LinkPreviewPreset;\n size?: LinkPreviewSize;\n layout?: LinkPreviewLayout;\n width?: string | number;\n height?: string | number;\n className?: string;\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 title,\n description,\n image,\n preset,\n size = \"medium\",\n layout = \"vertical\",\n width = \"100%\",\n height = \"auto\",\n className = \"\"\n}: LinkPreviewProps) {\n const config = sizeConfig[size];\n const isHorizontal = layout === \"horizontal\";\n\n // Use preset image if no custom image provided\n const imageUrl = image || (preset ? PRESET_IMAGES[preset] : undefined);\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 maxWidth: isHorizontal ? undefined : \"400px\",\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 {imageUrl && (\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(${imageUrl})`,\n backgroundSize: preset ? \"contain\" : \"cover\",\n backgroundPosition: \"center\",\n backgroundRepeat: \"no-repeat\",\n backgroundColor: preset ? \"#f6f8fa\" : \"transparent\"\n }}\n />\n )}\n <div\n style={{\n padding: config.padding,\n flex: isHorizontal ? 1 : undefined,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\"\n }}\n >\n <h3 style={{ margin: \"0 0 8px 0\", fontSize: config.titleSize }}>{title}</h3>\n {description && (\n <p\n style={\n {\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 >\n {description}\n </p>\n )}\n </div>\n </a>\n );\n}\n\nexport default LinkPreview;\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
|
|
6
|
+
* url="https://example.com"
|
|
7
|
+
* title="Example"
|
|
8
|
+
* description="Example description"
|
|
9
|
+
* image="https://example.com/image.png"
|
|
10
|
+
* />
|
|
6
11
|
*
|
|
7
|
-
* Usage:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* Usage with preset:
|
|
13
|
+
* <LinkPreview
|
|
14
|
+
* url="https://github.com/user/repo"
|
|
15
|
+
* title="My Repo"
|
|
16
|
+
* description="A cool repository"
|
|
17
|
+
* preset="github"
|
|
18
|
+
* />
|
|
11
19
|
*/
|
|
12
20
|
import React from "react";
|
|
13
|
-
export interface LinkPreviewData {
|
|
14
|
-
title: string;
|
|
15
|
-
description: string;
|
|
16
|
-
image: string;
|
|
17
|
-
url: string;
|
|
18
|
-
}
|
|
19
21
|
export type LinkPreviewSize = "small" | "medium" | "large";
|
|
20
22
|
export type LinkPreviewLayout = "vertical" | "horizontal";
|
|
23
|
+
export type LinkPreviewPreset = "github" | "npm";
|
|
21
24
|
export interface LinkPreviewProps {
|
|
22
25
|
url: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
image?: string;
|
|
29
|
+
preset?: LinkPreviewPreset;
|
|
23
30
|
size?: LinkPreviewSize;
|
|
24
31
|
layout?: LinkPreviewLayout;
|
|
25
32
|
width?: string | number;
|
|
26
33
|
height?: string | number;
|
|
27
34
|
className?: string;
|
|
28
|
-
onError?: (error: Error) => void;
|
|
29
|
-
onLoad?: (data: LinkPreviewData) => void;
|
|
30
|
-
apiEndpoint?: string;
|
|
31
35
|
}
|
|
32
|
-
export declare function LinkPreview({ url,
|
|
36
|
+
export declare function LinkPreview({ url, title, description, image, preset, size, layout, width, height, className }: 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;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,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,GAAG,KAAK,CAAC;AAQjD,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,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;CACpB;AA6BD,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,KAAK,EACL,WAAW,EACX,KAAK,EACL,MAAM,EACN,IAAe,EACf,MAAmB,EACnB,KAAc,EACd,MAAe,EACf,SAAc,EACf,EAAE,gBAAgB,qBA8ElB;AAED,eAAe,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,80 +1,72 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nextjs-link-preview",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "A Next.js component for
|
|
6
|
-
"keywords": [
|
|
7
|
-
"nextjs",
|
|
8
|
-
"next",
|
|
9
|
-
"react",
|
|
10
|
-
"link-preview",
|
|
11
|
-
"preview",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
],
|
|
20
|
-
"author": "Seth Carney",
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/sethcarney/nextjs-link-preview.git"
|
|
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
|
-
"publishConfig": {
|
|
31
|
-
"registry": "https://registry.npmjs.org/",
|
|
32
|
-
"access": "public"
|
|
33
|
-
},
|
|
34
|
-
"main": "dist/index.js",
|
|
35
|
-
"module": "dist/index.esm.js",
|
|
36
|
-
"types": "dist/index.d.ts",
|
|
37
|
-
"files": [
|
|
38
|
-
"dist",
|
|
39
|
-
"bin",
|
|
40
|
-
"README.md",
|
|
41
|
-
"LICENSE"
|
|
42
|
-
],
|
|
43
|
-
"bin": {
|
|
44
|
-
"nextjs-link-preview": "bin/setup.js"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "rollup -c",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
},
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"prettier": "^3.6.2",
|
|
74
|
-
"rollup": "^4.53.1",
|
|
75
|
-
"rollup-plugin-dts": "^6.2.3",
|
|
76
|
-
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
77
|
-
"tslib": "^2.8.1",
|
|
78
|
-
"typescript": "^5.9.3"
|
|
79
|
-
}
|
|
80
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-link-preview",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A simple, lightweight Next.js component for displaying beautiful link preview cards with preset image support",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nextjs",
|
|
8
|
+
"next",
|
|
9
|
+
"react",
|
|
10
|
+
"link-preview",
|
|
11
|
+
"preview",
|
|
12
|
+
"card",
|
|
13
|
+
"component",
|
|
14
|
+
"typescript",
|
|
15
|
+
"presentational",
|
|
16
|
+
"github",
|
|
17
|
+
"npm",
|
|
18
|
+
"preset"
|
|
19
|
+
],
|
|
20
|
+
"author": "Seth Carney",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sethcarney/nextjs-link-preview.git"
|
|
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
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/",
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"main": "dist/index.js",
|
|
35
|
+
"module": "dist/index.esm.js",
|
|
36
|
+
"types": "dist/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"bin",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"bin": {
|
|
44
|
+
"nextjs-link-preview": "bin/setup.js"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rollup -c",
|
|
48
|
+
"copy-component": "cp src/nextjs/components/LinkPreview.tsx nextjs-demo/src/components/LinkPreview.tsx",
|
|
49
|
+
"dev": "npm run copy-component && cd nextjs-demo && npm run dev",
|
|
50
|
+
"test": "npm run copy-component && cd nextjs-demo && npm run dev",
|
|
51
|
+
"prepublishOnly": "npm run build"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"next": ">=14.0.0",
|
|
55
|
+
"react": ">=18.0.0",
|
|
56
|
+
"react-dom": ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
61
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
62
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
63
|
+
"@types/react": "^19.2.2",
|
|
64
|
+
"@types/react-dom": "^19.2.2",
|
|
65
|
+
"prettier": "^3.6.2",
|
|
66
|
+
"rollup": "^4.53.1",
|
|
67
|
+
"rollup-plugin-dts": "^6.2.3",
|
|
68
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
69
|
+
"tslib": "^2.8.1",
|
|
70
|
+
"typescript": "^5.9.3"
|
|
71
|
+
}
|
|
72
|
+
}
|
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();
|