Ghost CMS x Bubble : /blog subfolder using Cloudflare Workers - Complete guide

ghost cms bubble blog subfolder cloudflare workers

UPDATE 01/04/21 : I was able to use cloudflare worker with bubble because I had a legacy domain with Accelerate with Cloudflare off. Read More
The new solution go there

Skip introduction if you already familiar with SEO and subfolder.

If you want to add a blog to your bubble app you have 2 ways :

  • a subfolder: my-bubble-app.com/blog
  • a subdomain: blog.my-bubbleapp.com

For SEO, a blog hosted as subfolder perform better than subdomain.

How to implement a blog on bubble /blog ?

Build your own blog engine on bubble

Building a blog using mardown

PROS

  • Native feel of your blog with your app design
  • You can CRUD your blog's articles whenever in your code

CONS

  • You will have to implement it (I did a minimal version in 5 days) and maintain it...
  • Rich Text Editor does not support HTML tags or Markdown Editor are not as complete like CMS
  • You will have to deal with SEO, metadata...

Use any CMS : Wordpress, Ghost as blog

However on the bubble forum this seem to be an endless issue :

This is a pretty big feature for us to build, so we have not scheduled it on our roadmap currently - Allen Yang, Bubble PM

I did some researched this week-end and found out :

My results :

REQUIREMENTS

In order for this to workout I assume you :

  • Use Cloudflare as your domain server
  • Have a blog setup: Wordpress, Ghost, whatever
  • Have a bubble app running
  • Have prettty good knowledge of dns / web

I highly recommend Cloudflare as it is mostly free and comes with free SSL, caching and more.

How does the routing /blog works ?

We use Cloudflare route to introduce a small piece of code called "Cloudflare Worker".

A Worker is a piece of code that will be executed between the browser request and the response from server.
Routing from bubble to ghost using Cloudflare Worker


The tricky part is to rewrite the HTML code returned by our blog

Relative path

<img src='/assets/asset.js>

we need to replace by <img src='/blog/assets/asset.js>

So the image will be properly loaded

Full path

<a href='https://blog.hackerhouse.paris/post/my-super-post/> we need to replace by <a href='https://hackerhouse.paris/blog/post/my-super-post'/>
So the user doesn't switch from hackerhouse.paris/blog to blog.hackerhouse.paris while navigating.

Array Attributes

<img srcset="/blog/content/images/size/w300/2020/11/Screenshot-2020-11-17-at-11.45.53.jpg 300w, /content/images/size/w600/2020/11/Screenshot-2020-11-17-at-11.45.53.jpg 600w, /content/images/size/w1000/2020/11/Screenshot-2020-11-17-at-11.45.53.jpg 1000w, /content/images/size/w2000/2020/11/Screenshot-2020-11-17-at-11.45.53.jpg 2000w"/>

Route /blog using Cloudflare Worker

1.Select your Domain
2.Select Workers
3. Add route
4. Add route and select none in worker

Replace hackerhouse.paris by your domain name
If you want to route /my-super-cool-blog instead of /blog  feel free !

Important

If routing is not working, make sure to disable Accelerate this domain with Cloudflare in bubble.

Create a test worker for /blog route

Go back to your workers page > Manage Workers :

Click on Save and Deploy

Don't worry so far we just created the worker, we haven't link it yet.

I just want you to deploy this in order to make sure your route is working properly before adding the right code to your shiny new blog ✨

Go back to your Workers route page and click on Edit
Select your previously worker created worker and Save

Tadam you should bubble-app.com (bubble) bubble-app.com (our worker)

If you have an error or your bubble app still displays now I recommend you to back and check every steps.

Route /blog to your blog CMS Ghost / Wordpress

My blog is running on Ghost and running on a $5 Digital Ocean's droplet

HackerHouse Blog
Thoughts, stories and ideas.

Here come's the magic : we will replace our Hello World code by the routing to our blog - here for me blog.hackerhouse.paris

Go back to your worker and click { } Quick edit
Here is the code

Copy and paste the following code and replace the variables:

  • blogUrl - 'https://blog.hackerhouse.paris' -> 'blog.my-domain.com'
    The url of your hosted blog
  • blogPath - '/blog' -> '/my-super-cool-blog'
    only if you want to change your subfolder
const blogUrl = 'blog.hackerhouse.paris';
const blogPath = '/blog';

class AttributeRewriter {
  constructor(attributeName) {
    this.attributeName = attributeName
  }
 
  element(element) {
    const re = new RegExp(`^https?:\/\/${blogUrl}`, 'i');
    const attribute = element.getAttribute(this.attributeName)    
    // when relative path ex: /assets/ -> /blog/assets
    if (attribute && !attribute.match(/^https?:\/\//)) {
        var attributes = attribute.split(',').map( a => a.trim().replace(/^\/?(\w+)/,  blogPath + '/$1'));
        console.log(attributes);
      element.setAttribute(
        this.attributeName, attributes
      )
    }
    // when full path ex: blog.hackerhouse.paris -> /blog (re-go through worker)
    else if (attribute && attribute.match(re)) {
      var attributes = attribute.split(',').map( a => a.trim().replace(re, blogPath));
      element.setAttribute(
        this.attributeName,
        attributes
      )
    }
  }
}

async function handleRequest(req) {
  const regexp = new RegExp(`(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\\${blogPath}\\b`);
  // we redirect any query from https://my-bubble-app.com/blog to blog.my-bubble-app.com
  console.log('req: ', req.url.toString().replace(regexp, blogUrl));
  const res = await fetch(req.url.toString().replace(regexp, blogUrl));
 
const rewriter = new HTMLRewriter()
  .on('a', new AttributeRewriter('href'))
  .on('img', new AttributeRewriter('src'))
  .on('link', new AttributeRewriter('href'))
  .on('script', new AttributeRewriter('src'))
  .on('img', new AttributeRewriter('srcset'))
  .on('meta', new AttributeRewriter('content'))
 
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

Once done click save and deploy

You should now see your shiny new blog on /blog

`

Conclusion Bubble + Ghost / Wordpress + Cloudflare

It works pretty well and quite fast but note there is a small pricing if you hit 100k requests / day

This guide also works for anything not only bubble :

  • / -> Bubble
  • /shop -> Shopify
  • /blog -> Wordpress
  • /help -> Zendesk

Etc...

I've wrote this tutorial on my free time. This is still experimental I hope it will help.

You can ask questions on the bubble forum I will try to help out

If you need personal 1-on-1 support you can hire me

UPDATE 01/04/21

Since bubble moved to cloudflare, you cannot proxy your traffic anymore thus, use the routing provided by cloudflare workers.

Sitemap worker

We use a separate worker to handle properly sitemap rewriting. He will live at blog.hackerhouse.world/worker/sitemap.xml

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  const res = await fetch(request.url.replaceAll('https://blog.hackerhouse.world/worker', 'https://blog.hackerhouse.world'));
  let body = await res.text();

  // we redirect any query from https://my-bubble-app.com/blog to blog.my-bubble-app.com
  const regexp = new RegExp(`blog.hackerhouse.world\/([a-zA-Z-\/0-9.]*)`, 'g')

  while ((match = regexp.exec(body)) !== null) {
    if (match.index === regexp.lastIndex) {
        regexp.lastIndex++;
    }
    console.log(`Found ${match[0]} (${match[1]}) start=${match.index} end=${regexp.lastIndex}.`);
    if (match[0].match(/\.xml$/)) {
        console.log(match[0]);
        body = body.replace(match[0], 'blog.hackerhouse.world/worker/' + match[1])
    }
    else if (match[0].match(/\.xsl$/)) {
        body = body.replace('//' + match[0], 'https://blog.hackerhouse.world/' + match[1])        
    }
    else {
        body = body.replace(match[0], 'hackerhouse.world/blog/' + match[1])        
    }
  }
  console.log('done', body);
  return new Response(body, {status: 200, headers: {'Content-Type': 'application/xml'}})
}

What this guy does :

  • replacing link url blog.hackerhouse.world/* -> hackerhouse.world/blog/*
  • replacing xml urls blog.hackerhouse.world/*.xml -> blog.hackerhouse.world/worker/*.xml (yes we always need rewrite for others sitemaps)
  • Replacing xsl relative path to absolute url in order to have the xml displayed properly

Then set the worker to the blog.hackerhouse.world/worker/*

Submit our worker sitemap to Google Search Console

Reminder:

  • blog.hackerhouse.world/sitemap.xml is the original sitemap generated from ghost
  • blog.hackerhouse.world/worker/sitemap.xml is the rewritten sitemap generated by our worker who fetch on blog.hackerhouse.world/sitemap.xml

Now we can submit to Google Search Console

If you have old blog url indexed by Google link me: just disable them:

Update blog's robots.txt

User-agent: *
Sitemap: https://blog.hackerhouse.world/worker/sitemap.xml
Disallow: /ghost/
Disallow: /p/
http://blog.hackerhouse.world/robots.txt

Resources

I've found help using the following resources :

Host product blog as /blog subdirectory, and proxy it from the edge
For a SAAS product, there will be many support content. Like Articles, Blogs, Tutorials, etc. These pages are typically hosted on a different hosting platform than the main product (and the root domain). The first question that is asked is, should these be in subdomain (https://blog.example.com) o…
Host Product Blog in Subdirectory Instead of Subdomain
Introducing the HTMLRewriter API to Cloudflare Workers
The HTMLRewriter can help solve two big problems web developers face today: making changes to the HTML, when they are hard to make at the server level, and making it possible for HTML to live on the edge, closer to the user — without sacrificing dynamic functionality.