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

00

What you’re building

Read first

The finished setup has four moving parts. Each one is handled in a step below.

partwhat it doesstep
Your filesHTML, CSS, favicon sitting in a folder01
GitHub repoHolds the files and their history02
GitHub PagesServes those files at a free .github.io address03
Your domainPoints at GitHub so the name is yours04–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.

partwho reads itwhat it holds
DNS zone recordordinary DNS lookups, and the Noto nameserverone IP address
On-chain recordweb3 browsers and resolversthe same IP, plus the redirect
CNAME fileGitHubyour domain, so GitHub answers for it
Two record layers, and they are not the same thing

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
Why bother with plain files?

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.

01

Write the files

5 min

Make 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.

Do this before your first push

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.

02

Create the repo and upload

5 min

Create it

Go to github.com/new and fill in the form.

The GitHub create a new repository page showing the owner, repository name and description fields, visibility set to Public, and the Add README, Add .gitignore and Add license options all left off.
The new repository form — every option here is a choice you have to make deliberately
  1. 1Open github.com/new.
  2. 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.
  3. 3The Description is optional and appears on the repo’s front page. Write whatever you like.
  4. 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.
  5. 5Add README — leave it off. This guide has you upload your own index.html below, and a README here just adds a file you don’t need.
  6. 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.
  7. 7Add license — No license is fine unless you intend to publish source others reuse.
  8. 8Click Create repository.
A note on what gets published

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.

The GitHub repository file listing showing an optional images folder, the .nojekyll file, the CNAME file, favicon.svg and index.html at the top level of the repository.
What the repository should look like once the files are in
  1. 1Your repo’s address: github.com/yourusername/yourreponame.
  2. 2Any images folder, if your site uses one. Entirely optional — a site with no images simply won’t have it.
  3. 3.nojekyll. Empty, no extension. Without it, files can silently vanish from the published site — see step 01.
  4. 4CNAME, added in step 04. Ignore it for now; you can add it later without disturbing anything else.
  5. 5index.html. This must be the one that gets served.
Four things have to be true, and all four are checked
  • Every file is at the top level. If index.html sits 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. Not myfile.html, not Index.html. GitHub only serves the root automatically for that exact lower-case name.
  • .nojekyll is 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
Never leave a token in a remote URL

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.

03

Switch on GitHub Pages

3 min

In your repo open Settings → Pages.

GitHub Pages settings showing Source set to Deploy from a branch, branch main and folder root, the Custom domain field filled in with a red DNS check unsuccessful warning and an improperly configured InvalidDomainError notice, Enforce HTTPS greyed out, and a green box confirming the site is live at the custom domain.
Everything on this page, in order — and the warnings you can safely ignore
  1. 1Open github.com/yourusername/yourrepo/settings/pages.
  2. 2Set Source to Deploy from a branch.
  3. 3Branch main, folder / (root), then Save.
  4. 4Paste your domain into Custom domain and Save. Then ignore the red “DNS check unsuccessful” warning underneath it.
  5. 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.
  6. 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.io address instead.
Red warnings that are actually fine

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.

Test the github.io URL right now

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.

04

Point the domain at GitHub

10 min

Two 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

  1. Open your repo on GitHub, click Add file → Create new file
  2. In the name box type CNAME — exactly that, all capitals, no extension
  3. Paste your domain into the content box below
  4. Scroll down and Commit changes
  5. 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
Getting the filename wrong

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.

What this file does to your Pages URL

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:

typenamevalueTTLneeded?
A@185.199.108.153300required
CNAMEwwwusername.github.io300optional

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 value people get wrong

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.

The Freename Records tab for the domain, showing a resolving status line, the Wallet Address box with Sign Transaction, the Forward Domain toggle left off, a DNS Records table with A, two NS and a www CNAME row, and a Web3 Records table with RECORD.A.0 and REDIRECT.WEBSITE.0.
The Freename Records tab — step 04 and step 05 both happen on this one screen
  1. 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.
  2. 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.
  3. 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.
  4. 4In DNS Records, add a single A row on @ with value 185.199.108.153, TTL 300. The two NS rows are added for you — leave them.
  5. 5Optional: a CNAME row on www pointing at yourusername.github.io. Your bare domain works without it; this just makes www behave the same way.
  6. 6In Web3 Records, add record.A.0 — lower-case record, capital A — with the same IP, 185.199.108.153. This on-chain slot is what the nameserver reads.
  7. 7Also in Web3 Records, add redirect.WEBSITE.0 — lower-case redirect, capital WEBSITE — with your Pages URL, https://yourusername.github.io/yourrepo/. Note it points at the github.io address, not at your web3 domain.
Forward Domain is not what you want

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.

05

Sign the record — web3 only

Critical
Skip this section if your domain is a normal one

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.

slotvalueneeded?
record.A.0185.199.108.153required — nameserver
redirect.WEBSITE.0https://username.github.io/<repo>/required — web3 browsers
Point the redirect at your Pages URL, not your own domain

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.

Capitalisation decides whether this works at all

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.

What “correct” looks like

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.

The order that works

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.

06

Verify it works

2 min

Good 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.

Three browser windows: the getweb2link tool with the web3 name entered and the mirrored web2 link returned, a preview of the site embedded in the tool, the site opened directly at its virtualsociety.top address, and the site opened at its web3 virtualsociety address.
Finding your web2 link, and the same site open on both addresses
  1. 1Go to virtualsociety.top/getweb2link.
  2. 2Type the web3 name you registered on .virtualsociety and press Resolve.
  3. 3You get back your mirrored web2 link — https://yourname.virtualsociety.top/.
  4. 4Your site is previewed right there in the page. If it renders, everything upstream already works.
  5. 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.
  6. 6Top right: your web2 link open on its own, in a normal browser, over HTTPS.
  7. 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.
This is the check worth doing first

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.

The virtualsociety.top DNS records tool with the web3 domain entered, showing its published A record pointing to the GitHub Pages IP, AAAA, CNAME, MX and TXT marked as not published, and the two Noto nameservers.
Reading the records your domain actually publishes
  1. 1Go to virtualsociety.top/tool/dnscheck.
  2. 2Type your web3 name and press Look up.
  3. 3You get your published records. The A row should read 185.199.108.153, with NS 1 and NS 2 showing the Noto nameservers. AAAA, MX and TXT showing none published is perfectly normal. CNAME is empty unless you added the optional www row 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.

Why the second one earns its place

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.

Do not test a web3 domain with a plain lookup

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 seewhat it means
Your siteDone.
404 from GitHubDNS is fine; the repo isn’t serving. Check the filename is exactly index.html.
Old host or a 502Your domain still points at whatever it pointed at before. The change hasn’t propagated — or was never signed.
Nothing at allNameservers are wrong, or the record was never committed to the chain.
About waiting

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 this

My 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:

  1. Delete the A row and add it again. Always first. This clears a stale internal pointer, which is the usual cause and is completely invisible in the dashboard.
  2. Check every key letter by letter for a typo or a leftover name from an earlier attempt.
  3. Confirm you pressed Sign Transaction. Unsaved records display perfectly and do nothing.
  4. 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:

  1. Delete the A row.
  2. 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.
  3. Press Sign Transaction, then wait about two to three minutes.
  4. Confirm the new value is live.
  5. 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.