The goal of this post is to show you how you can create your own static personal website for under $1 a month. I know, you're probably thinking: "Did I just read that right?". You did!
Of course, there are caveats to reaching that target, but at the beginning of your website's journey, it really should only cost $0.01 a month to host your static website. The target audience for this post is anyone with some JavaScript experience and a general understanding of web development. If you don't have that background, don't sweat it! I'll do my best to explain the ideas and concepts within this post so that anyone can follow!
Before we begin, let's get some definitions out of the way to help us understand the different terms we'll be using to create our first site. Feel free to refer to these definitions as we go through the process of adding our site to Google Cloud:
Website Domain: an address used for identifying a website on the internet, like yelp.com
Domain Registration Provider: a provider of domains, where you can buy a custom domain
Static Website: a website created with a fixed number of any combination of HTML, CSS, and JavaScript files
Next.js: a React.js framework that supports server side rendering and static site generation
Storage Object: an entity used for storage, think photos or videos or really any other kind of file
Google Cloud: a cloud computing platform offering a variety of services, we're concerned with storage
Google Cloud Storage: Google Cloud's object storage service, we'll use this for storing our website's static files
Bucket (Google Cloud Storage): a container that holds your website's files
This article assumes that the reader has some foundational knowledge of website development and programming. I will assume that each reader has the following:
A baseline understanding of website development
Comfortable with a CLI
An IDE installed on your computer - VSCode is what I use for example
I tend to think better by breaking up a goal into incremental chunks of work.
Let's frame our tasks that way as we create our website:
If you would like to create your website you'll need a domain to get started. Although we won't be using it initially, it's worth doing this now so we can use the custom domain in a later article. For me, my domain was afro-cloud.com, but you really can pick whatever comes to mind as long as a registration provider has the domain available. There are several domain registration providers that you can use, I used GoDaddy, but here is a subset of options available (I am not affiliated with any of these services):
GoDaddy
Bluehost
Squarespace
Now that we've set up our domain, let's create a Google Cloud account and enable Billing.
Great! Now with billing enabled, we can start to use Google Cloud services. We're all set with this tab for now, but we will come back to this page later in the article so keep it handy.
If you're having trouble following the steps that are to come in this article please get in touch and I'll do my best to help debug the issue. Ok, with that straightened out let's continue. We will be using Next.js as opposed to other React.js frameworks (or just React) because of their static export support. There are several different deployment options out there for hosting a React.js application but I've chosen to share the Google Cloud Storage and Next.js approach because of the SEO benefits and cost savings. With Next.js static exports, when a production build is made, an HTML file is created per route, along with the static assets (CSS & JS files) that correspond to the HTML file in distinct chunks. This is important because it can avoid loading unnecessary JavaScript bundles for the page being viewed which means faster page loads. All of the generated files from running "next build" will be exported into the "out" folder. But more on this later. Let's create the app.
First off, let's get a starter Next.js project installed on our machine. Luckily, Next.js has a "create-next-app" utility just like Create React App for those who have used that. To initiate the workflow we can run the following command:
npx create-next-app@latest
The command will walk us through some configuration options for our project (feel free to pick whatever you please; we just need the code generation).
I've bolded the options that I will use for this article:
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)?No / Yes
Nice, we have some code now! You'll notice that our node_modules folder is populated so we have all of the dependencies installed that we need to run our application. In that same terminal window that you used to create the project, let's run the following command: npm run dev. This will start the Next.js development workflow so that we can view our application locally. Next.js will provide a URL to view the project on your computer. In most cases this will be http://localhost:3000, but if you have another web app running it may select another port like 3001.
Upon clicking the link you should see something like the this:
Nice! We have a starter application up and running! But let's get rid of the starter page to do the more classical "Hello World" example. Update the src/app/page.tsx (or page.jsx) file to contain the following:
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<div className={styles.description}>
<p>Hello world!</p>
</div>
</main>
);
}
Save the file and head back to the browser tab running our application and reload the page. You should see "Hello world" on your screen! Now let's circle back to getting static exports to work. We will need to configure our next.config
file to enable the feature. Update the nextConfig
declaration to be:
const nextConfig = { output: "export", };
This will instruct Next.js during the build process to create individual HTML files corresponding to each route within our application. We only have one page for the time being so let's add another page to illustrate the benefits of static exports. Within the app/ directory, create a new directory called "test".
Within the newly created directory add a page.tsx (or page.jsx) file and add the following code:
export default function Test() {
return (
<main>
<p>Hello test!</p>
</main>
);
}
Now that we have a test page, let's add a link to it from our home page. Open up src/app/page.tsx (or page.jsx)
and update the file so it looks like:
import Link from "next/link";
import styles from "./page.module.css";
export default function Home() {
return (
<main className={styles.main}>
<div className={styles.description}>
<p>Hello world!</p>
<Link href="/test">Test Page!</Link>
</div>
</main>
);
}
Save the newly updated files and head back to the browser tab running our application and reload the page. You should see a new link with "Test Page!" and upon clicking on that link, the content on the screen should change to show "Hello test!". Nice work, now we have two routes in our application.
Now let's checkout the output of the static export feature we've been talking about. Going back to your terminal window run "npm run build". This command will run the "next build" command which will create a production-ready build for our work and store the result in a directory called out at the root of our project.
If we inspect the out directory we should see something like this:
Great job folks. Now let's shift gears to upload our code to Google Cloud so we can see the site live.
Now that we have our code ready, we need to upload it to Google Cloud Storage so that Google Cloud can serve our site on the Internet. Let's go back to the Google Cloud tab.
Open up the hamburger menu on the left-hand side of the screen and select "Cloud Storage". At the top of the screen, you should see a "CREATE" button. We'll click on that which will start the creation workflow. Since we won't be using a custom domain yet, name the bucket whatever you would like but be aware of the uniqueness restriction. I'll be using "somerandombucket123".
Next, we'll use the multi-region option within the US (that's where I'm writing this post) but feel free to adjust for your use case. We'll then select the standard default class option which should be pre-populated for you. The next option has to do with whether or not we want our bucket to be publicly accessible via the internet. In this case, since we want to serve these files to our viewers we will want to uncheck the "Enforce public access prevention on this bucket" so that all the files are accessible via the internet. We'll select "Uniform" access control and not select the "Data protection" offerings to keep costs low. Then we will hit the "Create" button.
Now that we have created the bucket we'll need to add new permission so that users can view our bucket files. Select the "Permissions" tab and click on the "Grant Access" button. Within the "New principals" input type in "allUsers" and select the role under "Cloud Storage" for "Environment and Storage Object Viewer".
A dialog will open up asking whether or not we want to make our bucket public, which we do, so select "Allow Public Access".so select "Allow Public Access". Now this bucket's files will be publicly accessible. Head back to the overview page by clicking the back arrow from your bucket detail page. You should see your newly created bucket with the configuration options we have used.
Next, we will need to instruct the bucket of our website and we can do this by clicking on the three dots of the newly created bucket's row. Select "Edit website configuration" and you should see something like this:
For the index page input, type in "index.html" and for the error page input type in "404.html". You'll notice that these files match that of the build output of our Next.js application which is what we want and will be using shortly. Finish up the change by clicking save.
Now we need to upload our files contained within the out directory of our code into this bucket so our website can be live! We can do this manually by navigating to the bucket details page for our bucket and selecting each file or folder individually. But, let's be programmatic about it and write some code to do this. Go back to your IDE or wherever you're making updates to your code and let's create a new file called upload.sh
at the root of our project. Add the following contents:
#!/bin/bash
ADD YOUR BUCKETNAME HERE (no quotes necessary)
For example: BUCKETNAME=somerandombucket123
echo "Removing out folder..."
sleep 1;
rm -rf out
echo "Creating production build..."
sleep 1;
npm run build
echo "Uploading assets to the cloud..."
sleep 1;
gsutil cp out/404.html gs://$BUCKETNAME
gsutil cp out/favicon.ico gs://$BUCKETNAME
gsutil cp out/index.html gs://$BUCKETNAME
gsutil cp out/index.txt gs://$BUCKETNAME
gsutil cp out/test.html gs://$BUCKETNAME
gsutil cp out/test.txt gs://$BUCKETNAME
gsutil cp -r out/_next gs://$BUCKETNAME
Be sure to replace "somerandombucket123" with your bucket name. Don't worry about the semantics of the code too much here.
Essentially what we are doing is:
Before we can run this script, we'll have to download the Google Cloud CLI. You can follow the instructions here. After installation, you will need to run: gcloud auth login in your terminal. This will authorize access for us to use the Google Cloud CLI. More instructions and background on that can be found in their documentation. Once you have successfully authorized, let's add a new run script to our package.json.
Add a new script entry within the "scripts" object to register our upload script:
"upload": "sh upload.sh"
Next, let's test it out. Open up your terminal again and run: "npm run upload". This will execute our script and you should see some output about the uploads happening to your bucket. If we navigate back the Google Cloud Storage page and open up your bucket you should see the files we just uploaded.
If you navigate over to: https://storage.googleapis.com/YOUR_BUCKET_NAME/index.html (where YOUR_BUCKET_NAME is the name of your bucket) you should see the site. But you'll notice that the default Next.js styling is gone and the link to our test page is broken.
Any ideas why?
If you open up your browser console you should be seeing a lot of resource not found errors. In other words, the browser can't find the files that it was instructed to load for your site. If you look closely, you can see that the URL for the resource isn't quite right, it's missing our bucket name in the path. If we had used a custom domain and configured DNS properly, we wouldn't run into this issue. But for this post, let's add some additional code to fix the routing.
Open up src/app/page.tsx (or page.jsx)
and update the file so it looks like:
import Link from "next/link";
import styles from "./page.module.css";
// Replace below with your bucketname.
const BUCKET_NAME = "somerandombucket123";
const isProd = process.env.NODE_ENV === "production";
export default function Home() {
return (
<main className={styles.main}>
<div className={styles.description}>
<p>Hello world!</p>
<Link href={isProd ? '/BUCKET_NAME/test.html' : "/test"}>
Test Page!
</Link>
</div>
</main>
);
}
Be sure to replace "somerandombucket123" with your bucket name. Next update the next.config
file to look like:
// Replace below with your bucketname.
const BUCKET_NAME = "somerandombucket123";
const isProd = process.env.NODE_ENV === "production";
/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: isProd
? 'https://storage.googleapis.com/BUCKET_NAME/'
: undefined,
output: "export",
};
export default nextConfig;
Again, be sure to replace "somerandombucket123" with your bucket name. You'll notice in the above code snippet that we've added additional logic to account for our bucket name when the node environment variable is production (set by Next.js). We're adding an asset prefix to fix the resource not found errors in the config file and accounting for the routing error in our home page by prefixing the route with our bucket name. Let's upload our code now and see if it works.
Once again, kick-off: npm run upload. Jump back to your website and reload the page. How did we do? The site should reflect what we have locally now. At the beginning of the process if we were to create the bucket to match our domain name we would have the resource errors but still would have the client routing issue. So unfortunately, one drawback to this approach is the additional code needed to add the .html suffix to routes for production serving.
At a later date, I'll cover adding the DNS records and configuration changes needed to our bucket to serve a custom domain, as well as configuring SSL for our website. Hopefully, you've learned something today and in the future, I'll talk through some ideas around:
Thanks for reading and cheers!
Also published here.