👀 Our most exciting product launch yet 🚀 Join us May 8th for Sanity Connect

List Referring Documents (Backlinks) in Sanity

By Devin Halladay & Sanctuary Computer

This recipe is a UX affordance for Sanity Studio, which makes it easier for editors to manage heavily cross-referenced documents. It provides a list of backlinks to referring documents.

Warning

This schema is for an older version of Sanity Studio (v2), which is deprecated.

Learn how to migrate to the new Studio v3 →

cms/components/inputs/ReferencedBy.tsx

// ./cms/components/inputs/ReferencedBy.tsx

import React, { FC } from 'react';
import { SanityDocument } from '@sanity/client';
import { withDocument } from 'part:@sanity/form-builder';
import Spinner from 'part:@sanity/components/loading/spinner';
import { WithReferringDocuments } from 'part:@sanity/base/with-referring-documents';
import ReferringDocumentsList from './ReferringDocumentsList';
import FormField from 'part:@sanity/components/formfields/default';

// You'll need to type these properly
type Props = {
  referringDocuments: Record<string, any>[];
  isLoading: boolean;
  published?: SanityDocument | null;
};

const ReferencedBy: FC<Props> = React.forwardRef((props) => {
  const { document, type } = props;
  
  // Access options from the schema definition
  // const { referenceType } = type.options;

  return (
    <FormField
      label="Referenced By"
      description="Documents which reference this document"
    >
      <WithReferringDocuments id={document._id}>
        {({ referringDocuments, isLoading }) => {
          // Optional: Read options from the schema to filter by a specific type
          // const documents = referringDocuments.filter(
          //   (doc) => doc.type === referenceType
          // );

          if (isLoading) {
            return <Spinner message="Looking for referring documents…" />;
          }

          if (!referringDocuments.length) return null;

          return <ReferringDocumentsList documents={referringDocuments} />;
        }}
      </WithReferringDocuments>
    </FormField>
  );
});

export default withDocument(ReferencedBy);

cms/components/inputs/ReferringDocumentsList.tsx

// ./cms/components/inputs/ReferringDocumentsList.tsx

import React from 'react';
import Preview from 'part:@sanity/base/preview';
import { IntentLink } from 'part:@sanity/base/router';
import schema from 'part:@sanity/base/schema';
import {
  Item as DefaultItem,
  List as DefaultList,
} from 'part:@sanity/components/lists/default';
import styles from '../../styles/ReferringDocumentsList.css';

const ReferringDocumentsList = (props) => {
  const { documents } = props;
  return (
    <DefaultList className={styles.root}>
      {documents.map((document) => {
        const schemaType = schema.get(document._type);
        return (
          <DefaultItem key={document._id} className={styles.item}>
            {schemaType ? (
              <IntentLink
                className={styles.link}
                intent="edit"
                params={{ id: document._id, type: document._type }}
              >
                <Preview value={document} type={schemaType} />
              </IntentLink>
            ) : (
              <div>
                A document of the unknown type <em>{document._type}</em>
              </div>
            )}
          </DefaultItem>
        );
      })}
    </DefaultList>
  );
};

export default ReferringDocumentsList;

cms/styles/ReferringDocumentsList.css

/* ./cms/styles/ReferringDocumentsList.css */

@import 'part:@sanity/base/theme/variables-style';

.root {
  composes: root from 'part:@sanity/components/lists/default-style';
}

.item {
  composes: lineBetween from 'part:@sanity/components/lists/default-item-style';
  background: var(--white);
  padding: var(--medium-padding);
  border-radius: var(--border-radius-base);
  color: inherit;
  margin-bottom: var(--medium-padding);
  border: 1px solid var(--list-border-color);
}

.link {
  color: inherit;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

cms/schemas/documents/lesson.tsx

// ./cms/schemas/documents/lesson.tsx

import ReferencedBy from '../../components/inputs/ReferencedBy';

export default {
  name: 'lesson',
  title: 'Lesson',
  type: 'document',
  fields: [
    // Documents which reference this lesson
    {
      name: 'referringDocuments',
      title: 'Referenced By',
      type: 'array',
      of: [{ type: 'course' }, { type: 'post' }, { type: 'category' }, { type: 'resource' }],
      readOnly: true,
      inputComponent: ReferencedBy,
      // Optional: You can read this option in ReferencedBy.tsx to filter the list
      // of backlinks by document type.
      // options: {
      //   referenceType: 'course',
      // },
    },
  ],
};

Note

I've created an issue in the Sanity repo requesting to export the components we built via the Sanity parts system. If they're able to fulfill the request, I'll revise this recipe to be much more straightforward.

Let's say your documents are intended to be densely linked or networked via references. Using Index as an example, let's say we have the following document types:

  • Course — With array of references to Lesson and a single reference to Cohort
  • Lesson — Part of a single course, and can reference any document type in array.
  • Post — Generic post which can reference any document type in an array.
  • Curated Category — Unique post which can reference any document type in an array.
  • Resource — A link, file, or portable text document which can reference any document type.

Our intention is to allow editors to create networked documents, so that on the client we can surface rich "backlinks" to every document that references a given course, lesson, post, category, or resource.

This is a difficult mental model to establish for editors, so we need to add a nice UX affordance to make it easier to grok the network of references, and easily navigate between them.

So, we'll add a custom Referring Documents field to each of our document schemas, which will display all the Sanity documents that reference the one an editor is viewing.

How To

We'll be using undocumented higher order components and styles sourced from the package @sanity/desk-tool, namely enhanceWithReferringDocuments and WithReferringDocuments. You can find the source code for these components in the package here and here.

1. Create your custom input component — ReferencedBy.tsx

First we’ll build a custom input component to be used in your Sanity schema later. This component uses the sanity part part:@sanity/base/with-referring-documents to grab our referring documents and return our document list.

2. Create a document list component to display your backlinks — ReferringDocumentsList.tsx

Next, we'll build another component to displays our backlinks, again using helpful parts from Sanity. We’re using IntentLink to route the editor directly to the editor in click, and Preview to render UI just like Sanity’s document panes.

3. Add CSS for the document list — ReferringDocumentsList.css

Next, we'll extend CSS from the Sanity parts system to make our list look nice. You could also do this straight in your component if you are using any CSS in JS techniques. The variables come from Sanity Desk styles, which can also be overridden if needed.

4. Add your new input to the document schema — lesson.tsx

And finally, we'll add our custom input to the document schema.

And there you have it! Pretty cool right? Sanity is starting to invest a lot of effort into connected content which opens up a really exciting new world of possibilities for editing experiences and end user features. I hope they release this feature natively!

Contributors