@capacms/sdk 1.0.0-next.6 → 1.0.0-next.8
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/CHANGELOG.md +55 -1
- package/README.md +192 -44
- package/dist/config.d.ts +0 -35
- package/dist/config.js +47 -1
- package/dist/next/client.d.ts +9 -8
- package/dist/next/client.js +19 -4
- package/dist/next/field-names.js +1 -1
- package/dist/next/graphql/introspection.d.ts +1 -1
- package/dist/next/graphql/introspection.js +1 -1
- package/dist/next/graphql/plan.js +2 -2
- package/dist/next/key-family.d.ts +9 -12
- package/dist/next/key-family.js +11 -19
- package/dist/nextjs/index.d.ts +182 -33
- package/dist/nextjs/index.js +244 -29
- package/package.json +2 -6
package/dist/nextjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_API_VERSION = exports.CAPA_ENV_ALIASES = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.EDIT_PARAM = exports.LAYOUT_PAGE = exports.gql = exports.MEDIA_TAG = exports.GRAPHQL_TAG = void 0;
|
|
3
|
+
exports.DRAFT_COOKIE_MAX_AGE = exports.DRAFT_ROBOTS_TAG = exports.CAPA_ADMIN_ORIGIN = exports.DEFAULT_API_VERSION = exports.CAPA_ENV_ALIASES = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.VIEW_PARAM = exports.PREVIEW_PARAM = exports.EDIT_PARAM = exports.LAYOUT_PAGE = exports.gql = exports.MEDIA_TAG = exports.GRAPHQL_TAG = void 0;
|
|
4
4
|
exports.withCache = withCache;
|
|
5
5
|
exports.modelTag = modelTag;
|
|
6
6
|
exports.tagsFor = tagsFor;
|
|
@@ -15,6 +15,11 @@ exports.getPublishedClient = getPublishedClient;
|
|
|
15
15
|
exports.getCapaClient = getCapaClient;
|
|
16
16
|
exports.graphql = graphql;
|
|
17
17
|
exports.safeSitePath = safeSitePath;
|
|
18
|
+
exports.frameAncestors = frameAncestors;
|
|
19
|
+
exports.draftHeaders = draftHeaders;
|
|
20
|
+
exports.capaHeaders = capaHeaders;
|
|
21
|
+
exports.frameDraftCookie = frameDraftCookie;
|
|
22
|
+
exports.clearDraftCookie = clearDraftCookie;
|
|
18
23
|
exports.createPreviewRoute = createPreviewRoute;
|
|
19
24
|
exports.exitPreviewRoute = exitPreviewRoute;
|
|
20
25
|
exports.capaMiddleware = capaMiddleware;
|
|
@@ -47,11 +52,12 @@ function modelTag(namespace) {
|
|
|
47
52
|
return `capa:model:${namespace}`;
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
|
-
* The tag `graphql()`
|
|
51
|
-
* `revalidateFromWebhook` revalidates on every content
|
|
52
|
-
* never stale after a publish, at the price of
|
|
53
|
-
* Name the models it reads with
|
|
54
|
-
* when one of those changes.
|
|
55
|
+
* The tag `graphql()` keeps a read under when it gives a `revalidate` and no
|
|
56
|
+
* `tags`, and that `revalidateFromWebhook` revalidates on every content
|
|
57
|
+
* change: such a page is never stale after a publish, at the price of
|
|
58
|
+
* refreshing on any publish. Name the models it reads with
|
|
59
|
+
* `tagsFor({ namespace })` to refresh it only when one of those changes. A
|
|
60
|
+
* read with neither `tags` nor `revalidate` is not kept at all.
|
|
55
61
|
*/
|
|
56
62
|
exports.GRAPHQL_TAG = "capa:graphql";
|
|
57
63
|
/**
|
|
@@ -283,6 +289,10 @@ function pagesFor(client) {
|
|
|
283
289
|
* clickable. It never switches the site to draft data.
|
|
284
290
|
*/
|
|
285
291
|
exports.EDIT_PARAM = "capa-edit";
|
|
292
|
+
/** The query parameter a Capa preview link carries: `?capa-preview=<token>`. */
|
|
293
|
+
exports.PREVIEW_PARAM = "capa-preview";
|
|
294
|
+
/** The query parameter of the editor's Published view: `?capa-view=published`. */
|
|
295
|
+
exports.VIEW_PARAM = "capa-view";
|
|
286
296
|
/**
|
|
287
297
|
* The request header `resolveEditRequest` sets once a `capa-edit` token has
|
|
288
298
|
* been verified, for `editMode()` to read. Any copy a browser sent is removed
|
|
@@ -321,6 +331,7 @@ function verifiedEdit(headers) {
|
|
|
321
331
|
* const edit = await resolveEditRequest(request, publishedClient());
|
|
322
332
|
* const response = NextResponse.next({ request: { headers: edit.headers } });
|
|
323
333
|
* if (edit.cacheControl) response.headers.set("Cache-Control", edit.cacheControl);
|
|
334
|
+
* if (edit.robotsTag) response.headers.set("X-Robots-Tag", edit.robotsTag);
|
|
324
335
|
* return response;
|
|
325
336
|
* }
|
|
326
337
|
*
|
|
@@ -347,7 +358,13 @@ async function resolveEditRequest(request, client) {
|
|
|
347
358
|
const draft = request.cookies?.has(exports.DRAFT_COOKIE) ??
|
|
348
359
|
(headers.get("cookie") ?? "").split(/;\s*/).some((c) => c.startsWith(`${exports.DRAFT_COOKIE}=`));
|
|
349
360
|
const edit = verified || draft;
|
|
350
|
-
return {
|
|
361
|
+
return {
|
|
362
|
+
edit,
|
|
363
|
+
verified,
|
|
364
|
+
headers,
|
|
365
|
+
cacheControl: edit ? exports.EDIT_CACHE_CONTROL : null,
|
|
366
|
+
robotsTag: edit ? exports.DRAFT_ROBOTS_TAG : null,
|
|
367
|
+
};
|
|
351
368
|
}
|
|
352
369
|
// ------------------------------------------------- five-minute integration ---
|
|
353
370
|
/**
|
|
@@ -423,10 +440,12 @@ function getPublishedClient(overrides = {}) {
|
|
|
423
440
|
* `CapaQuery` (from `capa-codegen --graphql`) types the builder, as with
|
|
424
441
|
* `createClient`; leave it out for an untyped client. `config` wins over env.
|
|
425
442
|
*
|
|
426
|
-
* Its
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
443
|
+
* Its REST reads are sent as `createClient` sends them, which Next does not
|
|
444
|
+
* keep. Its GraphQL reads, a document or the builder, are sent the same way
|
|
445
|
+
* unless a call gives `tags` or `revalidate`, so a publish shows up on both
|
|
446
|
+
* alike. Given either, a read is kept in Next's data cache exactly as
|
|
447
|
+
* `graphql()` keeps it: a published read with no errors, never a draft or an
|
|
448
|
+
* edit-mode page.
|
|
430
449
|
*/
|
|
431
450
|
async function getCapaClient(input) {
|
|
432
451
|
const draft = (await input.draftMode()).isEnabled === true;
|
|
@@ -493,13 +512,33 @@ async function graphql(document, variables, options = {}) {
|
|
|
493
512
|
return readInNext(read, document, variables, call, { tags, revalidate });
|
|
494
513
|
}
|
|
495
514
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
|
|
515
|
+
* Whether a read said anything about Next's cache: tags to keep it under, or a
|
|
516
|
+
* `revalidate`. One that said neither is sent as the REST reads are, so a
|
|
517
|
+
* publish shows up on both the same way.
|
|
518
|
+
*/
|
|
519
|
+
function namesCaching({ tags, revalidate }) {
|
|
520
|
+
return (tags !== undefined && tags.length > 0) || revalidate !== undefined;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* One GraphQL read in Next, for `graphql()` and `getCapaClient`.
|
|
524
|
+
*
|
|
525
|
+
* With no `tags` and no `revalidate` it is sent exactly as `entries.list` and
|
|
526
|
+
* `entries.get` send theirs: a plain fetch with no `cache` and no `next`, which
|
|
527
|
+
* Next does not keep, so the page shows a publish on its next render whether
|
|
528
|
+
* it reads by REST or by GraphQL. Given `tags` or a `revalidate`, Next's data
|
|
529
|
+
* cache holds a published read with no errors, and the fetch under it is never
|
|
530
|
+
* cached. A draft, an edit-mode read (its entries carry marks a cached copy
|
|
531
|
+
* would lose) and `revalidate: 0` are not cached.
|
|
500
532
|
*/
|
|
501
|
-
async function readInNext(read, document, variables, call,
|
|
533
|
+
async function readInNext(read, document, variables, call, options) {
|
|
502
534
|
const { draft, edit, settings, given } = read;
|
|
535
|
+
if (!draft && !edit && !namesCaching(options)) {
|
|
536
|
+
return (0, next_1.createClient)({ editMode: false, ...settings }).graphql(document, variables, call);
|
|
537
|
+
}
|
|
538
|
+
// `tags: []` names no tag, so the read is kept under `GRAPHQL_TAG` like one
|
|
539
|
+
// with no tags: kept under none, nothing could ever revalidate it.
|
|
540
|
+
const tags = options.tags && options.tags.length > 0 ? options.tags : undefined;
|
|
541
|
+
const revalidate = options.revalidate;
|
|
503
542
|
const baseFetch = settings.fetch ?? globalThis.fetch;
|
|
504
543
|
const cache = given ?? nextUnstableCache();
|
|
505
544
|
if (cache && !draft) {
|
|
@@ -547,26 +586,166 @@ function safeSitePath(value) {
|
|
|
547
586
|
return "/";
|
|
548
587
|
return value;
|
|
549
588
|
}
|
|
589
|
+
// -------------------------------------------------------- draft responses ---
|
|
590
|
+
/** The Capa admin's origin: what frames a draft in the editor, unless a site names another. */
|
|
591
|
+
exports.CAPA_ADMIN_ORIGIN = "https://app.capacms.com";
|
|
592
|
+
/** `X-Robots-Tag` on every draft and edit-mode response: a draft is never indexed, nor its links followed. */
|
|
593
|
+
exports.DRAFT_ROBOTS_TAG = "noindex, nofollow";
|
|
594
|
+
/** How long the draft cookie lasts, in seconds: one hour, as a preview token does. */
|
|
595
|
+
exports.DRAFT_COOKIE_MAX_AGE = 3600;
|
|
596
|
+
/**
|
|
597
|
+
* `frame-ancestors 'self' <origins>`, the CSP directive that lets the Capa
|
|
598
|
+
* editor frame a draft and nothing else frame it. Each origin is a scheme and
|
|
599
|
+
* a host, with a port when it has one, and is written as the URL parser
|
|
600
|
+
* normalises it, so no `;` or quote can reach the policy. Anything else, a
|
|
601
|
+
* path or a query included, throws a `TypeError`.
|
|
602
|
+
*/
|
|
603
|
+
function frameAncestors(adminOrigins = [exports.CAPA_ADMIN_ORIGIN]) {
|
|
604
|
+
const origins = new Set();
|
|
605
|
+
for (const value of adminOrigins)
|
|
606
|
+
origins.add(originOf(value));
|
|
607
|
+
return ["frame-ancestors 'self'", ...origins].join(" ");
|
|
608
|
+
}
|
|
609
|
+
function originOf(value) {
|
|
610
|
+
let url = null;
|
|
611
|
+
try {
|
|
612
|
+
url = typeof value === "string" ? new URL(value) : null;
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
url = null;
|
|
616
|
+
}
|
|
617
|
+
const bare = url !== null &&
|
|
618
|
+
(url.protocol === "https:" || url.protocol === "http:") &&
|
|
619
|
+
url.pathname === "/" &&
|
|
620
|
+
url.search === "" &&
|
|
621
|
+
url.hash === "" &&
|
|
622
|
+
url.username === "" &&
|
|
623
|
+
url.password === "";
|
|
624
|
+
if (!bare) {
|
|
625
|
+
throw new TypeError(`@capacms/sdk/nextjs: ${JSON.stringify(value)} is not an origin. Pass a scheme and a host, such as ${exports.CAPA_ADMIN_ORIGIN}.`);
|
|
626
|
+
}
|
|
627
|
+
return url.origin;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* The headers every draft response carries: `X-Robots-Tag: noindex, nofollow`,
|
|
631
|
+
* and `Content-Security-Policy: frame-ancestors 'self' https://app.capacms.com`
|
|
632
|
+
* (or the `adminOrigins` given). Throws for an origin that is not one.
|
|
633
|
+
*/
|
|
634
|
+
function draftHeaders(options = {}) {
|
|
635
|
+
return {
|
|
636
|
+
"X-Robots-Tag": exports.DRAFT_ROBOTS_TAG,
|
|
637
|
+
"Content-Security-Policy": frameAncestors(options.adminOrigins),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Rules for `next.config`'s `headers()` that send `draftHeaders` on draft
|
|
642
|
+
* responses only:
|
|
643
|
+
*
|
|
644
|
+
* // next.config.mjs
|
|
645
|
+
* import { capaHeaders } from "@capacms/sdk/nextjs";
|
|
646
|
+
* export default { async headers() { return [...capaHeaders()]; } };
|
|
647
|
+
*
|
|
648
|
+
* A rule matches a request carrying the draft cookie, or a `capa-preview`,
|
|
649
|
+
* `capa-edit` or `capa-view` query. A visitor's request carries none of
|
|
650
|
+
* them, so its response, cached or not, is exactly what it was. Next checks
|
|
651
|
+
* the cookie by name, not by value, so a forged cookie only adds these
|
|
652
|
+
* headers to the forger's own response.
|
|
653
|
+
*
|
|
654
|
+
* A site that sends its own `frame-ancestors` or `X-Frame-Options` must leave
|
|
655
|
+
* them off draft responses (`missing: [{ type: "cookie", key: DRAFT_COOKIE }]`
|
|
656
|
+
* on its rule): a browser applies every CSP it is sent, so the strictest wins.
|
|
657
|
+
*/
|
|
658
|
+
function capaHeaders(options = {}) {
|
|
659
|
+
const headers = () => Object.entries(draftHeaders(options)).map(([key, value]) => ({ key, value }));
|
|
660
|
+
const conditions = [
|
|
661
|
+
{ type: "cookie", key: exports.DRAFT_COOKIE },
|
|
662
|
+
{ type: "query", key: exports.PREVIEW_PARAM },
|
|
663
|
+
{ type: "query", key: exports.EDIT_PARAM },
|
|
664
|
+
{ type: "query", key: exports.VIEW_PARAM },
|
|
665
|
+
];
|
|
666
|
+
return conditions.map((condition) => ({ source: "/:path*", has: [condition], headers: headers() }));
|
|
667
|
+
}
|
|
668
|
+
/** The attributes a draft cookie needs to be sent inside the Capa editor's cross-site frame. */
|
|
669
|
+
const FRAMED = { path: "/", httpOnly: true, secure: true, sameSite: "none", partitioned: true };
|
|
670
|
+
function draftCookieMaxAge(maxAge = exports.DRAFT_COOKIE_MAX_AGE) {
|
|
671
|
+
if (typeof maxAge !== "number" || !Number.isInteger(maxAge) || maxAge <= 0) {
|
|
672
|
+
throw new TypeError(`@capacms/sdk/nextjs: maxAge must be a whole number of seconds above 0, and got ${String(maxAge)}.`);
|
|
673
|
+
}
|
|
674
|
+
return maxAge;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Re-set the cookie `draftMode().enable()` just set, so the Capa editor can
|
|
678
|
+
* use it: `HttpOnly; Secure; SameSite=None; Partitioned; Path=/` and
|
|
679
|
+
* `Max-Age` one hour (`maxAge` seconds). Next sets it with no `Max-Age`, so
|
|
680
|
+
* it unlocks every draft on the site until the browser closes, and without
|
|
681
|
+
* `Partitioned`, which Safari 26.2 and later need to send a cookie into a
|
|
682
|
+
* cross-site frame. Call it after `enable()`, in the same route handler.
|
|
683
|
+
* Resolves false, and sets nothing, when there is no draft cookie to re-set.
|
|
684
|
+
*/
|
|
685
|
+
async function frameDraftCookie(cookies, options = {}) {
|
|
686
|
+
const maxAge = draftCookieMaxAge(options.maxAge);
|
|
687
|
+
const jar = await cookies();
|
|
688
|
+
const current = jar.get(exports.DRAFT_COOKIE);
|
|
689
|
+
if (!current?.value)
|
|
690
|
+
return false;
|
|
691
|
+
jar.set({ name: exports.DRAFT_COOKIE, value: current.value, ...FRAMED, maxAge });
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Delete the draft cookie `frameDraftCookie` set. A partitioned cookie is
|
|
696
|
+
* deleted only by a `Set-Cookie` that is partitioned too, which
|
|
697
|
+
* `draftMode().disable()` is not. Call it after `disable()`; it replaces
|
|
698
|
+
* `disable()`'s own deletion, since a response sets one cookie per name. A
|
|
699
|
+
* draft cookie set without `Partitioned`, before a site used
|
|
700
|
+
* `frameDraftCookie`, ends when the browser closes, as it always did.
|
|
701
|
+
*/
|
|
702
|
+
async function clearDraftCookie(cookies) {
|
|
703
|
+
(await cookies()).set({ name: exports.DRAFT_COOKIE, value: "", ...FRAMED, expires: new Date(0) });
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The preview and exit routes' own redirect. Next's `redirect()` throws and
|
|
707
|
+
* answers for the route, so it cannot carry headers; this one is a plain
|
|
708
|
+
* `Response`, to which Next appends every cookie the route set. It sets no
|
|
709
|
+
* cookie itself: Next keeps one per name, and the response's own would win.
|
|
710
|
+
*/
|
|
711
|
+
function routeRedirect(location, headers) {
|
|
712
|
+
return new Response(null, {
|
|
713
|
+
status: 307,
|
|
714
|
+
headers: { Location: location, "Cache-Control": exports.EDIT_CACHE_CONTROL, "Referrer-Policy": "no-referrer", ...headers },
|
|
715
|
+
});
|
|
716
|
+
}
|
|
550
717
|
/**
|
|
551
718
|
* `app/api/capa/preview/route.ts`:
|
|
552
719
|
*
|
|
553
|
-
* import { draftMode } from "next/headers";
|
|
554
|
-
*
|
|
555
|
-
* export const GET = createPreviewRoute({ draftMode, redirect });
|
|
720
|
+
* import { cookies, draftMode } from "next/headers";
|
|
721
|
+
* export const GET = createPreviewRoute({ draftMode, cookies });
|
|
556
722
|
*
|
|
557
723
|
* Checks the token with Capa (the site never holds the signing key), turns
|
|
558
724
|
* draft mode on and lands on the entry's page. A bad or expired token lands on
|
|
559
725
|
* the page without draft mode and `?preview=expired`; Capa unreachable gives
|
|
560
|
-
* `?preview=unavailable`.
|
|
726
|
+
* `?preview=unavailable`. The token is checked with any key the site holds,
|
|
727
|
+
* its legacy key included (`getPublishedClient`, from `CAPA_KEY`).
|
|
728
|
+
*
|
|
729
|
+
* Given `cookies`, the draft cookie is re-set by `frameDraftCookie`: one hour,
|
|
730
|
+
* and sent inside the editor's frame. With no `redirect`, the route answers
|
|
731
|
+
* with its own 307, marked `X-Robots-Tag: noindex, nofollow`,
|
|
732
|
+
* `Referrer-Policy: no-referrer` (the token is in the URL),
|
|
733
|
+
* `Cache-Control: private, no-store` and the editor's `frame-ancestors`.
|
|
734
|
+
* Given Next's `redirect`, it is called instead, as before, and the redirect
|
|
735
|
+
* carries none of those.
|
|
561
736
|
*/
|
|
562
737
|
function createPreviewRoute(input) {
|
|
738
|
+
// Checked here, so a bad origin or maxAge fails where the route is built.
|
|
739
|
+
const headers = draftHeaders({ adminOrigins: input.adminOrigins });
|
|
740
|
+
const maxAge = draftCookieMaxAge(input.maxAge);
|
|
741
|
+
const go = (location) => input.redirect ? input.redirect(location) : routeRedirect(location, headers);
|
|
563
742
|
return async (request) => {
|
|
564
743
|
const url = new URL(request.url);
|
|
565
|
-
const token = url.searchParams.get("token") ?? url.searchParams.get(
|
|
744
|
+
const token = url.searchParams.get("token") ?? url.searchParams.get(exports.PREVIEW_PARAM) ?? "";
|
|
566
745
|
// Reached two ways: directly, or through the middleware's rewrite of a page
|
|
567
746
|
// URL carrying `?capa-preview=`, where Next may hand over the ORIGINAL URL.
|
|
568
747
|
// Then the page itself is the path.
|
|
569
|
-
const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has(
|
|
748
|
+
const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has(exports.PREVIEW_PARAM) ? url.pathname : null));
|
|
570
749
|
let claim = null;
|
|
571
750
|
let failed = false;
|
|
572
751
|
try {
|
|
@@ -578,18 +757,36 @@ function createPreviewRoute(input) {
|
|
|
578
757
|
const draft = await input.draftMode();
|
|
579
758
|
if (!claim) {
|
|
580
759
|
draft.disable?.();
|
|
581
|
-
|
|
760
|
+
if (input.cookies)
|
|
761
|
+
await clearDraftCookie(input.cookies);
|
|
762
|
+
return go(`${path}?preview=${failed ? "unavailable" : "expired"}`);
|
|
582
763
|
}
|
|
583
764
|
draft.enable?.();
|
|
765
|
+
if (input.cookies)
|
|
766
|
+
await frameDraftCookie(input.cookies, { maxAge });
|
|
584
767
|
await input.onEnable?.();
|
|
585
|
-
return
|
|
768
|
+
return go(safeSitePath(claim.path ?? path));
|
|
586
769
|
};
|
|
587
770
|
}
|
|
588
|
-
/**
|
|
771
|
+
/**
|
|
772
|
+
* `app/api/capa/exit/route.ts`:
|
|
773
|
+
*
|
|
774
|
+
* import { cookies, draftMode } from "next/headers";
|
|
775
|
+
* export const GET = exitPreviewRoute({ draftMode, cookies });
|
|
776
|
+
*
|
|
777
|
+
* Turns draft mode off and lands on `?path=`. Given `cookies`, the framed
|
|
778
|
+
* cookie `createPreviewRoute` set is deleted too. With no `redirect`, the route
|
|
779
|
+
* answers with its own 307, marked noindex and never cached.
|
|
780
|
+
*/
|
|
589
781
|
function exitPreviewRoute(input) {
|
|
590
782
|
return async (request) => {
|
|
591
783
|
(await input.draftMode()).disable?.();
|
|
592
|
-
|
|
784
|
+
if (input.cookies)
|
|
785
|
+
await clearDraftCookie(input.cookies);
|
|
786
|
+
const location = safeSitePath(new URL(request.url).searchParams.get("path"));
|
|
787
|
+
if (input.redirect)
|
|
788
|
+
return input.redirect(location);
|
|
789
|
+
return routeRedirect(location, { "X-Robots-Tag": exports.DRAFT_ROBOTS_TAG });
|
|
593
790
|
};
|
|
594
791
|
}
|
|
595
792
|
/**
|
|
@@ -603,12 +800,25 @@ function exitPreviewRoute(input) {
|
|
|
603
800
|
* - `?capa-view=published` renders without the draft cookie, for the editor's
|
|
604
801
|
* Published view;
|
|
605
802
|
* - `?capa-edit=<token>` turns edit mode on (ids and overlay, published data);
|
|
606
|
-
* - every edit-mode response is `private, no-store
|
|
803
|
+
* - every edit-mode response is `private, no-store`, and it and every request
|
|
804
|
+
* carrying `capa-edit` or `capa-view` is `X-Robots-Tag: noindex, nofollow`.
|
|
805
|
+
*
|
|
806
|
+
* Give it a `matcher` so a visitor's request never runs it (Next reads
|
|
807
|
+
* `config` from the file itself, so it is written out there):
|
|
808
|
+
*
|
|
809
|
+
* export const config = {
|
|
810
|
+
* matcher: [
|
|
811
|
+
* { source: "/:path*", has: [{ type: "query", key: "capa-preview" }] },
|
|
812
|
+
* { source: "/:path*", has: [{ type: "query", key: "capa-edit" }] },
|
|
813
|
+
* { source: "/:path*", has: [{ type: "query", key: "capa-view" }] },
|
|
814
|
+
* { source: "/:path*", has: [{ type: "cookie", key: "__prerender_bypass" }] },
|
|
815
|
+
* ],
|
|
816
|
+
* };
|
|
607
817
|
*/
|
|
608
818
|
function capaMiddleware(input) {
|
|
609
819
|
const previewRoute = input.previewRoute ?? "/api/capa/preview";
|
|
610
820
|
return async (request) => {
|
|
611
|
-
const token = request.nextUrl.searchParams.get(
|
|
821
|
+
const token = request.nextUrl.searchParams.get(exports.PREVIEW_PARAM);
|
|
612
822
|
if (token) {
|
|
613
823
|
const target = request.nextUrl.clone();
|
|
614
824
|
target.pathname = previewRoute;
|
|
@@ -618,7 +828,7 @@ function capaMiddleware(input) {
|
|
|
618
828
|
return input.NextResponse.rewrite(target);
|
|
619
829
|
}
|
|
620
830
|
const headers = new Headers(request.headers);
|
|
621
|
-
if (request.nextUrl.searchParams.get(
|
|
831
|
+
if (request.nextUrl.searchParams.get(exports.VIEW_PARAM) === "published") {
|
|
622
832
|
const cookies = request.cookies
|
|
623
833
|
.getAll()
|
|
624
834
|
.filter((cookie) => cookie.name !== exports.DRAFT_COOKIE)
|
|
@@ -635,6 +845,11 @@ function capaMiddleware(input) {
|
|
|
635
845
|
const response = input.NextResponse.next({ request: { headers: edit.headers } });
|
|
636
846
|
if (edit.cacheControl)
|
|
637
847
|
response.headers.set("Cache-Control", edit.cacheControl);
|
|
848
|
+
// A capa- link is the editor's, verified or not, and never a page to index.
|
|
849
|
+
const capaLink = request.nextUrl.searchParams.has(exports.EDIT_PARAM) || request.nextUrl.searchParams.has(exports.VIEW_PARAM);
|
|
850
|
+
const robots = edit.robotsTag ?? (capaLink ? exports.DRAFT_ROBOTS_TAG : null);
|
|
851
|
+
if (robots)
|
|
852
|
+
response.headers.set("X-Robots-Tag", robots);
|
|
638
853
|
return response;
|
|
639
854
|
};
|
|
640
855
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capacms/sdk",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.8",
|
|
4
4
|
"description": "The TypeScript SDK for Capa's content API: REST and GraphQL reads typed from your models, Next.js caching and live preview, and codegen.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://docs.capacms.com/api",
|
|
@@ -68,16 +68,12 @@
|
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"graphql": "^16.9.0",
|
|
71
|
-
"next": ">=14",
|
|
72
71
|
"react": ">=18"
|
|
73
72
|
},
|
|
74
73
|
"peerDependenciesMeta": {
|
|
75
74
|
"graphql": {
|
|
76
75
|
"optional": true
|
|
77
76
|
},
|
|
78
|
-
"next": {
|
|
79
|
-
"optional": true
|
|
80
|
-
},
|
|
81
77
|
"react": {
|
|
82
78
|
"optional": true
|
|
83
79
|
}
|
|
@@ -93,6 +89,6 @@
|
|
|
93
89
|
"scripts": {
|
|
94
90
|
"build": "tsc -p tsconfig.json",
|
|
95
91
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
96
|
-
"test": "tsc -p tsconfig.json && node --test test/comments.test.js test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js test/inflate.test.js test/field-names.test.js test/next-graphql.test.js test/graphql-codegen.test.js test/graphql-contract.test.js test/readme-snippets.test.js test/nextjs-app.test.js test/package-json.test.js test/changelog.test.js test/published-docs.test.js test/builder-messages.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit && tsc -p test/types/tsconfig.nonstrict.json --noEmit"
|
|
92
|
+
"test": "tsc -p tsconfig.json && node --test test/comments.test.js test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js test/inflate.test.js test/field-names.test.js test/next-graphql.test.js test/graphql-codegen.test.js test/graphql-contract.test.js test/readme-snippets.test.js test/nextjs-app.test.js test/nextjs-preview-app.test.js test/package-json.test.js test/changelog.test.js test/published-docs.test.js test/builder-messages.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit && tsc -p test/types/tsconfig.nonstrict.json --noEmit && tsc -p test/types/tsconfig.next.json --noEmit"
|
|
97
93
|
}
|
|
98
94
|
}
|