# 轉載換到更多讀者，同時讓自己的版本保持為主 — rel=canonical 與全文 RSS

> 反向連結與 rel=canonical 到底在做什麼、為什麼 Zenn 和 Qiita 會改變做法，以及讓轉載一篇文章從工程變成貼上的那條流程。

- Source: https://oharu121.com/zh-tw/blog/republishing-backlinks-rel-canonical-rss/
- Published: 2026-08-19T15:56:31+09:00
- Tags: SEO, Astro, Markdown, RSS

---
## 引言

搜尋引擎會要求的事情，我都做過了。網站送交給 Google 與 Bing，有網站地圖、有結構化資料，`robots.txt` 逐一列名放行每一隻 AI 爬蟲，每一頁也都是乾淨的 HTML。唯一缺的，正好是這些通通做不出來的那一項：**別人的網站連過來。**

最直覺的答案是把同一篇文章也放到讀者更多的地方，而那裡有一個我當時理解得不夠、因此躲不掉的陷阱。同一篇文章的兩份複本會互相競爭，而**通常是比較大的那個網站贏，用的還是你寫的字。** 防止這件事的標籤是 `rel=canonical`，而去查哪些平台支援它，翻出了決定後續一切的事實：**Zenn 和 Qiita 根本沒辦法設定。**

本文整理反向連結與 canonical 實際上在做什麼、平台端的限制會逼出什麼做法，以及這個部落格早就有、把轉載從一份工程變成一次貼上的那兩樣東西。

## 反向連結是什麼，以及為什麼它做不出來

反向連結就是別人的網站連到你的網站。定義只有這樣，它的份量取決於連過來的是誰。

搜尋引擎把一條連結當成一張小小的票。一個一票都沒有的新網域，與其說被懲罰，不如說是沒人認識，而這個沒人認識，正是限制爬蟲多常回來的東西。**其他所有 SEO 工作，都是讓爬蟲來了之後能讀懂這一頁。反向連結則是讓它來的那一半。**

所以在其他事情都做完之後，只剩下這一項。網站地圖是我可以自己產生的檔案。結構化資料是我可以自己輸出的標記。別的網站連過來，是另一個人做的決定，而唯一誠實的影響方式，是把文章放到那些可能會連過來的人本來就在的地方。

## 連結從哪裡來，哪一種會產生複本

反向連結就是一條連結。做出一條連結不需要複製任何東西。有人寫下「看看這個」並指向你的網址，機制就是這樣。這件事值得明講，因為下一節談的是轉載，而這兩件事很容易被讀成同一個動作。

它們不是。轉載是把文章的複本放到一個本來就有讀者的平台上，而製造出 `rel=canonical` 要解決的那個問題的，正是那份複本。被分享出去的連結不會產生複本，所以沒有東西要競爭，也沒有 canonical 要設定。

| 你做的事 | 會複製文章 | 需要 canonical | 目的是什麼 |
| --- | --- | --- | --- |
| 分享連結 | 不會 | 不需要 | 觸及，以及被發現 |
| 在派得上用場的地方留言附連結 | 不會 | 不需要 | 觸及，而且在已被信任的網域上 |
| 轉載全文 | **會** | **需要** | 那個平台的讀者 |
| 別人連到你 | 不會 | 不需要 | **真正影響排名的信號** |

**社群平台上的連結幾乎全是 `nofollow`。** Facebook、X 與 LinkedIn 預設就會替外部連結加上它。Google 在 2019 年 9 月把 `nofollow` 從指令改成提示，隔年 3 月又把這個處理方式延伸到檢索與索引，所以這種連結並非毫無價值。它也不是可以拿來規劃的東西。

**分享真正穩定做到的，是把文章放到某個擁有自己網站的人面前。** 會被跟隨的那條連結是他寫的。要走到上表最後一列，只有這一條路，而本文描述的任何機制都無法自動化它。

## rel=canonical 在做什麼

`rel=canonical` 是頁面 `<head>` 裡的一行：

```html
<link rel="canonical" href="https://oharu121.com/blog/some-article/">
```

它的意思是：*你正在讀的這一頁是複本，原文在那個網址。* 願意採信它的搜尋引擎會把兩者收攏成一個，並把原文當作要顯示的那一個。

沒有它的話，兩個內容相同的頁面看起來就像兩個剛好一致、彼此無關的頁面。引擎會挑一個，而它挑的依據是信任，那是大平台比個人部落格多的東西。

*Figure — CanonicalOrNot: 兩份複本本身完全沒變。決定哪一份被當成文章的，是一個標籤。*

有兩件事值得寫精確，因為兩者都容易被誇大。

**它是提示，不是指令。** Google 的文件明確寫著 canonical 只是眾多信號之一，夠強的相反信號可以蓋過它。實務上它會被採信，而且這是你唯一能握住的槓桿。

**它不會把讀者送去任何地方。** 對正在讀複本的人來說，canonical 是看不見的。他們會留在那個平台上，而這沒問題：**你從轉載想要的是那條連回來的連結，不是瀏覽數。**

## Zenn 與 Qiita 送不出這個標籤

代理程式最初的建議，是把文章轉載到 dev.to、Zenn 與 Qiita，並讓 canonical 指回原文，同時註明未經查證。查證之後，做法改了。

**dev.to 可以逐篇設定 canonical 網址**，欄位叫 `canonical_url`，而且從 RSS 訂閱源匯入時還能自動設好。

**Zenn 不行。** 一篇正是在調查這件事的文章指出，不論原始檔案的 frontmatter 怎麼寫，Zenn 公開頁面上的 canonical 都指向 Zenn 自己的網址。寫了也不會改變爬蟲看到的任何東西。

**Qiita 同樣沒有讓使用者設定的 canonical。**

*Figure — PlatformMatrix: 這不是平台大小的問題，而是平台願不願意說這篇文章是你的。*

如果那個建議沒查證就照做，結果會和目標完全相反：**把每一篇文章的全文交給兩個權重高得多的網站，而任何地方都沒有東西說明哪一份複本才是先來的。**

這個限制逼出來的是分流，不是撤退。canonical 可用的地方就轉載全文；不能用的地方就放一段簡短摘要並連回原文。摘要一樣觸及得到那些讀者，一樣換得到連結，只是不去參加一場注定會輸的競爭。

## 這個部落格為了別的目的早就做好的東西

先前的兩項變更在這裡派上了用場，而它們都不是為了這件事做的。

**每篇文章的 Markdown 複本。** 每篇文章都提供兩次：一次是頁面，一次是同一網址結尾加上 `.md` 的純 Markdown。當初做這個是為了讓回答引擎拿到內文，而不必先剝掉一層導覽外殼。結果它也正好就是你要貼進另一個編輯器的東西。

**RSS 訂閱源改成全文。** 每一則項目以前只有 160 個字的摘要和一條連結，現在帶著整篇文章，因為平台的匯入功能讀到只有摘要的訂閱源，只能生出一份空殼草稿。

*Figure — SyndicationPipeline: 一個原始檔早就把三樣都生出來了，為了這件事另外加的只有最後一樣。*

訂閱源這一邊有個 Markdown 複本沒有的限制。`compiledContent()` 不支援 MDX，而這裡每一篇文章都是 MDX，所以慣用的那條路並不存在。代理程式的做法是直接重用產生 Markdown 複本的那段轉換，再用 Astro 自己的 markdown 處理器把輸出算繪出來：

```ts
processor ??= createMarkdownProcessor({});
const { code } = await (await processor).render(mdxToMarkdown(body));
```

**訂閱源承接了 Markdown 複本失去的東西。** 圖表以圖說的形式抵達，照片以替代文字的形式抵達，因為兩者都沒有離開頁面之後還成立的網址。內文、標題、表格與程式碼都原樣通過。比起原本的兩句話，這是很大的進步，但它不等於那個頁面。

## 訂閱源的兩個錯誤，以及它們的共通點

這個訂閱源上線時帶著兩個代理程式寫出來的缺陷，而兩個都是靠審查找到的，不是靠任何檢查。

**根相對連結漏了出去。** 有一篇文章以 `/blog/…` 連到自己的第一部分。瀏覽器會用它所在的頁面來解析這個路徑。訂閱源閱讀器沒有那個頁面，於是用它自己的來源去解析，結果 404。三種語言，每個訂閱源各一條。

驗證當時回報一切正常。它找的是 `./`，而實際的連結是 `/`，所以那個形狀根本不在檢查的視野裡。

**只要混進一個控制字元，訂閱源就會無法解析。** Markdown 的轉換在改寫標籤之前，會用 NUL 哨兵把程式碼區段遮起來，最後再還原。在 `.md` 檔案裡，殘留的哨兵只是外觀問題。在 XML 裡則是致命的，而建置過程並不會驗證訂閱源：

```
a NUL inside an element parses: FAIL — not well-formed (invalid token)
```

`pnpm check` 和 `pnpm build` 都會維持綠燈，而每一個閱讀器都無法解析這個訂閱源。

兩者現在都處理掉了：`href="/…"` 與 `src="/…"` 會以網站來源解析，XML 禁止的控制字元範圍也會被移除。**這兩者的共通點是，本來該抓到它們的那個東西，是同一個人、在同一時間、帶著同一個盲點寫出來的。** 檢查和被檢查的對象彼此一致，不算證據。

## 操作步驟

針對一篇已經可以轉載的文章。

**在支援 canonical 的平台上**，轉載全文。要貼的東西 Markdown 複本已經備好了：

```bash
curl -sS https://oharu121.com/blog/<slug>.md | pbcopy
```

把 Markdown 複本開頭的標頭區塊連同那條 `---` 分隔線一起刪掉，否則它會和平台自己的 frontmatter 分隔符號打架。接著設定 canonical：

```yaml
---
title: <the title>
published: true
canonical_url: https://oharu121.com/blog/<slug>/
tags: seo, astro, webdev
---
```

**確認它生效了。** 打開發布後那一頁的原始碼，找一個指向你網址的 `<link rel="canonical">`。如果沒有，就是 frontmatter 沒有被解析，原因通常是那條忘了刪的 `---`。

**在不支援的平台上**，寫兩三段並連回原文。這個部落格已經有日文與繁體中文的翻譯，所以摘要可以用讀者的語言寫，並指向該語系的網址。

**在連結比轉載更有價值的場合**，到有人正好卡在這篇文章所解決的問題的那個 issue 底下留言。留言本身必須站得住腳，連結是完整版本，不是主體。

## 可以期待什麼

誠實的答案並不令人滿意，所以直說。

**反向連結不是開關。** 這些都不會讓一篇文章明天就排上去。改變的是爬蟲有沒有理由再回來，而那是慢慢累積的。

**先到的是推薦流量，對搜尋的影響要晚得多**，如果會來的話。平台上的讀者是立即的。再往下就取決於他們之中有沒有人寫了什麼並連過來，而那不是一條流程造得出來的事。

**這套工具真正換到的，是嘗試的成本。** 轉載以前是每篇文章、每個平台都要複製、貼上、重排一次的雜事，而那正是會安靜地不再發生的那種雜事。現在是一行 `curl` 加一段 frontmatter。**這條流程不會讓轉載變得更有效，它只是讓轉載便宜到可以一直做下去。**

## 總結

- **反向連結是別人網站連過來的連結，也是這整件事裡唯一沒有人能自己生出來的部分。** 其他每一項，都只是讓爬蟲來了之後能讀懂這一頁。
- **分享連結和轉載文章是兩個不同的動作。** 只有轉載會產生複本，也只有複本需要 canonical。社群連結幾乎全是 `nofollow`，而自 2019 年起那是提示而非指令：值得為了觸及去做，不值得當成排名信號來計算。
- **`rel=canonical` 宣告哪一份複本是原文**，讓轉載不會變成和自己競爭。它是提示而不是指令，也不會帶動任何讀者。
- **最容易想到的三個平台裡有兩個設不了它。** 不論原始檔怎麼寫，Zenn 的頁面都把 canonical 指向 Zenn，而 Qiita 沒有這個設定。全文去 canonical 可用的地方，摘要與連結去其他所有地方。
- **Markdown 複本與全文訂閱源是為了回答引擎而做的**，結果變成了轉載的流程。一個是拿來貼的，另一個是匯入功能會讀的。
- **和程式碼一起寫出來的檢查，會共用它的盲點。** 訂閱源的驗證找的是 `./`，而壞掉的連結是 `/`。
- **該期待的是嘗試成本下降，而不是結果到來。** 這仍然是有用的改變，因為每個平台要花掉半天的那個版本，就是不會被做的版本。

## 參考連結

- [Google 關於 canonical 網址的指引，包含 canonical 是信號而非指令這一點](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
- [Google 宣布 `nofollow` 改為提示，並導入 `rel="ugc"` 與 `rel="sponsored"` 的公告](https://developers.google.com/search/blog/2019/09/evolving-nofollow-new-ways-to-identify)
- [DEV Community 的 RSS 匯入設定，會替每一篇匯入的文章設定 canonical 網址](https://dev.to/settings/extensions)
- [一篇調查，指出不論 frontmatter 怎麼寫，Zenn 公開的 HTML 都帶著自己的 canonical](https://zenn.dev/zhener/articles/zenn-canonical-crosspost-seo)
- [定義 `content:encoded` 的 RSS 2.0 規格 content 模組](https://www.rssboard.org/rss-profile#namespace-elements-content-encoded)
