Using Notion & Cloudflare For Hosting Sites
Notion is the quickest way to get content out and products released
I use Notion for nearly everything these days. I have several membership sites running on it, and my entire business is managed by it.
I like how simple it is just to go into my website’s folder in Notion and just update a site live, without having to go through a dozen steps to make a quick change.
I am currently building a brand new product in Notion, and I like the speed at which I can add to it, upload videos, host PDF’s etc.
Now, Notion already provides a way for you to put a site live on the Internet just by clicking share on the top right-hand side of any page. But sometimes you want to have your own domain pointing to it, and make it feel less like a Notion site.
For the last couple of years, I have had my main danraine.com site live on Notion and I used a service called Super.so to manage all of the technical stuff.
But I wanted to bring that in-house, and much like my Renegade Ranking Engine, I wanted to use CloudFlare Workers.
So, this guide will show you how to host a notion site on your domain, managed by Cloudflare, free of charge. It only takes about five minutes to get set up, so let’s get started.
Sharing Your Page
I am assuming here that you already have a page that you want to use as your website.
This is my page in notion…
The first thing you are going to need to do is select the share menu in the top-right of the screen and copy the link. Make sure your options are the same as mine.
This will give you a link that looks like this.
https://danraine.notion.site/pagename-4cd74f120b1e4a52a1a27cc561e1e45c
The bold bit above is your page ID. You will need this later, so save it.
Transfer Your NameServers To Cloudflare
The first thing you will need is a Cloudflare account. Go ahead and get one set up and follow the steps to move your nameservers to Cloudflare.
The instructions for this part will be dependent on which registrar you used to purchase your domain. Cloudflare should help you out here.
Choose the free plan on Cloudflare because it is more than adequate and will let you have 100,000 page views a day without paying a cent. If you are getting more than that then you are probably not worrying about paying the $20 a month for a pro plan.
Setup A Cloudflare Worker
You can think of a Cloudflare worker as an intermediary between your notion site and the rest of the world. When someone requests a page on your site, the Cloudflare worker will go and fetch it from Notion, remove all the unnecessary Notion code, and make a few modifications to it before sending it to the visitor’s browser.
This happens in milliseconds and is cached by Cloudflare and is available all around the world in an instant.
When you first log in to Cloudflare, you are on your main dashboard. Click the option on the bottom-left named Workers & Pages.
Then choose Create application on the right-hand side.
Then choose the Create a hello world worker.
Enter a name for your worker, this can be anything at all. I called mine notion-hosting.
Scroll down to the bottom and select Deploy. You will now see a page like this.
Hit the Edit code button.
Now there is already some code in the workers.js file there. You can go and get rid of that as we are going to replace it with our own worker code to serve our Notion pages.
You can edit the configuration at the top of this, changing the bold items and making sure you put in the page id you collected at the beginning.
/* CONFIGURATION STARTS HERE */
/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'yourdomain.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 = {
'': '4c574f120b1e2a52a1a17cc561e1d45c',
};
/* Step 3: enter your page title and description for SEO purposes */
const PAGE_TITLE = 'Your Title';
const PAGE_DESCRIPTION = 'Your Description';
/* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */
const GOOGLE_FONT = 'Roboto';
/* 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 (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-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(`<div style="display:none">Powered by <a href="http://fruitionsite.com">Fruition</a></div>
<script>
window.CONFIG.domainBaseUrl = location.origin;
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);
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);
}
Once you have done that, hit Save And Deploy on the top-right then go back to the dashboard by choosing the option on the top left of the page.
On the next page, choose Triggers, then click Add Custom Domain.
Enter your domain name and select Add Custom Domain.
Then you need to wait a few minutes for it to initialise.
Once your domain becomes active, you need to add a route
Enter your domain name and choose your domain from the zone list.
And that is it. In future articles, I will show you how you can change the code to add a navigation menu at the top and add additional functionality.
Thanks for reading, take a look at all the benefits you get with The Raine Report below
Dan are we going to use notion on the 7 figure blogs?
Hi Dan, I love the idea of using notion for a web presence. When assigning a domain in DNS for Cloudflare and notion, can this be done as an A record, so that I maintain mx records for email on my usual host provider?