> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Reorganize your structure without breaking links

Rename, regroup, and re-nest Structure Builder items without breaking editors' saved links or dropping document types from the structure.

As a studio grows, its structure gets renamed, regrouped, and re-nested. These changes only affect navigation in the Structure Tool. They don't change documents, document IDs, or your schema, so a reorganization never needs a content migration. What they can break is URLs: bookmarks and shared links store the path through your structure, and a document type you forget to place drops out of the structure entirely.

This guide shows how to keep list item IDs stable through renames, give editors links that survive a reorganization, and check that every document type still has a place in your structure.

## Prerequisites

- A studio with a custom structure resolver. See [Get started with Structure Builder API](https://www.sanity.io/docs/studio/structure-builder-introduction).
- Optional: Vitest or another test runner, for the test in the "Fail CI when a document type has no place" section.

## How structure URLs store the path to a pane

The Structure Tool writes every open pane to the URL as an ID, separated by semicolons. Opening a list item with the ID `blogPosts`, then a document inside it, gives a URL like this:

```text
YOUR_STUDIO_URL/structure/blogPosts;a1b2c3d4
```

`YOUR_STUDIO_URL` is your studio's address, including any base path, such as `https://example.com/studio`. In a studio with more than one workspace, it also includes the workspace's base path.

When a studio loads a URL, it resolves each ID in order against the current structure. If an ID no longer matches an item in its parent list, the Structure Tool opens the panes up to the last ID that still matches and stops there. The pane for the missing ID and every pane after it, including the document, don't open. In a structure built with `S.list().items()`, the editor doesn't see an error. The only signal is a warning in the browser console. For the example URL, if the `blogPosts` item no longer exists, the warning reads `Pane resolution error at index 1: Pane returned no child`.

Two kinds of change break saved URLs:

- Changing a list item's ID, including an ID that Studio infers from a title you edit.
- Moving a list item to a different parent list. The item keeps its ID, but the path to it changes.

Reordering items within a list, adding new items or dividers, and editing the title of an item with an explicit ID keep every saved URL working.

## Pin list item IDs before renaming titles

If you don't call `.id()` on a list item, Studio infers the ID from the title by converting it to camelCase and transliterating characters that aren't allowed in IDs. `Blog posts` becomes `blogPosts`, and `Ürün kataloğu` becomes `urunKatalogu`. Editing the title then changes the ID and breaks every saved URL that passes through the item.

Before you rename an item, set its ID to the value it has today. To find it, open the item in Studio and copy its segment from the URL: the part after `/structure/`, up to the next semicolon. Then change the title:

**structure/index.ts**

```typescript
import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        // Inferred from the old title, "Blog posts". Pinning it keeps saved URLs working.
        .id('blogPosts')
        .title('Articles')
        .child(S.documentTypeList('post').title('Articles')),
    ])
```

Items created with `S.documentTypeListItem()` already use the schema type name as their ID, so you can change their titles without pinning anything.

> [!TIP]
> Set IDs on new list items
> Give every `S.listItem()` an explicit `.id()` when you create it, as the [Structure Builder API reference](https://www.sanity.io/docs/studio/structure-builder-reference) recommends. Titles are then free to change during any later reorganization.

## Share intent links instead of structure URLs

An intent link names a document, not the path to it:

```text
YOUR_STUDIO_URL/intent/edit/id=DOCUMENT_ID;type=DOCUMENT_TYPE
```

Replace `DOCUMENT_ID` with the document's ID and `DOCUMENT_TYPE` with its schema type name, such as `post`. `YOUR_STUDIO_URL` is the same studio address as in a structure URL.

When Studio opens an intent link, it searches the current structure for a pane that can show the document and opens it at the match closest to the root. If nothing in the structure matches, Studio opens the document in a fallback editor outside your structure. Because Studio resolves the path when the link is opened, an intent link keeps working after you rename, move, or regroup list items.

After an intent link resolves, Studio replaces it in the address bar with the structure URL. An editor who bookmarks the page at that point saves the path, not the intent link. Use intent links wherever you control the URL, such as dashboards, notifications, front-end edit buttons, and team wikis. For how to build them, see [Create Studio edit intent links](https://www.sanity.io/docs/visual-editing/studio-edit-intent-links).

Intent links only open a document in a list that Studio can match to the document's type. Lists from `S.documentTypeList()` and `S.documentTypeListItem()` match when their filter names the schema type. By default, a plain `S.documentList()` matches when it opens directly from the root list and its filter names the type. If your reorganization nests a `documentList` deeper or gives it a filter that doesn't name the type, add `canHandleIntent` so intent links still route into it. See [Handle intents in the Structure Tool](https://www.sanity.io/docs/studio/handle-intents-in-structure).

## Check that every document type has a place

Rebuilding lists by hand can drop a document type from the structure. Its documents still exist and still show up in search, but editors can't browse to them. Two patterns catch this.

### List unplaced types automatically

Keep the names of the types you place in a set, and end the root list with `S.documentTypeListItems()` filtered to everything else. A type you forget to place still appears at the bottom of the root list.

**structure/placement.ts**

```typescript
// Every document type you place in the structure
export const PLACED_TYPES = new Set(['post', 'author', 'category'])
```

**structure/index.ts**

```typescript
import type {StructureResolver} from 'sanity/structure'
import {PLACED_TYPES} from './placement'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .id('blogPosts')
        .title('Articles')
        .child(
          S.list()
            .title('Articles')
            .items([
              S.documentTypeListItem('post').title('Posts'),
              S.documentTypeListItem('author').title('Authors'),
              S.documentTypeListItem('category').title('Categories'),
            ]),
        ),
      S.divider(),
      ...S.documentTypeListItems().filter((item) => !PLACED_TYPES.has(item.getId() ?? '')),
    ])
```

`S.documentTypeListItems()` leaves out schema types used by a singleton registered in `document.singletons`, so this fallback doesn't list them. This requires `sanity` 6.17.0 or later. Place singletons explicitly, as shown in [Create a link to a single edit page in your main document type list](https://www.sanity.io/docs/studio/create-a-link-to-a-single-edit-page-in-your-main-document-type-list).

### Fail CI when a document type has no place

To make placement a deliberate decision, add a test that compares the document types in your schema against the `PLACED_TYPES` set in `structure/placement.ts`. It runs in Node without starting Studio.

**structure/placement.test.ts**

```typescript
import {expect, test} from 'vitest'
import {schemaTypes} from '../schemaTypes'
import {PLACED_TYPES} from './placement'

test('every document type has a place in the structure', () => {
  const unplaced = schemaTypes
    .filter((schemaType) => schemaType.type === 'document')
    .map((schemaType) => schemaType.name)
    .filter((name) => !PLACED_TYPES.has(name))

  expect(unplaced).toEqual([])
})
```

When someone adds a document type without placing it, the test fails and names the missing types:

```text
AssertionError: expected [ 'newsletter' ] to deeply equal []
```

The test reads `type` and `name` from the schema type definitions exported by `schemaTypes/index.ts`. If your schema lives elsewhere, change the import path.

## Next steps

- [Handle intents in the Structure Tool](https://www.sanity.io/docs/studio/handle-intents-in-structure): make filtered and nested document lists receive intent links.
- [Create Studio edit intent links](https://www.sanity.io/docs/visual-editing/studio-edit-intent-links): build links that open a document from outside Studio.
- [Structure Builder API reference](https://www.sanity.io/docs/studio/structure-builder-reference): every builder method, including `.id()`.

