@windstream/react-shared-components 0.2.41 → 0.2.42
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/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +1 -1
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -1
- package/dist/next/index.js.map +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/link/index.test.tsx +2 -2
- package/src/components/link/index.tsx +1 -1
- package/src/hooks/contentful/use-contentful-rich-text.tsx +69 -9
package/package.json
CHANGED
|
@@ -155,14 +155,14 @@ describe("Link", () => {
|
|
|
155
155
|
expect(el).toHaveAttribute("target", "_blank");
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
it("adds rel=noopener
|
|
158
|
+
it("adds rel=noopener for external links", () => {
|
|
159
159
|
render(
|
|
160
160
|
<Link href="https://example.com" external={true}>
|
|
161
161
|
External
|
|
162
162
|
</Link>
|
|
163
163
|
);
|
|
164
164
|
const el = screen.getByText("External").closest("a");
|
|
165
|
-
expect(el).toHaveAttribute("rel", "noopener
|
|
165
|
+
expect(el).toHaveAttribute("rel", "noopener");
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it("does not add target for disabled external links", () => {
|
|
@@ -20,10 +20,66 @@ const isEmptyParagraph = (node: any): boolean =>
|
|
|
20
20
|
Array.isArray(node?.content) &&
|
|
21
21
|
node.content.every(
|
|
22
22
|
(child: any) =>
|
|
23
|
-
child?.nodeType === "text" &&
|
|
24
|
-
(!child.value || child.value.trim() === "")
|
|
23
|
+
child?.nodeType === "text" && (!child.value || child.value.trim() === "")
|
|
25
24
|
);
|
|
26
25
|
|
|
26
|
+
// A link is "external" only when it belongs to a different app than the one
|
|
27
|
+
// currently rendering this shared component (business vs. main/www are separate
|
|
28
|
+
// apps). Matching is environment-agnostic so a prod link like
|
|
29
|
+
// business.gokinetic.com is still "internal" while running on business-dev.
|
|
30
|
+
const hostFromUrl = (value?: string): string => {
|
|
31
|
+
if (!value) return "";
|
|
32
|
+
try {
|
|
33
|
+
const withProtocol = /^https?:\/\//i.test(value)
|
|
34
|
+
? value
|
|
35
|
+
: `https://${value}`;
|
|
36
|
+
return new URL(withProtocol).hostname.toLowerCase();
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// e.g. "business-dev.gokinetic.com" -> "gokinetic.com|business",
|
|
43
|
+
// "kinetic-dev.gokinetic.com" / "www.gokinetic.com" -> "gokinetic.com|main"
|
|
44
|
+
const appIdentity = (host: string): string => {
|
|
45
|
+
const clean = host.toLowerCase().replace(/\.$/, "");
|
|
46
|
+
const base = clean.split(".").slice(-2).join(".");
|
|
47
|
+
const subdomain = clean
|
|
48
|
+
.slice(0, clean.length - base.length)
|
|
49
|
+
.replace(/\.$/, "");
|
|
50
|
+
const firstLabel = (subdomain.split(".")[0] || "").replace(
|
|
51
|
+
/-(dev|development|staging|stage|qa|test|uat|prod|production)$/,
|
|
52
|
+
""
|
|
53
|
+
);
|
|
54
|
+
const app =
|
|
55
|
+
firstLabel === "" || firstLabel === "www" || firstLabel === "kinetic"
|
|
56
|
+
? "main"
|
|
57
|
+
: firstLabel;
|
|
58
|
+
return `${base}|${app}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getCurrentHost = (): string => {
|
|
62
|
+
const envHost =
|
|
63
|
+
hostFromUrl(process.env.NEXT_PUBLIC_SITE_URL) ||
|
|
64
|
+
hostFromUrl(process.env.NEXT_PUBLIC_MAIN_SITE_URL);
|
|
65
|
+
if (envHost) return envHost;
|
|
66
|
+
if (typeof window !== "undefined")
|
|
67
|
+
return window.location.hostname.toLowerCase();
|
|
68
|
+
return "";
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isExternalUrl = (url: string): boolean => {
|
|
72
|
+
if (!/^https?:\/\//i.test(url)) return false; // relative links are always internal
|
|
73
|
+
try {
|
|
74
|
+
const linkHost = new URL(url).hostname.toLowerCase();
|
|
75
|
+
const currentHost = getCurrentHost();
|
|
76
|
+
if (!currentHost) return true; // host unknown (SSR without env) → treat as external
|
|
77
|
+
return appIdentity(linkHost) !== appIdentity(currentHost);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
27
83
|
const defaultOptions: Options = {
|
|
28
84
|
renderMark: {
|
|
29
85
|
[MARKS.BOLD]: text => <strong className="font-black">{text}</strong>,
|
|
@@ -80,12 +136,12 @@ const defaultOptions: Options = {
|
|
|
80
136
|
|
|
81
137
|
[INLINES.HYPERLINK]: (node, children) => {
|
|
82
138
|
const url = (node as any)?.data?.uri as string;
|
|
83
|
-
const external =
|
|
139
|
+
const external = isExternalUrl(url);
|
|
84
140
|
return (
|
|
85
141
|
<a
|
|
86
142
|
href={url}
|
|
87
143
|
target={external ? "_blank" : undefined}
|
|
88
|
-
rel={external ? "noopener
|
|
144
|
+
rel={external ? "noopener" : undefined}
|
|
89
145
|
>
|
|
90
146
|
{children}
|
|
91
147
|
</a>
|
|
@@ -167,22 +223,26 @@ export function renderContentfulRichText(
|
|
|
167
223
|
...options?.renderNode,
|
|
168
224
|
// Logic for links based on the isTargetBlank flag
|
|
169
225
|
[BLOCKS.PARAGRAPH]: (node, children) => (
|
|
170
|
-
<div
|
|
226
|
+
<div
|
|
227
|
+
className={[className, "whitespace-pre-line"]
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(" ")}
|
|
230
|
+
>
|
|
171
231
|
{isEmptyParagraph(node) ? "\n" : children}
|
|
172
232
|
</div>
|
|
173
233
|
),
|
|
174
234
|
[INLINES.HYPERLINK]: (node, children) => {
|
|
175
235
|
const url = (node as any)?.data?.uri as string;
|
|
176
|
-
const external =
|
|
236
|
+
const external = isExternalUrl(url);
|
|
177
237
|
|
|
178
|
-
// Priority: 1. Manual flag from Contentful, 2.
|
|
238
|
+
// Priority: 1. Manual flag from Contentful, 2. Same-domain URLs are internal
|
|
179
239
|
const shouldOpenBlank = target ?? external;
|
|
180
240
|
|
|
181
241
|
return (
|
|
182
242
|
<Link
|
|
183
243
|
href={url}
|
|
184
|
-
target={shouldOpenBlank ? "_blank" :
|
|
185
|
-
rel={shouldOpenBlank ? "noopener
|
|
244
|
+
target={shouldOpenBlank ? "_blank" : undefined}
|
|
245
|
+
rel={shouldOpenBlank ? "noopener" : undefined}
|
|
186
246
|
variant="default"
|
|
187
247
|
className={linkClassName}
|
|
188
248
|
>
|