Изображение, представляющее процесс подключения домена к Notion через Cloudflare Worker.
enjoyable stuff,  office

Как бесплатно подключить собственное доменное имя к Notion через Cloudflare Worker

Коротка инструкция о подключении вашего домен к Notion не подключая платный тариф

Notion – это мощная платформа, позволяющая создавать и управлять различными типами контента, включая заметки, базы данных, календари и многое другое. Настройка и персонализация страниц на Notion доступны благодаря различным шаблонам и инструментам.

Однако многие пользователи хотят придать своим страницам более индивидуальный вид, подключив собственные доменные имена. В этой статье я расскажу, как это сделать через Cloudflare Worker.

Шаг 1: Создание аккаунта на Cloudflare

Первым шагом необходимо создать аккаунт на Cloudflare. Cloudflare – это сервис CDN, который предоставляет различные инструменты и функции для управления вашим доменом.

Шаг 2: Добавление вашего домена

  1. Перейдите в раздел “Websites” и выберите “Add a site”.
  2. Введите название вашего домена и выберите бесплатный план.

Шаг 3: Создание DNS-записи для Cloudflare

Чтобы связать ваше доменное имя с Cloudflare и Worker, выполните следующие действия:

  1. Перейдите к настройкам DNS вашего домена на вашем регистраторе доменов.
  2. Замените существующие DNS-серверы на DNS-записи, предоставленные Cloudflare. Эти DNS-записи можно найти на странице настройки вашего домена в Cloudflare. Обычно это будет два сервера DNS, например, “ns1.cloudflare.com” и “ns2.cloudflare.com”.
  3. Подождите, пока изменения DNS-записей распространятся по всему интернету, что может занять до 24 часов.

Шаг 4: Создание Worker на Cloudflare

  1. Перейдите в панель управления Cloudflare и выберите вкладку “Workers & Pages”.
  2. Нажмите “Create a Worker”, затем “Deploy”, и после этого “Edit”.
  3. Вставьте JavaScript-код (код для Worker) и сохраните Worker. JavaScript-код для Worker будет описан далее.

Шаг 5: Создание “маршрута” для Worker

  1. В разделе “Websites” выберите созданный домен.
  2. В меню выберите “Workers Routes” и нажмите “Add Route”.
  3. Введите “вашдомен/*” в поле “Route” (замените “вашдомен” на ваш реальный домен) и выберите созданный вами Worker. Сохраните настройки.

Шаг 6: Проверка настроек

  1. После завершения всех настроек, убедитесь, что ваша страница Notion теперь доступна через ваше собственное доменное имя. Для этого просто перейдите по этому домену в браузере и убедитесь, что страница загружается корректно.
  2. Если страница не отображается, проверьте DNS-записи и настройки Worker на Cloudflare, чтобы убедиться, что они настроены правильно.

JavaScript-код для Worker (шаг 4):

  1. Перейдите на страницу Notion, которая будет вашим сайтом, и откройте к ней публичный доступ.
  2. Замените в строчке «Step 1» домен на ваш.
  3. Замените в строчке «const SLUG_TO_PAGE» значения в кавычках на ваши. Их можно взять в адресе страницы Notion, которую вы хотите сделать сайтом.
  4. Последнюю версию JavaScript можно найти по адресу: ссылка на скрипт

Сам скрипт:

  /* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = "idivjopu.ru"

/*
 * 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 = {
  "": "3905790c23ec4ae79eb02e468b7a508e",
}

/* 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 (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)
}

Заключение

Теперь у вас есть полная инструкция о том, как подключить собственное доменное имя к Notion с использованием Cloudflare Worker. Следуя этим шагам, вы сможете улучшить профессиональный вид вашего сайта и обеспечить более удобный доступ к вашей информации.

Часто задаваемые вопросы

  1. Могу ли я использовать платный план Cloudflare для этой цели?
    • Да, вы можете использовать платный план Cloudflare, чтобы получить дополнительные функции и улучшенную поддержку.
  2. Сколько времени займет процесс смены DNS-записей на моем домене?
    • Время, необходимое для обновления DNS-записей, может варьироваться, но обычно это занимает до 24 часов. Однако в большинстве случаев изменения начинают действовать гораздо быстрее.
  3. Могу ли я использовать этот метод с любым регистратором доменов?
    • Да, вы можете использовать этот метод с большинством регистраторов доменов. Процесс замены DNS-записей обычно аналогичен, независимо от регистратора.
  4. Что делать, если моя страница Notion все равно не загружается после настроек?
    • Если у вас возникают проблемы с доступом к вашей странице Notion после настроек, проверьте DNS-записи и код Worker на Cloudflare. Убедитесь, что они настроены правильно, и в случае затруднений, обратитесь в поддержку Cloudflare.
  5. Могу ли я использовать другие CDN-сервисы вместо Cloudflare?
    • Да, но Cloudflare предоставляет множество дополнительных функций и инструментов, делая его отличным выбором для большинства пользователей.

Следуя этой инструкции, вы сможете успешно подключить собственное доменное имя к Notion через Cloudflare Worker и улучшить доступ к вашим проектам и информации.