← back to Animals
rel=alternate JSON + HTTP Link header on /breeds and /photographers
f22df9f7f1eaef1acd4bb954eab77368f6453c0e · 2026-05-08 00:51:49 -0700 · Steve
Cross-fleet directory pattern (VCL fbd4be4, NPH 51036bf, lacountyeats 963d23a)
applied to animals. /api/breeds.json + /api/photographers.json already exist
as JSON twins; this commit just surfaces them via RFC 8288 Link headers.
Changes:
- src/server/index.js /breeds: HTTP Link with canonical + alternate (mirrors
the filter query params q/species/size/energy onto /api/breeds.json)
- src/server/index.js /photographers: HTTP Link with canonical + prev + next
+ alternate (paginated; respects ?page= query)
- ALSO fixed pre-existing ES-module crash on line 92: 'const helmet =
require()' replaced with 'await import()' destructure pattern. Server
was in pm2 crash loop before this commit; now listening on :9720.
Smokes (with basic auth admin:DWSecure2024!):
- /breeds → 200 + Link header (canonical + alternate)
- /photographers?page=2 → 200 + Link header (canonical + prev + next + alternate)
Cross-fleet rel=alternate now: VCL + NPH + lacountyeats + animals = 4 of 6
directory targets. Skipped: lawyer-directory (SPA, autocomplete pattern),
professional-directory (api-only, no HTML listing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit f22df9f7f1eaef1acd4bb954eab77368f6453c0e
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 00:51:49 2026 -0700
rel=alternate JSON + HTTP Link header on /breeds and /photographers
Cross-fleet directory pattern (VCL fbd4be4, NPH 51036bf, lacountyeats 963d23a)
applied to animals. /api/breeds.json + /api/photographers.json already exist
as JSON twins; this commit just surfaces them via RFC 8288 Link headers.
Changes:
- src/server/index.js /breeds: HTTP Link with canonical + alternate (mirrors
the filter query params q/species/size/energy onto /api/breeds.json)
- src/server/index.js /photographers: HTTP Link with canonical + prev + next
+ alternate (paginated; respects ?page= query)
- ALSO fixed pre-existing ES-module crash on line 92: 'const helmet =
require()' replaced with 'await import()' destructure pattern. Server
was in pm2 crash loop before this commit; now listening on :9720.
Smokes (with basic auth admin:DWSecure2024!):
- /breeds → 200 + Link header (canonical + alternate)
- /photographers?page=2 → 200 + Link header (canonical + prev + next + alternate)
Cross-fleet rel=alternate now: VCL + NPH + lacountyeats + animals = 4 of 6
directory targets. Skipped: lawyer-directory (SPA, autocomplete pattern),
professional-directory (api-only, no HTML listing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
src/server/index.js | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/server/index.js b/src/server/index.js
index 38a5fca..84d112e 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -89,7 +89,7 @@ const app = express();
// (Leaflet/OSM) + Wikimedia images. Defer CSP hardening until each external
// origin is catalogued. All other Helmet headers (HSTS, X-Frame-Options
// SAMEORIGIN, X-Content-Type-Options nosniff, Referrer-Policy, etc.) apply.
-const helmet = require('helmet');
+const { default: helmet } = await import('helmet');
app.use(helmet({ contentSecurityPolicy: false }));
// gzip everything compressible (HTML/JSON/CSS/JS) — typical 70-85% reduction
@@ -707,6 +707,19 @@ app.get('/photographers', async (req, res) => {
LIMIT ${PER_PAGE} OFFSET ${offset}`);
const photographers = rows.map(r => ({ ...r, slug: r.slug_raw.replace(/^-+|-+$/g, '') }));
const totalPages = Math.ceil(total.n / PER_PAGE);
+
+ // rel=alternate JSON mirror — /api/photographers.json already exists; surface
+ // it via HTTP Link header (RFC 8288). HTML link tag injection deferred until
+ // renderPhotographersIndex accepts an alternateJsonUrl prop.
+ const baseUrl = (process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 9720}`).replace(/\/+$/, '');
+ const apiUrl = `${baseUrl}/api/photographers.json`;
+ const canonicalUrl = `${baseUrl}/photographers${page > 1 ? `?page=${page}` : ''}`;
+ const linkParts = [`<${canonicalUrl}>; rel="canonical"`];
+ if (page > 1) linkParts.push(`<${baseUrl}/photographers${page > 2 ? `?page=${page-1}` : ''}>; rel="prev"`);
+ if (page < totalPages) linkParts.push(`<${baseUrl}/photographers?page=${page+1}>; rel="next"`);
+ linkParts.push(`<${apiUrl}>; rel="alternate"; type="application/json"`);
+ res.set('Link', linkParts.join(', '));
+
res.send(renderPhotographersIndex({ photographers, page, totalPages, total: total.n }));
});
@@ -836,6 +849,17 @@ app.get('/breeds', async (req, res) => {
FROM breeds b JOIN species s ON s.id=b.species_id
WHERE ${where.join(' AND ')}
ORDER BY b.common_name`, params);
+ // rel=alternate JSON mirror — /api/breeds.json already exists. RFC 8288.
+ const baseUrl = (process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 9720}`).replace(/\/+$/, '');
+ const filterQs = new URLSearchParams();
+ if (species) filterQs.set('species', species);
+ if (size) filterQs.set('size', size);
+ if (energy) filterQs.set('energy', energy);
+ if (q) filterQs.set('q', q);
+ const apiUrl = `${baseUrl}/api/breeds.json${filterQs.toString() ? '?' + filterQs.toString() : ''}`;
+ const canonicalUrl = `${baseUrl}/breeds${filterQs.toString() ? '?' + filterQs.toString() : ''}`;
+ res.set('Link', `<${canonicalUrl}>; rel="canonical", <${apiUrl}>; rel="alternate"; type="application/json"`);
+
res.send(renderBreedsIndex({ breeds, filters: { species, size, energy, q } }));
});
← 18ee2c0 yolo: Adoption events on /shows page
·
back to Animals
·
yolo: Surface adoptable animals on pound/shelter detail page 53b3de1 →