# [Next] Metadata and OG images 學習筆記 ###### tags: `Next` `前端筆記` <div style="display: flex; justify-content: flex-end"> > created: 2025/08/18 </div> ## SEO(Search Engine Optimization) 是一種透過優化網站內容和結構,提升網站在搜尋引擎結果頁面中自然排名的技術和策略。 ## Metadata 對 SEO 的幫助 Metadata 是幫助 SEO 的其中一種工具 => SEO 是整體策略,metadata 是其中一個工具。 主要是因為它們可以幫助搜尋引擎理解網頁內容(會轉成 HTML tags 給爬蟲機器人讀),並提升網站在搜尋結果頁面的可見度和點擊率。 ## 目標:使用者特定關鍵字搜尋時我們的網站會排比較前面 ### 需要先了解使用者透過 Google Search 後 Google 怎麼處理: 1. [**Crawling:**](https://developers.google.com/search/docs/fundamentals/how-search-works#crawling)  Google downloads text, images, and videos from pages it found on the internet with automated programs called crawlers. 2. [**Indexing:**](https://developers.google.com/search/docs/fundamentals/how-search-works#indexing)  Google analyzes the text, images, and video files on the page, and stores the information in the Google index, which is a large database. 3. [**Serving search results:**](https://developers.google.com/search/docs/fundamentals/how-search-works#serving)  When a user searches on Google, Google returns information that's relevant to the user's query. **會先經 Crawling** => 執行 Crawling 就是 [Googlebot](https://developers.google.com/search/docs/crawling-indexing/googlebot)(會自動發現和訪問網頁) **發現網頁後就會 Indexing** => 分析內容並且在巨大的資料庫根據分析的資訊建立索引(根據文件上的描述,這個索引也許會被用來幫助提供顯示結果時使用)(類似以網址當作獨一的 `key`) > Google also collects signals about the canonical page and its contents, which may be used in the next stage, where we serve the page in search results. Some signals include the language of the page, the country the content is local to, and the usability of the page. **然後最後就是 Serving search results** => 當使用者透過 Google Search 關鍵字搜尋時提供搜尋結果(Google 的機器就會從它建立的索引找到它認為符合的結果,而且會根據地點以及語言顯示的結果有所差異) ### 回到公司產品面:網址沒有語系資訊 因為公司的產品網址沒有語系資訊,所以在 Indexing 就很難針對特定語系去讓 Googlebot 建立索引。但主管原先提出一個想法,既然如此就去找一個類似於基準的 HTTP request header [Accept-Language header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language),讀取該次 Googlebot 爬蟲來的時候的語系建立對應的 Metadata,藉此讓建立索引的時候符合多國語系(因為產品的網址沒有辨別語系的手段)。 #### 為什麼可以這麼推斷? 因為 Googlebot 其實也是要網址進來你的網頁(雖然不是用瀏覽器)所以自然可以抓 HTTP 請求。 > You can identify the subtype of Googlebot by looking at the [HTTP  `user-agent`  request header](https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers) in the request. #### 但是結果這麼做是不可行的 > If your site has _locale-adaptive_ pages (that is, your site returns different content based on the perceived country or preferred language of the visitor), Google might not crawl, index, or rank all your content for different locales. This is because the default IP addresses of the Googlebot crawler appear to be based in the USA. In addition, the crawler sends HTTP requests without setting `Accept-Language` in the request header. [*ref.*](https://developers.google.com/search/docs/specialty/international/locale-adaptive-pages) 因為 Googlebot 爬網頁時就不會塞 `Accept-Language` 在 request header 之中,所以爬蟲不管怎麼進來,就是只會看到產品預設的語系(我們是繁體中文)。所以當英語使用者使用英文搜尋,我們的產品的可能不會被放在搜尋結果之中。 **因為 Google 在顯示搜尋結果時也會參照目前使用者「語言」或者「地區」提供它認為最有關聯性的結果。 => 需要靠 Indexing** ## 靜態單純定義即可 ```typescript! // src/app/layout.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: 'My Blog', description: '...', } export default function Page() {} ``` ### 順序 Metadata 有順序以及合併的機制,讓開發者可以在靜態先定義好 `default` 的值,動態的部分若有需求再額外合併。 1. `app/layout.tsx`  (Root Layout) 2. `app/blog/layout.tsx`  (Nested Blog Layout) 3. `app/blog/[slug]/page.tsx`  (Blog Page) 若有欄位是 nested 的話,順序後面的也會覆蓋前面定義的! ```typescript! // app/layout.ts export const metadata = { title: 'Acme', openGraph: { title: 'Acme', description: 'Acme is a...', }, } ``` ```typescript! // app/blog/page.ts export const metadata = { title: 'Blog', openGraph: { title: 'Blog', }, } // Output: // <title>Blog</title> // <meta property="og:title" content="Blog" /> ``` - `app/blog/page.ts` 定義的 `title` 覆蓋了 `app/layout.ts` 定義的 `title` - `app/blog/page.ts` 定義的 `openGraph` 覆蓋了 `app/layout.ts` 定義的 `openGraph` 不會是這樣子的操作: ```typescript! { openGraph: { title: 'Blog', description: 'Acme is a...', } } ``` ## 動態變動的話就可以用 `generateMetadata` 處理 - 這個 callback 就是專門產頁面 Metadata 的 - 放在 `layout.js` & `page.js` 搭配 server component 的環境打 API 動態取得 metadata - `metadata` object and `generateMetadata` function exports are **only supported in Server Components**. 其餘 API 可參照[文件](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) ### 也會遵照 Metadata 順序以及合併的機制嗎? Yes,因為 `generateMetadata` 最後也是會回傳 `Metadata`。`generateMetadata` 只是開一個 callback 讓開發者可以打額外的 API 去組合需要的 Metadata 資訊,所以還是會遵照這個機制。 ## `tamplate` `Metadata.title` 可以選擇傳入一般字串或者額外設定 `{ title: '%s' + 需要的東西, default: 預設顯示的東西 }`。 這樣子在專案內其設定的子層路由就會套用這個 `template`。 > 用 `%s` 定義動態前綴。 ```typescript! // app/layout.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: { template: '%s | Acme', default: 'Acme', // a default is required when creating a template }, } ``` ```typescript! // app/about/page.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: 'About', } // Output: <title>About | Acme</title> ``` 當有子層路由不想要繼承時可以用 `absolute` 去跳脫 `template` ```typescript! // app/about/page.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: { absolute: 'About', }, } // Output: <title>About</title> ``` ## `robots` 這個 tag 能告訴搜尋引擎是否要索引這個頁面或追蹤頁面上的連結。 ```typescript! import type { Metadata } from 'next' export const metadata: Metadata = { robots: { index: true, follow: true, nocache: false, googleBot: { index: true, follow: true, noimageindex: false, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1, }, }, } ``` ## `alternates` 避免搜尋引擎重複爬相同的頁面,造成 SEO 排行更新的效率不好。為了讓引擎知道哪個網址才是「標準」的網址,在 HTML 中可以用 `<link rel="canonical" href="">` 告訴引擎哪個網址才是標準。 以活動詳細頁來看,專案該網址應為 `https://accupass.com/event/2506110537432134189256` 但是會有 GA 等數據需要追蹤,所以也可以透過這個網址進入同一頁面 `https://www.accupass.com/event/2506110537432134189256?utm_source=Web&utm_medium=home_north&utm_campaign=accu_theme`, 所以這個情況就可以用 `alternates` 藉此改善網頁被判定為重複頁面及網頁權重稀釋所造成的排名波動。 ### 記得要使用絕對路徑 > Use absolute paths rather than relative paths with the  `rel="canonical"`  `link`  element. Even though relative paths are supported by Google, they can cause problems in the long run (for example, if you unintentionally allow your testing site to be crawled) and thus we don't recommend them. > **Good example**:  `https://www.example.com/dresses/green/green-dress.html` > **Bad example**:  `/dresses/green/green-dress.html` > The  `rel="canonical"`  `link element`  is only accepted if it appears in the  `<head>`  section of the HTML, so make sure at least the  [`<head>`  section is valid HTML](https://developers.google.com/search/docs/crawling-indexing/valid-page-metadata). 雖然 Google 也支援相對路徑,但是 Google 還是建議使用絕對路徑。也需要塞在 `<head>` 標籤之內。 *[ref. How to specify a canonical URL with rel="canonical" and other methods](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)* ```typescript! export async function generateMetadata( { params: { eventId } }: { params: { eventId: string } }, parent: ResolvingMetadata ): Promise<Metadata> { const event = await fetchEventDetail(eventId); return { // ..., alternates: { canonical: `${process.env.NEXT_PUBLIC_MAIN_FRONTEND_DOMAIN}/event/${eventId}`, }, }; } ``` 上面的程式碼最後會產出下列的 HTML: ![截圖 2025-08-18 15.50.41](https://hackmd.io/_uploads/HyZws8eYxe.png) ## openGraph `openGraph` 就是設定 [The Open Graph protocol](https://ogp.me/)。這個東西就可以讓社群分享網站時可以有圖片跟文字區塊,吸引其他人點擊。而這個東西有定義屬性讓其他社群可以根據這些屬性渲染出來。 可以用這個網址貼上測試網站在其他社群分享後會長怎麼樣 https://socialsharepreview.com/ ### `ImageResponse` 可以額外客製化 1. 若沒有「動態」圖片,想要讓網站能夠有「統一格式,但是又可以客製化部分樣式」 2. 若有「動態」圖片,但想要額外客製化 `ImageResponse` 可以使用 JSX element 建立實例,因為是 JSX element 所以也可以支援用 CSS 調整樣式。 ```jsx! // ref. https://nextjs.org/docs/app/api-reference/functions/image-response import { ImageResponse } from 'next/og' // Image metadata export const alt = 'My site' export const size = { width: 1200, height: 630, } export const contentType = 'image/png' // Image generation export default async function Image() { return new ImageResponse( ( // ImageResponse JSX element <div style={{ fontSize: 128, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > My site </div> ), // ImageResponse options { // For convenience, we can re-use the exported opengraph-image // size config to also set the ImageResponse's width and height. ...size, } ) } ``` 若想要動態修改部分資料的話,就如下方操作 ```jsx! // src/app/event/[eventId]/opengraph-image.tsx // 支援 APP route => 僅在這個 route 會套用這個設定 export default async function Image(params) { const { eventId } = params const event = await fetchEventDetail(eventId); return new ImageResponse( ( <div style={{ background: 'red', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: 48, }} > {event!.title} </div> ) ); } ``` 其他客製化範例如下(比方說在圖片上透過 CSS 達到壓上客製化文字的效果) [SEO Optimization with LATEST features of NEXT.JS](https://www.youtube.com/watch?v=kFzXOuBFN9Q) (26:43) 對應到這個 [repo](https://github.com/vahid-nejad/nextjs13-seo/blob/main/src/app/post/%5Bslug%5D/opengraph-image.tsx) ### 無腦使用 API 回傳的圖片 => `generateMetadata` ```jsx! // src/app/event/[eventId]/page.tsx export async function generateMetadata(params, parent) { const { eventId } = params const event = await fetchEventDetail(eventId); const previousImages = (await parent).openGraph?.images || []; if (!event) { return { title: 'Event not found | ACCUPASS', }; } return { title: `${event.title} | ACCUPASS`, // 從 API 動態取得圖片 openGraph: { images: [event.bannerUrl, ...previousImages], }, }; } ``` ## robots.txt 告訴爬蟲哪些不能爬用。 在 `app/robots.txt` 直接新增即可 [*robots.txt*](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots) ## sitemap.xml 增加爬蟲能力用。 在 `app/sitemap.xml` 直接新增即可 [sitemap.xml](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap) ## Recap > ✅ 我們建立網站 + 實施 SEO 策略 → Google 爬蟲來爬取 → 使用者搜尋時 Google 評估排名 > ❌ Google 爬蟲 → 爬蟲機器人讀取網頁的 Metadata → 建立 SEO 資訊 一開始我一直以為 SEO 是 Google 爬到我們的網頁才會建立,一直忘記 SEO 只是優化搜尋的手段而已,是我們要先建立好,然後 Google 自己的機器人去爬。 所以我才會覺得怎麼有一種跟主管雞同鴨講的感覺...(以這個錯誤認知去跟主管討論就會得到錯誤的方向) ### 最優解:在網址告訴 Google 目前網頁的語系是什麼 > Google recommends using different URLs for each language version of a page rather than using cookies or browser settings to adjust the content language on the page. > If you prefer to dynamically change content or reroute the user based on language settings, **be aware that Google might not find and crawl all your variations**. This is because the Googlebot crawler usually originates from the USA. In addition, the crawler sends HTTP requests without setting `Accept-Language` in the request header. 反正靠動態使用者切換語系或者讀取使用網頁 cookie 或者語系設定,Googlebot 讀取該網頁的所有語系。可以參照 [Using locale-specific URLs](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites#locale-specific-urls) 的建議設定網址。 ## 參考資料 1. [generateMetadata](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) 2. [營養師不開菜單的第二十七天 - Next.js SEO 極致優化,為網站提升曝光度!](https://ithelp.ithome.com.tw/articles/10334949) 3. [Creating OG Images with ImageResponse API for Next.js Dynamic Routes](https://medium.com/@beratgenc.dev/the-scenario-you-have-a-blog-and-it-is-essential-for-your-posts-to-be-shared-on-different-social-c362626e5b97) 4. [In-depth guide to how Google Search works](https://developers.google.com/search/docs/fundamentals/how-search-works) 5. [How Google crawls locale-adaptive pages](https://developers.google.com/search/docs/specialty/international/locale-adaptive-pages) 6. [Managing multi-regional and multilingual sites](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites)