← back to Restaurant Directory
rel=alternate JSON + HTTP Link header on / and /c/:slug
963d23ae8fb71a55e61a399cf20663fd3307167f · 2026-05-08 00:15:19 -0700 · Steve
Applies the cross-fleet directory pattern (VCL fbd4be4, NPH 51036bf) to
lacountyeats. /api/restaurants already exists as the JSON mirror; this commit
just surfaces it via the standard rel=alternate signals.
Changes:
- views/partials/head.ejs: conditional <link rel="alternate" type="application/json">
when alternateJsonUrl is in render context
- src/api/server.ts: HTTP Link header (RFC 8288) on / and /c/:slug routes;
alternateJsonUrl computed per-request reflecting current filter state
(q + city + cuisine + limit + offset on /, or city= filter on /c/:slug)
Smoke (PORT=9746):
- GET / → 200, Link header present with canonical + alternate (?limit=50&offset=0)
- HTML head includes <link rel="alternate" type="application/json">
Cross-fleet rel=alternate tally now: VCL + NPH + lacountyeats = 3 of 5
directory targets. Remaining: lawyer-directory (autocomplete-SPA, doesn't fit
this pattern), professional-directory, animals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M src/api/server.tsM src/web/views/partials/head.ejs
Diff
commit 963d23ae8fb71a55e61a399cf20663fd3307167f
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 00:15:19 2026 -0700
rel=alternate JSON + HTTP Link header on / and /c/:slug
Applies the cross-fleet directory pattern (VCL fbd4be4, NPH 51036bf) to
lacountyeats. /api/restaurants already exists as the JSON mirror; this commit
just surfaces it via the standard rel=alternate signals.
Changes:
- views/partials/head.ejs: conditional <link rel="alternate" type="application/json">
when alternateJsonUrl is in render context
- src/api/server.ts: HTTP Link header (RFC 8288) on / and /c/:slug routes;
alternateJsonUrl computed per-request reflecting current filter state
(q + city + cuisine + limit + offset on /, or city= filter on /c/:slug)
Smoke (PORT=9746):
- GET / → 200, Link header present with canonical + alternate (?limit=50&offset=0)
- HTML head includes <link rel="alternate" type="application/json">
Cross-fleet rel=alternate tally now: VCL + NPH + lacountyeats = 3 of 5
directory targets. Remaining: lawyer-directory (autocomplete-SPA, doesn't fit
this pattern), professional-directory, animals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
src/api/server.ts | 21 +++++++++++++++++++--
src/web/views/partials/head.ejs | 3 +++
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/api/server.ts b/src/api/server.ts
index 1d31fd7..4fb61b9 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -156,7 +156,21 @@ app.get('/', async (req, res, next) => {
LIMIT 6`
)).rows : [];
- res.render('list', { rows, total, page, limit, city, cuisine, q, cities, cuisines, heroCuisines, showHero, newestRows });
+ // rel=alternate JSON mirror — same query, paginated. Apps + agents can hit
+ // /api/restaurants with the same query string for JSON. RFC 8288 HTTP Link
+ // header in addition to the HTML <link rel> tag in head.ejs.
+ const baseUrl = (process.env.PUBLIC_URL || `http://localhost:${PORT}`).replace(/\/+$/, '');
+ const apiQs = new URLSearchParams();
+ if (q) apiQs.set('q', q);
+ if (city) apiQs.set('city', city);
+ if (cuisine) apiQs.set('cuisine', cuisine);
+ apiQs.set('limit', String(limit));
+ apiQs.set('offset', String(offset));
+ const alternateJsonUrl = `${baseUrl}/api/restaurants?${apiQs.toString()}`;
+ const canonicalUrl = baseUrl + '/' + (req.url.includes('?') ? req.url.split('?')[1] && '?' + req.url.split('?')[1] : '');
+ res.set('Link', `<${canonicalUrl}>; rel="canonical", <${alternateJsonUrl}>; rel="alternate"; type="application/json"`);
+
+ res.render('list', { rows, total, page, limit, city, cuisine, q, cities, cuisines, heroCuisines, showHero, newestRows, alternateJsonUrl });
} catch (e) { next(e); }
});
@@ -195,7 +209,10 @@ app.get('/c/:slug', async (req, res, next) => {
[city]
);
- res.render('city', { city, slug, total, cityCuisines, topRows });
+ const baseUrl = (process.env.PUBLIC_URL || `http://localhost:${PORT}`).replace(/\/+$/, '');
+ const alternateJsonUrl = `${baseUrl}/api/restaurants?city=${encodeURIComponent(city)}&limit=50`;
+ res.set('Link', `<${baseUrl}/c/${slug}>; rel="canonical", <${alternateJsonUrl}>; rel="alternate"; type="application/json"`);
+ res.render('city', { city, slug, total, cityCuisines, topRows, alternateJsonUrl });
} catch (e) { next(e); }
});
diff --git a/src/web/views/partials/head.ejs b/src/web/views/partials/head.ejs
index d78de85..3981384 100644
--- a/src/web/views/partials/head.ejs
+++ b/src/web/views/partials/head.ejs
@@ -8,6 +8,9 @@
</script>
<title><%= typeof title !== 'undefined' ? title + ' — LA County Eats' : 'LA County Eats — every restaurant in LA County' %></title>
<meta name="description" content="<%= typeof description !== 'undefined' ? description : 'Searchable directory of restaurants across Los Angeles County. ~37,000 spots. Address, neighborhood, cuisine, map.' %>">
+ <% if (typeof alternateJsonUrl !== 'undefined' && alternateJsonUrl) { %>
+ <link rel="alternate" type="application/json" href="<%= alternateJsonUrl %>" title="JSON results">
+ <% } %>
<meta name="theme-color" content="#fdf9ee" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1a1814" media="(prefers-color-scheme: dark)">
<link rel="preconnect" href="https://fonts.googleapis.com">
← bdac86f Remove all health-code/inspection/violation rendering and dr
·
back to Restaurant Directory
·
snapshot: initial structure (PLAN, REVIEW, db, migrations, s e322707 →