Ghost CMS x Bubble : /blog subfolder using Cloudflare Workers - Complete guide
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
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 :
- Cloudflare Worker can do the job https://www.codiva.io/blog/post/host-saas-product-blog-in-subdirectory-instead-of-subdomain/
- Cloudflare has a clean interface you don't need to install anything
- HTMLWriter Worker
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.
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
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
:
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 ✨
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
Here come's the magic : we will replace our Hello World
code by the routing to our blog - here for me blog.hackerhouse.paris
Copy and paste the following code and replace the variables:
blogUrl
- 'https://blog.hackerhouse.paris' -> 'blog.my-domain.com'
The url of your hosted blogblogPath
- '/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 ghostblog.hackerhouse.world/worker/sitemap.xml
is the rewritten sitemap generated by our worker who fetch onblog.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
Resources
I've found help using the following resources :