Tested end to end · Free forever
Put your site online
on a domain you own.
No server, no hosting bill, no build step. You write a folder of files, GitHub serves it free, and your own domain points at it. This guide is the complete process — every click screenshotted, every mistake that cost us hours written down so you don’t repeat them.
$0 · ~25 minutes · no code knowledge · nothing to install
What you’re building
Read firstThe finished setup has four moving parts. Each one is handled in a step below.
| part | what it does | step |
|---|---|---|
| Your files | HTML, CSS, favicon sitting in a folder | 01 |
| GitHub repo | Holds the files and their history | 02 |
| GitHub Pages | Serves those files at a free .github.io address | 03 |
| Your domain | Points at GitHub so the name is yours | 04–05 |
Three files, three consumers
Before you touch anything, it helps to know who reads each piece. Different software looks at a different one, and that is why a setup can look complete and still serve nothing.
| part | who reads it | what it holds |
|---|---|---|
| DNS zone record | ordinary DNS lookups, and the Noto nameserver | one IP address |
| On-chain record | web3 browsers and resolvers | the same IP, plus the redirect |
CNAME file | GitHub | your domain, so GitHub answers for it |
A web3 domain keeps records in two places: a normal DNS zone, and the blockchain. Freename keeps them in step, and when the zone row is tied to an on-chain slot, the resolver reads the chain value rather than the one you typed.
This is the single most common reason a site “works for me but not for anyone”. You add a record in the DNS table, it displays perfectly — and if that row is chained, the value being served is the one on-chain instead. You need both layers set, and they must agree.
What you need
- A GitHub account (free)
- A domain whose DNS you can edit — this guide uses a web3 domain, and section 05 covers what makes those different
- About twenty-five minutes
Because there’s nothing to break. No framework to update, no dependency that disappears, no runtime to patch. Your site is a folder that still opens in ten years — and if you ever want to leave, you drag the folder somewhere else.
Write the files
5 minMake a folder called mysite and put these inside. This very page is built exactly this way.
mysite/
├── index.html the page itself (required)
├── favicon.svg the little tab icon
├── .nojekyll empty file - see below
├── images/ optional, only if your site uses one
└── CNAME added in step 04, your domain only
Every one of these sits at the top level of the folder. That is not a
style preference — put index.html inside a subfolder and the site 404s,
and you’ll see it happen in step 06.
index.html
GitHub Pages serves index.html at the root automatically. This one keeps everything inline — no separate stylesheet, no JavaScript, nothing to break.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My site</title>
<style>
body { font-family: system-ui; margin: 0; }
.wrap { max-width: 900px; margin: auto; padding: 24px; }
</style>
</head>
<body>
<div class="wrap">
<h1>It works.</h1>
<p>Served from GitHub Pages.</p>
</div>
</body>
</html>
The .nojekyll file
Create a completely empty file called .nojekyll, with no
extension and nothing inside it. GitHub runs Pages through a system called Jekyll,
and Jekyll silently skips any folder whose name starts with an underscore. This file
turns Jekyll off so every file and folder you commit gets published exactly as-is.
If you leave it out you’ll lose an afternoon. A file you can plainly see in your repo simply won’t appear on the site, with no error message anywhere. If that ever happens, this file is the first thing to check.
Create the repo and upload
5 minCreate it
Go to github.com/new and fill in the form.
- 1Open
github.com/new. - 2Give the repository a name. Your Pages address will be
username.github.io/this name/, so pick something you can live with in a link. - 3The Description is optional and appears on the repo’s front page. Write whatever you like.
- 4Visibility — choose Public. Pages on a private repository is a paid feature, and this is the setting people most often leave on the wrong value.
- 5Add README — leave it off. This guide has you upload your own
index.htmlbelow, and a README here just adds a file you don’t need. - 6Add .gitignore — leave it as No .gitignore. The template picks a language for you, and for a plain static site there’s nothing to ignore.
- 7Add license — No license is fine unless you intend to publish source others reuse.
- 8Click Create repository.
Pages publishes the repository’s contents — not a build output folder you choose. Anything you commit is published at your domain. If the repo also holds anything you’d rather not serve, keep it in a different repository.
Upload
Open the empty repo, then drag your files in — or click Add file → Upload files and drop them there. Scroll down and Commit changes.
- 1Your repo’s address:
github.com/yourusername/yourreponame. - 2Any images folder, if your site uses one. Entirely optional — a site with no images simply won’t have it.
- 3
.nojekyll. Empty, no extension. Without it, files can silently vanish from the published site — see step 01. - 4
CNAME, added in step 04. Ignore it for now; you can add it later without disturbing anything else. - 5
index.html. This must be the one that gets served.
- Every file is at the top level. If
index.htmlsits inside a subfolder, the site 404s. In this screenshot it is the last row, level with everything else — that is correct. - The name is exactly
index.html. Notmyfile.html, notIndex.html. GitHub only serves the root automatically for that exact lower-case name. .nojekyllis present and empty.- Nothing in the repo that you don’t want published. Whatever is listed above gets served at your domain.
Or use the command line
Faster once you’re past the first commit, and you get proper history:
git init -b main
git add .
git commit -m "Add site"
git remote add origin https://github.com/username/repo.git
git push -u origin main
A URL like https://user:token@github.com/… is written in plain text to
.git/config and into your shell history. If you must, push and then
immediately reset it: git remote set-url origin https://github.com/username/repo.git.
Switch on GitHub Pages
3 minIn your repo open Settings → Pages.
- 1Open
github.com/yourusername/yourrepo/settings/pages. - 2Set Source to Deploy from a branch.
- 3Branch
main, folder/ (root), then Save. - 4Paste your domain into Custom domain and Save. Then ignore the red “DNS check unsuccessful” warning underneath it.
- 5Enforce HTTPS — leave it unchecked. Web3 domains are HTTP-only on Noto DNS, so no certificate can ever be issued for one. GitHub greys this out for exactly that reason.
- 6Your domain appears in the green box: “Your site is live at”. This is the only thing that proves it worked. Before you add a custom domain, this box shows your
github.ioaddress instead.
This trips up almost everyone. GitHub checks whether your domain points at it by querying public DNS — and web3 domains live on an alt-root, so every public resolver answers NXDOMAIN. GitHub concludes the domain is invalid and says so in red. Meanwhile the green box above is serving your site on that very domain.
Treat the green box as the source of truth, not the warnings. If the address in it is your own domain, you are done.
Open username.github.io/<repo>/ and confirm your site renders. If it
does, GitHub is working perfectly and anything that fails later is DNS — not
Pages. Knowing which half is broken saves a lot of time.
Point the domain at GitHub
10 minTwo pieces of setup, and the order matters.
4a — add the CNAME file
This one small file is what connects your domain to your repo. Saving the custom domain in
step 03 usually creates it for you, so open your repo and
look first. If CNAME is there, holding one line — your domain — and
nothing else, you can skip to 4b. If it isn’t, make it by hand: a file named exactly
CNAME containing one line, with no quotes, no slash and no trailing dot.
yourdomain.virtualsociety
Adding it through the browser
- Open your repo on GitHub, click Add file → Create new file
- In the name box type
CNAME— exactly that, all capitals, no extension - Paste your domain into the content box below
- Scroll down and Commit changes
- Wait a minute, reload Settings → Pages — the Custom domain box fills itself in
Adding it from the command line
echo "yourdomain.virtualsociety" > CNAME
git add CNAME
git commit -m "Add custom domain"
git push
It must be CNAME — not cname, not
CNAME.txt, not mycname. GitHub is case-sensitive here and
gives no warning at all. If your domain never appears in the Custom domain box, this
is the first thing to check.
The moment CNAME exists, GitHub redirects
username.github.io/<repo>/ to your domain with a real
HTTP 301, on the server — not with JavaScript. Over the
ordinary web that looks like a broken link, because your browser can’t follow a
name that lives on an alt-root. Inside a web3 resolver it works perfectly.
So keep the file active. Renaming it to CNAME.disabled
brings the plain github.io address back, but it costs you the domain
inside web3 browsers — so treat that as a temporary measure only.
And the other direction matters just as much: without this file GitHub answers
404 to your own domain, even when every DNS record is perfect. GitHub has no way
to know that yourdomain.virtualsociety is asking for
username.github.io/<repo>/. If your domain 404s while your records all
look right, check that CNAME exists, is named exactly that, and sits at the
top level of the repo.
4b — the DNS records
Add these on your domain’s Records tab. Only the first is required:
| type | name | value | TTL | needed? |
|---|---|---|---|---|
| A | @ | 185.199.108.153 | 300 | required |
| CNAME | www | username.github.io | 300 | optional |
GitHub Pages publishes on four anycast addresses, so you will see guides telling you to add all four. We tested it: one is enough. Add the extra three and your site still serves identically. We carry a single A record, and it has been stable.
The www CNAME must be your username’s Pages host
— username.github.io, never
username.github.io/myrepo. The A record is the opposite: it’s
GitHub’s, the same for every Pages site on the internet, and unrelated to your
account.
- 1Open your domain, then the Records tab. The status line under the domain name should read “resolves” — if it doesn’t yet, you’re looking at a name with nothing published, which is normal before step 05.
- 2Wallet Address plus the yellow Sign Transaction button. This button is what actually commits your records to the chain. Everything below is only configuration until you press it.
- 3Forward Domain — the trap. Its own caption on screen says it “will disable your root A record pointing to 185.199.108.153”. Leave it off.
- 4In DNS Records, add a single
Arow on@with value185.199.108.153, TTL300. The twoNSrows are added for you — leave them. - 5Optional: a
CNAMErow onwwwpointing atyourusername.github.io. Your bare domain works without it; this just makeswwwbehave the same way. - 6In Web3 Records, add
record.A.0— lower-caserecord, capitalA— with the same IP,185.199.108.153. This on-chain slot is what the nameserver reads. - 7Also in Web3 Records, add
redirect.WEBSITE.0— lower-caseredirect, capitalWEBSITE— with your Pages URL,https://yourusername.github.io/yourrepo/. Note it points at thegithub.ioaddress, not at your web3 domain.
Switch it on and your domain stops pointing at GitHub altogether — it forwards somewhere else instead, and your site never loads. It’s for domains you want to redirect elsewhere: one for sale, an old brand pointing at a new one. Not for hosting.
Sign the record — web3 only
Critical
Everything below applies only to web3 domains on an alt-root — names ending in
.virtualsociety, .hodl, .nfts,
.metaverse and similar. If your domain is a conventional
.com, section 04 is the end of the line and you’re done.
On a web3 domain your records live on a blockchain, and the nameserver reads them from there. That changes two things, and both surprise people badly.
It looks right but isn’t
The dashboard will happily save a record, display it back to you in a tidy table, and every field will look perfect — while your domain points somewhere else entirely. Saving is not publishing. Until the record reaches the chain, the nameserver has nothing to serve.
Two records, not six
GitHub Pages publishes on four IP addresses, and most guides tell you to fill four on-chain
slots. We tested it. One is enough. Add .0 and leave
.1, .2 and .3 empty — the site keeps serving
perfectly in every browser that reads the record. Fewer records, fewer things to go wrong.
| slot | value | needed? |
|---|---|---|
| record.A.0 | 185.199.108.153 | required — nameserver |
| redirect.WEBSITE.0 | https://username.github.io/<repo>/ | required — web3 browsers |
The value is https://username.github.io/<repo>/ — the
.github.io address, ending with a slash. That is deliberate: of the two
addresses your site has, it is the only one that ordinary DNS can resolve. Your web3 domain
lives on an alt-root and returns NXDOMAIN to anything on the ordinary web,
so a redirect pointing back at itself has nowhere to land.
Copy the address exactly as GitHub shows it, trailing slash included.
These keys are case-sensitive on the chain. record.A.0 and
RECORD.A.0 are two different keys: the second saves, appears in the table,
and is never read. This one detail has taken an entire portfolio offline — everything
looks correct, one browser works, another doesn’t — so type the key exactly as
the table shows it: lower-case record, capital A, lower-case
redirect, capital WEBSITE.
The dashboard will still display it as RECORD.A.0. That is a display label,
not the stored key — it upper-cases whatever you typed, which is exactly why the
mistake is invisible. If a record ever needs recreating, recreate it in the same case it
went in, never in the case the table renders.
Then sign it
Near the top of the same Records tab there’s a Wallet Address box with a Sign Transaction button. That button is what commits your saved records to the chain. Until you press it, you have configuration, not a live domain.
In DNS Records you should see an A row on
185.199.108.153, the two NS rows, and your optional
www CNAME. In Web3 Records you should see exactly
two rows: record.A.0 with the IP, and
redirect.WEBSITE.0 with your Pages URL, both shown in capitals in the table.
That is the whole configuration.
Then press Sign Transaction — and only then does any of it reach
the chain.
Save the records → sign the transaction → wait for the nameserver to pick it up. Nothing in step 04 will appear to work until step 05 is done, and no amount of waiting will fix it. This ordering is the single most common reason a web3 domain that looks correctly configured never loads.
Verify it works
2 minGood news first: you have two ways to open the finished site, and only one of them needs any web3 tooling at all.
The easy check — your web2 link
Every .virtualsociety name is automatically mirrored onto the ordinary web at
yourname.virtualsociety.top. You don’t configure this, don’t
pay for it, and don’t set anything up for it — it exists as soon as your
domain resolves. It opens in any browser, no plugin, no API key, and over HTTPS.
- 1Go to
virtualsociety.top/getweb2link. - 2Type the web3 name you registered on
.virtualsocietyand press Resolve. - 3You get back your mirrored web2 link —
https://yourname.virtualsociety.top/. - 4Your site is previewed right there in the page. If it renders, everything upstream already works.
- 5Click the link to open your site on the web2 address — this is the same action as number 3 above, it just also opens it.
- 6Top right: your web2 link open on its own, in a normal browser, over HTTPS.
- 7Bottom right: the same site on the web3 address,
yourname.virtualsociety. Identical page. This one needs Noto DNS or a web3 browser — the web2 one above doesn’t.
If the preview above renders your site, GitHub Pages is serving correctly and your DNS is resolving — which means any failure from here on is something else entirely. It also answers the question most people actually have: can a normal person, on a normal browser, open my site? Yes.
The authoritative check — what your domain publishes
To confirm the records themselves, use the DNS tool. This reads straight from Freename’s own nameservers, so it shows you the truth rather than a cache.
- 1Go to
virtualsociety.top/tool/dnscheck. - 2Type your web3 name and press Look up.
- 3You get your published records. The
Arow should read185.199.108.153, withNS 1andNS 2showing the Noto nameservers.AAAA,MXandTXTshowing none published is perfectly normal.CNAMEis empty unless you added the optionalwwwrow in step 04 — both states are correct.
The command line
Two commands, because they answer two different questions. The first asks “where does my domain point?” The second asks “is anything actually being served there?” Running both tells you which half is broken.
nslookup yourdomain.virtualsociety ns1.noto.network
You want to see 185.199.108.153.
curl -H "Host: yourdomain.virtualsociety" http://185.199.108.153/
You want to see your own HTML.
A correct DNS answer only proves the name resolves. It says nothing about whether a server is answering behind it. This second command skips DNS entirely and asks GitHub directly, pretending to be your domain — so if the first command looks perfect and the site is still broken, this one tells you the fault is at the serving end, not the DNS end.
Both commands work the same on macOS, Linux and Windows. If
curl isn’t to hand, open the web2 mirror link from the first check
instead — it proves the same thing without a terminal.
Ask any public resolver — Google, Cloudflare — about a web3 domain and it will always answer NXDOMAIN. That is correct, not a fault: the name lives on an alt-root, outside the ordinary DNS tree entirely. A public lookup tells you nothing about whether your setup is right, and can make a perfectly working domain look broken. Always query the alt-root’s own nameserver.
Then read the result
| what you see | what it means |
|---|---|
| Your site | Done. |
| 404 from GitHub | DNS is fine; the repo isn’t serving. Check the filename is exactly index.html. |
| Old host or a 502 | Your domain still points at whatever it pointed at before. The change hasn’t propagated — or was never signed. |
| Nothing at all | Nameservers are wrong, or the record was never committed to the chain. |
A change that has actually been written usually shows up within a few minutes — on this setup two to three was typical. Treat anything under that as propagation and simply wait.
Past a few minutes, waiting stops being the explanation. If the answer is still the old value, the change was never picked up: the record was not signed, or it was written identically to what was already there. Both are fixed by acting, not waiting — and the second one is the stall described in When it goes wrong.
When it goes wrong
Keep thisMy domain is stuck on 34.22.218.54
That is Freename’s parking address. Seeing it means the resolver found no usable record for your name — not that your site is down, but that nothing is pointing at it yet. It will not fix itself.
Work down this list, in order:
- Delete the
Arow and add it again. Always first. This clears a stale internal pointer, which is the usual cause and is completely invisible in the dashboard. - Check every key letter by letter for a typo or a leftover name from an earlier attempt.
- Confirm you pressed Sign Transaction. Unsaved records display perfectly and do nothing.
- Then wait. A minute or two is propagation. Much longer than that is a fault, not a delay.
If it is still parked after all four, the record is stalled — see the next entry, which is the fix that actually works.
My record is correct but nothing has changed
This is the stall, and it is the most confusing failure in the whole process, because
every value on screen is right. You look at the A row, it says
185.199.108.153, the status says resolves — and the site
still serves whatever it served before.
Why it happens: writing the same value again is invisible. The resolver sees nothing that changed, so it keeps serving the answer it already has. Save the record ten more times and it will stay stuck exactly as long as you keep saving the same thing.
The fix — change the value, wait, then change it back:
- Delete the
Arow. - Add it again with a different IP — use another GitHub Pages address,
185.199.109.153. Nothing is lost if this one lands: it is still a GitHub address, so the site keeps working. - Press Sign Transaction, then wait about two to three minutes.
- Confirm the new value is live.
- Now change it back to
185.199.108.153, sign again, and wait once more.
This is the single most useful thing in this guide. We lost days to a domain that sat on the parking address, re-saving an identical record, waiting for a change that was never going to come. A stalled record is not fixed by saving it again. It is fixed by making it visibly different first.
It works everywhere except Obian
Obian opens your site by reading the redirect record, not by asking for the
bare IP. If redirect.WEBSITE.0 is missing — or was written with the wrong
capitals, so it exists under a key nothing reads — Obian finds no address to open and
the domain fails there while every other browser works.
Check step 05: the record must be spelled
redirect.WEBSITE.0 exactly, with your Pages URL as the value. Delete it,
recreate it in that case, sign again, and wait two to three minutes. Changing host
does not help — a bare IP request answers 404 at GitHub and 308 at Vercel, so
no host serves a site once the redirect record is wrong.
My Pages URL redirects to a dead domain
That’s the CNAME file doing exactly its job — GitHub 301s the
Pages URL to your custom domain. If the domain isn’t live yet, rename
CNAME to CNAME.disabled and commit. Rename it back once the
domain works.
Records look perfect but the domain points elsewhere
On a web3 domain this is almost always the chain, not a typo. The dashboard and the
nameserver read from different places and only one is authoritative. Confirm each
address sits in its own RECORD.A.* slot, then press
Sign Transaction.
My record saved but nothing changed
Two suspects, in that order. The capitals: RECORD.A.0 saves,
displays and is ignored, because the key the chain stores is record.A.0 and the
two are different keys. Only the lower-case form in
step 05 is read. The rest of the name:
RECORD_IP.0, A.0 or REDIRECT.WEBSITE without the
.0 are three more keys that each save happily and each do nothing. Read the
key character by character against the table.
The cure is the same either way, and it is not editing: delete the record, add it again with the exact key, then sign.
Editing a record broke it
Changing an existing record in place sometimes leaves it without a valid chain slot, so it stops being served. If an edit doesn’t take effect, delete the record and create it fresh rather than editing it a second time.
A file I committed isn’t showing up
You didn’t create .nojekyll. Add the empty file and commit —
no restart needed.
GitHub says “the certificate does not exist yet”
That’s Enforce HTTPS being refused, and for a web3 domain it is
permanent. Those domains are HTTP-only — no certificate can be issued. Leave the
box unticked and carry on; nothing is broken, and your site will simply be reached
over http://.
A public resolver says my domain doesn’t exist
Expected, and harmless. Web3 domains aren’t in the ordinary DNS tree, so public
resolvers always return NXDOMAIN. Query ns1.noto.network instead.
www and the bare domain behave differently
Normal for a few minutes — the A records and the CNAME propagate separately.
Once both settle they serve the same site, because GitHub treats both hostnames as
yours via the CNAME file.