Services

Seo Search Performance Baseline


SEO, search availability, and site performance baseline

Date: 2026-06-22

Scope: public web surfaces in apps/www, apps/docs, and apps/blog, plus shared search and performance guardrail tooling.

Executive summary#

  • SEO is partially covered across all three public apps: app-router metadata exists, production/preview robot handling exists, and generated sitemap artifacts are present for www and docs.
  • Search is configured in code for docs/blog/www through Meilisearch-compatible clients, but the local dev Meilisearch service currently has no docs or blog indexes, so public search availability depends on a separate indexing step that is not proven by health checks alone.
  • Performance guardrails exist (perf:smoke, Cloudflare artifact budgets, bundle analyzer hooks), but the current artifact budget check reports significant budget misses for www, docs, and blog from existing build outputs.
  • The strongest immediate remediation candidates are: make search/index availability observable, tighten performance budget scoping so stale/non-deploy artifacts do not dominate results, and normalize SEO sitemap/robots coverage for the blog app.

Verified commands#

Repo and tool surface#

1
$ mise exec -- pnpm exec tsx --version
2
tsx v4.20.3
3
node v24.15.0
1
$ mise run health
2
✓ Postgres (localhost:3048, container:healthy)
3
✓ Meilisearch (localhost:3049, container:healthy)
4
✓ Auth (localhost:3060, container:healthy)
5
✓ Mailpit (localhost:3062, container:healthy)
6
✓ SaaS API (localhost:3068, container:healthy)
7
✓ Prometheus (localhost:3069, container:healthy)
8
✓ Grafana (localhost:3070, container:healthy)

Search service availability#

1
$ curl -fsS http://localhost:3049/health
2
{"status":"available"}
3
4
$ curl -fsS -H 'Authorization: Bearer meili-dev-key' http://localhost:3049/indexes
5
{"results":[{"uid":"courses",...}],"offset":0,"limit":20,"total":1}

Finding: Meilisearch is running, but only the courses index is present. The docs search client uses the docs index (apps/docs/lib/meilisearch.ts, apps/docs/lib/search/indexes.ts), while blog/www use the blog index (apps/blog/lib/meilisearch.ts, apps/www/lib/meilisearch.ts). Those indexes are not currently present in the verified local service.

Performance guardrails#

1
$ mise exec -- pnpm perf:smoke -- --list
2
www: http://localhost:3040
3
/
4
/services
5
docs: http://localhost:3041
6
/
7
/docs
8
blog: http://localhost:3063
9
/
10
/blog
1
$ mise exec -- pnpm perf:cf-budgets -- --allow-missing --dry-run
2
www: 4958 files from apps/www/.next-build
3
FAIL javascriptGzip: 2.28 MiB / 1.50 MiB
4
FAIL assetsRaw: 279.1 MiB / 25.0 MiB
5
blog: 8779 files from apps/blog/.open-next
6
FAIL javascriptGzip: 23.0 MiB / 1.50 MiB
7
FAIL assetsRaw: 552.8 MiB / 20.0 MiB
8
FAIL largestAssetRaw: 10.0 MiB / 5.00 MiB
9
docs: 5964 files from apps/docs/.open-next
10
FAIL javascriptGzip: 23.4 MiB / 1.50 MiB
11
FAIL assetsRaw: 393.4 MiB / 20.0 MiB
12
FAIL largestAssetRaw: 37.1 MiB / 5.00 MiB

The same command also reported failures for other apps because the script defaults to all configured apps. The public-surface failures above are the relevant ones for this review.

Note: mise run perf:cf-budgets -- --allow-missing --dry-run is not available as a mise task in the current task catalog (mise ERROR no task perf:cf-budgets found), while the root pnpm script exists.

1
$ mise exec -- pnpm --filter docs test -- app/api/crawlers/route.smoke.test.ts
2
Test Files 14 passed | 1 skipped (15)
3
Tests 85 passed | 2 skipped (87)

The docs test command excluded *.smoke.test.ts through the package script, so the named crawler smoke test was not actually included despite the path argument. This is a test-surface mismatch worth fixing separately.

SEO current state#

apps/www#

Evidence:

  • Root app metadata sets metadataBase, default title/description, canonical, Open Graph, Twitter, and icons in apps/www/app/layout.tsx.
  • Page helper pageMetadata() centralizes title/description/canonical/Open Graph/Twitter image defaults in apps/www/lib/metadata.ts.
  • Site-wide Organization JSON-LD is injected from apps/www/components/JsonLd.tsx via root layout.
  • apps/www/public/robots.txt allows public crawling, disallows /dashboard, and points to https://www.assistance.bg/sitemap_www.xml.
  • apps/www/public/sitemap.xml is a sitemap index for sitemap_www.xml and /docs/sitemap.xml.
  • apps/www/public/sitemap_www.xml exists and contains public URLs.

Risks / gaps:

  • robots.txt points only at sitemap_www.xml, not the sitemap index sitemap.xml; crawlers can still discover URLs, but the index is the more complete entrypoint.
  • pageMetadata() canonical values are relative (canonical: path) while Open Graph URLs are absolute. Next can resolve relative canonicals via metadataBase, but the apps are inconsistent: blog uses absolute canonicals.
  • www includes many /blog/... URLs in sitemap_www.xml, while the separate blog app also owns /blog; sitemap ownership should be explicit to avoid drift.

apps/docs#

Evidence:

  • Root metadata sets metadataBase: new URL(PROD_URL), robots based on NEXT_PUBLIC_VERCEL_ENV, Open Graph, and Twitter in apps/docs/app/layout.tsx.
  • Guide SEO helpers generate canonical URLs, TechArticle JSON-LD, breadcrumbs, and WebSite SearchAction in apps/docs/lib/seo.ts and apps/docs/features/docs/GuidesMdx.template.tsx.
  • apps/docs/public/robots.txt allows crawling and points to https://www.assistance.bg/docs/sitemap.xml.
  • apps/docs/public/sitemap.xml exists and contains docs URLs.
  • apps/docs/next.config.mjs adds host-aware X-Robots-Tag: all on assistance.bg and noindex on Vercel/Pages preview hosts.

Risks / gaps:

  • The root metadata robots value is controlled by NEXT_PUBLIC_VERCEL_ENV === 'production'. Non-Vercel production builds could emit noindex metadata even when host headers allow indexing.
  • The docs app has a SearchAction pointing to /api/search/docs, but local search index availability is not verified by the standard health task.
  • The focused crawler smoke test path was not honored because the package test script excludes *.smoke.test.ts.

apps/blog#

Evidence:

  • Root metadata sets metadataBase, title template, canonical, RSS alternate, Open Graph, and Twitter in apps/blog/app/layout.tsx.
  • createPageMetadata() emits absolute canonical URLs and Open Graph/Twitter metadata in apps/blog/lib/seo.ts.
  • Blog home and article pages inject JSON-LD (WebSite, Blog, BlogPosting, breadcrumbs) through apps/blog/components/JsonLd.tsx.
  • Dynamic robots.ts allows public crawling and points to ${SITE_ORIGIN}/sitemap.xml.
  • Dynamic sitemap.ts emits home, post, category, and tag URLs.
  • _headers sets production indexing headers and noindex headers for preview hosts.

Risks / gaps:

  • SITE_ORIGIN defaults to https://assistance.bg/blog, while www and docs use https://www.assistance.bg. That is a canonical host mismatch unless intentionally served on the apex host.
  • Dynamic robots disallow /docs/, which is outside the blog app's scoped responsibility and can create confusing crawler behavior if mounted at /blog.
  • apps/blog/public has _headers only; no static robots.txt or sitemap.xml artifact is present there. This is fine for runtime Next routes, but Cloudflare/static deployment expectations should be verified for the actual target.

Search availability current state#

Evidence:

  • Docs search route: apps/docs/app/api/search/docs/route.ts returns [] for empty query and catches search exceptions by returning [].
  • Docs search provider abstraction: apps/docs/lib/search/factory.ts supports meilisearch and cloudflare/d1 providers.
  • Docs Meilisearch client and indexer normalize incorrect local MEILI_URL values from port 7700 to the repo's dev port 3049.
  • Blog and www have identical lib/meilisearch.ts clients for the blog index.
  • Root scripts include index:docs, index:blog, and index:all in package.json.

Risks / gaps:

  • Verified local Meilisearch has only a courses index; docs and blog were absent.
  • mise run health proves the Meilisearch service is up, but not that required indexes exist or have documents.
  • Search routes catch failures and return an empty array, which prevents user-facing errors but can hide operational outages.
  • Root index:blog currently targets www (turbo run index:meilisearch --filter=www) even though there is also an apps/blog package with an index:meilisearch script. This may be intentional because www owns older blog content, but the split should be clarified.

Performance current state#

Evidence:

  • tools/scripts/performance-smoke.mjs measures route navigation timing, resource counts, script counts, and transfer/encoded size against tools/performance-smoke-routes.json.
  • tools/scripts/check-cloudflare-artifact-budgets.mjs measures JavaScript gzip size, raw asset size, largest asset, and worker size against tools/cloudflare-artifact-budgets.json.
  • apps/www/next.config.mjs and apps/docs/next.config.mjs include bundle analyzer hooks gated by ANALYZE=true.
  • apps/blog/next.config.mjs excludes @vercel/og from OpenNext server bundles and uses unoptimized images.
  • Current artifact budget check reports budget failures for www, docs, and blog.

Risks / gaps:

  • The artifact budget script counts entire output directories, including potentially stale or non-deployed artifacts, which can produce noisy failures (apps/www/.next-build alone reported 279.1 MiB raw assets).
  • perf:smoke requires servers to already be running, so it is useful as a local smoke gate but not a standalone CI guarantee.
  • The route set is narrow (/, /services, /docs, /blog) and does not include representative search/article/guide pages.
  • Blog has no bundle analyzer wrapper unlike www and docs.

Prioritized remediation list#

  1. Search availability proof: add a narrow health/index verification command or script that checks required Meilisearch indexes (docs, blog) and document counts, and document/run it from the root command surface.
  2. Search outage visibility: adjust docs/blog search behavior or telemetry/logging so unavailable indexes are distinguishable from valid zero-result searches in server logs and tests.
  3. Blog canonical host normalization: decide whether blog canonicals should use https://www.assistance.bg/blog or https://assistance.bg/blog; align apps/blog/lib/constants.ts, robots, sitemap, and metadata accordingly.
  4. Robots/sitemap entrypoints: point www robots at the sitemap index or include all relevant sitemap files; verify blog robots/sitemap behavior under the actual deployment mode.
  5. Performance budget scoping: update Cloudflare artifact budget checks to ignore stale/cache/source-map/non-deployed artifacts or run only against clean build outputs. The current check fails substantially on existing artifacts.
  6. Performance route coverage: add representative guide/article/search routes to tools/performance-smoke-routes.json once local preview coverage is stable.
  7. Docs crawler smoke test surface: provide a package script or Vitest invocation that includes *.smoke.test.ts when intentionally targeting smoke tests.
  8. Mise task catalog alignment: expose perf:cf-budgets (or document mise exec -- pnpm perf:cf-budgets) so the performance budget command is available from the preferred repo-level entrypoint.