如何使用 Next.js 生成動態站點地圖
如何為基於 Next.js 的網站或應用動態生成站點地圖,以提高您的網站在 Google 和 DuckDuckGo 等搜索引擎中的可發現性。
如果您正在使用 Next.js 構建需要對 Google 等搜索引擎可見的站點或應用程序,那麼擁有可用的站點地圖是必不可少的。站點地圖是您網站上 URL 的地圖,可讓搜索引擎更輕鬆地為您的內容編制索引,從而提高在搜索結果中排名的可能性。
在 Next.js 中,因為我們依靠內置的路由器向公眾公開路由,所以設置站點地圖的最簡單方法是創建一個特殊的頁面組件,該組件修改其響應標頭以向瀏覽器發出返回內容的信號是 text/xml
數據(瀏覽器和搜索引擎預計我們的站點地圖將作為 XML 文件返回)。
通過這樣做,我們可以利用 React 和 Next.js 的常用數據獲取和渲染便利,同時以瀏覽器期望的格式返回數據。
為了演示它是如何工作的,我們將使用 CheatCode Next.js 樣板作為起點。首先,從 Github 克隆一個副本:
git clone https://github.com/cheatcode/nextjs-boilerplate.git
接下來,cd
進入克隆目錄並通過 NPM 安裝樣板的依賴項:
cd nextjs-boilerplate && npm install
最後,使用(從項目的根目錄)啟動樣板:
npm run dev
一旦所有這些都完成了,我們就可以開始構建我們的站點地圖組件了。
創建站點地圖頁面組件
一、在/pages
在項目的根目錄下,創建一個名為 sitemap.xml.js
的新文件(文件,而不是文件夾) .我們選擇這個名稱的原因是 Next.js 會在我們的應用程序中自動在 /sitemap.xml
處創建一個路由 這是瀏覽器和搜索引擎爬蟲期望我們的站點地圖存在的位置。
接下來,在文件內部,讓我們開始構建組件:
/pages/sitemap.xml.js
import React from "react";
const Sitemap = () => {};
export default Sitemap;
你會注意到的第一件事是這個組件只是一個空的函數組件(這意味著當組件被 React 渲染時我們不會渲染任何標記)。這是因為,從技術上講,我們不想在此 URL 處呈現組件。相反,我們想劫持 getServerSideProps
方法(這由 Next.js 在服務器上接收入站請求時調用)說“而不是獲取一些數據並將其映射到我們組件的 props,而是覆蓋 res
對象(我們的響應),而是返回我們站點地圖的內容。”
這很可能令人困惑。進一步充實這一點,讓我們添加一個粗略版本的 res
覆蓋我們需要做的:
/pages/sitemap.xml.js
import React from "react";
const Sitemap = () => {};
export const getServerSideProps = ({ res }) => {
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- We'll render the URLs for our sitemap here. -->
</urlset>
`;
res.setHeader("Content-Type", "text/xml");
res.write(sitemap);
res.end();
return {
props: {},
};
};
export default Sitemap;
這應該使“覆蓋”概念更加具體。現在,我們可以看到,不是從 getServerSideProps
返回一個 props 對象 ,我們手動調用設置 Content-Type
響應頭,寫入響應體,結束請求(表示應該將響應發送回原來的請求)。
在這裡,我們已經指定了站點地圖的基本模板。就像我們上面暗示的那樣,站點地圖應該是 XML 數據格式(或者,text/xml
MIME 類型)。接下來,當我們獲取數據時,我們將填充 <urlset></urlset>
帶有 <url></url>
的標記 標籤。每個標籤將代表我們網站中的一個頁面,並提供該頁面的 URL。
在 getInitialProps
的底部 函數,我們處理對入站請求的響應。
首先,我們設置 Content-Type
響應中的標頭向瀏覽器發出我們正在返回 .xml
的信號 文件。這是因為 Content-Type
設置瀏覽器需要呈現的內容和 sitemap.xml
的期望 我們的 sitemap.xml.js
的一部分 文件的名稱是 Next.js 用於頁面 URL 的名稱。所以,如果我們調用我們的頁面 pizza.json.js
,Next.js 生成的 URL 將類似於 http://mydomain.com/pizza.json
(在這種情況下,我們會得到 http://mydomain.com/sitemap.xml
)。
接下來,我們調用 res.write()
,傳遞生成的sitemap
細繩。這將代表瀏覽器(或搜索引擎爬蟲)收到的響應正文。之後,我們用 res.end()
向請求返回“我們已經發送了所有我們可以發送的內容”的信號 .
滿足getServerSideProps
的要求 函數(根據 Next.js 的規則),我們返回一個帶有 props
的空對象 property 設置為一個空對象——明確一點,如果我們不這樣做,Next.js 會拋出一個錯誤。
為您的站點地圖獲取數據
現在是有趣的部分。接下來,我們需要獲取要在站點地圖中表示的站點上的所有內容。通常這是一切 ,但您可能需要排除某些網頁。
當涉及到什麼 我們在站點地圖中獲取返回的內容,有兩種類型:
- 靜態頁面 - 位於您網站/應用程序中固定 URL 的頁面。例如,
http://mydomain.com/about
. - 動態頁面 - 位於您網站/應用程序中可變 URL 的頁面,例如博客文章或其他一些動態內容。例如,
http://mydomain.com/posts/slug-of-my-post
.
檢索此數據可通過多種方式完成。首先,對於靜態頁面,我們可以列出我們的/pages
的內容 目錄(過濾掉我們想要忽略的項目)。對於動態頁面,可以採用類似的方法,從 REST API 或 GraphQL API 獲取數據。
首先,讓我們看一下獲取 static 的列表 我們應用程序中的頁面以及如何添加一些過濾以減少我們想要的內容:
/pages/sitemap.xml.js
import React from "react";
import fs from "fs";
const Sitemap = () => {};
export const getServerSideProps = ({ res }) => {
const baseUrl = {
development: "http://localhost:5000",
production: "https://mydomain.com",
}[process.env.NODE_ENV];
const staticPages = fs
.readdirSync("pages")
.filter((staticPage) => {
return ![
"_app.js",
"_document.js",
"_error.js",
"sitemap.xml.js",
].includes(staticPage);
})
.map((staticPagePath) => {
return `${baseUrl}/${staticPagePath}`;
});
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${staticPages
.map((url) => {
return `
<url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
`;
})
.join("")}
</urlset>
`;
res.setHeader("Content-Type", "text/xml");
res.write(sitemap);
res.end();
return {
props: {},
};
};
export default Sitemap;
我們在這裡添加了三件大事:
首先,我們添加了一個新的 baseUrl
getServerSideProps
頂部的值 函數將允許我們設置我們在站點地圖中呈現的每個 URL 的基礎。這是必要的,因為我們的站點地圖必須包含 absolute 路徑。
其次,我們添加了對 fs.readdirSync()
的調用 函數(使用 fs
在文件頂部導入),這是 Node.js 中內置的同步讀取目錄方法。這允許我們在我們傳遞的路徑下獲取目錄的文件列表(這裡,我們指定 pages
目錄,因為我們想獲取我們所有的靜態頁面)。
獲取後,我們指定調用 .filter()
在我們期望返回的數組上,過濾掉我們網站中的實用程序頁面(包括 sitemap.xml.js
本身)我們做不 希望出現在我們的站點地圖中。在此之後,我們映射每個有效頁面並將它們的路徑與 baseUrl
連接起來 我們根據我們當前的 NODE_ENV
確定 在頂部。
如果我們要console.log(staticPages)
,這樣的最終結果應該是這樣的:
[
'http://localhost:5000/documents',
'http://localhost:5000/login',
'http://localhost:5000/recover-password',
'http://localhost:5000/reset-password',
'http://localhost:5000/signup'
]
第三,回到我們的 sitemap
我們將站點地圖存儲為字符串的變量(在傳遞給 res.write()
),我們可以看到我們已經對其進行了修改以執行 .map()
通過我們的 staticPages
數組,返回一個字符串,其中包含向我們的站點地圖添加 URL 所需的標記:
/pages/sitemap.xml.js
${staticPages
.map((url) => {
return `
<url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
`;
})
.join("")}
就什麼而言 我們正在返回,這裡我們返回 Web 瀏覽器(或搜索引擎爬蟲)在讀取站點地圖時所期望的 XML 內容。對於我們要添加到地圖中的站點中的每個 URL,我們添加 <url></url>
標籤,放置一個 <loc></loc>
裡面的標籤指定位置 我們的 URL,<lastmod></lastmod>
指定 URL 內容最後更新時間的標記,<changefreq></changefreq>
指定如何的標籤 經常更新 URL 上的內容,並且 <priority></priority>
標記來指定 URL 的重要性(轉換為爬蟲應該多頻繁地爬取該頁面)。
在這裡,我們傳遞了我們的 url
到 <loc></loc>
然後設置我們的 <lastmod></lastmod>
將當前日期作為 ISO-8601 字符串(計算機/人類可讀日期格式的標準類型)。如果您有這些頁面上次更新的日期,最好盡可能準確地使用此日期並在此處傳遞該特定日期。
對於 <changefreq></changefreq>
,我們設置了一個合理的默認值 monthly
, 但這可以是以下任何一種:
never
yearly
,monthly
weekly
daily
hourly
always
類似於 <lastmod></lastmod>
標籤,您會希望它盡可能準確,以避免搜索引擎規則出現任何問題。
最後,對於 <priority></priority>
,我們設置一個 1.0
的基數 (最大程度的重要性)。如果您希望將其更改為更具體,此數字可以是 0.0
之間的任何值 和 1.0
使用 0.0
不重要,1.0
最重要。
雖然從技術上講,如果我們訪問 http://localhost:5000/sitemap.xml
在我們的瀏覽器中(假設您正在使用 CheatCode Next.js 樣板並之前啟動了開發服務器),我們應該會看到一個包含我們的靜態頁面的站點地圖!
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost:5000/documents</loc>
<lastmod>2021-04-14T01:36:47.469Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/login</loc>
<lastmod>2021-04-14T01:36:47.469Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/recover-password</loc>
<lastmod>2021-04-14T01:36:47.469Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/reset-password</loc>
<lastmod>2021-04-14T01:36:47.469Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/signup</loc>
<lastmod>2021-04-14T01:36:47.469Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
接下來,讓我們看看通過使用 GraphQL 獲取我們的動態頁面來擴展我們的站點地圖。
為我們的站點地圖生成動態數據
因為我們在示例中使用了 CheatCode Next.js 樣板,所以我們已經有了 GraphQL 客戶端所需的連接。為了將我們的工作置於上下文中,我們將將此功能與 CheatCode Node.js 樣板結合使用,其中包括一個使用 MongoDB 的示例數據庫、一個完全實現的 GraphQL 服務器以及一個我們可以用來提取測試數據的示例文檔集合來自。
首先,讓我們克隆一份 Node.js 樣板並進行設置:
git clone https://github.com/cheatcode/nodejs-server-boilerplate.git
然後是 cd
進入克隆項目並安裝所有依賴項:
cd nodejs-server-boilerplate && npm install
最後,繼續運行服務器(從項目的根目錄):
npm run dev
如果您繼續打開項目,我們將添加一些代碼來為數據庫播種一些文檔,這樣我們實際上可以為我們的站點地圖獲取一些東西:
/api/fixtures/documents.js
import _ from "lodash";
import generateId from "../../lib/generateId";
import Documents from "../documents";
import Users from "../users";
export default async () => {
let i = 0;
const testUser = await Users.findOne();
const existingDocuments = await Documents.find().count();
if (existingDocuments < 100) {
while (i < 100) {
const title = `Document #${i + 1}`;
await Documents.insertOne({
_id: generateId(),
title,
userId: testUser?._id,
content: "Test content.",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
i += 1;
}
}
};
首先,我們需要創建一個文件來保存將為我們生成測試文檔的夾具(為我們生成測試數據的代碼的暱稱)。為此,我們導出了一個函數來做一些事情:
- 檢索一個測試用戶(由包含的
/api/fixtures/users.js
創建 樣板中包含的夾具)。 - 檢索現有的
.count()
數據庫中的文檔。 - 運行
while
循環說“而existingDocuments
的數量 在數據庫中小於100
,插入一個文檔。”
對於文檔的內容,我們生成一個利用當前 i
的標題 循環的迭代加一為每個生成的文檔生成不同的標題。接下來,我們調用 Documents.insertOne()
函數,由我們導入的 Documents
提供 集合(已經在樣板文件中實現)到 .insertOne()
文件。
該文檔包括一個 _id
使用包含的 generateId()
設置為十六進製字符串 樣板中的函數。接下來,我們設置 title
,後跟 userId
設置為 _id
testUser
我們檢索然後我們設置了一些虛擬內容以及 createdAt
和 updatedAt
時間戳(我們將在接下來的站點地圖中發揮作用)。
/api/index.js
import graphql from "./graphql/server";
import usersFixture from "./fixtures/users";
import documentsFixture from "./fixtures/documents";
export default async (app) => {
graphql(app);
await usersFixture();
await documentsFixture();
};
為了完成所有這些工作,我們需要提取包含的 users
夾具和我們的新 documents
/api/index.js
中的功能 文件(此文件在服務器啟動時自動為我們加載)。因為我們的fixture是作為函數導出的,所以在我們導入它們之後,在從/api/index.js
導出的函數中 ,我們調用這些函數,確保 await
避免與我們的數據競爭條件的調用(請記住,在我們嘗試創建文檔之前,我們的用戶需要存在)。
在繼續之前,我們需要再做一個微小的更改,以確保我們可以獲取文檔進行測試:
/api/documents/graphql/queries.js
import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";
export default {
documents: async (parent, args, context) => {
return Documents.find().toArray();
},
[...]
};
默認情況下,示例 documents
Node.js 樣板中的解析器將查詢傳遞給 Documents.find()
僅為登錄用戶的 _id
請求返回文檔的方法 .在這裡,我們可以刪除這個查詢,只要求返回所有文檔,因為我們只是在測試它。
在服務器端就是這樣。讓我們跳回客戶端並將其連接到我們的站點地圖。
從我們的 GraphQL API 獲取數據
正如我們在上一節中看到的,Node.js 樣板還包括一個完全配置的 GraphQL 服務器和用於獲取文檔的現有解析器。回到我們的 /pages/sitemap.xml.js
文件,讓我們在 Next.js Boilerplate 中引入包含的 GraphQL 客戶端,並從現有的 documents
中獲取一些數據 GraphQL API 中的解析器:
/pages/sitemap.xml.js
import React from "react";
import fs from "fs";
import { documents as documentsQuery } from "../graphql/queries/Documents.gql";
import client from "../graphql/client";
const Sitemap = () => {};
export const getServerSideProps = async ({ res }) => {
const baseUrl = {
development: "http://localhost:5000",
production: "https://mydomain.com",
}[process.env.NODE_ENV];
const staticPages = fs
.readdirSync("pages")
.filter((staticPage) => {
return ![
"_app.js",
"_document.js",
"_error.js",
"sitemap.xml.js",
].includes(staticPage);
})
.map((staticPagePath) => {
return `${baseUrl}/${staticPagePath}`;
});
const { data } = await client.query({ query: documentsQuery });
const documents = data?.documents || [];
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${staticPages
.map((url) => {
return `
<url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
`;
})
.join("")}
${documents
.map(({ _id, updatedAt }) => {
return `
<url>
<loc>${baseUrl}/documents/${_id}</loc>
<lastmod>${updatedAt}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
`;
})
.join("")}
</urlset>
`;
res.setHeader("Content-Type", "text/xml");
res.write(sitemap);
res.end();
return {
props: {},
};
};
export default Sitemap;
在文件的頂部,我們從 /graphql/queries/Documents.gql
導入了示例 GraphQL 查詢文件 文件包含在 CheatCode Next.js 樣板中。在此之下,我們還從 /graphql/client.js
導入包含的 GraphQL 客戶端 .
回到我們的 getServerSideProps
函數,我們添加對 client.query()
的調用 在我們之前調用獲取 staticPages
的下方對我們的文檔執行 GraphQL 查詢 .隨著我們的列表,我們重複我們之前看到的相同模式,.map()
documents
我們發現並使用了與靜態頁面相同的 XML 結構。
這裡最大的不同是對於我們的 <loc></loc>
,我們在 .map()
中手動構建我們的 URL ,利用我們現有的 baseUrl
值並附加 /documents/${_id}
到它,其中 _id
是我們正在映射的當前文檔的唯一 ID。我們還將內聯調用交換為 new Date().toISOString()
傳遞給 <lastmod></lastmod>
使用 updatedAt
我們在數據庫中設置的時間戳。
而已!如果您訪問 http://localhost:5000/sitemap.xml
在瀏覽器中,您應該會看到我們現有的靜態頁面,以及我們動態生成的文檔 URL:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost:5000/documents</loc>
<lastmod>2021-04-14T03:06:24.018Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/login</loc>
<lastmod>2021-04-14T03:06:24.018Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/recover-password</loc>
<lastmod>2021-04-14T03:06:24.018Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/reset-password</loc>
<lastmod>2021-04-14T03:06:24.018Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/signup</loc>
<lastmod>2021-04-14T03:06:24.018Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/documents/y9QSUXFlSqzl3ZzN</loc>
<lastmod>2021-04-14T02:27:06.747Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/documents/6okKJ3vHX5K0F4A1</loc>
<lastmod>2021-04-14T02:27:06.749Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://localhost:5000/documents/CdyxBJnVk70vpeSX</loc>
<lastmod>2021-04-14T02:27:06.750Z</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
[...]
</urlset>
從這裡開始,一旦您的網站在線部署,您就可以將您的站點地圖提交給 Google 等搜索引擎,以確保您的網站被正確編入索引和排名。
在 Vercel 上處理 Next.js 構建問題
對於試圖讓上述代碼在 Vercel 上運行的開發人員,需要對 fs.readdirSync()
的調用進行一些小改動 以上。而不是使用 fs.readdirSync("pages")
就像我們上面展示的那樣,你需要修改你的代碼看起來像這樣:
/pages/sitemap.xml.js
const staticPages = fs
.readdirSync({
development: 'pages',
production: './',
}[process.env.NODE_ENV])
.filter((staticPage) => {
return ![
"_app.js",
"_document.js",
"_error.js",
"sitemap.xml.js",
].includes(staticPage);
})
.map((staticPagePath) => {
return `${baseUrl}/${staticPagePath}`;
});
這裡的變化是我們傳遞給 fs.readdirSync()
.在 Vercel 部署的 Next.js 應用程序中,頁面目錄的路徑會發生變化。添加我們上面看到的條件路徑可確保當您的站點地圖代碼運行時,它將頁面解析為正確的路徑(在本例中為 /build/server/pages
Vercel 構建您的應用程序時生成的目錄)。
總結
在本教程中,我們學習瞭如何使用 Next.js 動態生成站點地圖。我們學習瞭如何使用 getServerSideProps
Next.js 中的函數來劫持對 /sitemap.xml
的請求的響應 頁面並返回一個 XML 字符串,強制 Content-Type
標頭為 text/xml
模擬返回 .xml
文件。
我們還研究了使用 Node.js 在 MongoDB 中生成一些測試數據,並通過 GraphQL 查詢檢索這些數據以包含在我們的站點地圖中。