ReviewOS

also looking at this

stacks/uplink

fix(site): a missing page says 404, and the crawler files are checked on deploy

#60
Merged glennmichael123 wants to merge fix/crawler-surface into main
4 files +40 -5
.github/workflows/deploy.ymlmodified+14-1
Changes to .github/workflows/deploy.yml
@@ -241,4 +241,17 @@ jobs:
241241 *"not one Uplink issued"*) ;;
242242 *) echo "::error::/api/license/check did not answer as the license API: ${said:0:200}"; exit 1 ;;
243243 esac
244 echo "Home page, pricing, checkout, license API, 404s and assets check out."
244 # A miss has to say 404 in the status line, not only in the body: a
245 # crawler indexes a 200 whatever it reads, and a client treats it as
246 # a hit.
247 [ "$(status no-such-page-here)" = 404 ] || { echo "::error::A missing page answers $(status no-such-page-here), not 404."; exit 1; }
248 # Checked against the live site rather than the repository, because a
249 # CDN can serve its own managed robots.txt over this one and nothing
250 # in the repository would know.
251 [ "$(status sitemap.xml)" = 200 ] || { echo "::error::/sitemap.xml answers $(status sitemap.xml)."; exit 1; }
252 robots=$(curl -s --max-time 20 "${url}robots.txt")
253 case "$robots" in
254 *"Sitemap: https://uplink.stacksjs.com/sitemap.xml"*) ;;
255 *) echo "::error::/robots.txt is not the one in public/, so something is serving its own: ${robots:0:200}"; exit 1 ;;
256 esac
257 echo "Home page, pricing, checkout, license API, 404s, robots, sitemap and assets check out."
public/README.mdmodified+10-0
Changes to public/README.md
@@ -12,6 +12,16 @@ Let's say you want to overwrite the default `robots.txt` file. You can do this b
1212
1313If you want to overwrite the default `favicon.ico` file, you can do this by putting a file named `favicon.ico` in this folder.
1414
15## Uplink's crawler-facing files
16
17`robots.txt` and `sitemap.xml` are served from here rather than generated by a
18route, so they are the intended home and there is nothing to keep them in sync
19with. **Adding a public page means adding its URL to `sitemap.xml` by hand.**
20
21The deploy workflow checks all three of `/404`, `/sitemap.xml` and
22`/robots.txt` against the live site after every deploy, because a CDN in front
23of the app can serve its own managed `robots.txt` and quietly replace this one.
24
1525## Security
1626
1727If you want to make sure that a file is not publicly accessible, you can put it in the `storage/private` folder. This folder is not accessible by the web server.
public/sitemap.xmlmodified+12-4
Changes to public/sitemap.xml
@@ -1,7 +1,15 @@
11<?xml version="1.0" encoding="UTF-8"?>
2<!--
3 Four pages, matching the `const path` of the four public views. Adding a
4 public page means adding it here: nothing generates this file.
5
6 No <lastmod>: it was a hardcoded date on every entry, already wrong for `/`
7 the day after it was written, and a date that lies is worse for a crawler
8 than no date. No <priority> either, which Google has ignored since 2023.
9-->
210<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3 <url><loc>https://uplink.stacksjs.com/</loc><lastmod>2026-09-29</lastmod><priority>1.0</priority></url>
4 <url><loc>https://uplink.stacksjs.com/pricing</loc><lastmod>2026-09-29</lastmod><priority>0.8</priority></url>
5 <url><loc>https://uplink.stacksjs.com/privacy</loc><lastmod>2026-09-29</lastmod><priority>0.3</priority></url>
6 <url><loc>https://uplink.stacksjs.com/terms</loc><lastmod>2026-09-29</lastmod><priority>0.3</priority></url>
11 <url><loc>https://uplink.stacksjs.com/</loc></url>
12 <url><loc>https://uplink.stacksjs.com/pricing</loc></url>
13 <url><loc>https://uplink.stacksjs.com/privacy</loc></url>
14 <url><loc>https://uplink.stacksjs.com/terms</loc></url>
715</urlset>
resources/views/404.stxmodified+4-0
Changes to resources/views/404.stx
@@ -2,6 +2,10 @@
22// The not-found page. With `ui.defaultViews: false` the framework's own
33// errors/404 view is not served, so the app answers with this instead.
44const title = 'Not found - Uplink'
5// The body said Not found and the status line said 200, so a crawler indexed
6// every wrong URL as a real page and a client treated the miss as a hit. The
7// dashboard already does this; this page did not.
8notFound()
59</script>
610
711@nolayout