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 CHANGED
@@ -1,133 +1,187 @@
1
- # Next.js Link Preview
2
-
3
- A Next.js component for generating beautiful link preview cards with server-side metadata fetching.
4
-
5
- **No CORS issues** - Works with GitHub, Twitter, Reddit, and any public URL!
6
-
7
- ## Features
8
-
9
- - ✅ Server-side fetching via Next.js API routes (no CORS!)
10
- - ✅ Automatic Open Graph and meta tag extraction
11
- - ✅ Built-in caching (1-hour TTL) to reduce server load and prevent rate limiting
12
- - Three size variants (small, medium, large)
13
- - ✅ Two layouts (vertical, horizontal)
14
- - ✅ TypeScript support
15
- - ✅ Loading and error states
16
- - ✅ Fully customizable styling
17
-
18
- ## Installation
19
-
20
- ```bash
21
- npm install nextjs-link-preview axios cheerio
22
- ```
23
-
24
- ## Setup
25
-
26
- Run the setup command to create the API route:
27
-
28
- ```bash
29
- npx nextjs-link-preview
30
- ```
31
-
32
- This creates the API route at `src/app/api/preview/route.ts` (or `app/api/preview/route.ts` depending on your project structure).
33
-
34
- ## Usage
35
-
36
- ```tsx
37
- import { LinkPreview } from "nextjs-link-preview";
38
-
39
- export default function Page() {
40
- return <LinkPreview url="https://github.com" />;
41
- }
42
- ```
43
-
44
- That's it! The component handles everything automatically.
45
-
46
- ## Examples
47
-
48
- ### Size Variants
49
-
50
- ```tsx
51
- <LinkPreview url="https://github.com" size="small" />
52
- <LinkPreview url="https://github.com" size="medium" /> {/* default */}
53
- <LinkPreview url="https://github.com" size="large" />
54
- ```
55
-
56
- ### Layouts
57
-
58
- ```tsx
59
- <LinkPreview url="https://github.com" layout="vertical" /> {/* default */}
60
- <LinkPreview url="https://github.com" layout="horizontal" />
61
- ```
62
-
63
- ### With Callbacks
64
-
65
- ```tsx
66
- <LinkPreview
67
- url="https://github.com"
68
- onLoad={(data) => console.log("Loaded:", data)}
69
- onError={(error) => console.error("Error:", error)}
70
- />
71
- ```
72
-
73
- ### Custom Styling
74
-
75
- ```tsx
76
- <LinkPreview url="https://github.com" width="400px" className="my-custom-class" />
77
- ```
78
-
79
- ## Props
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 |
92
-
93
- ## Development
94
-
95
- ```bash
96
- # Run demo
97
- npm run demo
98
-
99
- # Build package
100
- npm run build
101
-
102
- # Format code
103
- npm run format
104
- ```
105
-
106
- ## How It Works
107
-
108
- 1. **API Route**: Next.js API route fetches HTML server-side (no CORS!)
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
112
-
113
- ### Caching Behavior
114
-
115
- The API route includes built-in caching to improve performance and prevent websites from blocking automated requests:
116
-
117
- - **Cache Duration**: 1 hour (3600 seconds)
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
123
-
124
- If you need to customize the cache TTL, you can modify the `CACHE_TTL` constant in your generated API route file.
125
-
126
- ## License
127
-
128
- MIT
129
-
130
- ## Links
131
-
132
- - [GitHub](https://github.com/sethcarney/nextjs-link-preview)
133
- - [npm](https://www.npmjs.com/package/nextjs-link-preview)
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
- * Next.js Link Preview Component
4
+ * Simple Link Preview Component
5
5
  *
6
- * This component uses the Next.js API route to fetch metadata server-side,
7
- * avoiding CORS issues entirely.
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
- * import { LinkPreview } from './components/LinkPreview';
11
- *
12
- * <LinkPreview url="https://github.com" size="medium" />
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, size, layout, width, height, className, onError, onLoad, apiEndpoint }: LinkPreviewProps): React.JSX.Element | null;
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 { LinkPreviewData, LinkPreviewLayout, LinkPreviewProps, LinkPreviewSize };
41
+ export type { LinkPreviewLayout, LinkPreviewPreset, LinkPreviewProps, LinkPreviewSize };
package/dist/index.esm.js CHANGED
@@ -1,6 +1,11 @@
1
1
  'use client';
2
- import React, { useState, useEffect } from '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 = "", onError, onLoad, apiEndpoint = "/api/preview" }) {
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
- data.image && (React.createElement("div", { style: {
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(${data.image})`,
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
- data.title && (React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize } }, data.title)),
118
- data.description && (React.createElement("p", { style: {
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
- } }, data.description)))));
84
+ } }, description)))));
127
85
  }
128
86
 
129
87
  export { LinkPreview, LinkPreview as default };
@@ -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 = "", onError, onLoad, apiEndpoint = "/api/preview" }) {
35
- const [data, setData] = React.useState(null);
36
- const [loading, setLoading] = React.useState(true);
37
- const [error, setError] = React.useState(null);
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
- data.image && (React.createElement("div", { style: {
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(${data.image})`,
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
- data.title && (React.createElement("h3", { style: { margin: "0 0 8px 0", fontSize: config.titleSize } }, data.title)),
122
- data.description && (React.createElement("p", { style: {
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
- } }, data.description)))));
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
- * Next.js Link Preview Component
2
+ * Simple Link Preview Component
3
3
  *
4
- * This component uses the Next.js API route to fetch metadata server-side,
5
- * avoiding CORS issues entirely.
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
- * import { LinkPreview } from './components/LinkPreview';
9
- *
10
- * <LinkPreview url="https://github.com" size="medium" />
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, size, layout, width, height, className, onError, onLoad, apiEndpoint }: LinkPreviewProps): React.JSX.Element | null;
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;;;;;;;;;;GAUG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAC3D,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,YAAY,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA6BD,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,IAAe,EACf,MAAmB,EACnB,KAAc,EACd,MAAe,EACf,SAAc,EACd,OAAO,EACP,MAAM,EACN,WAA4B,EAC7B,EAAE,gBAAgB,4BA+IlB;AAED,eAAe,WAAW,CAAC"}
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.5",
4
- "type": "module",
5
- "description": "A Next.js component for generating beautiful link preview cards with server-side metadata fetching - No CORS issues!",
6
- "keywords": [
7
- "nextjs",
8
- "next",
9
- "react",
10
- "link-preview",
11
- "preview",
12
- "metadata",
13
- "opengraph",
14
- "og",
15
- "card",
16
- "component",
17
- "typescript",
18
- "server-side"
19
- ],
20
- "author": "Seth Carney",
21
- "license": "MIT",
22
- "repository": {
23
- "type": "git",
24
- "url": "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
- "build:watch": "rollup -c --watch",
49
- "prepublishOnly": "npm run build",
50
- "demo": "cd nextjs-demo && npm run dev",
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}\""
57
- },
58
- "peerDependencies": {
59
- "next": ">=14.0.0",
60
- "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"
66
- },
67
- "devDependencies": {
68
- "@rollup/plugin-commonjs": "^29.0.0",
69
- "@rollup/plugin-node-resolve": "^16.0.3",
70
- "@rollup/plugin-typescript": "^12.3.0",
71
- "@types/react": "^19.2.2",
72
- "@types/react-dom": "^19.2.2",
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();