
Img.vision
Image & video hosting that is reliable and uncomplicated
We've been accepted in the AWS Activate Founders program! π
Here's an excerpt from the email we got:
Your application for the AWS Activate Founders package has been approved. We've added $1,000 in AWS Activate Credits and $350 in AWS Developer Support Credits to your AWS account.
That's 1000 dollars we can spend on AWS infrastructure.
Since we already run most of our infrastructure on AWS, it's a welcome gift π
Awesome! Thanks AWS! ππ
Following the trend of my post on May 1st, I've setup the Img.vision help center on the main domain.
The reason for this is SEO. Having a dedicated Help Center sitelink when searching for Img.vision sounds like something useful for customers.
The setup uses a custom Ghost CMS help center theme, Digital Ocean droplet and Cloudflare Workers as reverse proxy.
3 Likes
Comment
I've finally succeeded in moving our blog to the main domain, which is better for SEO.
We used to have cloud hosting of our Img.vision blog the "Img Handbook".
The last day of 2019 I tried to move the subdomain to the main domain for the SEO benefits it has. I failed back then.
But now I succeeded! πͺ
We are using Cloudflare Workers as a Reverse Proxy & a Digital Ocean droplet of just $5.
Here's the detailed journey:
- Setup DigitalOcean droplet with self-hosted Ghost CMS & Nginx
- Migration from Ghost (Pro) cloud hosting to self hosted droplet
- Configured Cloudflare Workers as Reverse Proxy to the droplet
- Setup updown.io uptime monitoring on droplet
- Restricted direct access to droplet to Cloudflare, updown & me
- Setup 301 redirects (had to remove DNS to Ghost Pro first)
- Initiated site move in Google Search Console
www.img.vision/handbook/
6 Likes
10 Comments
10 Comments
-
1
For anyone reaching this post - I've now also set up Ghost like this, and as a celebration of doing so, wrote a full guide: https://cloak.ist/blog/how-to-put-a-ghost-blog-at-a-subdirectory-using-cloudflare-workers/
-
1
Hi Mathias, I'm really struggling to do this. Do you have any more tips?
So far:
- Set up DigitalOcean 1-Click Ghost Droplet
- Didn't set it up at my URL, just set it up at the IP address for now
- Set up Cloudflare worker (using the script you linked to) to point /blog* on my website to the IP of the droplet
- Get a Cloudflare 1003 error
Any more tips greatly appreciated! Really want to get this working.
-
2
OK @mathias458, actually I finally figured out how to do this. Really interested in this step of yours though:
Restricted direct access to droplet to Cloudflare, updown and me
How did you do that?
Update: I did this using a Cloudflare Firewall rule with URI Full contains 'ghost.cloak.ist'. So I think I've finally got my dream blog set up.
-
1
Hey, nice to see someone try out the steps I did.
Check out this article regarding limiting access: https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-18-04
-
1
Hey @Mathias458, @louisbarclay
I followed both of your guides. My main website (polymersearch.com) is pointed at some page builder. I want to have polymersearch.com/blog serving the ghost blog, hosted on DO.
I've set-up the blog at blog.polymersearch.com/blog and CloudFlare workers as reverse proxy.
However I'm still not being able to access the ghost blog on polymersearch.com/blog
-
1
Hi @ribice
Here are the steps:
-
Setup ghost blog on alternativeDomain.com or alternativeSubdomain.domain.com
-
Add Cloudflare Worker script (provided below)
-
Change your ghost installation url using cli to where you want to show it, example:
ghost config url https://preferredSite.com/blog/Then restart ghost
- Go to Cloudflare, open your domain, go to Workers tab, and add a route, example:
https://preferredSite.com/blog/* -> to your cloudflare worker script
Make sure the domain preferredSite.com is "orange" on the DNS tab, so proxied by Cloudflare, so that Cloudflare can manipulate the traffic.
- You can now access your ghost at:
- Public url: https://preferredSite.com/blog/
- Ghost admin panel: https://alternativeDomain.com/blog/ghost/ or https://alternativeSubdomain.domain.com/blog/ghost/
Here is the Edge worker script, replace "alternativeDomain.com" & "preferredSite.com":
// Website you intended to retrieve for users. const upstream = 'alternativeDomain.com' // Custom pathname for the upstream website. const upstream_path = '/' // Website you intended to retrieve for users using mobile devices. const upstream_mobile = 'alternativeDomain.com' // Countries and regions where you wish to suspend your service. const blocked_region = [] // IP addresses which you wish to block from using your service. const blocked_ip_address = [] // Whether to use HTTPS protocol for upstream address. const https = true // Whether to disable cache. const disable_cache = false // Replace texts. const replace_dict = { '$upstream': '$custom_domain', '//alternativeDomain.com': '' } addEventListener('fetch', event => { event.respondWith(fetchAndApply(event.request)); }) async function fetchAndApply(request) { const region = request.headers.get('cf-ipcountry').toUpperCase(); const ip_address = request.headers.get('cf-connecting-ip'); const user_agent = request.headers.get('user-agent'); let response = null; let url = new URL(request.url); let url_hostname = url.hostname; if (https == true) { url.protocol = 'https:'; } else { url.protocol = 'http:'; } if (await device_status(user_agent)) { var upstream_domain = upstream; } else { var upstream_domain = upstream_mobile; } url.host = upstream_domain; if (url.pathname == '/') { url.pathname = upstream_path; } else { url.pathname = upstream_path + url.pathname; } if (blocked_region.includes(region)) { response = new Response('Access denied: WorkersProxy is not available in your region yet.', { status: 403 }); } else if (blocked_ip_address.includes(ip_address)) { response = new Response('Access denied: Your IP address is blocked by WorkersProxy.', { status: 403 }); } else { let method = request.method; let request_headers = request.headers; let new_request_headers = new Headers(request_headers); new_request_headers.set('Host', upstream_domain); new_request_headers.set('Referer', url.protocol + '//' + url_hostname); let original_response = await fetch(url.href, { method: method, headers: new_request_headers }) let original_response_clone = original_response.clone(); let original_text = null; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; if (disable_cache) { new_response_headers.set('Cache-Control', 'no-store'); } new_response_headers.set('Access-Control-Allow-Origin', 'https://*.preferredSite.com'); new_response_headers.set('Vary', 'Origin'); new_response_headers.set('Host', 'alternativeDomain.com'); new_response_headers.set('X-Ghost-Host', 'alternativeDomain.com'); new_response_headers.set('X-Forwarded-host', 'preferredSite.com'); new_response_headers.set('X-Forwarded-Proto', 'https'); new_response_headers.set('X-Forwarded-For', ip_address); new_response_headers.delete('X-Frame-Options'); if(new_response_headers.get("x-pjax-url")) { new_response_headers.set("x-pjax-url", response_headers.get("x-pjax-url").replace("//" + upstream_domain, "//" + url_hostname)); } const content_type = new_response_headers.get('content-type'); if (content_type != null && content_type.includes('text/html') && content_type.includes('UTF-8')) { original_text = await replace_response_text(original_response_clone, upstream_domain, url_hostname); } else { original_text = original_response_clone.body } response = new Response(original_text, { status, headers: new_response_headers }) } return response; } async function replace_response_text(response, upstream_domain, host_name) { let text = await response.text() var i, j; for (i in replace_dict) { j = replace_dict[i] if (i == '$upstream') { i = upstream_domain } else if (i == '$custom_domain') { i = host_name } if (j == '$upstream') { j = upstream_domain } else if (j == '$custom_domain') { j = host_name } let re = new RegExp(i, 'g') text = text.replace(re, j); } return text; } async function device_status(user_agent_info) { var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; var flag = true; for (var v = 0; v < agents.length; v++) { if (user_agent_info.indexOf(agents[v]) > 0) { flag = false; break; } } return flag; }----
Disclaimer: Using this config your ghost is proxied to a path, but I didn't succeed in getting the members functionality to work. The Send Magic Link fails with 400 Bad Request. This means no visitors can subscribe or login, admin panel is not affected.
I tested thoroughly and found this reverse proxy is the reason Send Magic Link doesn't work, it works fine before/while not reverse proxying.
If someone can solve this, let me know.
-
-
1
Hey @ribice, sorry about that. Feel free to email me (find email on my profile) and I'll happily help you out. Quite confused about why this would be the case - is the route for your Cloudflare worker definitely correct? Also - is there any Page Rule that could be overriding the worker route? Let's discuss over email!
-
1
Hey @louisbarclay I provided exact steps I use above.
Did you manage to solve members signup/login?
No email sent trying to sign up as a visitor.
-
-
-
-
-
1
Those that are interested in doing a Reverse Proxy with Cloudflare Workers, check out this open source code: https://github.com/Berkeley-Reject/Workers-Proxy
-
1
I see the url is gone now, I provided script above.
-
In August I recruited and assembled a team to build version 2 of Img.vision. We worked on it during our spare time in weekends and evenings.
Finally, the basic functionality is ready.
It's a major improvement over version 1 regarding architecture and performance.
Version 1 was built in MeteorJS and didn't follow best coding practices..eg. web services were synchronous leading to timeouts on bulk actions.
Version 2 was built from scratch using Java and ReactJS. We rely heavily on AWS. Most proud of the image/folder grid virtualization we implemented, this allows us to offer much larger image plans.
Thanks fly out to the team! πͺ π
1 Like
Comment
I wrote a team bible with our key values to motivate and guide us while building Img.vision.
I bought swag branded with Img.vision for the team and myself:
Caps & t-shirts π& π§’
And asked to team to make an inspiring photo.
The new Img.vision about page is live at https://www.img.vision/about/
2 Likes
Comment
Besides having a blog about Img.vision product updates, I wanted to focus time on helping my customers succeed at Images.
The better the images, the more value keeping those images online brings.
Started in January 2020, but destined to grow, the Img Handbook - a handbook about all things images and image hosting.
1 Like
Comment
Ghost Pro is the managed Ghost CMS for blogs that takes cares of maintenance and updates. The limitation is that Ghost is hosted on a separate subdomain, which means it doesn't share any domain authority with your main site.
So we tried to move it to a subfolder and stay on Ghost Pro... And failed.
Here's what we did:
We found this Cloudflare article describing how it could be done using Cloudflare Edge Workers:
https://blog.cloudflare.com/subdomains-vs-subdirectories-improved-seo-part-2/
We're using Cloudflare, so we thought we were all set!
Problem 1: The script provided doesn't work out of the box.
So we altered it to make it work.
Problem 2: CSS and images wouldn't load and internal links were wrong.
So we uploaded a new Ghost yaml file with new routes.
More info here: https://forum.ghost.org/t/clarifying-custom-home-page-routing/2613/2
And updated our Edge worker to detect the new prefixes in the URLs and rewrite them.
Images and urls worked.
Everything seemed fine.
So we tried to use Cloudflare to redirect our .blog subdomain to /blog/ subfolder. And that didn't work.
Cloudflare detects if you are adding a Ghost Pro CNAME and locks you out of the controls for this subdomain. The Ghost team has an Enterprise plan with Cloudflare and part of the deal is that Ghost manages your Cloudflare settings.
We tried using the standard Page rules, they were ignored, we tried using Edge Workers, but that didn't work either.
Well to be fair, that's not the only problem we encountered:
- Our internal Ghost search results was throwing up links to our .blog subdomain and not /blog/ folder.
- The meta tag rel="canonical" which Ghost adds to every page in the <head> of every page still linked to the .blog subdomain and not /blog/
- All links of the auto-generated sitemap.xml link to .blog subdomain and not /blog/
Lessons learned and hopefully this is a warning to people trying the same thing.
Happy new year guys! π
3 Likes
3 Comments
3 Comments
-
1
This is insaaaaaane. I am looking to start a simple blog for Cloakist and want it at /blog instead of blog. for SEO reasons. Your article has served as a great warning. What did you end up doing instead?
-
1
Solved this 4 months later with a Cloudflare Edge worker Reverse Proxy to self-hosted Ghost CMS on OceanDigital droplet. More info here: https://www.indiehackers.com/product/img-vision/ghost-blog-is-now-hosted-on-main-domain--M6Gt3CSAp3nIFeZKOC-
-
1
Awesome - thanks a lot for letting me know. I can see myself doing this in the future.
-
-
The new Img.vision blog is online at https://blog.img.vision/
I asked here on IH (https://www.indiehackers.com/post/where-are-you-hosting-your-startups-blog-4b4d0e79b0) what people use to blog for their startup got various answers, including a few mentions of Ghost.
I tried Ghost loved it right away.
Now I'm committed to posting more frequently πͺ
1 Like
Comment
About
Solve the issue of reliable image & video hosting online for small businesses.



1 Comment