@driftime/sanity-plugin-link 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +593 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1010 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +164 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +413 -0
- package/dist/render.js.map +1 -0
- package/dist/types-BR08fVY8.js +188 -0
- package/dist/types-BR08fVY8.js.map +1 -0
- package/dist/types-BXseTKgu.d.ts +176 -0
- package/dist/types-BXseTKgu.d.ts.map +1 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/icon-dark.svg" />
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/icon-light.svg" />
|
|
5
|
+
<img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/icon-light.svg" alt="Link plugin logo" width="48" />
|
|
6
|
+
</picture>
|
|
7
|
+
<h1>Link</h1>
|
|
8
|
+
<p><strong>A Sanity Studio plugin by Driftime®</strong></p>
|
|
9
|
+
<p>Links for Sanity Studio, covering every destination and resolved from routes declared once.</p>
|
|
10
|
+
<p>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@driftime/sanity-plugin-link"><img src="https://img.shields.io/npm/v/@driftime/sanity-plugin-link?style=flat-square&labelColor=1a1a1a&color=666666" alt="npm version" /></a>
|
|
12
|
+
<a href="https://github.com/driftime/sanity-plugins/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@driftime/sanity-plugin-link?style=flat-square&labelColor=1a1a1a&color=666666" alt="License: MIT" /></a>
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<br />
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
Every link on a site is a small decision about where a visitor goes next. Link gives authors one field for all of those decisions, in the document form and inside rich text, and gives developers one type in the schema and one function on the site.
|
|
21
|
+
|
|
22
|
+
Links to pages are stored as references, so they follow a page through every rename and move. The site's routes are declared once, with type checking, and from them the plugin derives the query that fetches each link and the `href` that renders it. A resolved link also knows whether it is external, whether it should open in a new tab, and whether it points at the current page, so navigation states and anchor attributes are already answered.
|
|
23
|
+
|
|
24
|
+
<br />
|
|
25
|
+
|
|
26
|
+
<figure>
|
|
27
|
+
<picture>
|
|
28
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-dialog-dark.png" />
|
|
29
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-dialog-light.png" />
|
|
30
|
+
<img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-dialog-light.png" alt="The link dialog in Sanity Studio on the Page tab, with tabs for each destination, a selected page, an anchor, two search parameters, and a label" />
|
|
31
|
+
</picture>
|
|
32
|
+
<p align="center"><sub><em>The link dialog on the Page tab, with a referenced page, an anchor, search parameters, and a label.</em></sub></p>
|
|
33
|
+
</figure>
|
|
34
|
+
|
|
35
|
+
<br />
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Link is built for Sanity Studio 6.10 and React 19 and declares both as peer dependencies, so the Studio needs to be on those versions already. Node 22.12 or later is required.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun add -E @driftime/sanity-plugin-link
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
<br />
|
|
46
|
+
|
|
47
|
+
## Studio
|
|
48
|
+
|
|
49
|
+
In the Studio, the plugin registers a `link` type and a Portable Text annotation, and takes the document types a page link can reference.
|
|
50
|
+
|
|
51
|
+
<br />
|
|
52
|
+
|
|
53
|
+
### Registering the Plugin
|
|
54
|
+
|
|
55
|
+
The plugin needs to know which document types are pages a link can point at. Register it with those types, and they are the only ones the reference browser offers.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { defineConfig } from "sanity";
|
|
59
|
+
import { linkPlugin } from "@driftime/sanity-plugin-link";
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
// ...
|
|
63
|
+
plugins: [
|
|
64
|
+
// ...
|
|
65
|
+
linkPlugin({ documentTypes: ["home", "page", "post"] }),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Purpose |
|
|
71
|
+
| --------------- | ------------------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `documentTypes` | `string[]` | required | Document types a page link can reference, in the order listed. |
|
|
73
|
+
| `destinations` | `SanityLinkDestination[]` | all | Destinations every link field offers. See [Restricting the Destinations](#restricting-the-destinations). |
|
|
74
|
+
| `title` | `SanityLinkTitleField` | reads `title` | Where a page's title is read from, for previews and as the label when none is written. |
|
|
75
|
+
|
|
76
|
+
<br />
|
|
77
|
+
|
|
78
|
+
### Adding a Link Field
|
|
79
|
+
|
|
80
|
+
A field of type `link` is defined like any other.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
defineField({
|
|
84
|
+
name: "link",
|
|
85
|
+
type: "link",
|
|
86
|
+
description: "Where the button takes the visitor.",
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Option | Type | Default | Purpose |
|
|
91
|
+
| -------------- | ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `destinations` | `SanityLinkDestination[]` | the plugin's list | Destinations this field offers. See [Restricting the Destinations](#restricting-the-destinations). |
|
|
93
|
+
|
|
94
|
+
As an array member it becomes a list of links, for example a navigation menu or a footer.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
defineField({
|
|
98
|
+
name: "menu",
|
|
99
|
+
type: "array",
|
|
100
|
+
of: [defineArrayMember({ type: "link" })],
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The type name is always `link`, so a Studio that already has a type by that name needs to rename it before installing.
|
|
105
|
+
|
|
106
|
+
<br />
|
|
107
|
+
|
|
108
|
+
<figure>
|
|
109
|
+
<picture>
|
|
110
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-fields-dark.png" />
|
|
111
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-fields-light.png" />
|
|
112
|
+
<img src="https://raw.githubusercontent.com/driftime/sanity-plugins/HEAD/packages/link/assets/link-fields-light.png" alt="Five link fields in a Sanity Studio document, each showing its destination's icon, label, and target" />
|
|
113
|
+
</picture>
|
|
114
|
+
<p align="center"><sub><em>Closed link fields in a document, each showing its destination and label.</em></sub></p>
|
|
115
|
+
</figure>
|
|
116
|
+
|
|
117
|
+
<br />
|
|
118
|
+
|
|
119
|
+
### Restricting the Destinations
|
|
120
|
+
|
|
121
|
+
A link field offers every destination unless restricted. Some fields are better with fewer, for example a contact link that should only ever be an email address or a phone number.
|
|
122
|
+
|
|
123
|
+
To set the default for every field, pass `destinations` to the plugin.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
linkPlugin({
|
|
127
|
+
documentTypes: ["home", "page", "post"],
|
|
128
|
+
destinations: ["page", "anchor", "url", "email"],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To set it for one field, pass `destinations` in the field's `options`.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
defineField({
|
|
136
|
+
name: "contact",
|
|
137
|
+
type: "link",
|
|
138
|
+
description: "How a visitor gets in touch.",
|
|
139
|
+
options: { destinations: ["email", "phone"] },
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A field's list **replaces** the plugin's list, so a field can offer any destination regardless of the plugin default. Destinations appear in the order listed. Restricting a field later is safe, because a link stored with a destination the field no longer offers still resolves.
|
|
144
|
+
|
|
145
|
+
<br />
|
|
146
|
+
|
|
147
|
+
### Links in Portable Text
|
|
148
|
+
|
|
149
|
+
Links inside body text deserve the same destinations as links anywhere else. `linkAnnotation` is a Portable Text annotation that opens the same dialog, with the highlighted text as the label. Add it to a block's `marks.annotations`.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { linkAnnotation } from "@driftime/sanity-plugin-link";
|
|
153
|
+
|
|
154
|
+
defineField({
|
|
155
|
+
name: "body",
|
|
156
|
+
type: "array",
|
|
157
|
+
of: [
|
|
158
|
+
defineArrayMember({
|
|
159
|
+
type: "block",
|
|
160
|
+
marks: { annotations: [linkAnnotation] },
|
|
161
|
+
}),
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`PortableTextLinkPlugins` makes pasting write `linkAnnotation`. Links in pasted formatted text, for example from a web page or a document, are kept, and a URL pasted over highlighted text turns that text into a link. Pass it to the array's `components.portableText.plugins`.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { linkAnnotation, PortableTextLinkPlugins } from "@driftime/sanity-plugin-link";
|
|
170
|
+
|
|
171
|
+
defineField({
|
|
172
|
+
name: "body",
|
|
173
|
+
type: "array",
|
|
174
|
+
components: { portableText: { plugins: PortableTextLinkPlugins } },
|
|
175
|
+
of: [
|
|
176
|
+
defineArrayMember({
|
|
177
|
+
type: "block",
|
|
178
|
+
marks: { annotations: [linkAnnotation] },
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
<br />
|
|
185
|
+
|
|
186
|
+
## Site
|
|
187
|
+
|
|
188
|
+
On the site, a link configuration declares the site's routes, GROQ fragments derived from those routes go into the site's queries, and `resolveLink` and `resolveRoute` turn what those queries fetch into anchors and paths. Each is covered in its own section.
|
|
189
|
+
|
|
190
|
+
Everything for the site is imported from `@driftime/sanity-plugin-link/render`. That path carries no Studio code, so it is safe in server components and anywhere else on the site.
|
|
191
|
+
|
|
192
|
+
<br />
|
|
193
|
+
|
|
194
|
+
### Creating the Link Configuration
|
|
195
|
+
|
|
196
|
+
The configuration is created once, in a file the rest of the site imports from, and holds the site's URL structure.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// config/links.ts
|
|
200
|
+
import { defineLinkConfig } from "@driftime/sanity-plugin-link/render";
|
|
201
|
+
|
|
202
|
+
export const { resolveLink, resolveRoute, routes, linkFragment, routeParamsFragment } = defineLinkConfig({
|
|
203
|
+
baseUrl: "https://acme.com",
|
|
204
|
+
routes: {
|
|
205
|
+
home: { path: "/" },
|
|
206
|
+
page: { path: "/[slug]", params: { slug: "slug.current" } },
|
|
207
|
+
post: { path: "/blog/[slug]", params: { slug: "slug.current" } },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
| Option | Type | Default | Purpose |
|
|
213
|
+
| ---------------------- | ----------------------- | ------------- | -------------------------------------------------------------------------------- |
|
|
214
|
+
| `baseUrl` | `string` | required | The site's origin, including the protocol. |
|
|
215
|
+
| `routes` | `SanityLinkRoutes` | none | A path pattern and its parameters for each document type. |
|
|
216
|
+
| `resolvers` | `SanityLinkResolvers` | none | A function per destination that adjusts its `href`. See [Resolvers](#resolvers). |
|
|
217
|
+
| `openExternalInNewTab` | `boolean` | `true` | Whether external links open in a new tab. See [External Links](#external-links). |
|
|
218
|
+
| `title` | `SanityLinkTitleConfig` | reads `title` | Where a page's title is read from. See [Page Titles](#page-titles). |
|
|
219
|
+
|
|
220
|
+
`baseUrl` is the site's origin. It decides what counts as external, and a URL an author enters at that origin is treated as internal and becomes a relative path.
|
|
221
|
+
|
|
222
|
+
`routes` maps each document type to the path the site renders it at. A `[name]` in a path is a parameter, and `params` gives a GROQ expression that fills it. Every type in `documentTypes` needs a route.
|
|
223
|
+
|
|
224
|
+
`defineLinkConfig` returns five values, all derived from the routes.
|
|
225
|
+
|
|
226
|
+
| Value | Type | Purpose |
|
|
227
|
+
| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
228
|
+
| `resolveLink` | `(props) => SanityLinkResolution` | Resolves a stored link when rendering. See [Rendering Links](#rendering-links). |
|
|
229
|
+
| `resolveRoute` | `(document) => string \| undefined` | Resolves a document to its path, for canonical URLs, sitemaps, and the Presentation Tool. See [Resolving Routes](#resolving-routes). |
|
|
230
|
+
| `routes` | `SanityLinkRoutes` | The routes passed in, typed. |
|
|
231
|
+
| `linkFragment` | `string` | GROQ for link projections. See [Link Projections](#link-projections). |
|
|
232
|
+
| `routeParamsFragment` | `string` | GROQ for queries of documents passed to `resolveRoute`. See [Document Projections](#document-projections). |
|
|
233
|
+
|
|
234
|
+
#### Route Parameters
|
|
235
|
+
|
|
236
|
+
A parameter is any GROQ expression, evaluated against the document, so a path can be built from whatever the document knows. It can be a field, such as `slug.current`, a field on a referenced document, such as `category->slug.current`, or a computed value, such as `string::split(publishedAt, "-")[0]`.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
defineLinkConfig({
|
|
240
|
+
// ...
|
|
241
|
+
routes: {
|
|
242
|
+
post: {
|
|
243
|
+
path: "/blog/[year]/[slug]",
|
|
244
|
+
params: { year: `string::split(publishedAt, "-")[0]`, slug: "slug.current" },
|
|
245
|
+
},
|
|
246
|
+
service: {
|
|
247
|
+
path: "/[category]/[slug]",
|
|
248
|
+
params: { category: "category->slug.current", slug: "slug.current" },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Every `[name]` in a path must have a key in `params`, and `params` can't have a key the path lacks. Either mismatch is a compile error, caught long before a broken link reaches a visitor.
|
|
255
|
+
|
|
256
|
+
<br />
|
|
257
|
+
|
|
258
|
+
### Querying
|
|
259
|
+
|
|
260
|
+
#### Link Projections
|
|
261
|
+
|
|
262
|
+
A page link is a reference until a query follows it. Add `linkFragment` to the projection of **every** link field and link annotation, after the `...`, and the query fetches exactly what resolving needs.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { linkFragment } from "@/config/links";
|
|
266
|
+
|
|
267
|
+
export const buttonFragment = `{ ..., link { ..., ${linkFragment} } }`;
|
|
268
|
+
|
|
269
|
+
export const bodyFragment = `body[] { ..., markDefs[] { ..., ${linkFragment} } }`;
|
|
270
|
+
|
|
271
|
+
export const homeQuery = `*[_type == "home"][0] {
|
|
272
|
+
...,
|
|
273
|
+
cta ${buttonFragment},
|
|
274
|
+
${bodyFragment}
|
|
275
|
+
}`;
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The fragment expands a page link's reference into the document's `_type`, title, and each parameter in its route, and expands a file link's asset into its URL. Because it is built from the routes, adding a route or a parameter changes what every query fetches without a single query being edited.
|
|
279
|
+
|
|
280
|
+
#### Document Projections
|
|
281
|
+
|
|
282
|
+
Add `routeParamsFragment` to the projection of any query whose documents are passed to `resolveRoute`. It computes each parameter in the document's route.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { routeParamsFragment } from "@/config/links";
|
|
286
|
+
|
|
287
|
+
export const pageQuery = `*[_type == "page" && slug.current == $slug][0] { ..., ${routeParamsFragment} }`;
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
<br />
|
|
291
|
+
|
|
292
|
+
### Rendering Links
|
|
293
|
+
|
|
294
|
+
`resolveLink` returns the `href`, the label, and the flags an anchor is built from. The markup stays with the site.
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { resolveLink } from "@/config/links";
|
|
298
|
+
import type { SanityLink } from "@driftime/sanity-plugin-link/render";
|
|
299
|
+
|
|
300
|
+
interface LinkProps {
|
|
301
|
+
link: SanityLink;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function Link({ link }: LinkProps) {
|
|
305
|
+
const { resolvedLink, opensNewTab } = resolveLink({ link });
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<a href={resolvedLink?.href} target={opensNewTab ? "_blank" : undefined}>
|
|
309
|
+
{resolvedLink?.label}
|
|
310
|
+
</a>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### Inputs
|
|
316
|
+
|
|
317
|
+
`resolveLink` takes one source and, for navigation, the current pathname.
|
|
318
|
+
|
|
319
|
+
| Input | Type | Purpose |
|
|
320
|
+
| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
321
|
+
| `link` | `SanityLink` | A stored link, fetched with `linkFragment` in its projection. |
|
|
322
|
+
| `route` | `SanityLinkRouteInput` | A link declared in code, as a document type and its parameters. Typed from the routes. |
|
|
323
|
+
| `href` | `string` | A plain `href`, for links that come from configuration rather than content. |
|
|
324
|
+
| `pathname` | `string` | The current path, needed only for the active-path values. In Next.js, `usePathname()` in a client component. |
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// A link the author chose
|
|
328
|
+
resolveLink({ link: page.cta });
|
|
329
|
+
|
|
330
|
+
// A navigation item, with the current path for its active state
|
|
331
|
+
resolveLink({ link: item.link, pathname });
|
|
332
|
+
|
|
333
|
+
// A link to a known document, for example a "Read the announcement" button
|
|
334
|
+
resolveLink({ route: { _type: "post", slug: "launch" } });
|
|
335
|
+
|
|
336
|
+
// A link to a page with no parameters
|
|
337
|
+
resolveLink({ route: { _type: "home" } });
|
|
338
|
+
|
|
339
|
+
// A link from configuration
|
|
340
|
+
resolveLink({ href: "https://github.com/driftime" });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
All three sources return the same shape, so one component renders every link on the site. When the source is missing or can't be resolved, `resolvedLink` is `undefined` and every flag is `false`, so an empty field renders safely without a check in front of it.
|
|
344
|
+
|
|
345
|
+
#### Output
|
|
346
|
+
|
|
347
|
+
| Value | Type | Purpose |
|
|
348
|
+
| -------------------- | -------------------- | --------------------------------------------------------------------- |
|
|
349
|
+
| `resolvedLink` | `SanityResolvedLink` | The `href`, `label`, and `download` for the anchor, or `undefined`. |
|
|
350
|
+
| `isExternal` | `boolean` | Whether the `href` has a different origin from `baseUrl`. |
|
|
351
|
+
| `opensNewTab` | `boolean` | Whether to set `target="_blank"`. |
|
|
352
|
+
| `hasAnchor` | `boolean` | Whether the `href` has a fragment. |
|
|
353
|
+
| `containsActivePath` | `boolean` | Whether the current pathname is the link's path or a path beneath it. |
|
|
354
|
+
| `isActivePath` | `boolean` | Whether the current pathname is exactly the link's path. |
|
|
355
|
+
|
|
356
|
+
`label` is the author's label, or the page's title for a page link with none. A `route` or `href` has no label. `download` is `true` for file links, and a page link's `href` includes the author's anchor and search parameters.
|
|
357
|
+
|
|
358
|
+
`containsActivePath` is for navigation items that stay active on child pages, and `isActivePath` is for the exact page only. Reading either without a `pathname` throws in development, so a navigation component that forgot to pass one fails at once instead of rendering nothing as active. In production the read returns `false`. `/` matches only itself, and a link with a fragment is never active.
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
interface NavigationLinkProps extends LinkProps {
|
|
362
|
+
pathname: string;
|
|
363
|
+
children?: React.ReactNode;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function NavigationLink({ link, pathname, children }: NavigationLinkProps) {
|
|
367
|
+
const { resolvedLink, opensNewTab, isActivePath } = resolveLink({ link, pathname });
|
|
368
|
+
if (!resolvedLink?.href) return children;
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<a
|
|
372
|
+
href={resolvedLink.href}
|
|
373
|
+
target={opensNewTab ? "_blank" : undefined}
|
|
374
|
+
download={resolvedLink.download}
|
|
375
|
+
aria-current={isActivePath ? "page" : undefined}
|
|
376
|
+
>
|
|
377
|
+
{children ?? resolvedLink.label}
|
|
378
|
+
</a>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
<br />
|
|
384
|
+
|
|
385
|
+
### Resolving Routes
|
|
386
|
+
|
|
387
|
+
Some documents are fetched directly rather than through a link, and still need their own URL, for a canonical link, a sitemap, or the Presentation Tool. `resolveRoute` returns the path for a document fetched with `routeParamsFragment` in its projection.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { resolveRoute } from "@/config/links";
|
|
391
|
+
|
|
392
|
+
export async function generateMetadata({ params }) {
|
|
393
|
+
const page = await client.fetch(pageQuery, params);
|
|
394
|
+
return { alternates: { canonical: resolveRoute(page) } };
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
It also takes a route declared in code, typed from the routes.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
resolveRoute({ _type: "post", slug: "launch" });
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
When a link or route resolves to `undefined`, a message in development says exactly why: an unexpanded reference, a type with no route, a parameter with no value, or a stored destination the plugin does not recognize.
|
|
405
|
+
|
|
406
|
+
<br />
|
|
407
|
+
|
|
408
|
+
### Options
|
|
409
|
+
|
|
410
|
+
#### External Links
|
|
411
|
+
|
|
412
|
+
External links open in a new tab by default, and downloads **never** do. Set `openExternalInNewTab` to `false` to keep every link in the same tab.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
defineLinkConfig({
|
|
416
|
+
// ...
|
|
417
|
+
openExternalInNewTab: false,
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Page Titles
|
|
422
|
+
|
|
423
|
+
A page link with no label falls back to the referenced document's title, read from `title`. When documents keep their title under another name, set `title.field` here and on the plugin, and the fragment, the label, and the Studio's previews all follow it.
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
defineLinkConfig({
|
|
427
|
+
// ...
|
|
428
|
+
title: { field: "name" },
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
When the title needs composing rather than reading, add a `resolver`. It receives the fetched document and returns the label.
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
defineLinkConfig({
|
|
436
|
+
// ...
|
|
437
|
+
title: { field: "name", resolver: (document) => `${document.name} | Acme Inc.` },
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### Resolvers
|
|
442
|
+
|
|
443
|
+
Occasionally an `href` needs something only the site can supply, for example a signed URL for files or a referral parameter on external links. A resolver is a function for one destination that receives the `href` the plugin built and returns the one to use.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
defineLinkConfig({
|
|
447
|
+
// ...
|
|
448
|
+
resolvers: {
|
|
449
|
+
file: (href, link) => signUrl(link.file?.asset),
|
|
450
|
+
url: (href) => `${href}?ref=acme`,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The second argument is the stored link, narrowed to the destination, so `link.file` is typed in `file` and `link.reference` in `page`. Return `undefined` to produce no link. For page links, the author's anchor and search parameters are appended after the resolver runs. Resolvers run for **stored links only**, so a `route` or `href` passed in code is returned as is.
|
|
456
|
+
|
|
457
|
+
A resolver can be `async`. If any resolver returns a promise, `resolveLink` returns a promise for every link, and its return type says so, so rendering code is written one way rather than two.
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
defineLinkConfig({
|
|
461
|
+
// ...
|
|
462
|
+
resolvers: {
|
|
463
|
+
file: async (href, link) => await signUrl(link.file?.asset),
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const { resolvedLink } = await resolveLink({ link });
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
<br />
|
|
471
|
+
|
|
472
|
+
## Reference
|
|
473
|
+
|
|
474
|
+
The rest of this document covers the shape of a stored link and everything the package exports.
|
|
475
|
+
|
|
476
|
+
<br />
|
|
477
|
+
|
|
478
|
+
### Stored Links
|
|
479
|
+
|
|
480
|
+
`SanityLink` is the type of a stored link. It is a discriminated union on `type`, so narrowing on `type` reveals that destination's fields. Pass a document type as a type parameter to type a page link's reference.
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import type { SanityLink } from "@driftime/sanity-plugin-link/render";
|
|
484
|
+
|
|
485
|
+
export interface SanityButton {
|
|
486
|
+
link?: SanityLink<SanityPage>;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
```json
|
|
491
|
+
{
|
|
492
|
+
"_type": "link",
|
|
493
|
+
"type": "page",
|
|
494
|
+
"reference": { "_type": "reference", "_ref": "a1b2c3d4" },
|
|
495
|
+
"anchor": "our-values",
|
|
496
|
+
"searchParams": [{ "_type": "linkSearchParam", "_key": "f3a1", "key": "utm_source", "value": "newsletter" }],
|
|
497
|
+
"label": "How Acme Inc. works"
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
| Field | Type | Destination | Purpose |
|
|
502
|
+
| -------------- | ------------------------- | ----------- | ----------------------------------------------------------------------------- |
|
|
503
|
+
| `_type` | `string` | all | `link` for a field, `linkMark` for a Portable Text annotation. |
|
|
504
|
+
| `type` | `SanityLinkDestination` | all | The destination. Narrows the union. |
|
|
505
|
+
| `label` | `string` | all | The link text. Optional on a page link, which falls back to the page's title. |
|
|
506
|
+
| `reference` | `SanityLinkReference` | `page` | The referenced document. Expanded by `linkFragment`. |
|
|
507
|
+
| `anchor` | `string` | `page` | An optional fragment, appended to the resolved path. |
|
|
508
|
+
| `searchParams` | `SanityLinkSearchParam[]` | `page` | Optional query string entries, appended to the resolved path. |
|
|
509
|
+
| `anchor` | `string` | `anchor` | A fragment on the current page. Resolves to `#` and the fragment. |
|
|
510
|
+
| `url` | `string` | `url` | An absolute URL. Resolves as written, or to a relative path at `baseUrl`. |
|
|
511
|
+
| `email` | `string` | `email` | An email address. Resolves to `mailto:`. |
|
|
512
|
+
| `subject` | `string` | `email` | An optional subject line, appended as `?subject=`. |
|
|
513
|
+
| `phone` | `string` | `phone` | A phone number. Resolves to `tel:` with spaces removed. |
|
|
514
|
+
| `file` | `SanityLinkFile` | `file` | A file asset. Resolves to the asset URL with `download` set. |
|
|
515
|
+
|
|
516
|
+
<br />
|
|
517
|
+
|
|
518
|
+
### API
|
|
519
|
+
|
|
520
|
+
| Export | Import from | Purpose |
|
|
521
|
+
| -------------------------- | ------------------------------------- | ----------------------------------------------------------- |
|
|
522
|
+
| `linkPlugin(config)` | `@driftime/sanity-plugin-link` | Registers the `link` type. |
|
|
523
|
+
| `linkAnnotation` | `@driftime/sanity-plugin-link` | A Portable Text annotation with the link dialog. |
|
|
524
|
+
| `PortableTextLinkPlugins` | `@driftime/sanity-plugin-link` | Makes pasted links write `linkAnnotation`. |
|
|
525
|
+
| `defineLinkConfig(config)` | `@driftime/sanity-plugin-link/render` | Takes the routes and returns the site's functions and GROQ. |
|
|
526
|
+
|
|
527
|
+
<br />
|
|
528
|
+
|
|
529
|
+
### Types
|
|
530
|
+
|
|
531
|
+
| Type | Import from | Purpose |
|
|
532
|
+
| --------------------------- | ------------------------------------- | ------------------------------------------------------------- |
|
|
533
|
+
| `SanityLinkConfig` | `@driftime/sanity-plugin-link` | Everything `linkPlugin` accepts. |
|
|
534
|
+
| `SanityLinkOptions` | `@driftime/sanity-plugin-link` | The `options` a link field accepts. |
|
|
535
|
+
| `SanityLinkDefinition` | `@driftime/sanity-plugin-link` | A field or array member of type `link`. |
|
|
536
|
+
| `SanityLinkTitleField` | `@driftime/sanity-plugin-link` | The plugin's `title` option. |
|
|
537
|
+
| `SanityLinkResolverConfig` | `@driftime/sanity-plugin-link/render` | Everything `defineLinkConfig` accepts. |
|
|
538
|
+
| `SanityLinkTitleConfig` | `@driftime/sanity-plugin-link/render` | The configuration's `title` option. |
|
|
539
|
+
| `SanityLinkRoutes` | `@driftime/sanity-plugin-link/render` | The `routes` option. |
|
|
540
|
+
| `SanityLinkRouteDefinition` | `@driftime/sanity-plugin-link/render` | One route, a path pattern and its parameters. |
|
|
541
|
+
| `SanityLinkRouteInput` | `@driftime/sanity-plugin-link/render` | A route declared in code, with its parameters. |
|
|
542
|
+
| `SanityLinkResolvers` | `@driftime/sanity-plugin-link/render` | The `resolvers` option. |
|
|
543
|
+
| `SanityResolveLinkProps` | `@driftime/sanity-plugin-link/render` | Everything `resolveLink` accepts. |
|
|
544
|
+
| `SanityLinkState` | `@driftime/sanity-plugin-link/render` | Everything `resolveLink` returns. |
|
|
545
|
+
| `SanityResolvedLink` | `@driftime/sanity-plugin-link/render` | The `href`, `label`, and `download` for an anchor. |
|
|
546
|
+
| `SanityLinkResolution` | `@driftime/sanity-plugin-link/render` | `SanityLinkState`, or a promise of it if a resolver is async. |
|
|
547
|
+
| `SanityLink` | `@driftime/sanity-plugin-link/render` | Any stored link, narrowed by `type`. |
|
|
548
|
+
| `SanityPageLink` | `@driftime/sanity-plugin-link/render` | A link to a page. |
|
|
549
|
+
| `SanityAnchorLink` | `@driftime/sanity-plugin-link/render` | A link to an anchor on the current page. |
|
|
550
|
+
| `SanityUrlLink` | `@driftime/sanity-plugin-link/render` | A link to a URL. |
|
|
551
|
+
| `SanityEmailLink` | `@driftime/sanity-plugin-link/render` | A link to an email address. |
|
|
552
|
+
| `SanityPhoneLink` | `@driftime/sanity-plugin-link/render` | A link to a phone number. |
|
|
553
|
+
| `SanityFileLink` | `@driftime/sanity-plugin-link/render` | A link to a file. |
|
|
554
|
+
| `SanityLinkDestination` | `@driftime/sanity-plugin-link/render` | The destination names. |
|
|
555
|
+
| `SanityLinkReference` | `@driftime/sanity-plugin-link/render` | A reference to a document, before a query expands it. |
|
|
556
|
+
| `SanityLinkDocument` | `@driftime/sanity-plugin-link/render` | The document a page link references. |
|
|
557
|
+
| `SanityLinkRouteParams` | `@driftime/sanity-plugin-link/render` | The route parameters a query adds to a document. |
|
|
558
|
+
| `SanityLinkSearchParam` | `@driftime/sanity-plugin-link/render` | One search parameter on a page link. |
|
|
559
|
+
| `SanityLinkFile` | `@driftime/sanity-plugin-link/render` | The file field of a file link. |
|
|
560
|
+
| `SanityLinkFileAsset` | `@driftime/sanity-plugin-link/render` | The asset behind a file link. |
|
|
561
|
+
|
|
562
|
+
<br />
|
|
563
|
+
|
|
564
|
+
## License
|
|
565
|
+
|
|
566
|
+
MIT © [Driftime®](https://driftime.com). See [LICENSE](https://github.com/driftime/sanity-plugins/blob/main/LICENSE).
|
|
567
|
+
|
|
568
|
+
<br />
|
|
569
|
+
|
|
570
|
+
## Acknowledgements
|
|
571
|
+
|
|
572
|
+
Icons for the Studio's own controls come from `@sanity/icons`, so they match the rest of the Studio. The plugin's own icons are based on [Lucide](https://lucide.dev), distributed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE).
|
|
573
|
+
|
|
574
|
+
<br />
|
|
575
|
+
<br />
|
|
576
|
+
|
|
577
|
+
<div align="center">
|
|
578
|
+
<p><strong>Built alongside <a href="https://cairn.driftime.com">Cairn</a>, a starting point for responsible web experiences.</strong></p>
|
|
579
|
+
<p>Part of a suite of Sanity Studio plugins by Driftime®</p>
|
|
580
|
+
<p><a href="https://github.com/driftime/sanity-plugins#readme">Sanity Plugins</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/handbook#readme">Handbook</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/icon#readme">Icon</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/color#readme">Color</a> · <a href="https://github.com/driftime/sanity-plugins/tree/main/packages/link#readme">Link</a></p>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<br />
|
|
584
|
+
|
|
585
|
+
<div align="center">
|
|
586
|
+
<a href="https://driftime.com">
|
|
587
|
+
<picture>
|
|
588
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://driftime.com/driftime-github-logo-dark.svg" />
|
|
589
|
+
<source media="(prefers-color-scheme: light)" srcset="https://driftime.com/driftime-github-logo.svg" />
|
|
590
|
+
<img src="https://driftime.com/driftime-github-logo.svg" alt="Driftime® Logo" width="100" />
|
|
591
|
+
</picture>
|
|
592
|
+
</a>
|
|
593
|
+
</div>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { a as SanityLinkDestination, h as linkTypeName } from "./types-BXseTKgu.js";
|
|
2
|
+
import { ObjectOptions, PortableTextPluginsProps, TypeAliasDefinition } from "sanity";
|
|
3
|
+
declare module "@sanity/types" {
|
|
4
|
+
interface IntrinsicDefinitions {
|
|
5
|
+
link: SanityLinkDefinition;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Options a link field takes. The list says what is on and replaces whatever the plugin was given,
|
|
10
|
+
* so one line tells you the whole answer.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
interface SanityLinkOptions extends ObjectOptions {
|
|
15
|
+
/** Destinations an author may choose from, offered in the order they are named. All when omitted. */
|
|
16
|
+
destinations?: SanityLinkDestination[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Shape of a field or array member holding a link, so its options are completed and checked the way
|
|
20
|
+
* a built-in type's are.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
interface SanityLinkDefinition extends Omit<TypeAliasDefinition<typeof linkTypeName, undefined>, "options"> {
|
|
25
|
+
options?: SanityLinkOptions;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Everything the plugin accepts, of which only the document types are required.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
interface SanityLinkConfig {
|
|
33
|
+
/** Document types an internal link may point at, offered in the order they are named. */
|
|
34
|
+
documentTypes: string[];
|
|
35
|
+
/** Destinations an author may choose from, offered in the order they are named. All when omitted. */
|
|
36
|
+
destinations?: SanityLinkDestination[];
|
|
37
|
+
/** Where a page's title is read from, for previews and for the label an internal link borrows. */
|
|
38
|
+
title?: SanityLinkTitleField;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Where the Studio reads a page's title from.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
interface SanityLinkTitleField {
|
|
46
|
+
/** Field a page holds its title in. Reads `title` when omitted. */
|
|
47
|
+
field?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Creates the link field type for Sanity Studio, offering a page, a section of it, an address, an
|
|
51
|
+
* email, a phone number, or a file behind one control, and resolving nothing about routing itself.
|
|
52
|
+
*
|
|
53
|
+
* @param config - Plugin configuration.
|
|
54
|
+
* @returns Sanity plugin definition.
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export declare const linkPlugin: import("sanity").Plugin<SanityLinkConfig>;
|
|
58
|
+
/**
|
|
59
|
+
* The link as a Portable Text annotation, for a consumer's own text types to offer alongside their
|
|
60
|
+
* other annotations. An annotated link takes the text it wraps as its label, so it never asks for
|
|
61
|
+
* one of its own.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export declare const linkAnnotation: {
|
|
66
|
+
name: "linkMark";
|
|
67
|
+
type: "link";
|
|
68
|
+
title: string;
|
|
69
|
+
icon: () => import("react").ReactElement<import("react").SVGProps<SVGSVGElement>, string | import("react").JSXElementConstructor<any>>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Writes this plugin's link when an author pastes one, from formatted markup or a bare address. Pass
|
|
73
|
+
* it to a text type's `components.portableText.plugins`.
|
|
74
|
+
*
|
|
75
|
+
* @param props - Portable Text plugin props the Studio supplies.
|
|
76
|
+
* @returns The editor's own plugins, with link pasting replaced.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export declare function PortableTextLinkPlugins(props: PortableTextPluginsProps): import("react").JSX.Element;
|
|
80
|
+
export type { SanityLinkConfig, SanityLinkDefinition, SanityLinkOptions, SanityLinkTitleField };
|
|
81
|
+
//# sourceMappingURL=index.d.ts.map
|