perf: add Cache-Control to stable recipe & fitness API endpoints
rand_array seeds with Math.floor(time / 86400000), i.e. the same
shuffle for every caller during a UTC day — so every list endpoint
that runs through it is safe to share publicly:
- /items/all_brief, /items/category/[c], /items/tag/[t],
/items/icon/[i], /items/in_season/[m]
→ public, max-age=28800 (8h), s-maxage=28800, SWR=1d
The distinct-value lists (no shuffle, change only on recipe edit):
- /items/category, /items/tag, /items/icon
→ public, max-age=3600 (1h), s-maxage=86400 (1d), SWR=1w
Individual recipes change when their author edits them:
- /items/[name]
→ public, max-age=300 (5m), s-maxage=3600 (1h), SWR=1d
Fitness exercise-picker filters are identical for every logged-in
user but require auth:
- /fitness/exercises/filters
→ private, max-age=3600
Skipped the calendar page itself: its HTML embeds data.session via the
faith layout's <UserHeader>, so public caching would leak identity.
This commit is contained in:
@@ -13,7 +13,7 @@ Order = impact. Font items + app.html preload intentionally skipped.
|
||||
- [x] 7. Muscle-heatmap endpoint — add projection + O(1) bucket math. Overview already had a projection; set-subfield narrowing was attempted but reverted (returned malformed sets). Timeseries cap not feasible: totals are lifetime-scoped.
|
||||
- [x] 8. Calendar payload trim — `yearDays` narrowed to `{iso, color}` (needle lookup only), new pre-filtered `feastDots` array carries feast-specific metadata. Also fixed a stray double `locals.session ?? (locals.session ?? …)` in both calendar page loaders.
|
||||
- [x] 9. History sessions endpoint — projection narrowed to exactly what SessionCard reads (drops notes, templates, mode, endTime, session-level gpsPreview); added `.lean()`.
|
||||
- [ ] 10. `Cache-Control` headers on stable API endpoints (all_brief, calendar, exercises metadata)
|
||||
- [x] 10. `Cache-Control` headers on stable API endpoints — added to recipe `/items/category`, `/items/tag`, `/items/icon` (public, 1h/1d with SWR), `/items/[name]` (public, 5m/1h with SWR), and fitness `/exercises/filters` (private, 1h). Skipped `all_brief` (per-request shuffle) and calendar page (`data.session` serialised into HTML via layout → would leak across users under public cache).
|
||||
- [ ] 11. Search — debounce 100 ms + server-side pre-normalized `_searchKey`
|
||||
|
||||
## Features
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.46.23",
|
||||
"version": "1.46.24",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -42,10 +42,15 @@ function resolveNutritionData(mappings: any[]): any[] {
|
||||
});
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
await dbConnect();
|
||||
const en = isEnglish(params.recipeLang!);
|
||||
|
||||
// Individual recipes change when the author edits them. 5 min browser + 1 h
|
||||
// edge cache with SWR lets proxies keep hot recipes fresh without blocking
|
||||
// on the DB; stale content beyond max-age is tolerable here.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400' });
|
||||
|
||||
const query = en
|
||||
? { 'translations.en.short_name': params.name }
|
||||
: { short_name: params.name };
|
||||
|
||||
@@ -4,12 +4,19 @@ import { dbConnect } from '$utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||
|
||||
await dbConnect();
|
||||
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
|
||||
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||
|
||||
// rand_array is seeded by `floor(now / 86400000)` — stable for a full UTC
|
||||
// day across every caller — so the response is safe to share. 8 h browser +
|
||||
// 8 h edge cache means at worst the shuffle rolls into the next day a few
|
||||
// hours late; with SWR the stale payload still ships while a fresh one is
|
||||
// computed.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=28800, s-maxage=28800, stale-while-revalidate=86400' });
|
||||
|
||||
return json(rand_array(recipes));
|
||||
};
|
||||
|
||||
@@ -3,12 +3,17 @@ import { Recipe } from '$models/Recipe';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, prefix } = briefQueryConfig(params.recipeLang!);
|
||||
await dbConnect();
|
||||
|
||||
const field = `${prefix}category`;
|
||||
const categories = await Recipe.distinct(field, approvalFilter).lean();
|
||||
|
||||
// Distinct category list changes only on recipe add/edit. 1 h browser cache +
|
||||
// 1 d edge cache with SWR keeps the chip bar snappy; worst case a newly
|
||||
// added category shows up an hour late.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800' });
|
||||
|
||||
return json(categories);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
|
||||
await dbConnect();
|
||||
|
||||
@@ -14,5 +14,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
).lean();
|
||||
|
||||
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!)));
|
||||
// rand_array is seeded per UTC day, same for every caller → cacheable.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=28800, s-maxage=28800, stale-while-revalidate=86400' });
|
||||
return json(recipes);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,12 @@ import { Recipe } from '$models/Recipe';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import type {BriefRecipeType} from '$types/types';
|
||||
|
||||
export const GET: RequestHandler = async ({params}) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
await dbConnect();
|
||||
const icons = await Recipe.distinct('icon').lean();
|
||||
|
||||
// Same cache budget as /items/category.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800' });
|
||||
|
||||
return json(icons);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||
await dbConnect();
|
||||
|
||||
@@ -14,5 +14,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
).lean();
|
||||
|
||||
const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!)));
|
||||
// rand_array is seeded per UTC day, same for every caller → cacheable.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=28800, s-maxage=28800, stale-while-revalidate=86400' });
|
||||
return json(recipes);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
|
||||
|
||||
await dbConnect();
|
||||
@@ -14,5 +14,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
).lean();
|
||||
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||
|
||||
// rand_array is seeded per UTC day, same for every caller → cacheable.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=28800, s-maxage=28800, stale-while-revalidate=86400' });
|
||||
return json(rand_array(recipes));
|
||||
};
|
||||
|
||||
@@ -3,10 +3,14 @@ import { Recipe } from '$models/Recipe';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { isEnglish, briefQueryConfig } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter } = briefQueryConfig(params.recipeLang!);
|
||||
await dbConnect();
|
||||
|
||||
// Same cache budget as /items/category — distinct-values list that only
|
||||
// changes on recipe edit.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800' });
|
||||
|
||||
if (isEnglish(params.recipeLang!)) {
|
||||
const recipes = await Recipe.find(approvalFilter, 'translations.en.tags').lean();
|
||||
const tagsSet = new Set<string>();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { rand_array } from '$lib/js/randomize';
|
||||
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
export const GET: RequestHandler = async ({ params, setHeaders }) => {
|
||||
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
|
||||
|
||||
await dbConnect();
|
||||
@@ -14,5 +14,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
).lean();
|
||||
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||
|
||||
// rand_array is seeded per UTC day, same for every caller → cacheable.
|
||||
setHeaders({ 'Cache-Control': 'public, max-age=28800, s-maxage=28800, stale-while-revalidate=86400' });
|
||||
return json(rand_array(recipes));
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dbConnect } from '$utils/db';
|
||||
import { Exercise } from '$models/Exercise';
|
||||
|
||||
// GET /api/fitness/exercises/filters - Get available filter options
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
export const GET: RequestHandler = async ({ locals, setHeaders }) => {
|
||||
const session = locals.session ?? await locals.auth();
|
||||
if (!session || !session.user?.nickname) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
@@ -12,15 +12,21 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
|
||||
try {
|
||||
await dbConnect();
|
||||
|
||||
|
||||
const [bodyParts, equipment, targets] = await Promise.all([
|
||||
Exercise.distinct('bodyPart', { isActive: true }),
|
||||
Exercise.distinct('equipment', { isActive: true }),
|
||||
Exercise.distinct('target', { isActive: true })
|
||||
]);
|
||||
|
||||
|
||||
const difficulties = ['beginner', 'intermediate', 'advanced'];
|
||||
|
||||
|
||||
// Auth-gated but identical for every user. `private` keeps it out of any
|
||||
// shared cache (auth headers vary per user) while still letting the
|
||||
// browser reuse the response for 1 h — the filter picker is opened
|
||||
// repeatedly during a single session.
|
||||
setHeaders({ 'Cache-Control': 'private, max-age=3600' });
|
||||
|
||||
return json({
|
||||
bodyParts: bodyParts.sort(),
|
||||
equipment: equipment.sort(),
|
||||
|
||||
Reference in New Issue
Block a user