The site is live. Before you tell anyone, this page adds the two files a static export does not produce by itself — robots.txt and sitemap.xml — trims the files that make pages slow, then checks from the outside, the way a visitor or a crawler sees it, everything pages 4 to 7 set up. It ends with the search engines and a short list for announcement day.
Before you start
The site is deployed with the script from page 6 and answers on its domain. Everything on this page runs on your Mac, from the project folder, except the HSTS edits on the server. Line up two friends as external testers for the end: one on mobile data, one on another operator's Wi-Fi. First, the home page answers:
$curl -s -o /dev/null -w '%{http_code}' https:///200
Add robots.txt and sitemap.xml
output: "export" writes only what the app declares. Next.js has two metadata files for this: app/robots.ts and app/sitemap.ts. At build time each becomes a plain file in out/.
Set the indexing choice in the panel first. Leave it off while the site still holds examples or features that only pretend to work, such as a contact form that simulates sending: crawlers would index them as they are. Switch it on, and redeploy, on launch day.
In a terminal on your Mac, go to the project folder, then copy each file block below and paste it in the terminal: each one writes its file.
$cd import type { MetadataRoute } from "next";export const dynamic = "force-static";const SITE = "https://";export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", allow: "/" }, sitemap: SITE + "/sitemap.xml", };}import type { MetadataRoute } from "next";export const dynamic = "force-static";const SITE = "https://";const ROUTES = [ "/", "/bio/", "/bookmarks/", "/chess/", "/clippings/", "/contact/", "/endeavor/", "/how-i-want-to-live/", "/journey/", "/lately/", "/library/", "/own/", "/pearl/", "/picture/", "/support/", "/thought/", "/travel/", "/writing/",];export default function sitemap(): MetadataRoute.Sitemap { const now = new Date(); return ROUTES.map((path) => ({ url: SITE + path, lastModified: now }));}Deploy with the script from page 6, which builds before uploading, then check both files were generated:
$cd $./scripts/deploy.sh$ls out/robots.txt out/sitemap.xml$curl -s https:///robots.txt | head -1User-Agent: *
If the build fails or out/robots.txt is missing
A public/robots.txt or public/sitemap.xml conflicts with the generated one: delete the one in public/. An error naming dynamic or revalidate on /robots.txt means the force-static line is missing. A project laid out in src/app/ takes both files there.
Trim the heaviest files
A 50 MB WAV on a page is a 50 MB download on a phone. List everything over 5 MB that the site serves:
$cd $find out -type f -size +5M -exec ls -lh {} \; | sort -k5 -hAudio: convert each WAV to Opus, or MP3 for the oldest browsers, then change the reference in the code.
$brew install ffmpeg$ffmpeg -i public/audio/track.wav -c:a libopus -b:a 128k public/audio/track.opus$ffmpeg -i public/audio/track.wav -c:a libmp3lame -q:a 2 public/audio/track.mp3$grep -rn '\.wav' app components lib 2>/dev/nullImages: nothing on a web page needs more than 2400 px on its long side.
$cd $cp -R public ../public-before-resize$find public -iname '*.jp*g' -size +1M -exec sips -Z 2400 {} \;$du -sh ../public-before-resize publicCheck from the outside
Redirects and status codes
$curl -sI http:/// | grep -iE '^(HTTP|location)'$curl -sI https://www./ | grep -iE '^(HTTP|location)'$curl -s -o /dev/null -w '%{http_code}\n' https:///no-such-page/HTTP/1.1 308 Permanent RedirectLocation: https:///HTTP/2 301location: https:///404$curl -s -o /dev/null -w '%{http_code}' https:///no-such-page/404
TLS
Open SSL Labs' test for your domain. It takes about two minutes. Aim for A; A+ needs an HSTS max-age of at least six months, which the step after these checks sets.
$open "https://www.ssllabs.com/ssltest/analyze.html?d="$curl -sv -o /dev/null https:/// 2>&1 | grep -E 'issuer|expire date'$echo | openssl s_client -connect :443 -servername 2>/dev/null | openssl x509 -noout -issuer | grep -o 'Let.s Encrypt'Let's Encrypt
Security headers
$open "https://securityheaders.com/?q=https:///&followRedirects=on"$curl -sI https:/// | grep -iE '^(strict-transport|x-content-type|x-frame|referrer|permissions|content-security)'Expect strict-transport-security, x-content-type-options: nosniff, referrer-policy and whatever else page 5 set. A missing content-security-policy is expected at this stage: it costs a grade, not security you already had.
IPv6 and HTTP/3
$dig +short AAAA $curl -6 -sI https:/// | head -1$curl -sI https:/// | grep -i '^alt-svc'Mail spoofing
If you told page 4 the domain sends no mail, check that the "no mail" records are published, so nobody can send phishing in your name:
$dig +short TXT _dmarc.$dig +short TXT | grep spf1Performance
In Chrome, open the site in a private window, then DevTools → Lighthouse → Mobile → Analyze page load. Or use PageSpeed Insights, which runs the same test from Google's servers:
$open "https://pagespeed.web.dev/report?url=https:///"Raise HSTS to a year
Page 5 started the HSTS header at one day. Once every check above passes, raise it to a year, on the server:
$ssh vps$sudo sed -i 's/max-age=86400"/max-age=31536000"/' /etc/caddy/Caddyfile$sudo -u caddy caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy$curl -sI https:/// | grep -i '^strict-transport-security'strict-transport-security: max-age=31536000
Search engines
Tell Google and Bing the site exists and where the sitemap is.
- In Google Search Console, add a property of type Domain with
. Google shows a TXT record starting withgoogle-site-verification=. - In the Infomaniak Manager, Zone DNS of the domain (as on page 4), add that TXT record on the domain itself, then click Verify in Search Console. It can take a few minutes.
- In Search Console → Sitemaps, enter
sitemap.xmland submit. - In Bing Webmaster Tools, sign in and choose the import from Google Search Console: it copies the site and the sitemap.
$dig +short TXT | grep google-site-verificationAnnounce
A last look before you post the link and ask your external testers to open the site on their phones:
- Favicon:
/favicon.icoanswers 200 (the command below). Next.js servesapp/favicon.icoorapp/icon.png. - Link preview image: a 1200 × 630 JPEG as
app/opengraph-image.jpg(Next.js adds the meta tags) andmetadataBaseset to https://in the root layout, so the image URL is absolute. Export it from the originals inphotos-26-og/; they stay out of the site. Paste the link in a message to yourself to see the preview. - 404 page: styled, with a way back home.
- Stubs: the contact form and chess save have no backend yet; hide them or label them "coming soon".
- Monitoring: the uptime monitor from page 7 is green and its alerts reach
. - After launch: the RSS feeds for
/latelyfrom the project's TODO can come later; a feed added next week works the same.
$curl -s -o /dev/null -w '%{http_code}\n' https:///favicon.ico$curl -s https:/// | grep -o '<meta property="og:image"[^>]*>'Done
The series is complete: a Next.js site built on your Mac, served over HTTPS by Caddy from your own Debian VPS at OVH, on a domain managed at Infomaniak, deployed atomically, backed up, monitored, and checked from the outside, with Google and Bing told where the sitemap is. The Run script of this page doubles as a smoke test after any big change.
A later page, not written yet — page 9 — will give the contact form and the chess saves a backend on the same VPS: a small Node service behind Caddy, with SQLite.