Subdomain to Subfolder: The Simple Cloudflare Reverse Proxy

For years, there has been a subdomain verse subfolder debate.

One side says this, another side says that.

People have run their own tests, shown that moving a subdomain to a subfolder can improve a blogs ranking, yet so many have been in denial because Google has said the opposite.

Well, Aleyda Solis came out with direct test results, and a lot more people have finally jumped on the subfolder bandwagon.

Most of the time though, running a blog off a subfolder instead of a subdomain isn’t technically feasible.

It’s a pain to set up.

It was a shocking process the last time I tried to do it with a larger site 4/5 years ago.

However, lucky for us things have changed over the last couple of years.

If you use Cloudflare, you can now how you can have a blog installed on a subdomain, yet force it to load, and make it looks like it exists, under a subfolder.

 

Aleyda’s test

Kicking it off here, this is the tweet that stirred things up again.

Clearly highlighting the tech challenges that she’s gone through to get it going, Aleyda shows there’s a definitive growth following the migration to a subfolder.

An almost instant improvement.

There are plenty of caveats that could exist here, as it’s SEO after all…. but once the migration was complete you can see a nice upwards trend.

In some projects like this, I have seen what I dub a ‘new shiny’ effect. New URLs getting a little boost when discovered/migrated too, and then sometimes a bit of a drop off afterwards.

I reached out for an update to see if there was such a dip, and Aleyda was kind enough to provide a new graph;

No post-launch dip! Nice.

 

Reverse proxies

Before we get into the different methods of setting this up, you need to understand reverse proxies.

Well, the super basics of them anyway…. which is where my knowledge of them stops anyway lol.

A reverse proxy allows you to essentially mask your website’s true file location, and load it somewhere else.

CloudFlare, in general, acts as a reverse proxy by being a CDN. It masks your server’s true location, by forwarding the requests from a Cloudflare ‘middle-man’ to it.

But this runs as URL in, URL out, where the URL is the same before and after the request and only the IP of the content is modified.

We can tweak and override this a bit, so that the URL of the request, differs from the URL of the content location.

So you could have a blog installed on the subdomain, and leave it there, but make it act and look like it’s actually installed on a subfolder.

A request will come through to https://domain.com/blog/ and then the reverse proxy will grab the content available at https://blog.domain.com, and then load it as if it actually exists at https://domain.com/blog/.

It allows you to bypass the biggest tech issue of running a blog on a subfolder, which is managing multiple different technologies installed in the same location.

Well, something along those lines anyway.

 

Subdomain to Subfolder with Nginx

Two previous blog moves from subdomain to subfolder that I have helped with, involved nginx reverse proxying.

Nginx is a server technology that does server things, but one thing it does is route traffic. You can give it filters or rules, and tell it to send specific requests one way, or another.

It’s like a little middleman that can move your site’s traffic around.

Using this, you tell it to reverse proxy your subfolder requests, to your subdomain, and have it look like everything loads under the subfolder.

Nginx has mostly been a more enterprise-level setup, so there’s a good chance you might not be using it.

If you have Nginx installed, here is a detailed guide on using it as a reverse proxy.

 

Subdomain to subfolder with Apache/.htaccesss

Similar to Nginx, Apache is another server tech that does similar things.

In particular, their htaccess file allows you to set this sort of thing up.

If you’re on a typical web hosting setup, this is the most likely setup you’ve got going.

To jump into a subfolder migration with the .htaccess, you can find a detailed guide here.

 

Subdomain to subfolder with Cloudflare

Now, this is my new favourite.

Why?

Because I can do it without any tech involvement, and it is independent of any other server tech.

And in under 2 minutes! Pretty sweet.

No messing around, it’s magical.

Many will also run Cloudflare before Nginx / Apache is hit, so it will work across both and be a bit more flexible.

Today, I will show you how you can do it too.

 

How to setup a reverse proxy with Cloudflare

It first started with a guide I found from 403.ie here.

There were a few others floating around, but this was the best one I could find to match the specific requirement of reverse proxying content from a subdomain to a subfolder leveraging Cloudflare.

However, it, unfortunately, didn’t work for me. It was close, but the WordPress side of things kept failing.

It took a few goes to work out whether it was the server (Siteground has some fun caching :/) or whether it was the Cloudflare setup.

Modifying the DB directly, some WordPress config scripts, and many other changes, but every time something else would break.

Missing CSS files, bad redirects, and constant server errors. Every time I patched something, something else would break.

I gave up, and called in some dev support.

A dev named Dat came through, and sorted me out.

 

Steps to setup the reverse proxy

The following instructions will help you get your reverse proxy setup in both Cloudflare, and your WordPress setup.

1. Create the Cloudflare workers
  • Log into your Cloudflare account, but don’t load up any of the sites, and you’ll see the ‘Workers & Pages’ setting option;

  • Jump in here, and click ‘create application’

  • Then find ‘create worker’

  • Name the first one;

sitename-reverse-proxy

Modifying the sitename to be your actual site name. This name can be anything, but including the sitename can help incase you want to do this multiple times, as each worker could be loaded under any domain.

  • Click deploy, and it will load in a default script, which we will replace.
  • Click on ‘edit code’;

  • Delete the default code, and then paste in the first set of code from below, for the ‘reverse proxy worker’.
  • Modify any mention of blog.domain.com or domain.com/blog to be the settings you require.

Be careful not to modify any existing, or add any new, trailing slashes or https mentions as they will break everything.

  • Click on ‘Save and deploy’ in the top right corner, and then ‘Save and deploy’ again on the little popup modal

  • Repeat the above steps for the redirect worker, named “sitename-redirect-worker”, and get that one deployed too.

 

2. Setup the Cloudflare routes
  • Open the website you wish to add the routes for, and then find ‘Workers Routes’

  • Click on ‘add route’

 

  • Create routes for the following two URLs (modifying them to match what you need), by selecting the redirect service worker you created, and ‘production’ environment

*blog.domain.com
*blog.example.com/*

 

  • Create routes for the following two URLs (modifying them to match what you need), by selecting the proxy service worker you created, and ‘production’ environment

*domain.com/blog
*domain.com/blog*

 

  • After both sets of 2 routes have been created, you will see something similar to this on your Workers Routes page;

 

3. Modify the WordPress Site URL

The easiest step of them all.

Load up the WordPress admin area, and jump into Settings > General.

Modify the Site Address, and not the WordPress address, as per the below settings;

 

4. Add a trailing slash redirect for the sub-folder

After all this, we couldn’t get a final issue solved in the end, unfortunately. The blog homepage was available with both the trialing slash, and no trailing slash. Just the homepage. Everything else works beautfulllllly.

  • To get this patched up, we load up Cloudflare and head to the Rules > Redirect Rules

  • Click ‘Create rule’ under the single redirects section

  • Create a rule that uses the non-trailing slash blog URL version as the incoming requests rule, and the same URL but with a trailing slash as the URL to redirect these requests to

5. Implement a redirect strategy

If this is an existing build you’re modifying, make sure you implement a full 301 redirect strategy! It should just be a simple 301 rule that forwards from the sub-domain to a sub-folder, but triple check it all.

There’s no point moving to a sub-folder if you break everything along the way.

 

The scripts

 

Reverse proxy worker

addEventListener('fetch', event => {
// Skip redirects for WordPress preview posts
if (event.request.url.includes('&preview=true')) { return; }

event.respondWith(handleRequest(event.request))
})

class AttributeRewriter {
constructor(rewriteParams) {
this.attributeName = rewriteParams.attributeName
this.old_url = rewriteParams.old_url
this.new_url = rewriteParams.new_url
}

element(element) {
const attribute = element.getAttribute(this.attributeName)
if (attribute && attribute.startsWith(this.old_url)) {
element.setAttribute(
this.attributeName,
attribute.replace(this.old_url, this.new_url),
)
}
}
}

const rules = [
{
from: 'domain.com/blog',
to: 'blog.domain.com'
},
// more rules here
]

const handleRequest = async req => {
// Redirect WordPress login to the subdomain
let baseUrl = req.url;
if (baseUrl.includes('domain.com/blog/wp-login.php')) {
return new Response('', { status: 302, headers: { 'Location': baseUrl.replace('domain.com/blog', 'blog.domain.com') } });
}

const url = new URL(req.url);

let fullurl = url.host + url.pathname;
var newurl = req.url;
var active_rule = { from: '', to: '' }
rules.map(rule => {
if (fullurl.startsWith(rule.from)) {
let url = req.url;
newurl = url.replace(rule.from, rule.to);
active_rule = rule;
console.log(rule);
}
})

const newRequest = new Request(newurl, new Request(req));
const res = await fetch(newRequest);

const rewriter = new HTMLRewriter()
.on('a', new AttributeRewriter({ attributeName: 'href', old_url: active_rule.from, new_url: active_rule.to }))
.on('img', new AttributeRewriter({ attributeName: 'src', old_url: active_rule.from, new_url: active_rule.to }))
.on('link', new AttributeRewriter({ attributeName: 'href', old_url: active_rule.from, new_url: active_rule.to }))
.on('script', new AttributeRewriter({ attributeName: 'src', old_url: active_rule.from, new_url: active_rule.to }))
// .on('*', new AttributeRewriter({ attributeName: 'anytext', old_url: active_rule.from, new_url: active_rule.to }))

if (newurl.indexOf('.js') !== -1 || newurl.indexOf('.xml') !== -1) {
return res;
} else {
return rewriter.transform(res);
}
}

 

 

Redirect worker

const base = "https://domain.com/blog"
const statusCode = 301

async function handleRequest(request) {
const excludedPaths = ['/wp-login.php', '/wp-admin', '/wp-admin/']
const url = new URL(request.url)
const { pathname, search, hash } = url
const destinationURL = base + pathname + search + hash

if (excludedPaths.some(path => pathname.startsWith(path))) {
return fetch(request)
} else {
return Response.redirect(destinationURL, statusCode)
}
}

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

 

Your subdomain to subfolder setup should be live

Following the steps above, your blog should now be publicly accessible under the sub-folder, and the admin panel will be accessible under the original sub-domain.

You should be able to directly load the new subfolder, and it’ll work as if you’re accessing the subdomain where it is actually installed.

I’d love to hear how it goes if you went ahead with the change! Both whether the instruction above completely worked for you, and how performance was if it was an existing build you modified.

About The Author

15 thoughts on “Subdomain to Subfolder: The Simple Cloudflare Reverse Proxy”

  1. Hi, Sam

    First of all, thank you so much for sharing this amazing method.
    I’ve been searching for this method for a long time, and I think I finally found the perfect guide.

    But unfortunately, I am getting a 404 error. I have tried several methods to solve the problem, but nothing worked, so I would like to ask a little more.

    I’m using Nginx, and on my real server, the website has the path below, with each server block. and origin website not proxied from Cloudflare.

    1) Does the origin site (domain.com in the article) also need to proxied from Cloudflare?

    2) Do I actually need to have the WordPress files for the subdomain in a subfolder, like this?
    ex) /var/www/example.com/blog.example.com

    3) Is it correct that in cloudflare, the DNS of the subdomain is pointed to A? Should I specify the origin site as the CNAME?

    4) do i need to add something to the Server block like Location~?

    I look forward to your advice, thanks.

  2. Hi, I asked about the 404 error last time, but i’ve resolved that issue.

    Now the only issue left for me is CORS – Redirect error on the admin page of example.com/blog.

    In the console, I see the following error

    Access to fetch at ‘https://blog.example.com/index.php?rest_route=%2Fwp%2Fv2%2Ftaxonomies%2Fpost_tag&context=edit&_locale=user’ from origin ‘https://example.com’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: Redirect is not allowed for a preflight request.

    I can’t solve this problem no matter how hard I try.

    Could you please advise if there is a workaround for this issue? Thank you very much.

    1. Hey there!

      Glad you resolved the 404 issues.

      For the admin, you access the admin under the subdomain where wp is installed, and sounds like you’re accessing under the subfolfer. The instructions and code ignore the admin section under the subdomain, so try access it at blog.domain.com/wp-admin/.

      It should be the only part that doesn’t redirect you.

      Cheers,
      Sam.

  3. Hello there, great post!
    Any idea why Yoast can’t scan the content?
    I got the next error.
    Thank you in advance.-

    Oops, something has gone wrong and we couldn’t complete the optimization of your SEO data. Please click the button again to re-start the process.

    Below are the technical details for the error. See this page for a more detailed explanation.

    Error details
    Request URL
    https://domain.tld/blog/wp-json/yoast/v1/indexing/prepare

    Request method
    POST

    Status code
    403

    Error message
    Cookie check failed

  4. Thanks Sammy for the excelent guide.
    I have a question, seems like wordpress have made some changes recently and step 3 became the most complex one.
    Can you explain how does that impact the overall flow, why do you need to modify the Site Address, and not the WordPress address?
    Can you provide an workaround?

  5. My solution is a bit more elegant and replaces all occurrences of the original site from the destination source. Also it replaces the tag for robots to allow indexing. It allows original site to stay not indexed by robots.

    “`
    addEventListener(‘fetch’, event => {
    // Skip redirects for WordPress preview posts
    if (event.request.url.includes(‘&preview=true’)) { return; }

    event.respondWith(handleRequest(event.request))
    })

    const rules = {
    src: ‘https://domain.com/blog’,
    dst: ‘https://blog.domain.com’
    }

    const replacers = [
    {
    from: ‘blog.domain.com/wp-content/’,
    to: ‘domain.com/blog/wp-content/’
    },
    {
    from: ‘blog.domain.com’,
    to: ‘domain.com/blog’
    },
    {
    from: /^/wp-content//gm,
    to: ‘domain.com/blog/wp-content/’
    }
    ]

    class AttributeRewriter {
    constructor(attributeName, from, to) {
    this.attributeName = attributeName;
    this.fullText = ”;
    this.replacers = from && to ? [{ from: from, to: to }] : replacers
    }

    element(element) {

    if (‘content-replace’ === this.attributeName) {
    return;
    }

    let attribute = element.getAttribute(this.attributeName)

    if (!attribute) {
    return
    }

    for (const replacer of this.replacers) {
    attribute = attribute.replaceAll(replacer.from, replacer.to)
    }

    element.setAttribute(
    this.attributeName,
    attribute,
    )
    }

    text(textChunk) {

    if (‘content-replace’ !== this.attributeName) {
    return;
    }

    // Accumulate the text chunks.
    this.fullText += textChunk.text;

    if (textChunk.lastInTextNode) {
    // Last chunk, perform the replacement.

    for (const replacer of replacers) {
    this.fullText = this.fullText.replaceAll(replacer.from, replacer.to);
    }

    textChunk.replace(this.fullText, { html: true });
    this.fullText = ”; // Reset accumulated text.
    }
    else {
    // Not the last chunk, remove the current chunk as we are accumulating it.
    textChunk.remove();
    }
    }
    }

    const handleRequest = async req => {
    const baseURL = req.url;

    // Redirect WordPress login to the subdomain
    if (baseURL.includes(`${rules.src}/wp-login.php`) || baseURL.includes(`${rules.src}/wp-admin`)) {
    return new Response(
    ”,
    {
    status: 302,
    headers: { ‘Location’: baseURL.replace(rules.src, rules.dst) }
    });
    }

    const newURL = baseURL.replace(rules.src, rules.dst);
    const newRequest = new Request(newURL, new Request(req));
    const proxyRes = await fetch(newRequest);

    const rewriter = new HTMLRewriter()
    .on(‘a’, new AttributeRewriter(‘href’))
    .on(‘img’, new AttributeRewriter(‘src’))
    .on(‘img’, new AttributeRewriter(‘srcset’))
    .on(‘link’, new AttributeRewriter(‘href’))
    .on(‘script’, new AttributeRewriter(‘src’))
    .on(‘div’, new AttributeRewriter(‘style’))
    .on(‘style’, new AttributeRewriter(‘content-replace’))
    .on(‘script’, new AttributeRewriter(‘content-replace’))
    .on(‘meta’, new AttributeRewriter(‘content’))
    .on(‘meta’, new AttributeRewriter(‘content’, ‘noindex, nofollow’, ‘index, follow’))

    if (newURL.indexOf(‘.js’) !== -1 || newURL.indexOf(‘.xml’) !== -1) {
    return proxyRes;
    }
    else {
    return rewriter.transform(proxyRes);
    }
    }
    “`

  6. I see you mentioned Siteground and their caching, I tried to implement this today, but the WP REST API was not working anymore, so SiteGround told me I had to change the Site Address (URL). That, of course, lead to multiple issues. Any ideas?

    1. Hey Andrei,

      Indeed, I am using siteground for one of the sites for this.

      That caching is a major issue when enabling / disabling, and understand your issues.

      I actually completely deleted, and reset it up a couple times, and that fixdd it sometimes. However, I have also updated the script I am using and haven’t shared the latest – something I can update shortly.

      If you can describe your issues, I can see if they’ll be patched in the update.

      Thanks,
      Sam.

      1. Well, I have the website set up at test.example.com, did all the process for the reverse proxy and redirect worker implementation in order to be accessible at example.com/test.

        Right now if I set the Site Address (URL) to https://example.com/test the WP REST API will not work. I’ve been in discussion yesterday with the SiteGround support and they told me that the WordPress Address and Site Address must be the same if I want the WP REST API to work.

        As soon as I set my Site Address to https://test.example.com, my links become something like this:
        https://example.com/test/?_gl=1%2A28hwzf%2A_ga%2ANjY1MzAyMjQxLjE3MDUzOTA2NDc.%2A_ga_8FBML5F6B8%2AMTcwNTM5MDY0Ni4xLjEuMTcwNTM5MDY2OC4zOC4wLjA.&_ga=2.9104565.1887216743.1705390647-665302241.1705390647

  7. Hello there, great post
    I have 502 bad gateway error every time, I tried this method and other methods as well, But I dont know where the problem is,

    my blog.example.com is based on other server and Cname records, Is this the problem ?
    subdomain is serving a node js app and base domain is wordpress

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top