From f3b6e4751ea1bda17a209367b54e96a5176725f0 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust <24361490+mathiazom@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:23:13 +0000 Subject: [PATCH] v3 - include root path in sitemap (#695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sitemap): include root path in sitemap * refactor(sitemap): s → slugDocument * refactor(app): extract sitemap base url parsing into common utility function --- src/app/(main)/page.tsx | 8 +++-- src/app/env.ts | 12 +++++++ src/app/robots.ts | 15 +++++++-- src/app/sitemap.ts | 38 ++++++++++++++++------- src/middleware.ts | 31 +++++++++++------- studio/components/CustomCallToActions.tsx | 4 +-- studio/lib/interfaces/global.ts | 5 +++ studio/lib/queries/document.ts | 5 +++ studio/lib/queries/navigation.ts | 6 +++- 9 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 src/app/env.ts create mode 100644 studio/lib/queries/document.ts diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 302bcb9fb..f22e0228b 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -6,12 +6,14 @@ import SectionRenderer from "src/utils/renderSection"; import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo"; import { LinkType } from "studio/lib/interfaces/navigation"; import { PageBuilder } from "studio/lib/interfaces/pages"; -import { LANDING_QUERY } from "studio/lib/queries/navigation"; +import { LANDING_PAGE_REF_QUERY } from "studio/lib/queries/navigation"; import { PAGE_QUERY, SEO_PAGE_QUERY } from "studio/lib/queries/page"; import { loadStudioQuery } from "studio/lib/store"; export async function generateMetadata(): Promise { - const { data: landingId } = await loadStudioQuery(LANDING_QUERY); + const { data: landingId } = await loadStudioQuery( + LANDING_PAGE_REF_QUERY, + ); const seo = await fetchSeoData(SEO_PAGE_QUERY, { id: landingId }); return generateMetadataFromSeo(seo); } @@ -36,7 +38,7 @@ const Home = async () => { const { perspective, isDraftMode } = getDraftModeInfo(); const { data: landingId } = await loadStudioQuery( - LANDING_QUERY, + LANDING_PAGE_REF_QUERY, {}, { perspective }, ); diff --git a/src/app/env.ts b/src/app/env.ts new file mode 100644 index 000000000..15f0036c8 --- /dev/null +++ b/src/app/env.ts @@ -0,0 +1,12 @@ +import { Result, ResultError, ResultOk } from "studio/utils/result"; + +export function readBaseUrl(): Result { + const url = process.env.NEXT_PUBLIC_URL; + if (url === undefined) { + return ResultError("Missing environment variable: NEXT_PUBLIC_URL"); + } + if (!URL.canParse(url)) { + return ResultError("Invalid environment variable: NEXT_PUBLIC_URL"); + } + return ResultOk(new URL(url)); +} diff --git a/src/app/robots.ts b/src/app/robots.ts index 3265d2247..fece8039f 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,11 +1,22 @@ import type { MetadataRoute } from "next"; +import { readBaseUrl } from "./env"; + export default function robots(): MetadataRoute.Robots { - return { + const robotsFile: MetadataRoute.Robots = { rules: { userAgent: "*", disallow: ["/studio", "/shared", "/api"], }, - sitemap: new URL("sitemap.xml", process.env.NEXT_PUBLIC_URL).toString(), }; + const baseUrlResult = readBaseUrl(); + if (baseUrlResult.ok) { + robotsFile.sitemap = new URL("sitemap.xml", baseUrlResult.value).toString(); + } else { + console.warn( + "Could not include sitemap in robots.txt, missing base url:", + baseUrlResult.error, + ); + } + return robotsFile; } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 1ae16174f..8528b95f7 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,13 +1,13 @@ import type { MetadataRoute } from "next"; import { client } from "studio/lib/client"; -import { Slug } from "studio/lib/interfaces/global"; +import { DocumentWithSlug } from "studio/lib/interfaces/global"; +import { PageBuilder } from "studio/lib/interfaces/pages"; +import { DOCUMENTS_WITH_SLUG_QUERY } from "studio/lib/queries/document"; +import { LANDING_PAGE_QUERY } from "studio/lib/queries/navigation"; import { token } from "studio/lib/token"; -interface SitemapDocument { - slug: Slug; - _updatedAt: string; -} +import { readBaseUrl } from "./env"; const clientWithToken = client.withConfig({ token }); @@ -15,11 +15,27 @@ export const dynamic = "force-dynamic"; export const fetchCache = "default-no-store"; export default async function sitemap(): Promise { - const documents = - await clientWithToken.fetch(`*[defined(slug)]`); - - return documents.map((s) => ({ - url: new URL(s.slug.current, process.env.NEXT_PUBLIC_URL).toString(), - lastModified: new Date(s._updatedAt), + const baseUrlResult = readBaseUrl(); + if (!baseUrlResult.ok) { + console.error("Failed to generate sitemap:", baseUrlResult.error); + return []; + } + const baseUrl = baseUrlResult.value; + const slugDocuments = await clientWithToken.fetch( + DOCUMENTS_WITH_SLUG_QUERY, + ); + const sitemapEntries = slugDocuments.map((slugDocument) => ({ + url: new URL(slugDocument.slug.current, baseUrl).toString(), + lastModified: new Date(slugDocument._updatedAt), })); + const landingPage = await clientWithToken.fetch( + LANDING_PAGE_QUERY, + ); + if (landingPage !== null) { + sitemapEntries.push({ + url: baseUrl.toString(), + lastModified: new Date(landingPage._updatedAt), + }); + } + return sitemapEntries; } diff --git a/src/middleware.ts b/src/middleware.ts index d77ee7226..f639dc33c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,28 +3,35 @@ import { NextRequest, NextResponse } from "next/server"; import { RedirectDestinationSlugPage } from "studio/lib/interfaces/redirect"; import { REDIRECT_BY_SOURCE_SLUG_QUERY } from "studio/lib/queries/redirects"; +import { readBaseUrl } from "./app/env"; import { HTTP_STATUSES } from "./utils/http"; export async function middleware(request: NextRequest) { + const baseUrlResult = readBaseUrl(); + if (!baseUrlResult.ok) { + console.error( + "Failed to run middleware, missing base url:", + baseUrlResult.error, + ); + return; + } + const baseUrl = baseUrlResult.value; const slug = request.nextUrl.pathname; const slugQueryParam = slug.replace(/^\/+/, ""); /* fetching redirect data via API route to avoid token leaking to client (middleware should run on server, but `experimental_taintUniqueValue` begs to differ...) */ - const redirectRes = await fetch( - new URL("/api/fetchData", process.env.NEXT_PUBLIC_URL), - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: REDIRECT_BY_SOURCE_SLUG_QUERY, - params: { slug: slugQueryParam }, - }), + const redirectRes = await fetch(new URL("/api/fetchData", baseUrl), { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ + query: REDIRECT_BY_SOURCE_SLUG_QUERY, + params: { slug: slugQueryParam }, + }), + }); if (redirectRes.ok) { const redirect: RedirectDestinationSlugPage | null = await redirectRes.json(); diff --git a/studio/components/CustomCallToActions.tsx b/studio/components/CustomCallToActions.tsx index 27f56cdbe..13c5c13ee 100644 --- a/studio/components/CustomCallToActions.tsx +++ b/studio/components/CustomCallToActions.tsx @@ -9,7 +9,7 @@ import { } from "sanity"; import { fetchWithToken } from "studio/lib/fetchWithToken"; -import { LANDING_QUERY } from "studio/lib/queries/navigation"; +import { LANDING_PAGE_REF_QUERY } from "studio/lib/queries/navigation"; type CustomCallToActionsProps = ArrayOfObjectsInputProps< { _key: string }, @@ -28,7 +28,7 @@ const CustomCallToActions: React.FC = (props) => { const fetchLandingId = async () => { try { setLoading(true); - const landingId = await fetchWithToken(LANDING_QUERY); + const landingId = await fetchWithToken(LANDING_PAGE_REF_QUERY); setLandingPageId(landingId); } catch (error) { console.error("Failed to fetch navigation manager", error); diff --git a/studio/lib/interfaces/global.ts b/studio/lib/interfaces/global.ts index 1be127644..11aed98eb 100644 --- a/studio/lib/interfaces/global.ts +++ b/studio/lib/interfaces/global.ts @@ -7,3 +7,8 @@ export interface Reference { _type: "reference"; _ref: string; } + +export interface DocumentWithSlug { + slug: Slug; + _updatedAt: string; +} diff --git a/studio/lib/queries/document.ts b/studio/lib/queries/document.ts new file mode 100644 index 000000000..4e3f88efd --- /dev/null +++ b/studio/lib/queries/document.ts @@ -0,0 +1,5 @@ +import { groq } from "next-sanity"; + +export const DOCUMENTS_WITH_SLUG_QUERY = groq` + *[defined(slug)] +`; diff --git a/studio/lib/queries/navigation.ts b/studio/lib/queries/navigation.ts index 922e3d161..81d7c1d04 100644 --- a/studio/lib/queries/navigation.ts +++ b/studio/lib/queries/navigation.ts @@ -38,6 +38,10 @@ export const NAV_QUERY = groq` } `; -export const LANDING_QUERY = groq` +export const LANDING_PAGE_REF_QUERY = groq` *[_type == "navigationManager"][0].setLanding._ref `; + +export const LANDING_PAGE_QUERY = groq` + *[_type == "navigationManager"][0].setLanding -> +`;