How to Set Up Your Site So Google Can Actually Find It
I finished building this site yesterday. Before I write anything else on it, I want to be certain Google can actually find what I publish — because publishing a page and having Google know it exists are two different things. On a hosted platform that gap is usually handled for you; on a custom-built site it can quietly go unchecked. So here is the setup, done first.
Ingredients
- Google Search Console — the only place Google tells you what it thinks of your site. Not analytics. This is the one that matters. (free)
- Your sitemap file — in Next.js this is
app/sitemap.js; elsewhere it’s a plugin or a staticsitemap.xml(free) - Your framework config —
next.config.tsfor me, because the canonical-domain redirect belongs in code, not in a hosting dashboard (free) - Patience, measured in weeks — the most underrated ingredient, and the reason people conclude their setup did nothing (free, but expensive)
Why This Is Worth Five Minutes
Inbound traffic comes from essentially three places: search, self-promotion, or paid ads. Self-promotion doesn’t scale past the people who already know you. Ads stop the instant you stop paying. Organic search is the only one that compounds — it works while you sleep, on something you wrote a year ago. So a plumbing bug in search isn’t a channel performing badly; it’s the quiet deletion of the only channel that keeps going after you stop pushing. Worth five minutes to rule out.
I just spent a few sessions building this site from scratch with Claude. Everything visible works. But “it loads in my browser” only proves a visitor who already has the URL can reach it — Google finding a page is a separate process, and there is no error message when it doesn’t happen. That seems like a bad thing to discover a year from now, so I’m doing it on day two.
The Short Version
If you read nothing else, read these three lines.
- Check. Open Search Console and compare the number of pages Google has discovered against the number you’ve actually published. If they don’t match, you have a plumbing problem, not a writing problem.
- Fix. Make your sitemap generate from wherever your content lives instead of listing routes by hand, and pick one canonical domain (apex or www) enforced by a redirect in code — with your sitemap and
robots.txtagreeing with it. - Submit, then wait. Hand Google the sitemap, then judge it by impressions, not clicks. Clicks lag by weeks and will make a working setup look like a failed one.
Step 1: Find Out What Google Actually Has
I registered the site in Search Console and submitted the sitemap before writing this post, which is the whole point — install the instrument before you need the reading. On a site this new Google has essentially nothing yet, and that’s fine; what matters is that the dashboard now exists, so the day something looks wrong I can see it rather than guess.
- Add your site as a property in Search Console and verify it — the bulk of the five minutes.
- Open the coverage report: indexed pages versus pages Google knows about but hasn’t indexed.
- Compare pages discovered against pages you’ve actually published. That one comparison is the entire diagnosis — not “is my ranking good,” just, does Google know these exist. Analytics can’t answer it; it only sees people who already arrived.
- Know where the gap tends to live: plenty of full-service website providers submit your sitemap for you, so on a hosted platform this may have been handled at launch. It’s custom-built sites, where you or an AI wrote the routing, that have nobody doing that step.
The one setup choice worth thinking about is which kind of property to register. I picked the domain-level one, which covers the apex and the www address and every subdomain in a single view — useful precisely because the www-versus-apex split is the other thing worth settling early.
🔧 Developer section: Search Console setup
- Registered a Search Console Domain property, not a URL-prefix one —
sc-domain:joseandgoose.comcovers apex,wwwand both protocols in a single property - Domain properties verify by DNS rather than by file upload: added a TXT record at the registrar, then waited on propagation
- Submitted
https://joseandgoose.com/sitemap.xmlunder Sitemaps - A sitemap has to sit inside the property you submit it to — another reason the Domain property is the easier choice when both hosts resolve
- Gotcha: the reports lag by days, so every check after a change is reading stale data
Step 2: Your Sitemap Probably Lists Your Nav, Not Your Content
I wired app/sitemap.js to generate from the same array that renders my writing index, rather than listing paths by hand. The result is that every post I publish from now on adds itself, and I never have to remember this again. A sitemap is a machine-readable list of every URL you want Google to look at — crawlers can find pages by following links, but this is the one place you say explicitly: here is everything.
- Open your sitemap file and read it — in Next.js,
app/sitemap.js. - Check whether it’s a hardcoded list of nav routes or generated from your content. The hand-written version is the trap: it lists home, about, work, writing, contact, and stops — so Google learns your
/writingindex exists but never the posts underneath it. - Point it at the collection your site already uses to render that index — for me the
postsarray inapp/lib/posts.ts. About eight lines: import it, map each entry to its URL, use the post’s own date as last-modified. Now publishing adds to the sitemap automatically, because there’s no second list to forget — and if you have to remember to do something, you will eventually not do it. - Open
localhost:3000/sitemap.xmland count before deploying. Read it once with your own eyes: it’s produced by code you never run and consumed by a robot that won’t complain if it’s wrong.
The mechanics are simpler than they sound. In the Next.js App Router you never write XML — you export a function that returns a list of objects, and the framework renders the file. The only real trap was dates: my posts store theirs as human-readable strings, which JavaScript will happily turn into an invalid date if you let it.
🔧 Developer section: Generating the sitemap from the posts array
- Next.js App Router convention:
app/sitemap.jsdefault-exports a function returning{ url, lastModified }objects, and the framework renders/sitemap.xml - Static routes sit in one
STATIC_PATHSarray —["", "/about", "/work-and-projects", "/writing", "/contact"]. The empty string produces the bare apex URL for the homepage - Posts come from
import { posts } from "@/app/lib/posts"and aposts.map()emitting`${BASE_URL}/writing/${post.slug}`, joined byreturn [...staticPages, ...postPages] - Kept
BASE_URLas oneconst, so the canonical host is declared in a single place rather than retyped per entry - Gotcha:
post.dateis a display string like"February 18, 2026", sonew Date()can returnInvalid Date. Guarded withisNaN(parsed.getTime()) ? now : parsed
The check that takes ten seconds and is the only verification available before you hand the file to Google.
Step 3: Pick One Domain and Enforce It in Code
I picked the apex as canonical and enforced it with a path-preserving 308 redirect in next.config.ts, then pointed the sitemap and robots.js at the same host. Now www answers with a redirect and the apex answers with the page — one address, no split. Most sites answer on two: the apex (example.com) and the www version. To you that’s one site; to a crawler it’s two hosts serving identical content, which splits your signal and makes Google guess which you meant.
- Pick one. It doesn’t matter which — it matters that you pick. I chose the apex,
joseandgoose.com. - Add a permanent (308) redirect from loser to winner, matching on the incoming host and preserving the path. A lazy redirect sends every www URL to the apex homepage, turning every deep link anyone shared into a dead end.
- Make every sitemap URL use the canonical host, and the
Sitemap:line inrobots.txttoo — all three have to agree. - Verify both directions live: www should return a 308 to the apex, and the apex a plain 200. Thirty seconds, and it’s the difference between “I configured a redirect” and “the redirect works.”
The rule itself is one object in the config file, but two details in it do all the work: a condition so it only fires on the www host, and a wildcard so the rest of the URL survives the trip. Get either wrong and you either loop forever or dump every visitor on the homepage.
🔧 Developer section: Enforcing the canonical host
- One
async redirects()function innext.config.tsreturning a single rule — no middleware, no dashboard toggle, and it lives ingit source: "/:path*"withdestination: "https://joseandgoose.com/:path*"— the wildcard is captured and replayed, so deep links survive instead of collapsing to the homepage- Gotcha:
has: [{ type: "host", value: "www.joseandgoose.com" }]is what prevents a loop. Without it the rule also matches requests already on the apex - Gotcha:
permanent: trueemits a 308, not a 301 — worth knowing before you go hunting for a 301 that never appears BASE_URLinapp/sitemap.jsand thesitemapfield inapp/robots.jsboth hardcode the apex, so nothing advertises a URL that redirects
Most hosts will do a www redirect for you with a toggle in their control panel. I put mine in next.config.ts instead, as a rule matching on the host header. A redirect in the repo is visible to anyone reading the code, moves with the project if I change hosts, and gets reviewed like any other change. One in a dashboard is invisible, undocumented, and a password reset away from being lost. Infrastructure you can’t see in git log is infrastructure you will eventually be confused by.
Step 4: Submit, Then Measure the Right Number
I submitted the sitemap and now the only sensible thing to do is nothing for a few weeks. This is where people quit: you ship the setup, check a week later, clicks haven’t moved, and it looks like nothing worked. Clicks are the wrong number to be watching that early.
- Submit the sitemap in Search Console. It’s a single button.
- Watch the discovered count over the next few days — that’s the first thing that responds, and it tells you Google has read the file.
- Then track impressions weekly and leave clicks alone. Impressions are the number that moves first.
- Give it weeks before judging anything. Indexing is not a deploy; the feedback loop is measured in reporting lag plus crawl schedule, and neither is under your control.
An impression is your page appearing in somebody’s search results — they saw it, whether or not they clicked. A click is the whole chain completing: indexed, ranked well enough to be on a page someone scrolled, and appealing enough to beat every other result. Impressions move first, by weeks; clicks are downstream of both impressions and ranking position. Judge an indexing setup by clicks in week one and you will always conclude it failed.
Two Things That Will Look Like Bad News
Both of these are normal, and both are the kind of thing that makes people undo a working change. Worth knowing before you see them.
Your average position will get worse as more pages index. That reads like a ranking collapse and usually isn’t. Average position averages over every query where you appeared at all. With only a few pages indexed you show up almost exclusively for narrow queries you happen to match well, and the average flatters you. As more pages index you start surfacing on far more, far deeper queries, most well down the results — more results, lower average, nothing actually demoted. The tell is whether impressions rose at the same time. A real demotion drags impressions down with the position, not up.
Google won’t tell you what was searched. Below an anonymization threshold it withholds the query strings entirely, because at low volume the exact search could identify the searcher. So early on you may see that something was clicked, on which page, from which country — and never learn what was typed. Worth knowing so you don’t hunt for a report that structurally cannot exist yet. Query data arrives when volume does. Similarly, treat analytics geography with suspicion at low volume: a fair amount of it is crawlers rather than readers.
What went fast
- The sitemap change — about eight lines on top of the static version, because the
postsarray already existed for the writing index. Reusing the list the site already renders from meant there was nothing new to maintain. - The redirect rule — one object in
next.config.ts. The whole canonical-domain question took longer to decide than to implement. - Verifying both —
localhost:3000/sitemap.xmlbefore deploy, then a live status check on both hosts. Seconds each, and either would have caught a silent failure.
What needed patience
- DNS verification — the domain-level Search Console property wants a TXT record at the registrar, and propagation is not instant. Nothing to debug, just waiting for the internet to agree.
- No same-day feedback — the reports lag by days, so every check after a change reads stale data. There is no way to tighten that loop, which makes it very easy to conclude too early that something didn’t work.
- Accepting the payoff is weeks out — I’ve set this up on a site with almost nothing on it. The honest expectation is that there will be nothing to look at for a while, and the discipline is not to go re-engineer something that isn’t broken in the meantime.
The whole thing is really one question: is your sitemap accurate? Open Search Console and compare pages discovered against pages published. That’s a five-minute check, not a project — and if the numbers don’t match, you’ve found the reason the one channel that compounds is doing nothing for you. I’d rather have this right on a site that’s two days old than discover it a year and twenty posts from now, which is the version of this story most people end up writing.