Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# M3 — Sitemap Production Audit & Reliability
|
||||
|
||||
## Current architecture
|
||||
|
||||
Routes:
|
||||
|
||||
```text
|
||||
GET /sitemap.xml SitemapController@index
|
||||
GET /sitemaps/{name}.xml SitemapController@show
|
||||
GET /robots.txt RobotsTxtController (Sitemap: {APP_URL}/sitemap.xml)
|
||||
```
|
||||
|
||||
Generation: `php artisan skinbase:sitemaps:generate` (scheduler 10:30 and 22:30) writes static XML on `sitemaps_public` (`public/`).
|
||||
|
||||
Also: `sitemaps:publish`, `validate`, release artifacts, optional live-build fallback.
|
||||
|
||||
Families (no groups/years/videos sitemaps):
|
||||
|
||||
artworks (sharded 10k), academy-*, users (sharded 10k), tags (now shardable at 25k), categories, collections, cards, stories, web-stories, news, news-google, forum-*, static-pages.
|
||||
|
||||
Canonical artwork URLs: `route('art.show')` `/art/{id}/{slug}`. Public/published only.
|
||||
|
||||
## Exact production issue
|
||||
|
||||
`/sitemap.xml` is a **stale file from 2026-05-13**, owned by **klevze** (`-rw-r--r--`). Children (artworks shards, academy, users, …) refresh daily as **skinbase**. Scheduler cannot overwrite the index (`filesystems.sitemaps_public.throw=false`, generate ignored failed `put`). Nginx `try_files $uri` serves the stale root file and **never** hits Laravel. Academy families exist on disk but are **absent from the May index**.
|
||||
|
||||
`location @php` is missing in the live vhost (documented in M0).
|
||||
|
||||
## Local vs production
|
||||
|
||||
Generate command already writes `sitemaps/sitemap.xml` on both. Production **file ownership** plus **nginx `$uri` = public/sitemap.xml** is the operational failure. Local did not fail writes or serve a dual path.
|
||||
|
||||
## Root cause
|
||||
|
||||
1. Index file not writable by `skinbase`.
|
||||
2. `put()` failures ignored.
|
||||
3. Nginx prefers the stale `/sitemap.xml` over `/sitemaps/index.xml` (which did not exist).
|
||||
|
||||
## Implementation
|
||||
|
||||
- Generate writes `sitemaps/index.xml` (new, creatable), plus tries `sitemaps/sitemap.xml` and `sitemap.xml`; **fails the command if none of the index paths write**.
|
||||
- Child writes report failure instead of counting a silent skip as success.
|
||||
- `SitemapController::index()` prefers static `sitemaps/index.xml`.
|
||||
- `deploy/nginx/sitemaps.conf`: `try_files /sitemaps/index.xml /sitemaps/sitemap.xml /index.php?$query_string;` (no `@php`)
|
||||
- Tags builder is shardable (25k) so it cannot exceed 50k URLs without an index.
|
||||
|
||||
## Caching
|
||||
|
||||
Unchanged: nginx `max-age=21600`; Laravel file response uses `sitemaps.cache_ttl_seconds` (900). No HTML cache. Generate is the refresh.
|
||||
|
||||
## Chunking
|
||||
|
||||
Artworks/users/forum-threads/collections/cards/stories already sharded at 10k. Tags 25k. Protocol: 50k URLs / 50MB. Production files are all well under 50MB (largest artworks shard ~3.5MB, tags ~2MB).
|
||||
|
||||
## Deployment
|
||||
|
||||
Production vhost (`/etc/nginx/sites-enabled/skinbase.org.conf`) has **no** `location @php`. PHP-FPM is:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
location = /index.php {
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm-skinbase.sock;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Do not** use `try_files … @php` (undefined named location). Use `/index.php?$query_string` like `location /`.
|
||||
|
||||
Exact replacement for the two sitemap blocks (currently ~lines 91–110):
|
||||
|
||||
```nginx
|
||||
location = /sitemap.xml {
|
||||
try_files /sitemaps/index.xml /sitemaps/sitemap.xml /index.php?$query_string;
|
||||
|
||||
add_header Cache-Control "public, max-age=21600" always;
|
||||
add_header Content-Type "application/xml; charset=UTF-8" always;
|
||||
etag on;
|
||||
}
|
||||
|
||||
location ~ ^/sitemaps/[A-Za-z0-9_\-]++\.xml$ {
|
||||
try_files $uri /index.php?$query_string;
|
||||
|
||||
add_header Cache-Control "public, max-age=21600" always;
|
||||
add_header Content-Type "application/xml; charset=UTF-8" always;
|
||||
etag on;
|
||||
}
|
||||
```
|
||||
|
||||
Do **not** add `$uri` to the `/sitemap.xml` try_files list: `$uri` is `/sitemap.xml`, the stale klevze-owned file, and would shadow PHP if `index.xml` is missing.
|
||||
|
||||
Order:
|
||||
|
||||
1. Deploy app.
|
||||
2. Run `php artisan skinbase:sitemaps:generate` as `skinbase` so `sitemaps/index.xml` exists.
|
||||
3. Patch the vhost, then:
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
`nginx -t` should pass: last `try_files` argument matches the existing front-controller pattern. Introducing `@php` would not.
|
||||
|
||||
Optional: `chown skinbase:skinbase` the old `sitemap.xml`.
|
||||
|
||||
## Legacy index files after `sitemaps/index.xml`
|
||||
|
||||
| Path | Role after M3 |
|
||||
| ---- | ------------- |
|
||||
| `public/sitemaps/index.xml` | **Canonical.** Scheduler can create this even when the May 2026 file is unwritable. Nginx prefers it. |
|
||||
| `public/sitemaps/sitemap.xml` | **Legacy fallback** for `/sitemap.xml` if `index.xml` is missing. Same inode as root `sitemap.xml` on current prod (symlink). Best-effort write; failure must not fail generate if `index.xml` succeeded. |
|
||||
| `public/sitemap.xml` | **Legacy `$uri`.** On prod this is a symlink to the unwritable klevze file. **Not required** once nginx prefers `index.xml`. Generate may skip it. |
|
||||
|
||||
Generate still *attempts* all three; success requires **at least one** index write (`index.xml` is enough).
|
||||
|
||||
## robots.txt
|
||||
|
||||
Already declares `Sitemap: https://skinbase.org/sitemap.xml`. **No robots change** for M3.
|
||||
|
||||
## URLs that should not be in sitemaps
|
||||
|
||||
Already excluded: private/unapproved artworks, inactive users, inactive tags, academy pricing query variants, `/pages/about` duplicate, forbidden `/admin` `/cp` etc. No extra removals in M3.
|
||||
|
||||
## Tests
|
||||
|
||||
`php artisan test tests/Feature/SitemapTest.php` (run after this doc).
|
||||
Reference in New Issue
Block a user