@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.41",
3
+ "version": "0.2.42",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -155,14 +155,14 @@ describe("Link", () => {
155
155
  expect(el).toHaveAttribute("target", "_blank");
156
156
  });
157
157
 
158
- it("adds rel=noopener noreferrer for external links", () => {
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 noreferrer");
165
+ expect(el).toHaveAttribute("rel", "noopener");
166
166
  });
167
167
 
168
168
  it("does not add target for disabled external links", () => {
@@ -88,7 +88,7 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
88
88
  ...(external &&
89
89
  !disabled && {
90
90
  target: "_blank",
91
- rel: "noopener noreferrer",
91
+ rel: "noopener",
92
92
  }),
93
93
  ...(disabled && {
94
94
  "aria-disabled": true,
@@ -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 = /^https?:\/\//.test(url);
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 noreferrer" : undefined}
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 className={[className, "whitespace-pre-line"].filter(Boolean).join(" ")}>
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 = /^https?:\/\//.test(url);
236
+ const external = isExternalUrl(url);
177
237
 
178
- // Priority: 1. Manual flag from Contentful, 2. Regex check for external URLs
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" : "_self"}
185
- rel={shouldOpenBlank ? "noopener noreferrer" : undefined}
244
+ target={shouldOpenBlank ? "_blank" : undefined}
245
+ rel={shouldOpenBlank ? "noopener" : undefined}
186
246
  variant="default"
187
247
  className={linkClassName}
188
248
  >