diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 022f5d120..6ed1eca7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,13 +27,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: pnpm diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bec2e9b35..f5545baf4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,7 +16,7 @@ concurrency: jobs: analyze: name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest + runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }} timeout-minutes: 360 permissions: actions: read @@ -31,13 +31,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: pnpm diff --git a/.github/workflows/deploy_vercel.yaml b/.github/workflows/deploy_vercel.yaml index 50ce67674..805d7a432 100644 --- a/.github/workflows/deploy_vercel.yaml +++ b/.github/workflows/deploy_vercel.yaml @@ -35,13 +35,13 @@ jobs: - name: Target environment run: echo "Deploying to ${{ inputs.environment }}" - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: "24" cache: pnpm diff --git a/.github/workflows/disable-default-codeql.yml b/.github/workflows/disable-default-codeql.yml index 429e48f63..99b3bfaa1 100644 --- a/.github/workflows/disable-default-codeql.yml +++ b/.github/workflows/disable-default-codeql.yml @@ -10,7 +10,7 @@ concurrency: jobs: disable-default: name: Disable Default CodeQL Configuration - runs-on: ubuntu-latest + runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }} permissions: security-events: write actions: read @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Disable Default CodeQL Setup uses: github/codeql-action/init@v3 diff --git a/.github/workflows/docker-build-publish-dev.yml b/.github/workflows/docker-build-publish-dev.yml index ae4dbe293..b8534a526 100644 --- a/.github/workflows/docker-build-publish-dev.yml +++ b/.github/workflows/docker-build-publish-dev.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Resolve image tags id: meta @@ -32,6 +32,7 @@ jobs: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} DIGITALOCEAN_REGISTRY: ${{ vars.DIGITALOCEAN_REGISTRY || 'ever' }} + DO_ENABLED: ${{ vars.DO_ENABLED }} run: | set -euo pipefail @@ -57,7 +58,10 @@ jobs: fi digitalocean_enabled=false - if [[ -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then + # DigitalOcean image pushes are opt-in: gated behind vars.DO_ENABLED == 'true' + # so the registry.digitalocean.com tag is never added (and buildx never pushes + # to DO) unless DO is explicitly enabled. DO code is kept, just skipped by default. + if [[ "${DO_ENABLED:-}" == "true" && -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then do_registry="$(printf '%s' "$DIGITALOCEAN_REGISTRY" | tr '[:upper:]' '[:lower:]')" tags+=( "registry.digitalocean.com/${do_registry}/${image_name}:latest" @@ -75,7 +79,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -83,26 +87,26 @@ jobs: - name: Login to Docker Hub if: steps.meta.outputs.dockerhub_enabled == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Install doctl - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Login to DigitalOcean Container Registry - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} run: doctl registry login --expiry-seconds 3600 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and publish image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: Dockerfile diff --git a/.github/workflows/docker-build-publish-prod.yml b/.github/workflows/docker-build-publish-prod.yml index 6ba3b48b5..c8f87a9c0 100644 --- a/.github/workflows/docker-build-publish-prod.yml +++ b/.github/workflows/docker-build-publish-prod.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Resolve image tags id: meta @@ -32,6 +32,7 @@ jobs: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} DIGITALOCEAN_REGISTRY: ${{ vars.DIGITALOCEAN_REGISTRY || 'ever' }} + DO_ENABLED: ${{ vars.DO_ENABLED }} run: | set -euo pipefail @@ -57,7 +58,10 @@ jobs: fi digitalocean_enabled=false - if [[ -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then + # DigitalOcean image pushes are opt-in: gated behind vars.DO_ENABLED == 'true' + # so the registry.digitalocean.com tag is never added (and buildx never pushes + # to DO) unless DO is explicitly enabled. DO code is kept, just skipped by default. + if [[ "${DO_ENABLED:-}" == "true" && -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then do_registry="$(printf '%s' "$DIGITALOCEAN_REGISTRY" | tr '[:upper:]' '[:lower:]')" tags+=( "registry.digitalocean.com/${do_registry}/${image_name}:latest" @@ -75,7 +79,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -83,26 +87,26 @@ jobs: - name: Login to Docker Hub if: steps.meta.outputs.dockerhub_enabled == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Install doctl - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Login to DigitalOcean Container Registry - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} run: doctl registry login --expiry-seconds 3600 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and publish image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: Dockerfile diff --git a/.github/workflows/docker-build-publish-stage.yml b/.github/workflows/docker-build-publish-stage.yml index 7723c7253..d3aed6b44 100644 --- a/.github/workflows/docker-build-publish-stage.yml +++ b/.github/workflows/docker-build-publish-stage.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Resolve image tags id: meta @@ -32,6 +32,7 @@ jobs: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} DIGITALOCEAN_REGISTRY: ${{ vars.DIGITALOCEAN_REGISTRY || 'ever' }} + DO_ENABLED: ${{ vars.DO_ENABLED }} run: | set -euo pipefail @@ -57,7 +58,10 @@ jobs: fi digitalocean_enabled=false - if [[ -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then + # DigitalOcean image pushes are opt-in: gated behind vars.DO_ENABLED == 'true' + # so the registry.digitalocean.com tag is never added (and buildx never pushes + # to DO) unless DO is explicitly enabled. DO code is kept, just skipped by default. + if [[ "${DO_ENABLED:-}" == "true" && -n "$DIGITALOCEAN_ACCESS_TOKEN" && -n "$DIGITALOCEAN_REGISTRY" ]]; then do_registry="$(printf '%s' "$DIGITALOCEAN_REGISTRY" | tr '[:upper:]' '[:lower:]')" tags+=( "registry.digitalocean.com/${do_registry}/${image_name}:latest" @@ -75,7 +79,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -83,26 +87,26 @@ jobs: - name: Login to Docker Hub if: steps.meta.outputs.dockerhub_enabled == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Install doctl - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Login to DigitalOcean Container Registry - if: steps.meta.outputs.digitalocean_enabled == 'true' + if: ${{ steps.meta.outputs.digitalocean_enabled == 'true' && vars.DO_ENABLED == 'true' }} run: doctl registry login --expiry-seconds 3600 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and publish image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: Dockerfile diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f32f53dab..ec3510fc4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,13 +22,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: pnpm diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 66d32a9e9..5914a6b81 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -108,13 +108,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' cache: pnpm @@ -250,7 +250,7 @@ jobs: - name: Upload Playwright report (shard ${{ matrix.shard }}) if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: playwright-report-shard-${{ matrix.shard }} path: apps/web-e2e/playwright-report @@ -258,7 +258,7 @@ jobs: - name: Upload test results (shard ${{ matrix.shard }}) if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: playwright-test-results-shard-${{ matrix.shard }} path: apps/web-e2e/test-results diff --git a/.github/workflows/k8s-build.yml b/.github/workflows/k8s-build.yml index 3867dac9f..b665b694f 100644 --- a/.github/workflows/k8s-build.yml +++ b/.github/workflows/k8s-build.yml @@ -11,7 +11,7 @@ permissions: packages: write jobs: build: - runs-on: ${{ vars.RUNNER_LINUX_X64_8 || vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }} + runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }} strategy: fail-fast: false matrix: @@ -19,10 +19,13 @@ jobs: - image: directory-web-template dockerfile: Dockerfile context: . + - image: directory-web-template-docs + dockerfile: apps/docs/Dockerfile + context: . steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -35,7 +38,7 @@ jobs: main|master) echo "tag=prod" >> "$GITHUB_OUTPUT" ;; *) echo "tag=${GITHUB_REF_NAME//\//-}" >> "$GITHUB_OUTPUT" ;; esac - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} diff --git a/apps/docs/Dockerfile b/apps/docs/Dockerfile new file mode 100644 index 000000000..9c1e75254 --- /dev/null +++ b/apps/docs/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1.7 +# Ever Works directory-web-template — Docusaurus documentation site (apps/docs = @ever-works/docs). +# Serves docs.demo.ever.works. Mirrors the repo's OWN root Dockerfile (turbo prune) + the ever-works +# docs build. Docs content lives in the repo-root docs/ (Docusaurus `path: ../../docs`). +# docker build -f apps/docs/Dockerfile -t ghcr.io/ever-works/directory-web-template-docs . + +ARG NODE_VERSION=22-alpine +ARG PNPM_VERSION=10.31.0 +ARG TURBO_VERSION=2.9.14 + +# ---- base ------------------------------------------------------------------ +FROM node:${NODE_VERSION} AS base +RUN corepack enable && \ + corepack prepare pnpm@${PNPM_VERSION} --activate && \ + npm install -g turbo@${TURBO_VERSION} +ENV CI=true +ENV NEXT_TELEMETRY_DISABLED=1 + +# ---- pruner: isolate the docs workspace + its deps ------------------------- +FROM base AS pruner +WORKDIR /app +COPY . . +RUN turbo prune @ever-works/docs --docker + +# ---- builder: install deps + build the static site ------------------------ +FROM base AS builder +WORKDIR /app +# NB: do NOT set NODE_ENV=production for the install — Docusaurus build tooling +# lives in devDependencies. NODE_OPTIONS raises the heap for the multi-locale build. +ENV NODE_OPTIONS="--max-old-space-size=4096" +RUN apk add --no-cache git libc6-compat python3 make g++ pkgconfig + +COPY --from=pruner /app/out/json/ . +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# Internal Verdaccio cache override (fork-safe; empty => public npm), same as the root image. +ARG VERDACCIO_REGISTRY="" +RUN if [ -n "$VERDACCIO_REGISTRY" ]; then \ + echo "registry=${VERDACCIO_REGISTRY}" >> /app/.npmrc && \ + { sed -i "s|https://registry.npmjs.org|${VERDACCIO_REGISTRY%/}|g" /app/pnpm-lock.yaml 2>/dev/null || true; }; \ + fi + +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +COPY --from=pruner /app/out/full/ . +# Docusaurus reads content from ../../docs (the repo-root docs/); turbo prune omits +# non-workspace dirs, so copy it in explicitly (resolves to /app/docs -> apps/docs/../../docs). +COPY docs/ ./docs/ + +# English-only build. The full multi-locale SSG (build) renders every locale (en/es/fr/pt/…) +# of the large API docs, which on the ARC RAM-disk runner runs for ~2h and then dies with +# ENOSPC. `build:en` (docusaurus build --locale en) is a fraction of the size/time and fits. +# TODO: re-enable full-locale once the docs image builds on a disk-backed runner. +RUN pnpm --filter @ever-works/docs build:en + +# ---- runtime: nginx serving the static build ------------------------------ +FROM nginx:1.27-alpine +COPY apps/docs/nginx.conf /etc/nginx/conf.d/default.conf +RUN rm -rf /usr/share/nginx/html/* +COPY --from=builder /app/apps/docs/build /usr/share/nginx/html +EXPOSE 80 diff --git a/apps/docs/nginx.conf b/apps/docs/nginx.conf new file mode 100644 index 000000000..d87152f84 --- /dev/null +++ b/apps/docs/nginx.conf @@ -0,0 +1,46 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Standard text-asset compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + application/rss+xml + image/svg+xml; + + # Long cache for hashed/static assets emitted by Docusaurus + location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|webp|ico)$ { + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + try_files $uri =404; + } + + # No cache for HTML so deploys ship instantly + location ~* \.html$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + try_files $uri =404; + } + + # Serve clean URLs (Docusaurus emits both /foo/ and /foo/index.html) + location / { + try_files $uri $uri/ $uri.html $uri/index.html =404; + } + + # Custom 404 page from Docusaurus + error_page 404 /404.html; + location = /404.html { + internal; + } +} diff --git a/apps/web/lib/cache-config.ts b/apps/web/lib/cache-config.ts index e0db4710d..41c93364d 100644 --- a/apps/web/lib/cache-config.ts +++ b/apps/web/lib/cache-config.ts @@ -68,3 +68,21 @@ export const CACHE_TAGS = { /** Comparisons by locale */ COMPARISONS_LOCALE: (locale: string) => `comparisons:${locale}` } as const; + +/** + * Max number of items a *listing* may contain to still be persisted in Next's Data Cache + * (`unstable_cache`). + * + * `unstable_cache` hard-caps a single entry at 2MB and *silently drops* anything larger — the + * entry is then recomputed AND re-serialized on every request (`Failed to set Next.js data cache … + * items over 2MB can not be cached`). On a large directory this pegs CPU. Listing items are + * metadata-only (the markdown body is stripped in `fetchItems`, ~0.8KB/item), so ~2000 items + * ≈ 1.6MB stays safely under the 2MB ceiling. Bigger catalogues skip the persistent layer and + * fall back to the in-memory `fetchItems` cache (10-min TTL, cleared on content sync) instead. + * + * Override with the `CONTENT_DATA_CACHE_MAX_ITEMS` env var if your items are unusually large/small. + */ +export const DATA_CACHE_MAX_ITEMS: number = (() => { + const raw = Number(process.env.CONTENT_DATA_CACHE_MAX_ITEMS); + return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 2000; +})(); diff --git a/apps/web/lib/content.ts b/apps/web/lib/content.ts index 52817845f..2f4a6a2ef 100644 --- a/apps/web/lib/content.ts +++ b/apps/web/lib/content.ts @@ -10,7 +10,7 @@ import { dirExists, fsExists, getContentPath } from './lib'; import { getContentConfigPaths, mergeConfigObjects } from './content-config-file'; import { unstable_cache } from 'next/cache'; import { PaymentInterval, PaymentProvider } from './constants'; -import { CACHE_TAGS, CACHE_TTL as CONTENT_CACHE_TTL } from './cache-config'; +import { CACHE_TAGS, CACHE_TTL as CONTENT_CACHE_TTL, DATA_CACHE_MAX_ITEMS } from './cache-config'; import { Collection } from '@/types/collection'; import type { ComparisonData, ComparisonDetail, ComparisonDimension } from '@/types/comparison'; import type { ItemLocationData } from '@/lib/types/item'; @@ -2131,11 +2131,28 @@ export async function fetchHeroContent(source: string, locale: string = 'en'): P // Listings: short-TTL unstable_cache keyed by content revision so cold starts // don't re-walk the YAML tree. Single-item / page reads keep the longer TTL. +/** + * A listing may only be persisted in Next's Data Cache (`unstable_cache`) if it fits the 2MB + * per-entry ceiling. Beyond it, `unstable_cache` silently discards the entry and recomputes + + * re-serializes the whole payload on EVERY request (`items over 2MB can not be cached`), which + * pegs CPU on large directories. When a listing is too big we skip the persistent layer and let + * the in-memory `fetchItemsCache` (10-min TTL, cleared on content sync) serve it instead. + * See {@link DATA_CACHE_MAX_ITEMS}. + */ +const canPersistListing = (itemCount: number): boolean => itemCount <= DATA_CACHE_MAX_ITEMS; + export const getCachedItems = async (options: FetchOptions = {}) => { if (!CONTENT_CACHE_ENABLED) { return fetchItems(options); } + // `fetchItems` is in-memory cached, so this is cheap on the hot path and lets us size-gate + // the (2MB-capped) Data Cache layer before paying to serialize an oversized catalogue. + const result = await fetchItems(options); + if (!canPersistListing(result.items.length)) { + return result; + } + const locale = options.lang || 'en'; const revision = await getContentRevision(); const optionsKey = JSON.stringify(options); @@ -2290,6 +2307,11 @@ export const getCachedItemsByCategory = async (raw: string, options: FetchOption return fetchByCategory(raw, options); } + const result = await fetchByCategory(raw, options); + if (!canPersistListing(result.items.length)) { + return result; + } + const locale = options.lang || 'en'; const revision = await getContentRevision(); const optionsKey = JSON.stringify(options); @@ -2308,6 +2330,11 @@ export const getCachedItemsByTag = async (raw: string, options: FetchOptions = { return fetchByTag(raw, options); } + const result = await fetchByTag(raw, options); + if (!canPersistListing(result.items.length)) { + return result; + } + const locale = options.lang || 'en'; const revision = await getContentRevision(); const optionsKey = JSON.stringify(options); @@ -2326,6 +2353,11 @@ export const getCachedItemsByCategoryAndTag = async (category: string, tag: stri return fetchByCategoryAndTag(category, tag, options); } + const result = await fetchByCategoryAndTag(category, tag, options); + if (!canPersistListing(result.items.length)) { + return result; + } + const locale = options.lang || 'en'; const revision = await getContentRevision(); const optionsKey = JSON.stringify(options);