stephenou / fruitionsite

Build your website with Notion for free
https://fruitionsite.com
MIT License
1.6k stars 223 forks source link

Most up-to-date combination of code to get your fruition site working! #258

Open DudeThatsErin opened 1 year ago

DudeThatsErin commented 1 year ago

I have it working here: https://appseeker.org

I wanted to share this cause it took me 4 hours tonight to get my website working on Firefox, Brave, and Chrome with the dark/light toggle and everything else.

This is the code I'm using:

Remember to replace MYDOMAIN with your domain URL and YOURPAGEID with the page IDs you would like.

  /* CONFIGURATION STARTS HERE */

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

  /*
   * 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 = {
    '': 'YOURPAGEID'
  };

  /* 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 (url.pathname.endsWith(".js")){
      response = await fetch(url.toString());
      let body = await response.text();
      response = new Response(
        body,
        response
      );
      response.headers.set("Content-Type", "application/x-javascript");
      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></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 = 'https://${MY_DOMAIN}';
      localStorage.__console = true;
      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 enableConsoleEffectAndSetMode(mode){
        if (__console && !__console.isEnabled) {
          __console.enable();
          window.location.reload();
        } else {
          __console.environment.ThemeStore.setState({ mode: mode });
         localStorage.setItem('newTheme', JSON.stringify({ mode: mode }));
        }
      }
      function onDark() {
        el.innerHTML = '<div title="Change to Light Mode" style="margin-left: 14px; 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');
        enableConsoleEffectAndSetMode('dark')
      }
      function onLight() {
        el.innerHTML = '<div title="Change to Dark Mode" style="margin-left: 14px; 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');
        enableConsoleEffectAndSetMode('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)
        const timeout = device === 'web' ? 0 : 500
        setTimeout(() => {
          nav.appendChild(el)
        }, timeout)
        // get the current theme and add the toggle to represent that theme
        const currentTheme = JSON.parse(localStorage.getItem('newTheme'))?.mode
        if (currentTheme) {
          if (currentTheme === 'dark') {
            onDark()
          }else{
            onLight()
          }
        } else {
          // enable smart dark mode based on user-preference
          if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
            onDark()
          } else {
            onLight()
          }
        }
        // try to detect if user-preference change
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
          toggle()
        })
      }
      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);
  }

This code WILL show everything on the top bar. If you want to hide everything on the top bar find the class HeadRewriter() and replace the code with this:

  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(5) { display: none !important; }
      div.notion-topbar > div > div:nth-child(6) { display: none !important; }
      div.notion-topbar > div > div:nth-child(7) { display: none !important; }
      div.notion-topbar > div > div:nth-child(1n).toggle-mode { display: block !important; }

      div.notion-topbar-mobile > div:nth-child(4) { display: none !important; }
      div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }
      </style>`,
      {
        html: true,
      }
    )
  }
}

I want to note that I am showing everything cause I like to have comments on my sites and it was a troubleshooting step cause the dark/light mode button was not showing for me in any browser though the rest of the website was working perfectly. The code above does not hide the toggle at all (in fact, it makes sure it is showing) but for some reason that wasn't working for me.

Anyway, I wanted to share others so they didn't have to hassle.

One thing this code does not fix is the issue where when you press the back button it wants you to login to Notion. #237 I'm not sure how to fix it. The solutions in that issue do not work for me. I don't get the same errors as that BUT I also don't get a website that works when I try to combine this stuff.

matchai commented 1 year ago

Works perfectly for me. Thank you for sharing! 🙏

joaco05 commented 1 year ago

you saved my site

SenatorMeeseeks commented 1 year ago

This is awesome! :) Thank you! Has anyone figured out how to make the favicons and meta tags work?

d8rt8v commented 1 year ago

Hmm, not working. Returns 301as an original JS code.

joaco05 commented 1 year ago

@D8mbSniper preview on cloudflare not works, also make sure to put your site id and domain at the top.

Also cloudflare can take some time to update. i recommend to purge cloudflare cache and try again

d8rt8v commented 1 year ago

@joaco05 Thanks for the help!

I've set A record of @ to 192.0.2.0 Edited the script and purged cache, and waited for 10 minutes. I've also added me.mydomain.com/* as a route Still the worker redirects me to https://mydomain.com/notion_id with 301 in Cloudflare and ERR_NAME_NOT_RESOLVED in my browser :(

Am i missing something?

d8rt8v commented 1 year ago

It worked after 30 mins and purging cache both locally and on cloudflare

abeltomy commented 1 year ago

works perfectly.how can i just add search tab on

seungy0 commented 1 year ago

Cool. thanks for your help. It works great for me. But in my case, I need to remove this code.

function enableConsoleEffectAndSetMode(mode){
        if (__console && !__console.isEnabled) {
          __console.enable();
          window.location.reload(); <-- this
        } else {
          __console.environment.ThemeStore.setState({ mode: mode });
         localStorage.setItem('newTheme', JSON.stringify({ mode: mode }));
        }
      }
masih32 commented 1 year ago

Thank you

boraoztunc commented 11 months ago

Hey @DudeThatsErin thanks for the code, and the second part that removing the topbar items. But next to the dark mode toggle there is still a three-dot menu item that appears, can we remove that too?

Screen Shot 2023-11-28 at 12 00 08
zard-zhang commented 11 months ago

Thanks for your code, it's very useful to me!

DudeThatsErin commented 11 months ago

Hey @DudeThatsErin thanks for the code, and the second part that removing the topbar items. But next to the dark mode toggle there is still a three-dot menu item that appears, can we remove that too?

Screen Shot 2023-11-28 at 12 00 08

Yes, just change this: div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }

to this: div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: none !important; }

boraoztunc commented 11 months ago

Yes, just change this: div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }

to this: div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: none !important; }

Thanks @DudeThatsErin but I think you misunderstood, I do not want to remove the dark mode toggle. I was trying to remove the 3dot Notion menu. I managed to get it done, removing all but keeping the dark mod toggle, both on desktop and mobile.

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 > div > div:nth-child(7) { display: none !important; }
div.notion-topbar > div > div:nth-child(1n).toggle-mode { display: block !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-mobile > div:nth-child(7) { display: none !important; }
div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }
velsa commented 9 months ago

Thanks a lot to everyone for amazing fixes to Fruition. I have combined all the fixes and added more functionality in my own version of Notion hosting (based on Fruition).

See #274 :)

DudeThatsErin commented 9 months ago

Thanks a lot to everyone for amazing fixes to Fruition. I have combined all the fixes and added more functionality in my own version of Notion hosting (based on Fruition).

See #274 :)

I wish I could use it but I don't have access to npm via my hosting provider where my domain is registered.

velsa commented 9 months ago

Thanks a lot to everyone for amazing fixes to Fruition. I have combined all the fixes and added more functionality in my own version of Notion hosting (based on Fruition). See #274 :)

I wish I could use it but I don't have access to npm via my hosting provider where my domain is registered.

Maybe I am not understanding you correctly, but the idea is that you install notehost and create worker repo with it on your LOCAL machine. And then you configure it locally and deploy it to Cloudflare.