Notion Custom Domain
Notion Custom Domain

Notion Custom Domain

Tags
Notion
NextJs
CloudFlare
If you're looking to add a custom domain to your Notion Pages, you're in luck! In this document, you'll learn two different ways to achieve this. The first method is more focused on pulling information from the Notion API to generate your website's content. The second method simply delivers your Notion content under a different domain.
Before we dive into the first method, it's important to note that you'll need to have some knowledge of coding and web development to follow along. If you're not comfortable with these skills, don't worry - the second method may be a better fit for you.
With that said, let's get started with the first method. This approach involves using the Notion API to generate your website's content. This method is more complex, but it offers a lot of flexibility and control over how your content is displayed. To save some time I’ve attached a demo repository below.
You can follow along with the repository or you can Deploy To Vercel now. As you make changes in your repository they will be reflected on Vercel.

With NextJS

This is a more full-featured Next.js example project using react-notion-x, with the most important code in pages/[pageId].tsx and components/NotionPage.tsx. You can view this example live on Vercel.
Config can be found in lib/config.ts
This demo adds a few nice features:
  • Includes larger optional components via next/dynamic
    • Collection, CollectionRow
    • Code
    • Equation
    • Pdf

Getting Started

Visit the Notion Page you’d like to have on custom domain and share it to the web.
notion image
This link contains your page id https://weckmann.notion.site/Notion-Custom-Domain-with-Cloudflare-612bf2c7378e4ca3a3128674723aee9e
In the attached Github repository navigate to /lib/config.ts and adjust lines 4 and 5 appropriately.
First, run the development server:
npm run dev # or yarn dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying pages/index.js. The page auto-updates as you edit the file.

Preview Images

This demo uses next/image as a custom image component. It also generates preview images at page build time using lqip-modern.
Note that preview image generation can be very slow, so it's recommended that you either cache the results in a key-value database or disable it by setting previewImagesEnabled to false in lib/config.ts
Note that custom images will only be enabled if either the image has a valid preview image defined for its URL, or if you set forceCustomImages to true.

Custom Domains

For a detailed explanation of assigning custom domains to your Vercel project please see the latest documentation by clicking here.

With CloudFlare

In this article we’ll be covering how to add a custom domain to your Notion.so site with a Cloudflare worker. This article assumes your domain is on Cloudflare and have you have a basic understanding of the Cloudflare Platform.

What is a Cloudflare worker?

Cloudflare workers allow you to deploy serverless code instantly across the globe to give it exceptional performance, reliability, and scale. With a Cloudflare worker you have scaling built in. No more configuring auto-scaling, load balancers, or paying for capacity you don’t use. Traffic is automatically routed and load balanced across thousands of servers. Sleep well as your code scales effortlessly.

Why choose Cloudflare Serverless over other systems?

💰 Cost

The first 100,000 requests each day are free and paid plans start at just $5/10 million requests, making Workers as much as ten-times less expensive than other serverless platforms.

🏎️ 0ms Cold Starts

Most serverless platforms experience a cold start every time you deploy or your service increases in popularity. Workers can run your code instantly, without cold starts.

🛠️ No Maintenance

Spend more time building, less time configuring. No VMs, no servers, and no containers to spin up or manage. Deploy using our CLI, web interface, or API.
You can read more about Cloudflare Workers by clicking here.
 

Let’s get started.

Visit the Notion Page you’d like to have on custom domain and share it to the web.
notion image
This link contains your page id https://weckmann.notion.site/Notion-Custom-Domain-with-Cloudflare-612bf2c7378e4ca3a3128674723aee9e

Configure your worker script.

For the purposes of this article the configuration is broken up into sections, you can copy the entire script at the bottom of this article. The worker script can also be generated at fruitionsite.com. I had a hard time following their tutorial so I decided to publish this article and the steps I took that worked for me.
/* Step 1: enter your domain name like fruitionsite.com */ const MY_DOMAIN = "sub.example.tld"; /* * 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 = { "": "XXX", // Put your home page ID here // "about": "XXX" // You can add more pages here like so }; /* Step 3: enter your page title and description for SEO purposes */ const PAGE_TITLE = ""; const PAGE_DESCRIPTION = "";
The comments in the script above outline what needs to filled in with what information pretty well. Massive thanks to those contributing to FruitionSite. Be sure to copy the full script from the bottom of this article.

Add your worker to Cloudflare.

Visit your Cloudflare Dashboard and select Workers from the left side menu.
notion image
Select the blue Create a Service button.
notion image
Name your service and select a start template.
notion image
Select Quit Edit after creating your worker.
notion image
Remove the template script on the left hand side and replace with your worker script.
notion image
notion image
Select the Preview tab to make sure your site is loading properly.
 

You’re good to go live!

Select Save and Deploy. Your Notion site isn’t going to render on your custom domain yet. You can see it at your Worker URL for now. The next steps will outline setting up your custom domain.

Custom domain routing.

Go back to your Worker dashboard and select the worker you just created. From there select the Triggers tab.
notion image
Below Custom Domains select Add Route. I spent a too long assuming the Custom Domains section is what I needed. Truthfully, I’m not sure if it will work that way or not, but this is how I made it work. If you have any information on this please comment below.
notion image
Add your route the sub.domain.tld/* format. This will trigger your worker on all paths. If you want it run on a specific path replace that * with that path like so sub.domain.tld/blog/*. You also need to select your zone.
notion image
Select Add Route. From here jump into the DNS settings for your domain.
Create a new proxied A record for your domain and point it towards 192.0.2.0
notion image
Save your record and visit your new Notion site!
 

Complete Worker Script.

 
/* Step 1: enter your domain name like fruitionsite.com */ const MY_DOMAIN = "sub.example.tld"; /* * 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 = { "": "XXX", // Put your home page ID here // "about": "XXX" // You can add more pages here }; /* 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 = ""; /* 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( `<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); } } document.querySelector("svg.notionLogo").parentElement.innerHTML = "Toggle 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'); __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)); }; document.querySelector(".notion-focusable > .notionLogo").style.display = "none"; </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); }
 

Contact Me

Discord: CLIBasedNerd#8711