🌐 Using the Notion page as a personal website with your domain on Cloudflare
Tags: tutorial, productivity, beginners, tooling
Published: 2021-03-17 21:18:00

## Introduction

Hi, DEV people! 🙂 Today, I give you a handy step-by-step guide to help you set up a [Notion](https://notion.so/) page on your domain with free serve by [Cloudflare](https://www.cloudflare.com/) reverse-proxy. 

A nice bonus out of the box is protection against DDoS attacks, secure access to resources, automatically renew a SSL cert by Cloudflare for your domain and DNS servers.

> ⚠️ This method was published on this [webite](https://fruitionsite.com/). In this article I want to give you a more compact instructions and a ready to _copy-paste_ script worker with some CSS edits.

<a name="toc"></a>
### 📝 Table of contents

- [What needs to be prepared?](#what-needs-to-be-prepared)
  - [Public Notion page](#public-notion-page)
  - [On the side of your domain registrar](#on-the-side-of-your-domain-registrar)
  - [On the Cloudflare side](#on-the-cloudflare-side)
- [Create a script for Cloudflare Worker](#create-a-script-for-cloudflare-worker)
- [An example of how it worked out for me](#an-example-of-how-it-worked-out-for-me)

<a name="#what-needs-to-be-prepared"></a>
## What needs to be prepared?

The whole process will not take more than **10 minutes** if you follow the instructions below. But be careful, some settings will affect your domain.

[↑ Table of contents](#toc)

<a name="#public-notion-page"></a>
### Public Notion page

- Create a new Notion page.
- Make the design to your liking.
- Click the **Share** button (in the upper right corner).
- Copy the ID of your public page.

> 🤔 How do I know the ID of a public page in Notion? The ID is a set of letters and numbers after the domain `notion.so/<PAGE ID HERE>`.

You can create multiple pages and link them however you want.

[↑ Table of contents](#toc)

<a name="#on-the-side-of-your-domain-registrar"></a>
### On the side of your domain registrar

- [Buy a new domain](https://www.reg.com/domain/new/?rlink=reflink-5542869) or choose existing.
- Go to **NS records settings** of chosen domain.
- Don't close this page and open `cloudflare.com` in another browser tab. We'll be back here again as we configure Cloudflare.

[↑ Table of contents](#toc)

<a name="#on-the-cloudflare-side"></a>
### On the Cloudflare side

- Login or [create](https://dash.cloudflare.com/sign-up) a new Cloudflare account.
- Click **Add a Site** button and enter your chosen domain name.

> ☝️ Attention! If you would like to use a subdomain, you _should_ still enter your root domain name here. Setting up subdomains will be described later.

- Select the **Free plan**. 
- Click **Continue** on the **DNS Record** page.
- Copy two NS (nameservers), which end with `.ns.cloudflare.com`.
- Paste copied NS to the domain records settings page (at your registrar).
- Return to the Cloudflare page.
- Wait for a minute, then click **Done**.
- Select **Flexible SSL/TLS** encryption mode.
- Turn on **Always Use HTTPS**, **Auto Minify**, and **Brotli**.
- Click **Done**.
- Go to the **Workers** page and then click **Manage Workers**.
- Choose any available subdomain for your worker.
- Click **Set Up** and then click to **Confirm**.
- Choose the **Free plan**.

[↑ Table of contents](#toc)

![Cloudflare Worker IDE](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/evlnttrrjusltlbox5sy.png)

<a name="#create-a-script-for-cloudflare-worker"></a>
## Create a script for Cloudflare Worker

- Click **Create a Worker**.
- Paste this code to the **Worker IDE**.

> 👇 Don't forget to fill the config variables with your data!

```javascript
/* CONFIGURATION STARTS HERE */

/* 
 * Step 1: enter your domain name 
 */

const MY_DOMAIN = "your-domain.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 = {
  "": "<NOTION PAGE ID>",        // main page
  "about": "<NOTION PAGE ID>",   // page with about info
  "blog": "<NOTION PAGE ID>",    // your blog page
  "contact": "<NOTION PAGE ID>", // feedback form
  // ...
};

/* 
 * Step 3: enter your page title and description 
 * for SEO purposes 
 */

const PAGE_TITLE = "My Page Title";
const PAGE_DESCRIPTION = "Page desctiption";

/* 
 * 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";
  // Create robots.txt file for SEO.
  if (url.pathname === "/robots.txt") {
    return new Response("Sitemap: https://" + MY_DOMAIN + "/sitemap.xml");
  }
  // Create sitemap.xml file for SEO.
  if (url.pathname === "/sitemap.xml") {
    let response = new Response(generateSitemap());
    response.headers.set("content-type", "application/xml");
    return response;
  }
  let fullPathname = request.url.replace("https://" + MY_DOMAIN, "");
  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: 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 = "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 style="margin-left: 10px; 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 style="margin-left: 10px; 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);
}
```

- Click **Save and Deploy** button.
- Go to the **Workers** page and select **Add Route**.
- Place `your-domain.com/*` (or `subdomain.your-domain.com/*`, if you would like to use a subdomain) as the **Route** and select the **Worker** you just created.

> 👌 Please note, if you would like to enable other all subdomains like `www`, place `*your-domain.com/*`.

- Click to **Save** button.

You can now visit your website! 🎉

[↑ Table of contents](#toc)

<a name="#an-example-of-how-it-worked-out-for-me"></a>
## An example of how it worked out for me

So, I moved my personal resume website and now editing or adding information happens at the speed of light.

Here is a short video that demonstrates this:

{% youtube rmJn7dzFp3g %}

> 😉 As a reminder, this is a standard Notion page in a standard layout, but slightly improved through the Cloudflare Worker script.

[↑ Table of contents](#toc)

<a name=""></a>
## Photos and videos by

- Vic Shóstak https://github.com/koddr

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv