npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sanity-plugin-link-field

v1.7.0

Published

A custom Link field for Sanity Studio

Downloads

29,807

Readme

🔗 sanity-plugin-link-field

Latest Stable Version Weekly Downloads License Made by Winter

A custom Link field (and associated React component) that allows editors to easily create internal and external links, as well as mailto, tel-links, and more, all using the same intuitive UI.

🔌 Installation

npm install sanity-plugin-link-field

🗒️ Setup

1. Configure the plugin

Add the plugin to your sanity.config.ts:

// sanity.config.ts
import {defineConfig} from 'sanity'
import {linkField} from 'sanity-plugin-link-field'

export default defineConfig({
  //...
  plugins: [linkField()],
})

This will enable the new link field type. By default, it will allow internal links to point to any document of the type page. You can adjust this according to your needs by using the linkableSchemaTypes option:

// ...
export default defineConfig({
  //...
  plugins: [
    linkField({
      linkableSchemaTypes: ['page', 'product', 'article'],
    }),
  ],
})

If you set it to an empty array, the internal link option will be hidden entirely for all link fields.

[!TIP] See Options for all the plugin level options you can set.

2. Add the field to your schema

You can now use the link type throughout your schema:

// mySchema.ts
import {defineField, defineType} from 'sanity'

export const mySchema = defineType({
  // ...
  fields: [
    // ...
    defineField({
      name: 'link',
      title: 'Link',
      type: 'link',
    }),
  ],
})

Editors will be able to switch between internal links (using native references in Sanity), external links (for linking to other websites) as well as e-mail (mailto) and phone (tel) links:

Additional built-in link types (asset, sms, whatsapp, and fax) are available but hidden by default. Enable them with enabledBuiltInLinkTypes in your plugin or field options (see Options).

The link object also includes additional fields for adding custom URL parameters and/or URL fragments to the end of an internal or external link. This can be used to add UTM campaign tracking or link to specific sections of a page, respectively. If you use the provided Link component, these will be handled automatically on the frontend.

You can also choose to enable an additional input field for setting the link's text/label:

defineField({
  name: 'link',
  title: 'Link',
  type: 'link',
  options: {
    enableText: true
  }
})

[!TIP] See Options for all the field level options you can set.

3. Making a required link field

Since the link field is just an object field internally, the normal .required() validator will not work. Instead, the plugin includes a helper to properly validate a link field and make it required:

import {requiredLinkField} from 'sanity-plugin-link-field'

// ...
defineField({
  name: 'link',
  title: 'Link',
  type: 'link',
  validation: (rule) => rule.custom(requiredLinkField),
})

Pass requiredLinkField directly to rule.custom (rather than wrapping it) so it also receives the validation context. This lets it enforce the requireText option in addition to requiring the link itself.

[!NOTE] A field-level validation overrides the link type's own validation. requiredLinkField re-checks requireText for you, but if you were to write a fully custom validation that doesn't call requiredLinkField, it would not enforce requireText.

4. Rendering links on the frontend

Spreading internal links

In order to render internal links in your frontend, you need to add a projection to your groq query so that the relevant fields (such as the slug) are included from the linked documents:

*[_type == "page" && slug.current == $slug][0] {
  // ...
  link {
    ...,
    internalLink->{_type,slug,title}
  },
}

Rendering links

How you render your links is up to you and will depend on your frontend framework of choice, as well as how you manage slugs/pathnames in your project. This plugin does include a simple React component to render the link correctly regardless of its type:

import {Link} from 'sanity-plugin-link-field/component'

import {resolveHref} from '@/lib/sanity/sanity.links'

// ...
<Link
  link={link}
  hrefResolver={({internalLink}) => resolveHref(internalLink?._type, internalLink?.slug?.current)}
>
  This is my link
</Link>
// ...

Notice the hrefResolver property. This is a callback used to resolve the href for internal links, and will differ depending on how your project is set up. The example above uses a resolveHref function defined elsewhere that will return the correct path depending on the document type and slug.

If a hrefResolver is not provided, the component will naively attempt to look at the slug property of the linked document and generate a href like so: /${link.internalLink.slug?.current}. This will of course only work on the off chance that your documents all have a slug property (like if you're using this approach to managing slugs).

For asset links, pass assetHrefResolver to resolve the Sanity file reference into a CDN or download URL (for example via @sanity/asset-utils or your project's file URL helper). Without it, asset links fall back to #.

Regardless of how you choose to manage slugs for internal links, the component will automatically handle external links, add target="_blank" as needed, and add mailto: to e-mail links as well as tel: to phone links. For tel: links, it will strip any spaces in the phone number since these are not allowed in such links. Additionally, it will render the link's text label (if enabled), or try and fall back to a good textual representation of the link if one hasn't been passed to the component (using the children property).

Using next/link or similar framework specific components

If you're using Next.js, you'll want to use next/link for routing. In this case, the Link component accepts an as property:

import {default as NextLink} from 'next/link'
import {Link} from 'sanity-plugin-link-field/component'

// ...
<Link link={link} as={NextLink}>
  This is my link
</Link>
// ...

To avoid having to remember to do this every time, you could create a convenience component in your project like so:

import { default as NextLink } from 'next/link';
import { Link as SanityLink, type LinkProps } from 'sanity-plugin-link-field/component';

export function Link(props: LinkProps) {
  return <SanityLink as={NextLink} hrefResolver={...} {...props} />;
}

You can then use it throughout your project:

import {Link} from '@/components/Link'

// ...
<Link link={link}>This is my link</Link>
// ...

Using with TypeScript

The plugin exports a type called LinkValue that you can use for your link fields.

5. Using with Portable Text

As with any other field, the link field can be used in a Portable Text editor by adding it as an annotation, eg:

defineArrayMember({
  type: 'block',
  marks: {
    annotations: [
      // ...
      {
        name: 'link',
        title: 'Link',
        type: 'link',
      },
    ],
  },
})

In this example, the built-in link annotation in Sanity will be replaced with a much more user-friendly and powerful link selector. If you want to keep the built-in link annotation as well, you can use a different name, such as customLink, in your own annotation.

You will need to adjust your groq queries to spread internal links:

content[] {
  ...,
  markDefs[]{
    ...,
    _type == "link" => {
      ...,
      internalLink->{_type,slug,title}
    }
  }
}

Finally, you'll need to adjust your frontend rendering logic to handle these links, something along the lines of:

marks: {
  link: ({ children, value }) => (
    <Link
      link={value}
      hrefResolver={...}
    >
      {children}
    </Link>
  )
}

⚙️ Advanced

Custom link types

In addition to the built-in link types, it's possible to define a set of custom link types for the user to choose from. This can be used to allow users to link to pre-defined routes that do not exist in Sanity, such as hardcoded routes in your frontend application or dynamic routes loaded from an external system.

To enable this feature, simply define your custom link types using the customLinkTypes property when initializing the plugin:

// sanity.config.ts
import {defineConfig} from 'sanity'
import {linkField} from 'sanity-plugin-link-field'

export default defineConfig({
  //...
  plugins: [
    linkField({
      customLinkTypes: [
        {
          title: 'Archive Page',
          value: 'archive',
          icon: OlistIcon,
          description: 'Link to an archive page.',
          options: [
            {
              title: 'Blog',
              value: '/blog',
            },
            {
              title: 'News',
              value: '/news',
            },
          ],
        },
      ],
    }),
  ],
})

The "Archive Page" type will now show up as an option when editing a link field, and selecting it will present the user with a dropdown menu with the available routes:

You can also provide a callback for the options parameter to load the available options dynamically. The callback will receive the current document, the path to the link field being edited, as well as the current user:

// ...
customLinkTypes: [
  // Load movies from external system
  {
    title: 'Movie',
    value: 'movie',
    icon: FilmIcon,
    description: 'Link to a movie from the cinema system.',
    options: async (document, fieldPath, user) => {
      // Do a fetch request here to get available movies from an API route

      // ...

      return options;
    }
]

Rendering custom links on the frontend

Custom link objects have the following structure in the schema, where url will be the value of the user-selected option:

{
  _type: 'link',
  blank: false,
  type: 'myType'
  value: 'myCustomValue'
}

How you handle this on the frontend is up to you; you can either pass the value directly to your <a> as its href or do any other processing you like with it; it's just a string value.

If you're using the built-in Link component, it will handle custom links just like external links, and use the value as the href. It will also add any custom parameters or anchors configured by the user, if enabled.

🔧 Options

Plugin level

When configuring the plugin in sanity.config.ts, these are the global options you can set. These will affect all link fields throughout your Studio.

| Option | Default Value | Description | | ------------- | ------------- | ------------- | | linkableSchemaTypes | ['page'] | An array of schema types that should be allowed in internal links. | | weakReferences | false | Make internal links use weak references | | referenceFilterOptions | undefined | Custom filter options passed to the reference input component for internal links. Use it to filter the documents that should be available for linking, eg. by locale. | | descriptions | See linkField.tsx | Override the descriptions of the different subfields. | | enabledBuiltInLinkTypes | ['internal', 'external', 'email', 'phone'] | Built-in link types that should be shown in the dropdown. Use this to enable optional built-in types like asset, sms, whatsapp, and fax. | | enableLinkParameters | true | Whether the user should be able to set custom URL parameters for internal and external links. | | enableAnchorLinks | true | Whether the user should be able to set custom anchors (URL fragments) for internal and external links. | | customLinkTypes | [] | Any custom link types that should be available in the dropdown. This can be used to allow users to link to pre-defined routes that don't exist within Sanity, such as hardcoded routes in the frontend application, or dynamic content that is pulled in from an external system. See Custom link types |

Field level

For each individual link field you add to your schema, you can set these options:

| Option | Default Value | Description | | ------------- | ------------- | ------------- | | enableText | false | Whether the link should include an optional field for setting the link text/label. If enabled, this will be available on the resulting link object under the .text property. | | requireText | false | Whether the text/label should be required when enableText is true. If you add field-level validation, ensure it includes requiredLinkField (passed directly to rule.custom) so the validation context is available. See Making a required link field for more details. | | textLabel | Text | The label for the text input field, if enabled using the enableText option. | | enabledBuiltInLinkTypes | undefined | Built-in link types to show for this specific field. Overrides the plugin-level enabledBuiltInLinkTypes. | | linkableSchemaTypes | undefined | Allowed schema types for internal links on this specific field. Overrides plugin-level linkableSchemaTypes. | | customLinkTypes | undefined | Custom link types for this specific field. Overrides plugin-level customLinkTypes. | | linkSectionLabel | undefined | Custom label for the link field section in the Studio for this specific field. This option is only available at the field level. | | weakReferences | undefined | Whether internal links should use weak references for this specific field. Overrides plugin-level weakReferences. | | referenceFilterOptions | undefined | Reference input filter options for this specific field. Overrides plugin-level referenceFilterOptions. |

🙌 Credits

This plugin was originally built and open-sourced by @marcusforsberg and Winter Agency.

Further development and improvements were contributed by @frodeste, generously sponsored by ProsessPilotene.

Thanks to

Additional contributions were made by:

🔏 License

MIT © Winter Agency

🧪 Develop & test

This plugin supports Sanity Studio v3 through v6.

Requirements: Node.js >=22.12 (see .nvmrc for the recommended LTS version) and npm >=11.17.

This plugin uses @sanity/plugin-kit with default configuration for build & watch scripts.

Example Studio

This repository includes a minimal example Studio in example/ that exercises the plugin, schema validation, and Sanity TypeGen.

npm run build
cd example
npm install
npm run dev

Run the integration checks (schema validate, schema extract, typegen, and schema assertions):

npm run test:example

See Testing a plugin in Sanity Studio for hot-reload development against an external Studio using npm run link-watch.

Release new version

Run "CI & Release" workflow. Make sure to select the main branch and check "Release new version".

Semantic release will only release on configured branches, so it is safe to run release on any branch.