@uptechworks/url-preview-field 0.1.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 ADDED
@@ -0,0 +1,73 @@
1
+ # url-preview-field
2
+
3
+ A custom field plugin for Strapi that allows you to preview a URL with a social share card, similar to what you see when sharing a link on social media.
4
+
5
+ ## Features
6
+
7
+ - **Custom URL Field:** Adds a new custom field type (`url`) to Strapi content types.
8
+ - **Live Preview:** When entering a URL, the field fetches and displays a preview card with the page’s title, image, and domain.
9
+ - **Social Card Style:** The preview mimics the appearance of social media link previews.
10
+ - **Advanced Settings:** Optionally mark the field as required in the content type builder (UI only).
11
+ - **Backend Metadata Fetching:** Uses a Strapi backend route to fetch Open Graph and Twitter card metadata for the given URL.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # In your Strapi project root
17
+ npm install url-preview-field
18
+ # or
19
+ yarn add url-preview-field
20
+ ```
21
+
22
+ You will need to add the following to your `config/middlewares.ts` to ensure the image from the URL can be displayed. In the img-src add the nessecary domains, otherwise Strapi will not display the image.
23
+
24
+ ```
25
+ {
26
+ name: 'strapi::security',
27
+ config: {
28
+ contentSecurityPolicy: {
29
+ directives: {
30
+ 'script-src': ["'self'", "'unsafe-inline'"],
31
+ 'img-src': ["'self'", 'data:', 'strapi.io'],
32
+ },
33
+ },
34
+ },
35
+ },
36
+ ```
37
+
38
+ Then, enable the plugin in your Strapi project as you would with any other plugin.
39
+
40
+ ## Usage
41
+
42
+ 1. **Add the Field to a Content Type:**
43
+ - In the Strapi admin panel, edit or create a content type.
44
+ - Add a new field, select Custom then select the “URL” custom field (provided by this plugin).
45
+
46
+ 2. **Enter a URL:**
47
+ - In the content manager, enter a URL in the field.
48
+ - The plugin will fetch and display a preview card with the page’s metadata (title, image, domain, etc.).
49
+
50
+ ## How It Works
51
+
52
+ - **Admin UI:**
53
+ The plugin registers a custom field with a React component that handles user input and displays the preview card.
54
+ - **Backend:**
55
+ The plugin exposes a POST endpoint (`/url-preview-field/url-metadata`) that fetches the target URL, parses its HTML for Open Graph/Twitter metadata, and returns the relevant information to the frontend.
56
+
57
+ ## Development
58
+
59
+ ### Scripts
60
+
61
+ - `npm run build` – Build the plugin for production.
62
+ - `npm run watch` – Watch for changes and rebuild automatically.
63
+ - `npm run verify` – Verify the plugin.
64
+ - `npm run test:ts:front` – Type-check the admin (frontend) code.
65
+ - `npm run test:ts:back` – Type-check the server (backend) code.
66
+
67
+ ## Limitations
68
+
69
+ - The plugin fetches metadata using the backend; the server must be able to access the target URLs.
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const icons = require("@strapi/icons");
7
+ const admin = require("@strapi/strapi/admin");
8
+ const styledComponents = require("styled-components");
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
12
+ if (e) {
13
+ for (const k in e) {
14
+ if (k !== "default") {
15
+ const d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: () => e[k]
19
+ });
20
+ }
21
+ }
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
27
+ const PreviewCard = styledComponents.styled(designSystem.Box)`
28
+ border: 1px solid #e4e7eb;
29
+ border-radius: 4px;
30
+ max-width: 400px;
31
+ margin-top: 8px;
32
+ padding: 16px;
33
+ background: white;
34
+ `;
35
+ const UrlPreview = styledComponents.styled.div`
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ color: #666;
40
+ font-size: 12px;
41
+ margin-bottom: 8px;
42
+ `;
43
+ const PreviewImage = styledComponents.styled.img`
44
+ width: 100%;
45
+ height: 200px;
46
+ object-fit: cover;
47
+ border-radius: 4px;
48
+ margin-bottom: 12px;
49
+ `;
50
+ const PreviewTitle = styledComponents.styled(designSystem.Typography)`
51
+ font-size: 1.25rem;
52
+ font-weight: bold;
53
+ color: #222;
54
+ margin-bottom: 8px;
55
+ `;
56
+ const PreviewLink = styledComponents.styled.a`
57
+ color: #0099e5;
58
+ text-decoration: underline;
59
+ font-weight: 500;
60
+ font-size: 1rem;
61
+ margin-top: 8px;
62
+ display: inline-block;
63
+ `;
64
+ const UrlFieldInput = React__namespace.forwardRef(
65
+ ({ hint, disabled, labelAction, label, name, required, attribute, onChange, error, ...props }, forwardedRef) => {
66
+ const { value } = admin.useField(name);
67
+ const [inputValue, setInputValue] = React.useState(value || "");
68
+ const [metadata, setMetadata] = React.useState(null);
69
+ const [loading, setLoading] = React.useState(false);
70
+ const [fetchError, setFetchError] = React.useState(null);
71
+ const fetchUrlMetadata = async (url) => {
72
+ if (!url || url.trim() === "") {
73
+ setMetadata(null);
74
+ setFetchError(null);
75
+ return;
76
+ }
77
+ setLoading(true);
78
+ setFetchError(null);
79
+ try {
80
+ const response = await fetch("/url-preview-field/url-metadata", {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": "application/json"
84
+ },
85
+ body: JSON.stringify({ url })
86
+ });
87
+ if (!response.ok) {
88
+ throw new Error(`Metadata fetch failed: ${response.status} ${response.statusText}`);
89
+ }
90
+ const data = await response.json();
91
+ console.log("Metadata received:", JSON.stringify(data));
92
+ setMetadata(data);
93
+ } catch (err) {
94
+ console.error("Error fetching URL metadata:", err);
95
+ const errorMessage = err instanceof Error ? err.message : "Unknown error occurred";
96
+ setFetchError(`Failed to fetch URL metadata: ${errorMessage}`);
97
+ setMetadata(null);
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ };
102
+ React.useEffect(() => {
103
+ const timeoutId = setTimeout(() => {
104
+ if (inputValue) {
105
+ fetchUrlMetadata(inputValue);
106
+ } else {
107
+ setMetadata(null);
108
+ setFetchError(null);
109
+ }
110
+ }, 1e3);
111
+ return () => clearTimeout(timeoutId);
112
+ }, [inputValue]);
113
+ React.useEffect(() => {
114
+ setInputValue(value || "");
115
+ }, [value]);
116
+ const handleChange = (e) => {
117
+ const newValue = e.target.value;
118
+ setInputValue(newValue);
119
+ if (onChange) {
120
+ onChange(e);
121
+ }
122
+ };
123
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { name, id: name, error, hint, required, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
124
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
125
+ /* @__PURE__ */ jsxRuntime.jsx(
126
+ designSystem.Field.Input,
127
+ {
128
+ type: "url",
129
+ placeholder: "https://example.com",
130
+ value: inputValue,
131
+ onChange: handleChange,
132
+ disabled
133
+ }
134
+ ),
135
+ loading && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
136
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true }),
137
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "Fetching preview..." })
138
+ ] }),
139
+ fetchError && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: fetchError }),
140
+ metadata && !loading && !fetchError && /* @__PURE__ */ jsxRuntime.jsx(PreviewCard, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 3, alignItems: "flex-start", children: [
141
+ /* @__PURE__ */ jsxRuntime.jsxs(UrlPreview, { children: [
142
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Globe, { width: "12", height: "12" }),
143
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: metadata.domain })
144
+ ] }),
145
+ metadata.image && /* @__PURE__ */ jsxRuntime.jsx(
146
+ PreviewImage,
147
+ {
148
+ src: metadata.image,
149
+ alt: metadata.title,
150
+ onError: (e) => {
151
+ e.currentTarget.style.display = "none";
152
+ }
153
+ }
154
+ ),
155
+ metadata.title && /* @__PURE__ */ jsxRuntime.jsx(PreviewTitle, { as: "div", children: metadata.title }),
156
+ metadata.url && /* @__PURE__ */ jsxRuntime.jsxs(
157
+ PreviewLink,
158
+ {
159
+ href: metadata.url,
160
+ target: "_blank",
161
+ rel: "noopener noreferrer",
162
+ children: [
163
+ "View on ",
164
+ metadata.domain,
165
+ " >"
166
+ ]
167
+ }
168
+ )
169
+ ] }) })
170
+ ] }) });
171
+ }
172
+ );
173
+ exports.UrlFieldInput = UrlFieldInput;
@@ -0,0 +1,156 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useState, useEffect } from "react";
4
+ import { Field, Flex, Loader, Typography, Box } from "@strapi/design-system";
5
+ import { Globe } from "@strapi/icons";
6
+ import { useField } from "@strapi/strapi/admin";
7
+ import { styled } from "styled-components";
8
+ const PreviewCard = styled(Box)`
9
+ border: 1px solid #e4e7eb;
10
+ border-radius: 4px;
11
+ max-width: 400px;
12
+ margin-top: 8px;
13
+ padding: 16px;
14
+ background: white;
15
+ `;
16
+ const UrlPreview = styled.div`
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 8px;
20
+ color: #666;
21
+ font-size: 12px;
22
+ margin-bottom: 8px;
23
+ `;
24
+ const PreviewImage = styled.img`
25
+ width: 100%;
26
+ height: 200px;
27
+ object-fit: cover;
28
+ border-radius: 4px;
29
+ margin-bottom: 12px;
30
+ `;
31
+ const PreviewTitle = styled(Typography)`
32
+ font-size: 1.25rem;
33
+ font-weight: bold;
34
+ color: #222;
35
+ margin-bottom: 8px;
36
+ `;
37
+ const PreviewLink = styled.a`
38
+ color: #0099e5;
39
+ text-decoration: underline;
40
+ font-weight: 500;
41
+ font-size: 1rem;
42
+ margin-top: 8px;
43
+ display: inline-block;
44
+ `;
45
+ const UrlFieldInput = React.forwardRef(
46
+ ({ hint, disabled, labelAction, label, name, required, attribute, onChange, error, ...props }, forwardedRef) => {
47
+ const { value } = useField(name);
48
+ const [inputValue, setInputValue] = useState(value || "");
49
+ const [metadata, setMetadata] = useState(null);
50
+ const [loading, setLoading] = useState(false);
51
+ const [fetchError, setFetchError] = useState(null);
52
+ const fetchUrlMetadata = async (url) => {
53
+ if (!url || url.trim() === "") {
54
+ setMetadata(null);
55
+ setFetchError(null);
56
+ return;
57
+ }
58
+ setLoading(true);
59
+ setFetchError(null);
60
+ try {
61
+ const response = await fetch("/url-preview-field/url-metadata", {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json"
65
+ },
66
+ body: JSON.stringify({ url })
67
+ });
68
+ if (!response.ok) {
69
+ throw new Error(`Metadata fetch failed: ${response.status} ${response.statusText}`);
70
+ }
71
+ const data = await response.json();
72
+ console.log("Metadata received:", JSON.stringify(data));
73
+ setMetadata(data);
74
+ } catch (err) {
75
+ console.error("Error fetching URL metadata:", err);
76
+ const errorMessage = err instanceof Error ? err.message : "Unknown error occurred";
77
+ setFetchError(`Failed to fetch URL metadata: ${errorMessage}`);
78
+ setMetadata(null);
79
+ } finally {
80
+ setLoading(false);
81
+ }
82
+ };
83
+ useEffect(() => {
84
+ const timeoutId = setTimeout(() => {
85
+ if (inputValue) {
86
+ fetchUrlMetadata(inputValue);
87
+ } else {
88
+ setMetadata(null);
89
+ setFetchError(null);
90
+ }
91
+ }, 1e3);
92
+ return () => clearTimeout(timeoutId);
93
+ }, [inputValue]);
94
+ useEffect(() => {
95
+ setInputValue(value || "");
96
+ }, [value]);
97
+ const handleChange = (e) => {
98
+ const newValue = e.target.value;
99
+ setInputValue(newValue);
100
+ if (onChange) {
101
+ onChange(e);
102
+ }
103
+ };
104
+ return /* @__PURE__ */ jsx(Field.Root, { name, id: name, error, hint, required, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
105
+ /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
106
+ /* @__PURE__ */ jsx(
107
+ Field.Input,
108
+ {
109
+ type: "url",
110
+ placeholder: "https://example.com",
111
+ value: inputValue,
112
+ onChange: handleChange,
113
+ disabled
114
+ }
115
+ ),
116
+ loading && /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
117
+ /* @__PURE__ */ jsx(Loader, { small: true }),
118
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Fetching preview..." })
119
+ ] }),
120
+ fetchError && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: fetchError }),
121
+ metadata && !loading && !fetchError && /* @__PURE__ */ jsx(PreviewCard, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, alignItems: "flex-start", children: [
122
+ /* @__PURE__ */ jsxs(UrlPreview, { children: [
123
+ /* @__PURE__ */ jsx(Globe, { width: "12", height: "12" }),
124
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: metadata.domain })
125
+ ] }),
126
+ metadata.image && /* @__PURE__ */ jsx(
127
+ PreviewImage,
128
+ {
129
+ src: metadata.image,
130
+ alt: metadata.title,
131
+ onError: (e) => {
132
+ e.currentTarget.style.display = "none";
133
+ }
134
+ }
135
+ ),
136
+ metadata.title && /* @__PURE__ */ jsx(PreviewTitle, { as: "div", children: metadata.title }),
137
+ metadata.url && /* @__PURE__ */ jsxs(
138
+ PreviewLink,
139
+ {
140
+ href: metadata.url,
141
+ target: "_blank",
142
+ rel: "noopener noreferrer",
143
+ children: [
144
+ "View on ",
145
+ metadata.domain,
146
+ " >"
147
+ ]
148
+ }
149
+ )
150
+ ] }) })
151
+ ] }) });
152
+ }
153
+ );
154
+ export {
155
+ UrlFieldInput
156
+ };
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const en = {};
4
+ exports.default = en;
@@ -0,0 +1,4 @@
1
+ const en = {};
2
+ export {
3
+ en as default
4
+ };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ const React = require("react");
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const designSystem = require("@strapi/design-system");
5
+ const icons = require("@strapi/icons");
6
+ const styledComponents = require("styled-components");
7
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
8
+ const v = glob[path];
9
+ if (v) {
10
+ return typeof v === "function" ? v() : Promise.resolve(v);
11
+ }
12
+ return new Promise((_, reject) => {
13
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
14
+ reject.bind(
15
+ null,
16
+ new Error(
17
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
18
+ )
19
+ )
20
+ );
21
+ });
22
+ };
23
+ const PLUGIN_ID = "url-preview-field";
24
+ const Initializer = ({ setPlugin }) => {
25
+ const ref = React.useRef(setPlugin);
26
+ React.useEffect(() => {
27
+ ref.current(PLUGIN_ID);
28
+ }, []);
29
+ return null;
30
+ };
31
+ const IconBox = styledComponents.styled(designSystem.Flex)`
32
+ /* Hard code color values */
33
+ /* to stay consistent between themes */
34
+ background-color: #f0f0ff; /* primary100 */
35
+ border: 1px solid #d9d8ff; /* primary200 */
36
+
37
+ svg > path {
38
+ fill: #4945ff; /* primary600 */
39
+ }
40
+ `;
41
+ const UrlFieldIcon = () => {
42
+ return /* @__PURE__ */ jsxRuntime.jsx(IconBox, { justifyContent: "center", alignItems: "center", width: 7, height: 6, hasRadius: true, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ExternalLink, {}) });
43
+ };
44
+ const index = {
45
+ register(app) {
46
+ app.registerPlugin({
47
+ id: PLUGIN_ID,
48
+ initializer: Initializer,
49
+ isReady: false,
50
+ name: PLUGIN_ID
51
+ });
52
+ app.customFields.register({
53
+ name: "url",
54
+ pluginId: PLUGIN_ID,
55
+ // Use the PLUGIN_ID constant to match server registration
56
+ type: "string",
57
+ intlLabel: {
58
+ id: "url-preview-field.url.label",
59
+ defaultMessage: "URL"
60
+ },
61
+ intlDescription: {
62
+ id: "url-preview-field.url.description",
63
+ defaultMessage: "Enter an URL to see a social share card preview."
64
+ },
65
+ icon: UrlFieldIcon,
66
+ components: {
67
+ Input: async () => Promise.resolve().then(() => require("../_chunks/UrlFieldInput-BhujW-Fo.js")).then((module2) => ({
68
+ default: module2.UrlFieldInput
69
+ }))
70
+ }
71
+ });
72
+ },
73
+ async registerTrads({ locales }) {
74
+ return Promise.all(
75
+ locales.map(async (locale) => {
76
+ try {
77
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("../_chunks/en-B4KWt_jN.js")) }), `./translations/${locale}.json`, 3);
78
+ return { data, locale };
79
+ } catch {
80
+ return { data: {}, locale };
81
+ }
82
+ })
83
+ );
84
+ }
85
+ };
86
+ module.exports = index;
@@ -0,0 +1,87 @@
1
+ import { useRef, useEffect } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Flex } from "@strapi/design-system";
4
+ import { ExternalLink } from "@strapi/icons";
5
+ import { styled } from "styled-components";
6
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
7
+ const v = glob[path];
8
+ if (v) {
9
+ return typeof v === "function" ? v() : Promise.resolve(v);
10
+ }
11
+ return new Promise((_, reject) => {
12
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
13
+ reject.bind(
14
+ null,
15
+ new Error(
16
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
17
+ )
18
+ )
19
+ );
20
+ });
21
+ };
22
+ const PLUGIN_ID = "url-preview-field";
23
+ const Initializer = ({ setPlugin }) => {
24
+ const ref = useRef(setPlugin);
25
+ useEffect(() => {
26
+ ref.current(PLUGIN_ID);
27
+ }, []);
28
+ return null;
29
+ };
30
+ const IconBox = styled(Flex)`
31
+ /* Hard code color values */
32
+ /* to stay consistent between themes */
33
+ background-color: #f0f0ff; /* primary100 */
34
+ border: 1px solid #d9d8ff; /* primary200 */
35
+
36
+ svg > path {
37
+ fill: #4945ff; /* primary600 */
38
+ }
39
+ `;
40
+ const UrlFieldIcon = () => {
41
+ return /* @__PURE__ */ jsx(IconBox, { justifyContent: "center", alignItems: "center", width: 7, height: 6, hasRadius: true, "aria-hidden": true, children: /* @__PURE__ */ jsx(ExternalLink, {}) });
42
+ };
43
+ const index = {
44
+ register(app) {
45
+ app.registerPlugin({
46
+ id: PLUGIN_ID,
47
+ initializer: Initializer,
48
+ isReady: false,
49
+ name: PLUGIN_ID
50
+ });
51
+ app.customFields.register({
52
+ name: "url",
53
+ pluginId: PLUGIN_ID,
54
+ // Use the PLUGIN_ID constant to match server registration
55
+ type: "string",
56
+ intlLabel: {
57
+ id: "url-preview-field.url.label",
58
+ defaultMessage: "URL"
59
+ },
60
+ intlDescription: {
61
+ id: "url-preview-field.url.description",
62
+ defaultMessage: "Enter an URL to see a social share card preview."
63
+ },
64
+ icon: UrlFieldIcon,
65
+ components: {
66
+ Input: async () => import("../_chunks/UrlFieldInput-D0OIBuvW.mjs").then((module) => ({
67
+ default: module.UrlFieldInput
68
+ }))
69
+ }
70
+ });
71
+ },
72
+ async registerTrads({ locales }) {
73
+ return Promise.all(
74
+ locales.map(async (locale) => {
75
+ try {
76
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-Byx4XI2L.mjs") }), `./translations/${locale}.json`, 3);
77
+ return { data, locale };
78
+ } catch {
79
+ return { data: {}, locale };
80
+ }
81
+ })
82
+ );
83
+ }
84
+ };
85
+ export {
86
+ index as default
87
+ };
@@ -0,0 +1,5 @@
1
+ type InitializerProps = {
2
+ setPlugin: (id: string) => void;
3
+ };
4
+ declare const Initializer: ({ setPlugin }: InitializerProps) => null;
5
+ export { Initializer };
@@ -0,0 +1,2 @@
1
+ declare const PluginIcon: () => import("react/jsx-runtime").JSX.Element;
2
+ export { PluginIcon };
@@ -0,0 +1 @@
1
+ export declare const UrlFieldIcon: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import { type InputProps } from '@strapi/strapi/admin';
3
+ type UrlFieldInputProps = InputProps & {
4
+ labelAction?: React.ReactNode;
5
+ attribute?: {
6
+ options?: {
7
+ url?: string;
8
+ };
9
+ };
10
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
11
+ error?: string;
12
+ };
13
+ declare const UrlFieldInput: React.ForwardRefExoticComponent<UrlFieldInputProps & React.RefAttributes<HTMLButtonElement>>;
14
+ export { UrlFieldInput };
@@ -0,0 +1,10 @@
1
+ declare const _default: {
2
+ register(app: any): void;
3
+ registerTrads({ locales }: {
4
+ locales: string[];
5
+ }): Promise<{
6
+ data: any;
7
+ locale: string;
8
+ }[]>;
9
+ };
10
+ export default _default;
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_ID = "url-preview-field";
@@ -0,0 +1,2 @@
1
+ declare const getTranslation: (id: string) => string;
2
+ export { getTranslation };
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ const fetch = require("node-fetch");
3
+ const cheerio = require("cheerio");
4
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
5
+ function _interopNamespace(e) {
6
+ if (e && e.__esModule) return e;
7
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
8
+ if (e) {
9
+ for (const k in e) {
10
+ if (k !== "default") {
11
+ const d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: () => e[k]
15
+ });
16
+ }
17
+ }
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+ const fetch__default = /* @__PURE__ */ _interopDefault(fetch);
23
+ const cheerio__namespace = /* @__PURE__ */ _interopNamespace(cheerio);
24
+ const bootstrap = ({ strapi }) => {
25
+ };
26
+ const destroy = ({ strapi }) => {
27
+ };
28
+ const PLUGIN_ID = "url-preview-field";
29
+ const register = ({ strapi }) => {
30
+ strapi.customFields.register({
31
+ name: "url",
32
+ plugin: PLUGIN_ID,
33
+ type: "string",
34
+ inputSize: {
35
+ default: 4,
36
+ isResizable: true
37
+ }
38
+ });
39
+ };
40
+ const config = {
41
+ default: {},
42
+ validator() {
43
+ }
44
+ };
45
+ const contentTypes = {};
46
+ const controller = ({ strapi }) => ({
47
+ index(ctx) {
48
+ ctx.body = strapi.plugin("url-preview-field").service("service").getWelcomeMessage();
49
+ },
50
+ async fetchMetadata(ctx) {
51
+ const { url } = ctx.request.body;
52
+ ctx.body = await strapi.plugin("url-preview-field").service("service").getUrlMetadata(url);
53
+ }
54
+ });
55
+ const controllers = {
56
+ controller
57
+ };
58
+ const middlewares = {};
59
+ const policies = {};
60
+ const routes = [
61
+ {
62
+ method: "GET",
63
+ path: "/",
64
+ // name of the controller file & the method.
65
+ handler: "controller.index",
66
+ config: {
67
+ policies: []
68
+ }
69
+ },
70
+ {
71
+ method: "POST",
72
+ path: "/url-metadata",
73
+ handler: "controller.fetchMetadata",
74
+ config: {
75
+ auth: false
76
+ }
77
+ }
78
+ ];
79
+ const service = ({ strapi }) => ({
80
+ getWelcomeMessage() {
81
+ return "Welcome to Strapi 🚀";
82
+ },
83
+ async getUrlMetadata(url) {
84
+ try {
85
+ const response = await fetch__default.default(url);
86
+ if (!response.ok) {
87
+ throw new Error(`Failed to fetch URL: ${response.statusText}`);
88
+ }
89
+ const html = await response.text();
90
+ const $ = cheerio__namespace.load(html);
91
+ const title = $('meta[property="og:title"]').attr("content") || $("title").text() || "";
92
+ const image = $('meta[property="og:image"]').attr("content") || $('meta[name="twitter:image"]').attr("content") || "";
93
+ const domain = (() => {
94
+ try {
95
+ return new URL(url).hostname.replace(/^www\./, "");
96
+ } catch {
97
+ return "";
98
+ }
99
+ })();
100
+ console.log(
101
+ `Found Data: ${JSON.stringify({
102
+ title,
103
+ image,
104
+ url,
105
+ domain
106
+ })}`
107
+ );
108
+ return {
109
+ title,
110
+ image,
111
+ url,
112
+ domain
113
+ };
114
+ } catch (err) {
115
+ console.error("Error fetching URL metadata:", err);
116
+ return {
117
+ title: "",
118
+ image: "",
119
+ url,
120
+ domain: "",
121
+ error: err.message
122
+ };
123
+ }
124
+ }
125
+ });
126
+ const services = {
127
+ service
128
+ };
129
+ const index = {
130
+ register,
131
+ bootstrap,
132
+ destroy,
133
+ config,
134
+ controllers,
135
+ routes,
136
+ services,
137
+ contentTypes,
138
+ policies,
139
+ middlewares
140
+ };
141
+ module.exports = index;
@@ -0,0 +1,122 @@
1
+ import fetch from "node-fetch";
2
+ import * as cheerio from "cheerio";
3
+ const bootstrap = ({ strapi }) => {
4
+ };
5
+ const destroy = ({ strapi }) => {
6
+ };
7
+ const PLUGIN_ID = "url-preview-field";
8
+ const register = ({ strapi }) => {
9
+ strapi.customFields.register({
10
+ name: "url",
11
+ plugin: PLUGIN_ID,
12
+ type: "string",
13
+ inputSize: {
14
+ default: 4,
15
+ isResizable: true
16
+ }
17
+ });
18
+ };
19
+ const config = {
20
+ default: {},
21
+ validator() {
22
+ }
23
+ };
24
+ const contentTypes = {};
25
+ const controller = ({ strapi }) => ({
26
+ index(ctx) {
27
+ ctx.body = strapi.plugin("url-preview-field").service("service").getWelcomeMessage();
28
+ },
29
+ async fetchMetadata(ctx) {
30
+ const { url } = ctx.request.body;
31
+ ctx.body = await strapi.plugin("url-preview-field").service("service").getUrlMetadata(url);
32
+ }
33
+ });
34
+ const controllers = {
35
+ controller
36
+ };
37
+ const middlewares = {};
38
+ const policies = {};
39
+ const routes = [
40
+ {
41
+ method: "GET",
42
+ path: "/",
43
+ // name of the controller file & the method.
44
+ handler: "controller.index",
45
+ config: {
46
+ policies: []
47
+ }
48
+ },
49
+ {
50
+ method: "POST",
51
+ path: "/url-metadata",
52
+ handler: "controller.fetchMetadata",
53
+ config: {
54
+ auth: false
55
+ }
56
+ }
57
+ ];
58
+ const service = ({ strapi }) => ({
59
+ getWelcomeMessage() {
60
+ return "Welcome to Strapi 🚀";
61
+ },
62
+ async getUrlMetadata(url) {
63
+ try {
64
+ const response = await fetch(url);
65
+ if (!response.ok) {
66
+ throw new Error(`Failed to fetch URL: ${response.statusText}`);
67
+ }
68
+ const html = await response.text();
69
+ const $ = cheerio.load(html);
70
+ const title = $('meta[property="og:title"]').attr("content") || $("title").text() || "";
71
+ const image = $('meta[property="og:image"]').attr("content") || $('meta[name="twitter:image"]').attr("content") || "";
72
+ const domain = (() => {
73
+ try {
74
+ return new URL(url).hostname.replace(/^www\./, "");
75
+ } catch {
76
+ return "";
77
+ }
78
+ })();
79
+ console.log(
80
+ `Found Data: ${JSON.stringify({
81
+ title,
82
+ image,
83
+ url,
84
+ domain
85
+ })}`
86
+ );
87
+ return {
88
+ title,
89
+ image,
90
+ url,
91
+ domain
92
+ };
93
+ } catch (err) {
94
+ console.error("Error fetching URL metadata:", err);
95
+ return {
96
+ title: "",
97
+ image: "",
98
+ url,
99
+ domain: "",
100
+ error: err.message
101
+ };
102
+ }
103
+ }
104
+ });
105
+ const services = {
106
+ service
107
+ };
108
+ const index = {
109
+ register,
110
+ bootstrap,
111
+ destroy,
112
+ config,
113
+ controllers,
114
+ routes,
115
+ services,
116
+ contentTypes,
117
+ policies,
118
+ middlewares
119
+ };
120
+ export {
121
+ index as default
122
+ };
@@ -0,0 +1,5 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ declare const bootstrap: ({ strapi }: {
3
+ strapi: Core.Strapi;
4
+ }) => void;
5
+ export default bootstrap;
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ default: {};
3
+ validator(): void;
4
+ };
5
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ declare const controller: ({ strapi }: {
3
+ strapi: Core.Strapi;
4
+ }) => {
5
+ index(ctx: any): void;
6
+ fetchMetadata(ctx: any): Promise<void>;
7
+ };
8
+ export default controller;
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ controller: ({ strapi }: {
3
+ strapi: import("@strapi/types/dist/core").Strapi;
4
+ }) => {
5
+ index(ctx: any): void;
6
+ fetchMetadata(ctx: any): Promise<void>;
7
+ };
8
+ };
9
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ declare const destroy: ({ strapi }: {
3
+ strapi: Core.Strapi;
4
+ }) => void;
5
+ export default destroy;
@@ -0,0 +1,67 @@
1
+ declare const _default: {
2
+ register: ({ strapi }: {
3
+ strapi: import("@strapi/types/dist/core").Strapi;
4
+ }) => void;
5
+ bootstrap: ({ strapi }: {
6
+ strapi: import("@strapi/types/dist/core").Strapi;
7
+ }) => void;
8
+ destroy: ({ strapi }: {
9
+ strapi: import("@strapi/types/dist/core").Strapi;
10
+ }) => void;
11
+ config: {
12
+ default: {};
13
+ validator(): void;
14
+ };
15
+ controllers: {
16
+ controller: ({ strapi }: {
17
+ strapi: import("@strapi/types/dist/core").Strapi;
18
+ }) => {
19
+ index(ctx: any): void;
20
+ fetchMetadata(ctx: any): Promise<void>;
21
+ };
22
+ };
23
+ routes: ({
24
+ method: string;
25
+ path: string;
26
+ handler: string;
27
+ /**
28
+ * Plugin server methods
29
+ */
30
+ config: {
31
+ policies: any[];
32
+ auth?: undefined;
33
+ };
34
+ } | {
35
+ method: string;
36
+ path: string;
37
+ handler: string;
38
+ config: {
39
+ auth: boolean;
40
+ policies?: undefined;
41
+ };
42
+ })[];
43
+ services: {
44
+ service: ({ strapi }: {
45
+ strapi: import("@strapi/types/dist/core").Strapi;
46
+ }) => {
47
+ getWelcomeMessage(): string;
48
+ getUrlMetadata(url: string): Promise<{
49
+ title: string;
50
+ image: string;
51
+ url: string;
52
+ domain: string;
53
+ error?: undefined;
54
+ } | {
55
+ title: string;
56
+ image: string;
57
+ url: string;
58
+ domain: string;
59
+ error: string;
60
+ }>;
61
+ };
62
+ };
63
+ contentTypes: {};
64
+ policies: {};
65
+ middlewares: {};
66
+ };
67
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_ID = "url-preview-field";
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ declare const register: ({ strapi }: {
3
+ strapi: Core.Strapi;
4
+ }) => void;
5
+ export default register;
@@ -0,0 +1,18 @@
1
+ declare const _default: ({
2
+ method: string;
3
+ path: string;
4
+ handler: string;
5
+ config: {
6
+ policies: any[];
7
+ auth?: undefined;
8
+ };
9
+ } | {
10
+ method: string;
11
+ path: string;
12
+ handler: string;
13
+ config: {
14
+ auth: boolean;
15
+ policies?: undefined;
16
+ };
17
+ })[];
18
+ export default _default;
@@ -0,0 +1,21 @@
1
+ declare const _default: {
2
+ service: ({ strapi }: {
3
+ strapi: import("@strapi/types/dist/core").Strapi;
4
+ }) => {
5
+ getWelcomeMessage(): string;
6
+ getUrlMetadata(url: string): Promise<{
7
+ title: string;
8
+ image: string;
9
+ url: string;
10
+ domain: string;
11
+ error?: undefined;
12
+ } | {
13
+ title: string;
14
+ image: string;
15
+ url: string;
16
+ domain: string;
17
+ error: string;
18
+ }>;
19
+ };
20
+ };
21
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ declare const service: ({ strapi }: {
3
+ strapi: Core.Strapi;
4
+ }) => {
5
+ getWelcomeMessage(): string;
6
+ getUrlMetadata(url: string): Promise<{
7
+ title: string;
8
+ image: string;
9
+ url: string;
10
+ domain: string;
11
+ error?: undefined;
12
+ } | {
13
+ title: string;
14
+ image: string;
15
+ url: string;
16
+ domain: string;
17
+ error: string;
18
+ }>;
19
+ };
20
+ export default service;
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "keywords": [],
4
+ "type": "commonjs",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/uptech/strapi-url-preview-field.git"
8
+ },
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ "./strapi-admin": {
12
+ "types": "./dist/admin/src/index.d.ts",
13
+ "source": "./admin/src/index.ts",
14
+ "import": "./dist/admin/index.mjs",
15
+ "require": "./dist/admin/index.js",
16
+ "default": "./dist/admin/index.js"
17
+ },
18
+ "./strapi-server": {
19
+ "types": "./dist/server/src/index.d.ts",
20
+ "source": "./server/src/index.ts",
21
+ "import": "./dist/server/index.mjs",
22
+ "require": "./dist/server/index.js",
23
+ "default": "./dist/server/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "strapi-plugin build",
31
+ "watch": "strapi-plugin watch",
32
+ "watch:link": "strapi-plugin watch:link",
33
+ "verify": "strapi-plugin verify",
34
+ "test:ts:front": "run -T tsc -p admin/tsconfig.json",
35
+ "test:ts:back": "run -T tsc -p server/tsconfig.json"
36
+ },
37
+ "dependencies": {
38
+ "@strapi/design-system": "^2.0.0-rc.29",
39
+ "@strapi/icons": "^2.0.0-rc.29",
40
+ "axios": "^1.6.0",
41
+ "cheerio": "^1.1.2",
42
+ "node-fetch": "^2.7.0",
43
+ "react-intl": "^7.1.11"
44
+ },
45
+ "devDependencies": {
46
+ "@strapi/sdk-plugin": "^5.3.2",
47
+ "@strapi/strapi": "^5.18.0",
48
+ "@strapi/typescript-utils": "^5.18.0",
49
+ "@types/cheerio": "^0.22.35",
50
+ "@types/node-fetch": "^2.6.12",
51
+ "@types/react": "^19.1.8",
52
+ "@types/react-dom": "^19.1.6",
53
+ "prettier": "^3.6.2",
54
+ "react": "^18.3.1",
55
+ "react-dom": "^18.3.1",
56
+ "react-router-dom": "^6.30.1",
57
+ "styled-components": "^6.1.19",
58
+ "typescript": "^5.8.3"
59
+ },
60
+ "peerDependencies": {
61
+ "@strapi/sdk-plugin": "^5.3.2",
62
+ "@strapi/strapi": "^5.18.0",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
65
+ "react-router-dom": "^6.30.1",
66
+ "styled-components": "^6.1.19"
67
+ },
68
+ "strapi": {
69
+ "kind": "plugin",
70
+ "name": "url-preview-field",
71
+ "displayName": "URL Share Preview",
72
+ "description": "A custom field plugin for Strapi that allows you to preview the URL. The preview is similar to the one you see when you share a link on social media."
73
+ },
74
+ "name": "@uptechworks/url-preview-field",
75
+ "description": "A custom field plugin for Strapi that allows you to preview the URL. The preview is similar to the one you see when you share a link on social media.",
76
+ "license": "MIT",
77
+ "author": "Uptech Studio <jon@uptechstudio.com>",
78
+ "main": "index.js",
79
+ "bugs": {
80
+ "url": "https://github.com/uptech/strapi-url-preview-field/issues"
81
+ },
82
+ "homepage": "https://github.com/uptech/strapi-url-preview-field#readme"
83
+ }