Git Product home page Git Product logo

fruitionsite's Introduction

Fruition: Free, Open Source Toolkit For Customizing Your Notion Pages

  • Use cases: perfect for your portfolio, blog, landing page, and business site
  • Features: pretty URLs, custom domains, Google Fonts, SEO support, script injection
  • Benefits: completely free, no lock-in, and open source

For step-by-step setup instructions, visit https://fruitionsite.com

This repo has 2 independent parts:

  1. worker.js is the Cloudflare Worker script
  2. everything else is a React app that helps generate the Worker script via a UI.

fruitionsite's People

Contributors

kehanlu avatar marcklingen avatar okisdev avatar pabliqe avatar stephenou avatar wonathanjong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fruitionsite's Issues

ERR_TOO_MANY_REDIRECTS

I've tried to set this up, following every instruction. But it doesn't work at all. No idea where it's failing.

The domain is kallelofdahl.co

Error code: ERR_TOO_MANY_REDIRECTS

The worker route is
image

The code is

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'kallelofdahl.co';

/*

  • 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 = {
    '': '64a81aaef0b64c25bff7b104b2363eaf',
    'film': '6d1404e78aeb4e17987ab729af647457',
    };

/* Step 3: enter your page title and description for SEO purposes */
const PAGE_TITLE = 'A great Page Title';
const PAGE_DESCRIPTION = 'An even better 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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 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(<div style="display:none">Powered by <a href="http://fruitionsite.com">Fruition</a></div> <script> 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: 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 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);
}

My site says "Not found"

Hello,

I created my site like it's shown on the fruitionsite.com and in the video, I set up the worker with the script, added a route, but my website just shows Not found!

My script is the following, I changed nothing just copy pasted it from the configurator:

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'mgcodes.one';

/*
* 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 = {
  '': '91b47b7aeb424340a0ea0e80ddee8225',
  'aboutme': 'ce9c7739b7344c47968a7649b95eadf4',
  'projects': '210569fd9fa146c3b030a879171c56ff',
  'blog': 'bb156fe15dae46d7b06a7648f8eaea1c',
};

/* 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);
  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;
  }
  const notionUrl = 'https://www.notion.so' + url.pathname;
  let response;
  if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
    response = await fetch(notionUrl);
    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');
  } else if ((url.pathname.startsWith('/api'))) {
    // Forward API
    response = await fetch(notionUrl, {
      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', '*');
  } 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(notionUrl, {
      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; }
    </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>
    const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)};
    const PAGE_TO_SLUG = {};
    const slugs = [];
    const pages = [];
    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);
      }
    }
    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();
        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);
}

My DNS settings are:
Screen Shot 2020-07-04 at 13 45 40

I really don't know what I did wrong, please help me.

Thank you

Pretty URL not working

Hi!

The pretty URL (www.mar-sounds.com) to my Notion website [https://www.notion.so/mar-sounds-1b502c9028f94eb598e27e715f3f21b6] has stopped working: it directs to a blank page that says

"2020 Copyright. All Rights Reserved.

The Sponsored Listings displayed above are served automatically by a third party. Neither Parkingcrew nor the domain owner maintain any relationship with the advertisers.

Privacy Policy"

Thanks in advance for your help (and thanks for creating Fruition in the first place!)
Maria

Utilize Worker-Caching

Hey there,

I've recently used your tool and fiddled around with it, works quite well! However, load times are really annoying sometimes. I found a "fix" using the caching function Cloudflare provides with its workers.

I'd like to contribute what I came up with, however the Solution isn't for everyone, so adjustments to the Configurator are also required. Are you interested in adding Caching? If so I'll make a PR soon.

Best regards,
Felix

some meta tags retaining notion data

Here are several meta tags retaining notion data rather than for the new site:

meta name="twitter-site"
meta name="twitter:image"
meta property="og:site_name"
meta property="og:image"

possibly:

meta property="og:locale" for non-en_US locations.

Options to Cache?

Are there plans to add a caching option for faster load times?

Hosting Potion seems to be doing this, would be awesome to have with furitionsite

Seems like the comment button cannot be displayed?

image
I have turned on the comment function.
But it seems that I can’t comment on my website, and I don’t seem to see a website that can be commented in the showcase example. I don’t know if this is a bug.

Issue with Google Search Console / indexing

My site seems to be working fine to me, however it doesn't seem to be indexed by Google and my attempt at submitting it via Google Search Console show that Google thinks the page is 404ing.

Any ideas?

My website is a total blank

what I did wrong? I followed all steps :(
could you help me?

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'marimuller.design';

/*

  • 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 = {
    '': '0d03f9f118af4d1f80508841f3112073',
    };

/* 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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);
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;
}
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

Not sure why but it's not working

Hi there,
I saw your video with Marie and I followed the instructions on your site, I think. I created the worker on cloudfare. www.monicacoaching.com still gives me error. I checked google domains (where i have the domain) and updated the servers. Any ideas, why it's not working? Thank you so much. I am an English teacher based in Seoul.

Below is my code:

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'monicacoaching.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 = {
    '': 'b12b2afd767e4b309bd6274ffd8a03b8',
    'emails': '4a991d04dfac41da8e8ed1559a5870c0',
    };

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

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);
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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}: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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

DNS problem

Hi,
What am I doing wrong? I can't quite figure it out, thanks:

dns management


/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'example.org';

/*

  • 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 = {
    '': '771ef38657244c27b9389734a9cbff44',
    };

/* 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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);
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;
}
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

BUG: Worker script can make a page crash

Hi,
I just found that the worker script will make a page crash. The rewrite function can rewrite everything!
For example, any page contains string <title> will crash, because the worker script rewrite the response of JSON while querying the page data. It is expected to rewrite the page HTML, not the query response.

DOS attack

If someone use Fruition and allow comment on their page, a prankster can easily leave a comment <title> then the page will crash on their domain. (But it still works normal on notion.so domain)

Reproduce the bug

On notion.so

https://www.notion.so/kehanlu/This-page-will-crash-with-Fruition-1616267a1c4645c3824fde8e0f0ea87d
image

On mydomain

image

API response JSON is rewrited.
image

Is dark-mode overriding search functionality?

Hi @stephenou, I'm not sure if this is "feature or bug", but seems that Fruition latests scripts are overriding the search bar to put the dark-mode switcher.

I don't think this is a good thing since search is a pretty powerful thing for public notions.

What do you think about it?

Header in Fruition

Captura de Pantalla 2020-10-17 a la(s) 14 09 29

Header in Notion (public link)

Captura de Pantalla 2020-10-17 a la(s) 14 09 08
Captura de Pantalla 2020-10-17 a la(s) 14 09 13

"Issue with your iOS app" error

Hi @stephenou, thanks for creating Fruition, very happy user!

I'm getting a recurring error that I think is related to Fruition (although I've also reported it to Notion and will update accordingly).

When I access this URL on Safari (iOS and Mac) I get the following error (screenshots attached):

"Hello, we've noticed an issue with your iOS app. Please delete this app and re-install it from the App Store."

I've deleted the Notion apps and it makes no difference to the error message.

FYI - clicking directly on the URL above doesn't actually load on Safari for me (possibly a related error?). So best to access it via:

https://kufner.io/ -> "Things I'm thinking about" -> "Heads I win, Tails you lose"

Thanks for the help :)

IMG_6848
Screenshot 2020-10-19 at 08 27 17

Google Indexing All Notion.so pages

Hello! I don't know if someone is having this same issue.

Google is Indexing all Notion.so pages inside my domain, for example: if I my domain is example.com, Google is indexing example.com/customers (and its content is notion.so/customers).
For now, my workaround has been adding those specific urls into my Cloudflare Workers routes without a specifying any Worker.

Does anybody know how can I fix this without all this hassle?

Thanks in advance.
Javier.

Ugly slug if pages 2 levels deep

Created by @edgardarascom in the mayneyao gist:
https://gist.github.com/mayneyao/b9fefc9625b76f70488e5d8c2a99315d#gistcomment-3293741

@stephenou this is amazing! That's the main thing I was missing, custom slugs, so if I want to change my system at some point, no links would ever brake!

One thing, if I go 2 levels deep into sub-pages, I still get the ugly slug:
https://fruitionsite.com/Fruition-3d694da045a745b0a36708fb5e6d09d8

Every slug must be registered somewhere right?

Custom uploaded header images fail

Hi team.

I get this error message on screen when I try and go to the url of a custom uploaded header image:

{"errorId":"ebe6ebcb-ab44-4ac1-96b9-a6d45d472556","name":"ValidationError","message":"For secure file URLs, must specify table and id."}

Note that Unsplash images (with their own URLs) work find, as to "in-built" Notion header images. It's just your own images which you upload, and get saved top AWS S3, that fail with this error.

They're not recognised as "public".

Is there any way around this?

An example custom header image URL is https://www.qldvotes.org.au/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F20a0e53e-dbd3-41d0-ae41-d0b56124b9a4%2Fact-view-social.jpg?table=block&id=790e17ae-8450-47c8-920b-a6320b7bae7f&width=800&userId=&cache=v2

Thanks.

A

Canonical URL

If Canonical URL could be added to worker.js so that the pretty URL slugs are included in the REL meta tag, that would be wonderful, especially for republishing the posts on Medium and other platforms. It is essential for SEO. See https://moz.com/learn/seo/canonicalization for Moz's guidelines

Escape inputs (page title/description)

Inputs such as Page Title and Page Description should be automatically escaped. At this moment, it requires the user to notice the error and manually escape their page title/description if they contain a '

image

Mismatch between origin and baseUrl (dev)

image
/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'www.matrimonylaws.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 = {
    '': '3e06fa8377a249e295f169b80f7c17e4',
    };

/* 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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);
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;
}
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

Adding support for “Domain“ option in workspace configuration

In my tests, I have noticed that the Domain option found in the workspace configuration settings page is not currently supported.

lux-screenshot0743

This option allows to create a longer “root“ URL by adding some text to the notion.so/ root.

It would be nice to see this option being supported in the future.

Issues with setting up Fruition, looking to see where I messed up (assuming the mistake is on my end)

I am trying to set up Fruition so I can utilize Notion as a blog option but ran into some issues with getting it going. As far as I can tell, I followed your instructions accurately, but I am continuing to get the same error message every time I try to load my site. I have added as much collateral/support info to this message as well in the hopes that you can help me resolve this issue. Thank you for your help! I've added screen shots of my cloudflare/namecheap settings, as well as the worker script I received from your website.
FruitionScript.pdf
Cloudflare DNS Settings
Cloudflare HTTPS Settings
Cloudflare MinifyBrotli Settings
CloudFlare SSL Flexible Settings
Namecheap DNS Settings
Notion Sharing Settings
One Screen I get when I try to go to my site URL
The other screen I get when I go to my URL

DNS_PROBE_FINISHED_NXDOMAIN

After doing all setup, I have not managed to make it work. Have no idea why. DNS_PROBE_FINISHED_NXDOMAIN is all I can share with you.

And the script of course:
/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'senda.works';

/*

  • 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 = {
    '': '101ce05cea1c41f08eaf99b93ad995e3',
    };

/* Step 3: enter your page title and description for SEO purposes */
const PAGE_TITLE = 'Senda';
const PAGE_DESCRIPTION = 'El alquiler sin sobresaltos.';

/* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */
const GOOGLE_FONT = 'Open Sans';

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

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);
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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}: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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

Dark Mode Breaks the Page

I am not sure if anyone has noticed this before, but the dark mode breaks your Notion Page down. Whether it be through using the toggle button on the top or the nifty original Ctrl/cmd+Shift+L provided by Notion.

All images are not displayed

All websites, including showcase websites in https://fruitionsite.com/ stopped displaying images from today + gives error 400.
It was okay until yesterday & I accessed the websites from South Korea.
The original notion links accessed from 'share website' displays images nicely.

Screen Shot 2020-09-05 at 6 06 09 PM

Prevent on page preview

Hello,

Probably this a Notion thing rather than fruitionsite, but I want to ask if you know how to avoid see a page in a "preview" way

May be is a thing we can handle adding some JavaScript code? 🤔

Anchor Links

Hi! Thanks for putting this together.

Very possibly missing something but I'm trying to anchor to a content block. However, every time I click the anchor link it opens in a new page. Is there anyway to prevent this?

Avoid the 301 redirect

Is there any way to avoid the 301 redirect and simply serve the page content at the root route (/) ?

➜  ~ curl -I https://fruitionsite.com/
HTTP/2 301 
location: https://fruitionsite.com/771ef38657244c27b9389734a9cbff44
...

Ability to stub to specific database views

The ability to handle specific database views as a stubbed URL would be really useful

https://notes.bozzie.org/commonplace links to https://www.notion.so/sboswell/486759ad7b794a94935b415a1a24aea9

It would be cool to link for example

https://notes.bozzie.org/commonplace/life

to the specific database view (note the v query string): https://www.notion.so/sboswell/486759ad7b794a94935b415a1a24aea9?v=a0b78636fb4146d3acf134099b2302c1

The stubbing of /commonplace seems to override any query strings applied to the page!

Redirecting a different page to the same domain

A few days ago I managed to make my website with fruition. I got the domain on Porkbun and followed the instructions to make my domain redirect the visitor to what I'll call "my notion page 1".

However, I realized I want my visitors to be redirected to my notion page 2 instead.

I tried remaking my steps with fruition to see if I would have any success and after several trials, I just can't seem to be able to change it...

I suck at programming, my knowledge in this area is less than zero. So how can I change the page my domain is pointing to?

Thank you

Redirect breaks everything

I had multiple issues. First, my previous dns records over at godaddy didn't show up at cloudflare, but were seemingly still active. Took some time to find out that I need to delete previous redirections at godaddy and then export & import the settings to Cloudflare.

Then, it kept redirecting to https://welf.co/a1b922394eff463bb3a3e3dabfad044a -- which is part of the notion page I was trying to link. Now, everything is deployed as it should be, with A DNS records pointing to 1.1.1.1, but it's still not working.

"Safari can't find the server welf.co"

header - Top night mode toggle button misplaced and Notion Log In and Copy Link display on Mobile phone.

Since yesterday, something may have changed at Notion. We are having an issue with top bar look on mobile devices. Please see attached screenshots. It switches the place of the night vision toggle button and adds three buttons to login to notion.

Your site has the same problem as my site, but some of the showcased sites don't have they also don't have the night/dark button so I guess, that is what causing the issue. Not sure though. Let me know if you have a quick fix for it - like disabling the dark switch button - or if you are aware and working on it.

`function updateSlug()` glitching due to `location.pathname.slice(-32)`

return location.pathname.slice(-32);

↑↑↑ Here in this line for function getPage(), the slice causes slug to misbehave in specific cases where slug might be more than that length.

Case in Point:

> PAGE_TO_SLUG
{something-something-0d44ee0e01944fabbb047c7bb9f885fd: "demo3", 
4569ee8b29d64be6b8cb25be1f9123b0: "demo2"}

Here demo2 gets history.replaceState correctly, but, demo3 doesn't!

Simply changing the function to :

function getPage() {
        return location.pathname.slice(1);
      }

should work.

Error code: SSL_ERROR_NO_CYPHER_OVERLAP and Mismatch between origin and baseUrl (dev)

Hi, @stephenou
Thank you for creating Fruition.

I have run into a problem.

My source code

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'mindtriibe.club';

/*

  • 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 = {
    '': '7106a2c2c13c4703819514c76f38e665',
    };

/* Step 3: enter your page title and description for SEO purposes */
const PAGE_TITLE = 'MindTriibe';
const PAGE_DESCRIPTION = 'Bộ lạc Sách của Bạn ';

/* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */
const GOOGLE_FONT = 'Josefin Sans';

/* 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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);
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;
}
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

I had it, and then I broke it.

I followed your amazing guide to the first website I made, but I had to delete it and change domains because I was getting too much spam (from the .us domain). I've registered a new domain and followed every step, but I'm having trouble getting the site to work! Hope you can help me out—and thanks! Here is the code:

/* CONFIGURATION STARTS HERE */

/* Step 1: enter your domain name like fruitionsite.com */
const MY_DOMAIN = 'thecitizendemocrat.org';

/*

  • 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 = {
    '': 'ba6e61413085438782e4539639a127d1',
    };

/* 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 = '';
slugs.forEach(
(slug) =>
(sitemap +=
'https://' + MY_DOMAIN + '/' + slug + '')
);
sitemap += '';
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);
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;
}
const notionUrl = 'https://www.notion.so' + url.pathname;
let response;
if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
response = await fetch(notionUrl);
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');
} else if ((url.pathname.startsWith('/api'))) {
// Forward API
response = await fetch(notionUrl, {
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', '*');
} 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(notionUrl, {
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; } </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> const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)}; const PAGE_TO_SLUG = {}; const slugs = []; const pages = []; 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); } } 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(); 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);
}

Can't generate script

Can't generate the script at 'Step 2: Customize and generate the script (2 mins)'
'Copy the code' button is inactive.

Primus message store ERR_BLOCKED_BY_CLIENT

Any idea of how to get rid off the regular message emitted by the msgstore.www.notion.so.
Also I notice a very weird behaviour; while websockets seems to work well in iOS iPhone device, seeing pages being updated almost in realtime, it is not the case in desktop client, both Safari and Chrome.

GET https://msgstore.www.notion.so/primus/?sessionId=0dac675a-94e0-5cd6-766f-b6a037fecb12&_primuscb=N7_cHfF&EIO=3&transport=polling&t=N6_cHfF&b62=1 net::ERR_BLOCKED_BY_CLIENT

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.