Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
DudeThatsErin opened this issue Oct 6, 2023 · 17 comments

Comments

@DudeThatsErin
Copy link

DudeThatsErin commented Oct 6, 2023

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
Copy link

matchai commented Oct 9, 2023

Works perfectly for me. Thank you for sharing! 🙏

@joaco05
Copy link

joaco05 commented Oct 10, 2023

you saved my site

@SenatorMeeseeks
Copy link

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

@d8rt8v
Copy link

d8rt8v commented Oct 19, 2023

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

@joaco05
Copy link

joaco05 commented Oct 19, 2023

@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
Copy link

d8rt8v commented Oct 20, 2023

@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
Copy link

d8rt8v commented Oct 20, 2023

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

@abeltomy
Copy link

works perfectly.how can i just add search tab on

@seungy0
Copy link

seungy0 commented Oct 27, 2023

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
Copy link

masih32 commented Nov 5, 2023

Thank you

@boraoztunc
Copy link

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
Copy link

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

@DudeThatsErin
Copy link
Author

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
Copy link

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
Copy link

velsa commented Jan 31, 2024

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
Copy link
Author

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
Copy link

velsa commented Feb 1, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests