Last active
February 22, 2023 02:37
-
-
Save jschang19/70edce091cab0b9e5bb971ddf8d9d49b to your computer and use it in GitHub Desktop.
Notion 自訂網域的程式碼,此段程式碼可讓 Cloudflare 串接個人 Notion Page,達到自訂 Notion 網域與網址的效果,請將此段程式碼貼到你的 Cloudflare Worker 編輯器內。更詳細的操作教學請參考 https://jcshawn.com/notion-custom-domain/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment