Notion 網域自訂完整教學,把連結改成你的個人網址

想將 Notion 變成自己的部落格網站嗎?想把 Notion 頁面的預設連結換成自己的網域?本篇文教你自訂 Notion 網域,附有完整的圖文教學與程式碼,只要照著步驟設定即可把 notoin.site 改成你的網址!


Notion 網域自訂步驟 ✅

先簡單說一下大致的 Notion 網域修改流程:

  1. 先買自己的網址 Domain
  2. 註冊 Cloudflare 並設定 DNS
  3. 設定 Cloudflare Worker 轉址程式,就完成了

整體過程不複雜,就需要多一點時間,不過筆者在下文都寫好所有綁定網域的教學了,只需一步一步照做就能輕鬆完成!

相關文章:4 個解決 Mac 藍牙延遲狀況的解法

自訂結果預覽

下圖是綁定網域後的成果,左邊是瀏覽器,網址是我自己的 teachers-bar.com 網域,右邊是 Notion 原始內容,之後別人打 teachers-bar.com 就能到我的 Notion Page,不用打落落長的連結:

如果你想用 Notion 當部落格、個人網站,或是要分享做好的模板,依照此文章的教學你也能做到同樣效果喔!趕快繼續往下閱讀操作👇


Notion 修改網域教學

此段落會仔細講解 Notion 綁定個人網域的流程,請依照順序仔細操作:

第一步:註冊個人網域 🔗

如果你還沒有個人專屬網域的話,必須先去註冊一個,網域就是我們常說的網站網址,像是我的網域是 jcshawn.com。網路有很多網域註冊商,個人推薦你用 Google Domain 或 Namecheap 註冊,價格便宜且服務穩定。

有網域的讀者可以直接跳到第二步驟

註冊一個網域並不貴,以 Google Domain 來說一年只要 360 塊新台幣左右,Namecheap 會更便宜。 你可以將喜歡的名稱設為個人網域,並按照註冊商的付費流程訂閱就好。

註冊完後,先找一下網域後台的 DNS 設定頁面,等等會用到:

Google Domain DNS 設定範例截圖,在選單左側,找不到的可以爬文或問註冊商客服

第二步:註冊 Cloudflare & 設定 DNS 🛠️

再來要到 Cloudflare 註冊帳號,透過 Cloudflare 可以將你的網域串接到 Notion 頁面中。網路有許多註冊教學了,這裡就不贅述,尚未註冊的讀者可以參考這篇註冊並設定 DNS 轉移,只需參考前文的註冊流程就好:

Cloudflare 帳號註冊連結

我用 Google Domain 做示範。在 Cloudflare 主頁點選「新增網站」:

輸入註冊好的個人網域:

選取最下方的 Cloudflare 免費方案:

Cloudflare 會自動掃描並匯入原有的設定,這裡按照我圖片的設定填寫就好:

  1. 先新增一個 A 記錄,名稱填你的網域( 不要加 www )IPv4 寫 216.58.200.46
  2. 再增加一個 CNAME 記錄,名稱寫 www,目標設為你的網域
  3. 好了記得按儲存
Notion 自訂網域 DNS 設定

接著 Cloudflare 會給你兩個名稱伺服器網址,回到註冊網域的後台 > DNS,改為自訂名稱伺服器,填入剛剛被分配的網址並按儲存:

再來到 Cloudflare 的網站主頁,下面會有個檢查名稱伺服器的選項,點擊之後等一個小時左右即可( 通常更快 ),成功後你會收到 Cloudflare 的電子郵件通知。如果過了一天都沒有成功,確認有沒有做下方的事項:

  • 要先在 Cloudflare 設定 A 記錄
  • 如果原本的域名註冊商有提供 DNSSEC 服務,先取消發布再重新檢查一次 Cloudflare 看看

在 Cloudflare 後台 > SSL / TLS 選單中,將加密模式設為「彈性」:

請把加密模式設定為彈性

第三步:設定 Cloudflare 路由規則 ( Worker )

再來就是重頭戲部分了!我們要設定讓 Cloudflare 把網域連到 Notion Page,並同時把 notion.site 網址改成自己註冊的網域。

請先複製下面這段程式碼到你的記事本或文字編輯器,並依照中文說明修改:

/*
* Modifired via https://github.com/stephenou/fruitionsite/blob/master/worker.js
* This code is baesd on Fruitonsite service, localized by Chun Shawn in Traditional-Chinese.
* You're able to set a custom domain with this code by using Cloudflare
* Check for https://fruitionsite.com/ for toturials.
*
* < Notion 自訂網域 Cloudflare 程式碼分享 >
*
* 此程式由 Fruitonsite 製作( https://fruitionsite.com/ )
* 由張君祥 Chun Shawn 翻譯繁體中文( https://jcshawn.com )
* 將此程式碼加入到你的 Cloudflare Workers 路由設定,就可以將 Notion 連結改為你的個人網域
* 請依照說明依序設定,歡迎參考翻譯者寫的手把手設定教學( https://jcshawn.com/notion-custom-domain/ )
*
*/
/* Notion 自定網域個人化設定開始 */
/* 第一步:在引號填入你的 Custom Domain,例如 jcshawn.com */
const MY_DOMAIN = "teachers-bar.com";
/*
* 第二步。填入不想有英數結尾的 Notion Page 連結 ID ( xxx.notion.site/… 後面的字串就是代碼 )
* 左側的字詞是你要自訂的連結代號,不用加引號
* 右側要在引號填入相對應的 Notion page ID
* 舉例來說,about : "jughew2oi31u9u302prjdf",相對應的網址就是 jcshawn.com/about
* 第一行爲主網址,只需填寫主頁的 Page ID,不用每一個 page 都填寫
*/
const SLUG_TO_PAGE = {
"": "6de78fbc608440108ee70b2e713f2e1e",
thanks: "9d9864f5338b47b0a7f42e0f0e2bbf46",
showcase: "92053970e5084019ac096d2df7e7f440",
roadmap: "7d4b21bfb4534364972e8bf9f68c2c36"
};
/* 第三步:分別輸入你的 SEO 標題與說明 */
const PAGE_TITLE = "這裡填寫網頁標題";
const PAGE_DESCRIPTION =
"這裡寫網頁說明";
/* 第四步 : 自訂 Google Fonts 來做網頁字體,這裡我用支援中文的思源黑體,若要自訂請參考:https://fonts.google.com */
const GOOGLE_FONT = "Noto Sans TC";
/* 第五步:填入你的自訂 <script>,無此需求可略過 */
const CUSTOM_SCRIPT = ;
/* Notion 自訂網域個人化結束,請勿更動下方的程式碼 */
const PAGE_TO_SLUG = {};
const slugs = [];
const pages = [];
Object.keys(SLUG_TO_PAGE).forEach(slug => {
const page = SLUG_TO_PAGE[slug];
slugs.push(slug);
pages.push(page);
PAGE_TO_SLUG[page] = slug;
});
addEventListener("fetch", event => {
event.respondWith(fetchAndApply(event.request));
});
function generateSitemap() {
let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
slugs.forEach(
(slug) =>
(sitemap +=
"<url><loc>https://" + MY_DOMAIN + "/" + slug + "</loc></url>")
);
sitemap += "</urlset>";
return sitemap;
}
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, POST, PUT, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
function handleOptions(request) {
if (
request.headers.get("Origin") !== null &&
request.headers.get("Access-Control-Request-Method") !== null &&
request.headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS pre-flight request.
return new Response(null, {
headers: corsHeaders
});
} else {
// Handle standard OPTIONS request.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, PUT, OPTIONS"
}
});
}
}
async function fetchAndApply(request) {
if (request.method === "OPTIONS") {
return handleOptions(request);
}
let url = new URL(request.url);
url.hostname = 'www.notion.so';
if (url.pathname === "/robots.txt") {
return new Response("Sitemap: https://" + MY_DOMAIN + "/sitemap.xml");
}
if (url.pathname === "/sitemap.xml") {
let response = new Response(generateSitemap());
response.headers.set("content-type", "application/xml");
return response;
}
let response;
if (url.pathname.startsWith("/app") && url.pathname.endsWith("js")) {
response = await fetch(url.toString());
let body = await response.text();
response = new Response(
body
.replace(/www.notion.so/g, MY_DOMAIN)
.replace(/notion.so/g, MY_DOMAIN),
response
);
response.headers.set("Content-Type", "application/x-javascript");
return response;
} else if (url.pathname.startsWith("/api")) {
// Forward API
response = await fetch(url.toString(), {
body: url.pathname.startsWith('/api/v3/getPublicPageData') ? null : request.body,
headers: {
"content-type": "application/json;charset=UTF-8",
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
},
method: "POST"
});
response = new Response(response.body, response);
response.headers.set("Access-Control-Allow-Origin", "*");
return response;
} else if (slugs.indexOf(url.pathname.slice(1)) > 1) {
const pageId = SLUG_TO_PAGE[url.pathname.slice(1)];
return Response.redirect("https://" + MY_DOMAIN + "/" + pageId, 301);
} else if (
pages.indexOf(url.pathname.slice(1)) === 1 &&
url.pathname.slice(1).match(/[0-9a-f]{32}/)
) {
return Response.redirect('https://' + MY_DOMAIN, 301);
} else {
response = await fetch(url.toString(), {
body: request.body,
headers: request.headers,
method: request.method
});
response = new Response(response.body, response);
response.headers.delete("Content-Security-Policy");
response.headers.delete("X-Content-Security-Policy");
}
return appendJavascript(response, SLUG_TO_PAGE);
}
class MetaRewriter {
element(element) {
if (PAGE_TITLE !== "") {
if (
element.getAttribute("property") === "og:title" ||
element.getAttribute("name") === "twitter:title"
) {
element.setAttribute("content", PAGE_TITLE);
}
if (element.tagName === "title") {
element.setInnerContent(PAGE_TITLE);
}
}
if (PAGE_DESCRIPTION !== "") {
if (
element.getAttribute("name") === "description" ||
element.getAttribute("property") === "og:description" ||
element.getAttribute("name") === "twitter:description"
) {
element.setAttribute("content", PAGE_DESCRIPTION);
}
}
if (
element.getAttribute("property") === "og:url" ||
element.getAttribute("name") === "twitter:url"
) {
element.setAttribute("content", MY_DOMAIN);
}
if (element.getAttribute("name") === "apple-itunes-app") {
element.remove();
}
}
}
class HeadRewriter {
element(element) {
if (GOOGLE_FONT !== "") {
element.append(
`<link href='https://fonts.googleapis.com/css?family=${GOOGLE_FONT.replace(' ', '+')}:Regular,Bold,Italic&display=swap' rel='stylesheet'>
<style>* { font-family: "${GOOGLE_FONT}" !important; }</style>`,
{
html: true
}
);
}
element.append(
`<style>
div.notion-topbar > div > div:nth-child(3) { display: none !important; }
div.notion-topbar > div > div:nth-child(4) { display: none !important; }
div.notion-topbar > div > div:nth-child(5) { display: none !important; }
div.notion-topbar > div > div:nth-child(6) { display: none !important; }
div.notion-topbar-mobile > div:nth-child(3) { display: none !important; }
div.notion-topbar-mobile > div:nth-child(4) { display: none !important; }
div.notion-topbar > div > div:nth-child(1n).toggle-mode { display: block !important; }
div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }
</style>`,
{
html: true
}
);
}
}
class BodyRewriter {
constructor(SLUG_TO_PAGE) {
this.SLUG_TO_PAGE = SLUG_TO_PAGE;
}
element(element) {
element.append(
`<script>
window.CONFIG.domainBaseUrl = 'https://${MY_DOMAIN}';
const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)};
const PAGE_TO_SLUG = {};
const slugs = [];
const pages = [];
const el = document.createElement('div');
let redirected = false;
Object.keys(SLUG_TO_PAGE).forEach(slug => {
const page = SLUG_TO_PAGE[slug];
slugs.push(slug);
pages.push(page);
PAGE_TO_SLUG[page] = slug;
});
function getPage() {
return location.pathname.slice(-32);
}
function getSlug() {
return location.pathname.slice(1);
}
function updateSlug() {
const slug = PAGE_TO_SLUG[getPage()];
if (slug != null) {
history.replaceState(history.state, '', '/' + slug);
}
}
function onDark() {
el.innerHTML = '<div title="Change to Light Mode" style="margin-left: auto; margin-right: 14px; min-width: 0px;"><div role="button" tabindex="0" style="user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; border-radius: 44px;"><div style="display: flex; flex-shrink: 0; height: 14px; width: 26px; border-radius: 44px; padding: 2px; box-sizing: content-box; background: rgb(46, 170, 220); transition: background 200ms ease 0s, box-shadow 200ms ease 0s;"><div style="width: 14px; height: 14px; border-radius: 44px; background: white; transition: transform 200ms ease-out 0s, background 200ms ease-out 0s; transform: translateX(12px) translateY(0px);"></div></div></div></div>';
document.body.classList.add('dark');
__console.environment.ThemeStore.setState({ mode: 'dark' });
};
function onLight() {
el.innerHTML = '<div title="Change to Dark Mode" style="margin-left: auto; margin-right: 14px; min-width: 0px;"><div role="button" tabindex="0" style="user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; border-radius: 44px;"><div style="display: flex; flex-shrink: 0; height: 14px; width: 26px; border-radius: 44px; padding: 2px; box-sizing: content-box; background: rgba(135, 131, 120, 0.3); transition: background 200ms ease 0s, box-shadow 200ms ease 0s;"><div style="width: 14px; height: 14px; border-radius: 44px; background: white; transition: transform 200ms ease-out 0s, background 200ms ease-out 0s; transform: translateX(0px) translateY(0px);"></div></div></div></div>';
document.body.classList.remove('dark');
__console.environment.ThemeStore.setState({ mode: 'light' });
}
function toggle() {
if (document.body.classList.contains('dark')) {
onLight();
} else {
onDark();
}
}
function addDarkModeButton(device) {
const nav = device === 'web' ? document.querySelector('.notion-topbar').firstChild : document.querySelector('.notion-topbar-mobile');
el.className = 'toggle-mode';
el.addEventListener('click', toggle);
nav.appendChild(el);
// enable smart dark mode based on user-preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
onDark();
} else {
onLight();
}
// try to detect if user-preference change
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
toggle();
});
}
const observer = new MutationObserver(function() {
if (redirected) return;
const nav = document.querySelector('.notion-topbar');
const mobileNav = document.querySelector('.notion-topbar-mobile');
if (nav && nav.firstChild && nav.firstChild.firstChild
|| mobileNav && mobileNav.firstChild) {
redirected = true;
updateSlug();
addDarkModeButton(nav ? 'web' : 'mobile');
const onpopstate = window.onpopstate;
window.onpopstate = function() {
if (slugs.includes(getSlug())) {
const page = SLUG_TO_PAGE[getSlug()];
if (page) {
history.replaceState(history.state, 'bypass', '/' + page);
}
}
onpopstate.apply(this, [].slice.call(arguments));
updateSlug();
};
}
});
observer.observe(document.querySelector('#notion-app'), {
childList: true,
subtree: true,
});
const replaceState = window.history.replaceState;
window.history.replaceState = function(state) {
if (arguments[1] !== 'bypass' && slugs.includes(getSlug())) return;
return replaceState.apply(window.history, arguments);
};
const pushState = window.history.pushState;
window.history.pushState = function(state) {
const dest = new URL(location.protocol + location.host + arguments[2]);
const id = dest.pathname.slice(-32);
if (pages.includes(id)) {
arguments[2] = '/' + PAGE_TO_SLUG[id];
}
return pushState.apply(window.history, arguments);
};
const open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
arguments[1] = arguments[1].replace('${MY_DOMAIN}', 'www.notion.so');
return open.apply(this, [].slice.call(arguments));
};
</script>${CUSTOM_SCRIPT}`,
{
html: true
}
);
}
}
async function appendJavascript(res, SLUG_TO_PAGE) {
return new HTMLRewriter()
.on("title", new MetaRewriter())
.on("meta", new MetaRewriter())
.on("head", new HeadRewriter())
.on("body", new BodyRewriter(SLUG_TO_PAGE))
.transform(res);
}
view raw worker.js hosted with ❤ by GitHub

補充,你的 Notion page 要記得設為公開權限,而 Page ID 就是 xxxx.notion.site/ 後面的英數字串:

記得打開 Page 的 share to web 權限

這段程式碼複製到備忘錄後還需要修改!

  1. 在第十九行引號內填寫你的網域
  2. 如果你想有特定幾頁 Notion Page 想拿掉 url 後面的英數亂碼,在第二十九行開始依照格式設定後綴名稱與 Page ID

    ( 但不用每一頁分頁都要加,填寫特別重要的幾頁就好 )
  3. 在 37, 38 行填寫 SEO 標題,這個標題會成為瀏覽器標籤與社群連結中顯示的名字

回到 Cloudflare 主頁,選擇 Worker 選單 > 管理 Workers :

點選「 建立 Worker 」按鈕:

接著會出現一個程式碼編輯器,把旁邊的程式碼全部刪掉,再將你改好的 code 貼到左側,並點擊「部署」:

點擊儲存與部署

部署完後,可以先按編輯器的「傳送」按鈕測試,理論上旁邊要出現 301 轉址訊息,而目標網址有包含你的網域,有錯誤的請重新檢查程式碼:

回去 Cloudflare 的路由選單,點選「新增路由」:

新增兩個路由,路由欄位分別填入:

  • 你的網域/* ( 後面加斜槓跟星號 )
  • *.你的網域/* ( 前面多加一個星號跟半形句點 )

設定完的路由應該要像這樣:

Cloudflare 路由設定結果,要有兩個

到這裡就完成嘍!!在瀏覽器輸入你的自訂網域,就會顯示 Notion Page 頁面,連結也變成自訂網址了!

Notion GIF - Find & Share on GIPHY

網址自訂技術總結 👍

最多只要 20 分鐘,就能把你的個人 Notion 頁面轉型成網站或部落格,可以說是用 Notion 的編輯方式製作網站!或是你有做 Notion 模板想更快分享給別人,也可以用本文方法綁定更好記憶的網址。

你也想自訂 Notion 個人網域嗎?事不宜遲,快照的這篇文章的教學操作吧!未來再分享怎麼把 Notion 網頁提交給 Google,讓別人搜尋到你的文章。

想把 Notion 用得更有效率?

如果你入門 Notion,或是想把 Notion 當作自己的主力數位工具,推薦你看雷蒙三十和 Mr.K 的 Notion 實戰課程:打造專屬數位工作術 線上課程,可以學到更多專業的 Notion 應用模式,不再只是套用模板,而是創造專述你的工作系統。

此課程是我認識的前輩們開設的 Hahow 課程,個人上課過也真心推薦,而非業配~~請安心服用
前往 Hahow 課程

延伸閱讀

我參考的資料

嗨囉我是 Shawn,首先感謝你看完全部內容!我是位澎湖高中生,也是科技編輯跟業餘 Programmer,希望教大家如何運用科技提高生產力,並紀錄著自己樸實無華的平凡人生。

如果對於文中內容有任何問題,或是商業合作洽詢,可在下方管道與我聯繫:
e-mail: [email protected]
Telegram: jschang666

2 Comments

  1. 您好! 請問是否可以使用 Subdomain 而不是 整個 Domain?
    比如 blog.mydomain.com, 而不是 http://www.mydomain.com?
    另外, 請問 216.58.200.46 是 Notion 的固定 IP 是嗎?
    您推薦的雷蒙的課程我也是最近購課, 所以算是同學了, 再麻煩了, 感謝您!

    1. Hi,是可以用子網域的,只需把新增的 DNS 的 A 記錄換成 CNAME,並指向任意的連結( 像 google.com ),再將轉址程式碼和 Cloudflare Worker 的路由設定改成子網域即可。

      216.58…. 這組 IP 是我隨機抓到的 Google 伺服器 IP,不是 Notion 的 IP。用途是作 DNS A 記錄的轉址目標,Cloudflare Worker 會優先把請求轉到 Notion Page,並改寫網址,不會真的導到這組 IP,所以這組 IP 填其他的也沒差。

留個言吧!