nextjs-link-preview 1.0.5 → 2.0.0

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