上次搞了个 微信读书笔记自动同步到 Notion,现在我已经把读书笔记的内容都迁移到 Notion上了 微信读书笔记。
如果直接使用 Notion,生成的公开地址就是 Notion 自己的服务器,比如:微信读书笔记 https://zhuxiaojian.notion.site/80580de1183c45ecb8efc8b2eabee30a,这样的地址显然不够美观,而且别人看到这个地址也不一定愿意点进去。
于是根据网上的方案,我也改造成了 https://weread.ppanda.com/ 的方式。
具体方案如下,原文来自于 免费为Notion绑定域名,这里只是搬运留存。
具体效果可以参考:https://book.malinkang.com/,这个方案是基于开源方案fruitionsite:https://github.com/stephenou/fruitionsite。但是代码已经3年没有更新了,很多功能已经生效。所以我复制了一份源码修改了一些bug。
域名注册
可以选择在腾讯云或者阿里云上或者其他平台上购买域名。
- 腾讯云购买地址:https://buy.cloud.tencent.com/domain/。
- 阿里云购买地址:https://wanwang.aliyun.com/
修改DNS信息
注册cloudflare: https://dash.cloudflare.com/,登录成功之后添加点击添加域。
输入我们购买的域名,然后点击继续。
进入下一个页面,选择免费计划。这里免费计划在最下面,如果屏幕小的可能会看不到。
然后点击继续,会看到我们需要配置的DNS服务器。
打开购买域名的网站,将你购买的域名的DNS修改为cloudflare的DNS服务器。
腾讯云修改DNS教程:https://cloud.tencent.com/document/product/302/5518#id.3Aserveraddress 阿里云修改DNS教程:https://help.aliyun.com/zh/dws/user-guide/change-dns-servers-for-a-domain-name#6a5c8962350ho
生成代码
打开网站:https://site.notionhub.app/
输入我们的域名和要绑定域名的Notion链接,请确保你的Notion已经发布到网络上。
如果你需要配置多个页面,比如https://malinkang.com/book,https://malinkang.com/podcast这种,点击ADD A PRETTY LINK
添加对应的路径和NOTION链接即可。
最后可以点击TOGGLE STYLE AND SCRIPT SETTINGS
来设置网站标题描述和字体。
全部设置完成点击复制代码。
配置Cloudflare Worker
打开Cloudflare:https://dash.cloudflare.com/,选中Workers和Pages,然后点击创建按钮
输入名字,然后点击部署
点击编辑代码
将我们上一步生成的代码,粘贴到箭头所指的地方,然后点击部署。部署完成后点返回按钮返回。
选中设置->域和路由,点击添加,输入我们的域名然后点击添加域即可。
这里贴一下我生成后的代码
/* CONFIGURATION STARTS HERE */
/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'weread.ppanda.com';
/*
* Step 2: enter your URL slug to page ID mapping
* The key on the left is the slug (without the slash)
* The value on the right is the Notion page ID
*/
const SLUG_TO_PAGE = {
'': '80580de1183c45ecb8efc8b2eabee30a',
};
/* Step 3: enter your page title and description for SEO purposes */
const PAGE_TITLE = '朱小贱的读书笔记';
const PAGE_DESCRIPTION = '朱小贱的读书笔记';
/* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */
const GOOGLE_FONT = '';
/* Step 5: enter any custom scripts you'd like */
const CUSTOM_SCRIPT = `朱小贱的读书笔记`;
/* CONFIGURATION ENDS HERE */
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 (url.pathname.endsWith(".js")){
response = await fetch(url.toString());
let body = await response.text();
response = new Response(
body,
response
);
response.headers.set("Content-Type", "application/x-javascript");
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 {
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 > div > div:nth-child(7) { 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>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-KWRKSLG');</script>
<!-- End Google Tag Manager -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-KMYVHL4B8D"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-KMYVHL4B8D');
</script>`, {
html: true
})
}
}
class BodyRewriter {
constructor(SLUG_TO_PAGE) {
this.SLUG_TO_PAGE = SLUG_TO_PAGE;
}
element(element) {
element.append(`<div style="display:none">Powered by <a href="http://fruitionsite.com">Fruition</a></div>
<script>
window.CONFIG.domainBaseUrl = 'https://${MY_DOMAIN}';
localStorage.__console = true;
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 enableConsoleEffectAndSetMode(mode) {
if (__console && !__console.isEnabled) {
__console.enable()
window.location.reload()
} else {
__console.environment.ThemeStore.setState({ mode: mode })
}
}
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')
enableConsoleEffectAndSetMode('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')
enableConsoleEffectAndSetMode('light')
}
function toggle() {
if (!__console.isEnabled) __console.enable();
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);
onLight();
}
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);
}
原理是用当前域名替换了真实的 notion api 服务,效果还不错。