import { type LinkField, type LinkResolverFunction, type PrismicDocument, asLinkAttrs, type AsLinkAttrsConfig, } from "@prismicio/client" import { DEV } from "esm-env" import { type ComponentProps, type ComponentType, type ForwardedRef, type HTMLAttributeAnchorTarget, type ReactNode, forwardRef, } from "react" import { devMsg } from "./lib/devMsg.js" /** The default component rendered for internal and external links. */ const defaultComponent = "a" /** Props provided to a component when rendered with ``. */ export interface LinkProps { /** The URL to link. */ href: string /** * The `target` attribute for anchor elements. If the Prismic field is configured to open in a new * window, this prop defaults to `_blank`. */ target?: HTMLAttributeAnchorTarget /** * The `rel` attribute for anchor elements. If the `target` prop is set to `"_blank"`, this prop * defaults to `"noopener noreferrer"`. */ rel?: string /** Children for the component. */ children?: ReactNode } export type PrismicLinkProps< InternalComponentProps = ComponentProps, ExternalComponentProps = ComponentProps, > = Omit & { /** * The `rel` attribute for the link. By default, `"noreferrer"` is provided if the link's URL is * external. This prop can be provided a function to use the link's metadata to determine the * `rel` value. */ rel?: string | AsLinkAttrsConfig["rel"] /** * The link resolver used to resolve links. * * @remarks * If your app uses route resolvers when querying for your Prismic repository's content, a link * resolver does not need to be provided. * @see Learn about link resolvers and route resolvers {@link https://prismic.io/docs/routes} */ linkResolver?: LinkResolverFunction /** * The component rendered for internal URLs. Defaults to ``. * * If your app uses a client-side router that requires a special Link component, provide the Link * component to this prop. */ internalComponent?: ComponentType /** The component rendered for external URLs. Defaults to ``. */ externalComponent?: ComponentType /** * The children to render for the link. If no children are provided, the link's `text` property * will be used. */ children?: ReactNode } & ( | { document: PrismicDocument | null | undefined href?: never field?: never } | { field: LinkField | null | undefined href?: never document?: never } | { href: LinkProps["href"] field?: LinkField | null | undefined document?: never } ) /** * Renders a link from a Prismic link field or page. * * @example * ;```tsx * Click here; * ``` * * @see Learn how to display links and use variants for styling: {@link https://prismic.io/docs/fields/link} */ export const PrismicLink = forwardRef(function PrismicLink< InternalComponentProps = ComponentProps, ExternalComponentProps = ComponentProps, >( props: PrismicLinkProps, ref: ForwardedRef, ) { const { field, document: doc, linkResolver, internalComponent, externalComponent, children, ...restProps } = props if (DEV) { if (field) { if (!field.link_type) { console.error( `[PrismicLink] This "field" prop value caused an error to be thrown.\n`, field, ) throw new Error( `[PrismicLink] The provided field is missing required properties to properly render a link. The link will not render. For more details, see ${devMsg( "missing-link-properties", )}`, ) } else if ( ("text" in field ? Object.keys(field).length > 2 : Object.keys(field).length > 1) && !("url" in field || "uid" in field || "id" in field) ) { console.warn( `[PrismicLink] The provided field is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg( "missing-link-properties", )}`, field, ) } } else if (doc) { if (!("url" in doc || "id" in doc)) { console.warn( `[PrismicLink] The provided document is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg( "missing-link-properties", )}`, doc, ) } } } const { href: computedHref, rel: computedRel, ...attrs } = asLinkAttrs(field ?? doc, { linkResolver, rel: typeof restProps.rel === "function" ? restProps.rel : undefined, }) let rel: string | undefined = computedRel if ("rel" in restProps && typeof restProps.rel !== "function") { rel = restProps.rel } const href = ("href" in restProps ? restProps.href : computedHref) || "" const InternalComponent = (internalComponent || defaultComponent) as ComponentType const ExternalComponent = (externalComponent || defaultComponent) as ComponentType const Component = href ? isInternalURL(href) ? InternalComponent : ExternalComponent : InternalComponent return ( {"children" in props ? children : field?.text} ) }) as < InternalComponentProps = ComponentProps, ExternalComponentProps = ComponentProps, >( props: PrismicLinkProps & { ref?: ForwardedRef }, ) => ReactNode /** * Determines if a URL is internal or external. * * @param url - The URL to check if internal or external. * @returns `true` if `url` is internal, `false` otherwise. */ // TODO: This does not detect all relative URLs as internal such as `about` or `./about`. This function assumes relative URLs start with a "/" or "#"`. export function isInternalURL(url: string): boolean { const isInternal = /^(\/(?!\/)|#)/.test(url) const isSpecialLink = !isInternal && !/^https?:\/\//.test(url) return isInternal && !isSpecialLink }