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
wwwanddocs. - Search is configured in code for docs/blog/www through Meilisearch-compatible clients, but the local dev Meilisearch service currently has no
docsorblogindexes, 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 forwww,docs, andblogfrom 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 --version2tsx v4.20.33node v24.15.01$ mise run health2✓ 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/health2{"status":"available"}34$ curl -fsS -H 'Authorization: Bearer meili-dev-key' http://localhost:3049/indexes5{"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 -- --list2www: http://localhost:30403 /4 /services5docs: http://localhost:30416 /7 /docs8blog: http://localhost:30639 /10 /blog1$ mise exec -- pnpm perf:cf-budgets -- --allow-missing --dry-run2www: 4958 files from apps/www/.next-build3 FAIL javascriptGzip: 2.28 MiB / 1.50 MiB4 FAIL assetsRaw: 279.1 MiB / 25.0 MiB5blog: 8779 files from apps/blog/.open-next6 FAIL javascriptGzip: 23.0 MiB / 1.50 MiB7 FAIL assetsRaw: 552.8 MiB / 20.0 MiB8 FAIL largestAssetRaw: 10.0 MiB / 5.00 MiB9docs: 5964 files from apps/docs/.open-next10 FAIL javascriptGzip: 23.4 MiB / 1.50 MiB11 FAIL assetsRaw: 393.4 MiB / 20.0 MiB12 FAIL largestAssetRaw: 37.1 MiB / 5.00 MiBThe 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.
Focused SEO-related test evidence#
1$ mise exec -- pnpm --filter docs test -- app/api/crawlers/route.smoke.test.ts2Test Files 14 passed | 1 skipped (15)3Tests 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 inapps/www/app/layout.tsx. - Page helper
pageMetadata()centralizes title/description/canonical/Open Graph/Twitter image defaults inapps/www/lib/metadata.ts. - Site-wide Organization JSON-LD is injected from
apps/www/components/JsonLd.tsxvia root layout. apps/www/public/robots.txtallows public crawling, disallows/dashboard, and points tohttps://www.assistance.bg/sitemap_www.xml.apps/www/public/sitemap.xmlis a sitemap index forsitemap_www.xmland/docs/sitemap.xml.apps/www/public/sitemap_www.xmlexists and contains public URLs.
Risks / gaps:
robots.txtpoints only atsitemap_www.xml, not the sitemap indexsitemap.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 viametadataBase, but the apps are inconsistent: blog uses absolute canonicals.wwwincludes many/blog/...URLs insitemap_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 onNEXT_PUBLIC_VERCEL_ENV, Open Graph, and Twitter inapps/docs/app/layout.tsx. - Guide SEO helpers generate canonical URLs, TechArticle JSON-LD, breadcrumbs, and WebSite
SearchActioninapps/docs/lib/seo.tsandapps/docs/features/docs/GuidesMdx.template.tsx. apps/docs/public/robots.txtallows crawling and points tohttps://www.assistance.bg/docs/sitemap.xml.apps/docs/public/sitemap.xmlexists and contains docs URLs.apps/docs/next.config.mjsadds host-awareX-Robots-Tag: allonassistance.bgandnoindexon 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 inapps/blog/app/layout.tsx. createPageMetadata()emits absolute canonical URLs and Open Graph/Twitter metadata inapps/blog/lib/seo.ts.- Blog home and article pages inject JSON-LD (
WebSite, Blog, BlogPosting, breadcrumbs) throughapps/blog/components/JsonLd.tsx. - Dynamic
robots.tsallows public crawling and points to${SITE_ORIGIN}/sitemap.xml. - Dynamic
sitemap.tsemits home, post, category, and tag URLs. _headerssets production indexing headers and noindex headers for preview hosts.
Risks / gaps:
SITE_ORIGINdefaults tohttps://assistance.bg/blog, whilewwwand docs usehttps://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/publichas_headersonly; no staticrobots.txtorsitemap.xmlartifact 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.tsreturns[]for empty query and catches search exceptions by returning[]. - Docs search provider abstraction:
apps/docs/lib/search/factory.tssupportsmeilisearchandcloudflare/d1providers. - Docs Meilisearch client and indexer normalize incorrect local
MEILI_URLvalues from port 7700 to the repo's dev port 3049. - Blog and www have identical
lib/meilisearch.tsclients for theblogindex. - Root scripts include
index:docs,index:blog, andindex:allinpackage.json.
Risks / gaps:
- Verified local Meilisearch has only a
coursesindex;docsandblogwere absent. mise run healthproves 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:blogcurrently targetswww(turbo run index:meilisearch --filter=www) even though there is also anapps/blogpackage with anindex:meilisearchscript. This may be intentional becausewwwowns older blog content, but the split should be clarified.
Performance current state#
Evidence:
tools/scripts/performance-smoke.mjsmeasures route navigation timing, resource counts, script counts, and transfer/encoded size againsttools/performance-smoke-routes.json.tools/scripts/check-cloudflare-artifact-budgets.mjsmeasures JavaScript gzip size, raw asset size, largest asset, and worker size againsttools/cloudflare-artifact-budgets.json.apps/www/next.config.mjsandapps/docs/next.config.mjsinclude bundle analyzer hooks gated byANALYZE=true.apps/blog/next.config.mjsexcludes@vercel/ogfrom OpenNext server bundles and uses unoptimized images.- Current artifact budget check reports budget failures for
www,docs, andblog.
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-buildalone reported 279.1 MiB raw assets). perf:smokerequires 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
wwwanddocs.
Prioritized remediation list#
- 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. - 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.
- Blog canonical host normalization: decide whether blog canonicals should use
https://www.assistance.bg/blogorhttps://assistance.bg/blog; alignapps/blog/lib/constants.ts, robots, sitemap, and metadata accordingly. - Robots/sitemap entrypoints: point
wwwrobots at the sitemap index or include all relevant sitemap files; verify blog robots/sitemap behavior under the actual deployment mode. - 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.
- Performance route coverage: add representative guide/article/search routes to
tools/performance-smoke-routes.jsononce local preview coverage is stable. - Docs crawler smoke test surface: provide a package script or Vitest invocation that includes
*.smoke.test.tswhen intentionally targeting smoke tests. - Mise task catalog alignment: expose
perf:cf-budgets(or documentmise exec -- pnpm perf:cf-budgets) so the performance budget command is available from the preferred repo-level entrypoint.