diff --git a/docs/references/api-reference.mdx b/docs/references/api-reference.mdx index e07980de34..02a9236af0 100644 --- a/docs/references/api-reference.mdx +++ b/docs/references/api-reference.mdx @@ -20,49 +20,41 @@ Complete API documentation for all Temporal SDKs and server APIs. href: "https://dotnet.temporal.io/api/", title: ".NET SDK API", description: "Complete .NET SDK API documentation with all namespaces, classes, and methods.", - external: true, }, { href: "https://pkg.go.dev/go.temporal.io/sdk", title: "Go SDK API", description: "Complete Go SDK API documentation on pkg.go.dev with all packages, types, and methods.", - external: true, }, { href: "https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/index.html", title: "Java SDK API", description: "Complete Java SDK API documentation on javadoc.io with all classes, interfaces, and annotations.", - external: true, }, { href: "https://php.temporal.io/namespaces/temporal.html", title: "PHP SDK API", description: "Complete PHP SDK API documentation with all namespaces, classes, and interfaces.", - external: true, }, { href: "https://python.temporal.io/", title: "Python SDK API", description: "Complete Python SDK API documentation with all modules, classes, and functions.", - external: true, }, { href: "https://ruby.temporal.io/", title: "Ruby SDK API", description: "Complete Ruby SDK API documentation with all modules, classes, and methods.", - external: true, }, { href: "https://docs.rs/temporalio-sdk/latest/temporalio_sdk/", title: "Rust SDK API", description: "Complete Rust SDK API documentation on docs.rs with all modules, structs, and associated functions/methods.", - external: true, }, { href: "https://typescript.temporal.io", title: "TypeScript SDK API", description: "Complete TypeScript SDK API documentation with all interfaces, types, and namespaces.", - external: true, }, ]} /> diff --git a/docs/security.mdx b/docs/security.mdx index b595e9e9ec..7d798863c2 100644 --- a/docs/security.mdx +++ b/docs/security.mdx @@ -18,7 +18,6 @@ Find security information for your Temporal deployment, whether you're using Tem href: "https://trust.temporal.io", title: "Company Security", description: "Learn about Temporal Technologies' general security practices, compliance certifications, and organizational security measures.", - external: true, }, { href: "/evaluate/cloud/security", @@ -34,6 +33,5 @@ Find security information for your Temporal deployment, whether you're using Tem href: "https://temporal.io/pages/cloud-security-white-paper", title: "Temporal Cloud Security Whitepaper", description: "Learn how Temporal Cloud provides provable security by design - orchestrating encrypted workflows without ever accessing your sensitive data.", - external: true, }, ]} /> diff --git a/src/components/PatternCards.tsx b/src/components/PatternCards.tsx index 567bb81f81..c5eed3b143 100644 --- a/src/components/PatternCards.tsx +++ b/src/components/PatternCards.tsx @@ -1,12 +1,12 @@ import React from 'react'; import Link from '@docusaurus/Link'; import useBaseUrl from '@docusaurus/useBaseUrl'; +import { isExternalHref } from '@site/src/utils/links'; type PatternCardItem = { href: string; title: string; description: string; - external?: boolean; icon?: string; }; @@ -31,7 +31,8 @@ export default function PatternCards({ items, className }: PatternCardsProps) { key={item.href} to={item.href} className="pattern-card" - {...(item.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})} + target={isExternalHref(item.href) ? '_blank' : undefined} + rel={isExternalHref(item.href) ? 'noopener noreferrer' : undefined} >
{item.icon ? ( diff --git a/src/components/TemporalNavbarLink.js b/src/components/TemporalNavbarLink.js new file mode 100644 index 0000000000..6c0aea726b --- /dev/null +++ b/src/components/TemporalNavbarLink.js @@ -0,0 +1,91 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { isRegexpStringMatch } from '@docusaurus/theme-common'; +import IconExternalLink from '@theme/Icon/ExternalLink'; +import { isExternalHref } from '@site/src/utils/links'; + +/** + * Registered as the 'default' navbar item type (see NavbarItem/ComponentTypes), + * replacing every plain navbar link/label item — top-level and dropdown + * children alike. Docusaurus's own default opens every absolute URL in a new + * tab and shows the external-link icon for it, including links to other + * temporal.io properties (learn.temporal.io, community.temporal.io, etc.). + * This reimplements that rendering (mirroring NavbarItem/DefaultNavbarItem + + * NavbarItem/NavbarNavLink) so both the tab behavior and the icon use the + * temporal.io-aware check instead. It's also the place to add any future + * top-nav item behavior, since it's ours to extend. + */ +export default function TemporalNavbarLink({ + mobile = false, + position, // consumed only so it isn't spread onto the DOM + isDropdownItem = false, + className, + activeClassName, + activeBasePath, + activeBaseRegex, + to, + href, + label, + html, + prependBaseUrlToHref, + ...props +}) { + const toUrl = useBaseUrl(to); + const activeBaseUrl = useBaseUrl(activeBasePath); + const normalizedHref = useBaseUrl(href, { forcePrependBaseUrl: true }); + const external = isExternalHref(href ?? to); + + const linkClassName = clsx( + isDropdownItem ? 'dropdown__link' : mobile ? 'menu__link' : 'navbar__item navbar__link', + className, + ); + const resolvedActiveClassName = + activeClassName ?? (mobile ? 'menu__link--active' : 'navbar__link--active'); + + const content = html ? ( + + ) : ( + <> + {label} + {external && } + + ); + + const link = href ? ( + + {content} + + ) : ( + + activeBaseRegex + ? isRegexpStringMatch(activeBaseRegex, location.pathname) + : location.pathname.startsWith(activeBaseUrl), + })} + {...props} + > + {content} + + ); + + if (isDropdownItem) { + return
  • {link}
  • ; + } + if (mobile) { + return
  • {link}
  • ; + } + return link; +} diff --git a/src/components/elements/GridCard/GridCard.tsx b/src/components/elements/GridCard/GridCard.tsx index 19ff6bfdbc..b27ec534d0 100644 --- a/src/components/elements/GridCard/GridCard.tsx +++ b/src/components/elements/GridCard/GridCard.tsx @@ -1,24 +1,9 @@ import React from 'react'; import Link from '@docusaurus/Link'; import clsx from 'clsx'; +import { isExternalHref } from '@site/src/utils/links'; import styles from './GridCard.module.css'; -function isExternal(href: string): boolean { - return href.startsWith('http://') || href.startsWith('https://'); -} - -// A link to temporal.io itself (e.g. the Code Exchange) still opens in a new -// tab like any other external href, but doesn't get the external-link icon — -// it isn't "leaving Temporal" the way a partner's docs site is. -function isOffTemporalDomain(href: string): boolean { - try { - const { hostname } = new URL(href); - return hostname !== 'temporal.io' && !hostname.endsWith('.temporal.io'); - } catch { - return true; - } -} - function ExternalLinkIcon() { return (

    {title} - {showExternalIcon && } + {external && }

    {description}

    diff --git a/src/components/elements/Sdk/SdkOverviewCards/SdkOverviewCards.tsx b/src/components/elements/Sdk/SdkOverviewCards/SdkOverviewCards.tsx index 7021459146..677e588f92 100644 --- a/src/components/elements/Sdk/SdkOverviewCards/SdkOverviewCards.tsx +++ b/src/components/elements/Sdk/SdkOverviewCards/SdkOverviewCards.tsx @@ -9,6 +9,7 @@ import Link from '@docusaurus/Link'; import SdkSvg from '../../SdkSvgs/SdkSvg'; import { SDKS } from '../../../../constants/sdks'; import sdkVersions from '../../../../data/sdk-versions.json'; +import { isExternalHref } from '@site/src/utils/links'; import styles from './sdk-overview-cards.module.css'; export const SdkOverviewCards = () => { @@ -35,8 +36,8 @@ export const SdkOverviewCards = () => { API reference diff --git a/src/theme/Footer/LinkItem/index.js b/src/theme/Footer/LinkItem/index.js new file mode 100644 index 0000000000..ffcaffa2d6 --- /dev/null +++ b/src/theme/Footer/LinkItem/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import IconExternalLink from '@theme/Icon/ExternalLink'; +import { isExternalHref } from '@site/src/utils/links'; + +// Swizzled (not wrapped) because the default Footer/LinkItem decides the +// external-link icon from Docusaurus's own protocol-only "external" check, +// which isn't exposed as a prop — so wrapping it can't fix the icon to match +// the temporal.io-aware target/rel decision below. This otherwise mirrors the +// original implementation. +export default function FooterLinkItem({ item }) { + const { to, href, label, prependBaseUrlToHref, className, ...props } = item; + const toUrl = useBaseUrl(to); + const normalizedHref = useBaseUrl(href, { forcePrependBaseUrl: true }); + const external = isExternalHref(to ?? href); + + return ( + + {label} + {external && } + + ); +} diff --git a/src/theme/Footer/Logo/index.js b/src/theme/Footer/Logo/index.js new file mode 100644 index 0000000000..3ad475696b --- /dev/null +++ b/src/theme/Footer/Logo/index.js @@ -0,0 +1,47 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import { useBaseUrlUtils } from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; +import { isExternalHref } from '@site/src/utils/links'; +import styles from './styles.module.css'; + +function LogoImage({ logo }) { + const { withBaseUrl } = useBaseUrlUtils(); + const sources = { + light: withBaseUrl(logo.src), + dark: withBaseUrl(logo.srcDark ?? logo.src), + }; + return ( + + ); +} + +// Swizzled (not wrapped) because the default only ever overrides `target`, +// leaving Link's default rel="noopener noreferrer" in place even when the +// link — https://temporal.io — doesn't leave the temporal.io family and +// doesn't open a new tab. Otherwise mirrors the original implementation. +export default function FooterLogo({ logo }) { + if (!logo.href) { + return ; + } + + const external = isExternalHref(logo.href); + return ( + + + + ); +} diff --git a/src/theme/Footer/Logo/styles.module.css b/src/theme/Footer/Logo/styles.module.css new file mode 100644 index 0000000000..faf0e60f3e --- /dev/null +++ b/src/theme/Footer/Logo/styles.module.css @@ -0,0 +1,9 @@ +.footerLogoLink { + opacity: 0.5; + transition: opacity var(--ifm-transition-fast) + var(--ifm-transition-timing-default); +} + +.footerLogoLink:hover { + opacity: 1; +} diff --git a/src/theme/Logo/index.js b/src/theme/Logo/index.js new file mode 100644 index 0000000000..c53de1d0ae --- /dev/null +++ b/src/theme/Logo/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import OriginalLogo from '@theme-original/Logo'; +import { useThemeConfig } from '@docusaurus/theme-common'; +import { isExternalHref } from '@site/src/utils/links'; + +// Used by Navbar/Logo (Footer/Logo is a separate component and already +// behaves correctly). The default only sets target/rel when navbar.logo.target +// is explicitly configured, so it falls through to Docusaurus's own default +// of opening every absolute URL in a new tab — including the logo's link to +// temporal.io itself. Cancel that for the temporal.io family. +export default function Logo(props) { + const { + navbar: { logo }, + } = useThemeConfig(); + + return isExternalHref(logo?.href) ? ( + + ) : ( + + ); +} diff --git a/src/theme/MDXComponents.tsx b/src/theme/MDXComponents.tsx index dc0c096aaf..b41a146b15 100644 --- a/src/theme/MDXComponents.tsx +++ b/src/theme/MDXComponents.tsx @@ -12,6 +12,7 @@ import TabItem from '@theme/TabItem'; import LLMActions from '@site/src/components/LLMActions/LLMActions'; import ZoomableImage from '@site/src/components/elements/Images/ZoomableImage'; import NoZoom from '@site/src/components/elements/Images/NoZoom'; +import { isExternalHref } from '@site/src/utils/links'; import styles from '@site/src/theme/DocItem/Content/styles.module.css'; function H1WithLLMActions(props: React.ComponentProps<'h1'>): JSX.Element { @@ -23,6 +24,19 @@ function H1WithLLMActions(props: React.ComponentProps<'h1'>): JSX.Element { ); } +// The default (@theme/MDXComponents/A) opens every absolute URL in a new +// tab, including links to other temporal.io properties. Cancel target/rel for +// the temporal.io family so prose links behave like they do on temporal.io, +// while keeping everything else (footnote anchor styling, etc.) unchanged. +const DefaultA = MDXComponents.a; +function A(props: React.ComponentProps & { href?: string }): JSX.Element { + return isExternalHref(props.href) ? ( + + ) : ( + + ); +} + export default { ...MDXComponents, Tabs, @@ -30,4 +44,5 @@ export default { NoZoom, h1: H1WithLLMActions, img: ZoomableImage, + a: A, }; \ No newline at end of file diff --git a/src/theme/NavbarItem/ComponentTypes.js b/src/theme/NavbarItem/ComponentTypes.js index 9432c0ee82..f37458edbc 100644 --- a/src/theme/NavbarItem/ComponentTypes.js +++ b/src/theme/NavbarItem/ComponentTypes.js @@ -1,7 +1,9 @@ import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes'; import AskAiNavbarItem from '@site/src/components/AskAiNavbarItem'; +import TemporalNavbarLink from '@site/src/components/TemporalNavbarLink'; export default { ...ComponentTypes, + default: TemporalNavbarLink, 'custom-askAI': AskAiNavbarItem, -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/links.ts b/src/utils/links.ts new file mode 100644 index 0000000000..42f143902e --- /dev/null +++ b/src/utils/links.ts @@ -0,0 +1,16 @@ +/** + * Mirrors the `isExternalHref` check temporal.io itself uses to decide which + * links open in a new tab. A link only counts as external if its hostname + * isn't `temporal.io` or a `*.temporal.io` subdomain — so links between + * Temporal properties (docs.temporal.io, learn.temporal.io, community.temporal.io, + * etc.) stay in the same tab, just like they do on temporal.io. + */ +export function isExternalHref(href: string | null | undefined): boolean { + if (!href || !href.startsWith('http')) return false; + try { + const { hostname } = new URL(href); + return hostname !== 'temporal.io' && !hostname.endsWith('.temporal.io'); + } catch { + return false; + } +}