Services

Seo Review


SEO Review Consolidated Report

Date: 2026-06-26

This report consolidates the three audit artifacts under .awao/seo-review/:

  • .awao/seo-review/www.md
  • .awao/seo-review/product-apps.md
  • .awao/seo-review/content-apps.md

It preserves findings that are supported by those artifacts and their cited file references.

Executive summary#

The audited surfaces have solid foundations in several public areas: apps/www centralizes much of its metadata, has structured data helpers, and ships robots/sitemap files; apps/blog and apps/docs use metadata helpers for canonical/social metadata; apps/manager is correctly protected from indexing with both HTML robots metadata and deployment headers.

The most urgent SEO risks are concentrated in private/internal apps and content discovery:

  1. apps/admin is an internal admin/auth surface but lacks any noindex control, so it can be indexed if publicly deployed (apps/admin/index.html:4-7, apps/admin/src/router.tsx:15-17).
  2. apps/studio is an authenticated dashboard without global noindex controls in metadata or headers (apps/studio/pages/_app.tsx:167-172, apps/studio/next.config.ts:500-529).
  3. apps/academy can render unpublished course/content documents through its catch-all route because it does not filter matched documents by published (apps/academy/app/(course)/[...slug]/page.tsx:71-83).
  4. Multiple public sitemaps are incomplete or drift-prone, especially apps/www, apps/academy, and apps/docs.
  5. Canonical host handling is inconsistent across public surfaces, with mixed www.assistance.bg and assistance.bg usage in apps/www and apps/academy.

Global SEO risks and opportunities#

Risks#

  • Private surfaces can leak into search. apps/admin has no HTML robots tag or X-Robots-Tag header, and apps/studio lacks global noindex controls despite being described by route/layout evidence as an authenticated dashboard.
  • Sitemap coverage does not match indexable content. apps/www omits key App Router landing pages, apps/academy omits most published lesson/doc/learning pages, and apps/docs has a static sitemap missing current guide URLs.
  • Canonical signals are inconsistent. apps/www mixes https://www.assistance.bg with hardcoded https://assistance.bg; apps/academy uses the apex host while blog/docs use www.assistance.bg.
  • Some indexable pages are not intentionally controlled. UI Library example/demo routes inherit indexable app-level metadata and production headers, while Academy lesson pages are indexable but mostly absent from the sitemap.
  • Metadata drift is likely where helper usage is inconsistent. Manual metadata and legacy <Head> helpers remain in apps/www, and public docs apps lack explicit canonical URLs on inspected root/docs metadata.

Opportunities#

  • Standardize canonical origins and metadata helpers across public apps to reduce drift.
  • Generate sitemaps from route/content sources instead of maintaining static or legacy route lists.
  • Adopt layered noindex controls for private apps: HTML metadata plus deployment-level X-Robots-Tag headers.
  • Separate public docs/showcase routes from demo/auth/dashboard routes so indexability policy matches route intent.
  • Improve structured-data validity by fixing missing crawlable image assets referenced by JSON-LD.

App-by-app findings#

apps/www#

Strengths#

  • Centralized metadata helper creates absolute canonical, Open Graph, and Twitter metadata from shared constants (apps/www/lib/metadata.ts:19, apps/www/lib/metadata.ts:25, apps/www/lib/metadata.ts:27, apps/www/lib/metadata.ts:41).
  • Root layout sets metadataBase, default canonical, Open Graph, Twitter card metadata, icons, and site-wide Organization JSON-LD (apps/www/app/layout.tsx:42, apps/www/app/layout.tsx:44, apps/www/app/layout.tsx:48, apps/www/app/layout.tsx:61, apps/www/app/layout.tsx:97).
  • Structured data helpers cover Organization, WebSite, Service, BreadcrumbList, FAQPage, and BlogPosting (apps/www/components/JsonLd.tsx:30, apps/www/components/JsonLd.tsx:69, apps/www/components/JsonLd.tsx:96, apps/www/components/JsonLd.tsx:114, apps/www/components/JsonLd.tsx:149, apps/www/components/JsonLd.tsx:164).
  • Robots allow public crawling, block /dashboard, and point to the sitemap index (apps/www/public/robots.txt:1, apps/www/public/robots.txt:3, apps/www/public/robots.txt:5). Dashboard sign-in is explicitly noindex,nofollow (apps/www/app/dashboard/sign-in/page.tsx:7).

Findings#

  • P1: Sitemap omits main App Router landing pages. The generator only glob-matches legacy pages/*, content directories, and legacy content collections (apps/www/internals/generate-sitemap.mjs:10-17). The audit found important routes such as /services, /consulting, /infrastructure, /pricing, /resources, /runners, /trainings, /about, /contact-us, and /saas missing from apps/www/public/sitemap_www.xml; verification output showed count: 87, hasHome: false, hasServices: false.
  • P1: Canonical/OG domains are mixed. SITE_ORIGIN centralizes https://www.assistance.bg (apps/www/lib/constants.ts:34-39), but some pages hardcode apex URLs, including about (apps/www/app/about/page.tsx:13-14), SaaS (apps/www/app/saas/page.tsx:32-33), runners detail (apps/www/app/runners/[slug]/page.tsx:60-61), and case-study detail (apps/www/app/case-studies/[slug]/page.tsx:39).
  • P1: JSON-LD logo URL points to a missing file. ORGANIZATION_LOGO_PATH is /images/og/assistance-logo.png (apps/www/lib/constants.ts:46), used in JSON-LD (apps/www/components/JsonLd.tsx:36, apps/www/components/JsonLd.tsx:140), but the audit found no matching asset under apps/www/public/images/og/.
  • P2: /contact uses a client-side redirect. apps/www/app/contact/page.tsx:13 calls router.replace('/contact-us'), making the redirect JavaScript-dependent.
  • P2: Sitemap index references a docs sitemap outside the app output. apps/www/public/sitemap.xml:7 and apps/www/internals/generate-sitemap.mjs:139 reference https://www.assistance.bg/docs/sitemap.xml, while the checked apps/www/out only included sitemap.xml and sitemap_www.xml.
  • P2: Legacy/manual metadata increases drift risk. apps/www/lib/seo.tsx still emits <Head> tags manually, and apps/www/app/(pages)/beta/BetaPage.tsx uses next/head.

apps/admin#

  • P0: Internal admin app can be indexed if deployed publicly. apps/admin/index.html:4-7 lacks <meta name="robots" content="noindex, nofollow">; apps/admin/src/router.tsx:15-17 shows login/authenticated app routes; apps/admin/vite.config.ts:31-32 exposes dev/preview hosts; and the audit found no apps/admin/public/_headers file.

apps/studio#

  • P1: Authenticated dashboard lacks global noindex controls. Global metadata in apps/studio/pages/_app.tsx:167-172 does not set robots=noindex,nofollow; authenticated layouts set titles/descriptions without noindex (apps/studio/components/layouts/ProjectLayout/index.tsx:218-220, apps/studio/components/layouts/OrganizationLayout.tsx:60-63); global headers in apps/studio/next.config.ts:500-529 omit X-Robots-Tag; and no robots/sitemap files were found in the audit.

apps/manager#

  • Positive control: apps/manager/index.html:33-35 includes robots=noindex,nofollow, and apps/manager/public/_headers:1-6 blocks indexing with X-Robots-Tag while adding security headers.
  • apps/manager/vite.config.ts:11-20 replaces title/description placeholders with brand-aware text at build time.

apps/design-system#

  • P1: Public docs app has no first-party robots.txt or sitemap. The audit found no robots.* or sitemap.* under the app, while the app appears intended to be indexable through root metadata (apps/design-system/app/layout.tsx:15-32), per-doc metadata (apps/design-system/app/(app)/docs/[[...slug]]/page.tsx:45-64), and production _headers that allow indexing (apps/design-system/public/_headers:1-5).
  • P2: Canonical/OG URL handling should be tightened. Root metadata uses metadataBase: https://assistance.bg and a relative Open Graph URL (apps/design-system/app/layout.tsx:19-25). Per-doc metadata generates Open Graph URLs (apps/design-system/app/(app)/docs/[[...slug]]/page.tsx:52), but no alternates.canonical was found in inspected metadata.

apps/ui-library#

  • P1: Public docs app has no first-party robots.txt or sitemap. The audit found no robots.* or sitemap.*; root and doc metadata are indexable (apps/ui-library/app/layout.tsx:17-36, apps/ui-library/app/(app)/docs/[[...slug]]/page.tsx:45-55), and production _headers explicitly allow indexing (apps/ui-library/public/_headers:1-5).
  • P1: Example/demo routes are probably indexable. Example layouts such as apps/ui-library/app/example/password-based-auth/layout.tsx:6-8 set only a title; app-level metadata is indexable by default; and production headers allow indexing for the whole domain (apps/ui-library/public/_headers:1-5).
  • P2: Root metadata uses article semantics and unstable timestamps. apps/ui-library/app/layout.tsx:23-24 sets openGraph.type: 'article', and apps/ui-library/app/layout.tsx:28-29 sets publishedTime/modifiedTime from new Date().toISOString().
  • P2: Canonical/OG URL handling should be tightened. Root metadata uses metadataBase: https://assistance.bg and relative Open Graph URL (apps/ui-library/app/layout.tsx:21-27); per-doc Open Graph URLs are generated at apps/ui-library/app/(app)/docs/[[...slug]]/page.tsx:54, but no inspected metadata declared alternates.canonical.

apps/academy#

  • P1: Unpublished course lessons can render through the catch-all route. getPageFromParams reads Contentlayer documents (apps/academy/app/(course)/[...slug]/page.tsx:71) and matches allCourses, allLearnings, and allDocs without checking published (apps/academy/app/(course)/[...slug]/page.tsx:76, apps/academy/app/(course)/[...slug]/page.tsx:79, apps/academy/app/(course)/[...slug]/page.tsx:83). Contentlayer defines published fields/defaults (apps/academy/contentlayer.config.js:116, apps/academy/contentlayer.config.js:187, apps/academy/contentlayer.config.js:236). The audit found two explicitly unpublished course files: apps/academy/content/courses/nextjs/api-development/lab-04-database.md and apps/academy/content/courses/svelte/sveltekit-fullstack/lab-02-blog-dynamic-routes.md.
  • P1: Sitemap omits most indexable content pages. apps/academy/app/sitemap.ts:8 includes only allCourses.filter((c) => c.isIndex && c.published), and apps/academy/app/sitemap.ts:18 derives category pages from that subset. The audit counted 125 published course index pages, 1,963 published course lesson/lab pages, 21 learning articles, and 12 foundation/platform docs not represented by current sitemap logic.
  • P2: Canonical host differs from blog/docs. Blog defaults to https://www.assistance.bg/blog (apps/blog/lib/constants.ts:1), docs uses https://www.assistance.bg${BASE_PATH} (apps/docs/lib/constants.ts:14), while academy uses the apex host in metadata/sitemap helpers (apps/academy/app/layout.tsx:29, apps/academy/lib/seo.ts:3, apps/academy/app/sitemap.ts:4).
  • Positive control: protected academy routes explicitly noindex via apps/academy/app/(protected)/layout.tsx:7; training/category pages use buildSeoMetadata and ItemList JSON-LD (apps/academy/app/(trainings)/page.tsx:19, apps/academy/app/(trainings)/[category]/page.tsx:15).

apps/docs#

  • P1: Metadata can emit noindex,nofollow outside Vercel production. apps/docs/lib/constants.ts:10 defines production from NEXT_PUBLIC_VERCEL_ENV === 'production'; root metadata uses that flag for robots (apps/docs/app/layout.tsx:29-31); apps/docs/public/robots.txt:1 allows crawling, so HTML robots metadata can suppress indexing if the env var is missing or differs in production.
  • P2: Static sitemap is missing current guide content. apps/docs/public/sitemap.xml has 201 <loc> entries, while content scan found 212 guide pages. The static sitemap is missing 12 guide URLs listed in the source artifact, and apps/docs/public/robots.txt:3 points crawlers at this static sitemap.
  • Positive control: guide pages generate metadata from MDX frontmatter via genGuideMeta (apps/docs/features/docs/GuidesMdx.utils.tsx:202) and use canonical resolution (apps/docs/lib/seo.ts:22).

apps/blog#

  • P2: Sitemap uses request/build time for taxonomy and root lastModified. Category entries use new Date() (apps/blog/app/sitemap.ts:15-17), tag entries use new Date() (apps/blog/app/sitemap.ts:22-24), and the blog root uses new Date() (apps/blog/app/sitemap.ts:31-32).
  • Positive control: root and article metadata use a shared helper with canonical, Open Graph, and Twitter metadata (apps/blog/lib/seo.ts:83, apps/blog/app/[slug]/page.tsx:41). Articles have dynamicParams = false (apps/blog/app/[slug]/page.tsx:12) and JSON-LD for BlogPosting/breadcrumbs.

Prioritized remediation checklist#

P0#

  • Add layered noindex protection to apps/admin:
    • Add <meta name="robots" content="noindex, nofollow"> to apps/admin/index.html.
    • Add apps/admin/public/_headers with X-Robots-Tag: noindex, nofollow, following the pattern in apps/manager/public/_headers:1-6.

P1#

  • Add global noindex controls for authenticated apps/studio dashboard routes, preferably via X-Robots-Tag in apps/studio/next.config.ts; document any intentional public exceptions.
  • Filter unpublished apps/academy Contentlayer matches in apps/academy/app/(course)/[...slug]/page.tsx so published === false returns notFound().
  • Rework apps/www sitemap generation to include App Router landing pages, /, and primary conversion pages while excluding private/test/client-only redirect routes.
  • Expand apps/academy/app/sitemap.ts to include all published public allCourses, allLearnings, and allDocs, or explicitly noindex content types that should not be search landing pages.
  • Replace or generate apps/docs/public/sitemap.xml from current content so guide additions cannot drift from the sitemap.
  • Add app/robots.ts and app/sitemap.ts to apps/design-system and apps/ui-library for public docs/showcase discovery.
  • Add noindex metadata to apps/ui-library/app/example/* demo layouts or a shared example layout segment.
  • Standardize canonical host usage for apps/www and apps/academy; update hardcoded apex/www URLs to shared constants once the canonical host is confirmed.
  • Fix apps/www JSON-LD logo path by adding apps/www/public/images/og/assistance-logo.png or updating ORGANIZATION_LOGO_PATH to an existing crawlable asset.
  • Make apps/docs production indexability controlled by an explicit app-owned environment contract instead of only NEXT_PUBLIC_VERCEL_ENV.

P2#

  • Replace apps/www/app/contact/page.tsx client-side redirect with a server/static redirect compatible with the static export deployment.
  • Verify production serves https://www.assistance.bg/docs/sitemap.xml; remove the reference from apps/www/public/sitemap.xml if not served.
  • Migrate remaining apps/www manual/legacy <Head> usage to App Router metadata or isolate legacy helpers.
  • Set apps/ui-library root Open Graph type to website and remove dynamic root publishedTime/modifiedTime values.
  • Add explicit alternates.canonical metadata for apps/design-system and apps/ui-library root/docs pages.
  • Compute apps/blog taxonomy/root sitemap lastModified from newest relevant published posts instead of new Date().

Verification commands and manual checks#

Use mise exec -- ... for one-off raw tools that need managed runtimes, per repo guidance.

Sitemap and route coverage checks#

bash
1
mise exec -- node -e "const fs=require('fs'); const xml=fs.readFileSync('apps/www/public/sitemap_www.xml','utf8'); const urls=[...xml.matchAll(/<loc>(.*?)<\/loc>/g)].map(m=>m[1]); console.log({count:urls.length, hasHome:urls.includes('https://www.assistance.bg/'), hasServices:urls.includes('https://www.assistance.bg/services'), hasBlog:urls.some(u=>u.includes('/blog/')), hasCaseStudy:urls.some(u=>u.includes('/case-studies/'))});"
bash
1
mise exec -- node -e "const fs=require('fs'); const xml=fs.readFileSync('apps/docs/public/sitemap.xml','utf8'); console.log((xml.match(/<loc>/g)||[]).length);"

Manual checks:

  • Confirm all primary apps/www landing pages are present in the generated production sitemap after remediation.
  • Confirm https://www.assistance.bg/docs/sitemap.xml is served in production if it remains referenced by apps/www/public/sitemap.xml.
  • Confirm generated docs/academy/design-system/ui-library sitemaps include current content routes and exclude private/demo routes as intended.

Noindex/private-surface checks#

bash
1
rg -n "robots|X-Robots-Tag|noindex|nofollow" apps/admin apps/manager apps/studio

Manual checks:

  • Deploy or preview apps/admin and apps/studio, then inspect response headers for X-Robots-Tag: noindex, nofollow.
  • View rendered HTML for private/auth routes and verify <meta name="robots" content="noindex, nofollow"> where applicable.
  • Verify apps/manager retains its existing noindex behavior.

Canonical and Open Graph checks#

bash
1
rg -n "https://assistance\.bg|https://www\.assistance\.bg|alternates|canonical|openGraph" apps/www apps/academy apps/blog apps/docs apps/design-system apps/ui-library

Manual checks:

  • Confirm the intended canonical host is enforced consistently in metadata, sitemaps, structured data, and redirects.
  • Inspect public pages with browser devtools or rendered HTML to verify canonical and Open Graph URLs are absolute and host-consistent.

Structured data and asset checks#

bash
1
find apps/www/public/images -path '*og*' -o -name '*logo*'
2
rg -n "ORGANIZATION_LOGO_PATH|logo" apps/www/lib apps/www/components

Manual checks:

  • Validate representative public pages with Google's Rich Results Test or schema validator after fixing the logo URL.
  • Confirm referenced JSON-LD image/logo URLs return crawlable 200 responses with appropriate image dimensions.

Content publication checks#

bash
1
rg -n "published" apps/academy/app apps/academy/contentlayer.config.js
2
find apps/academy/content/courses -name '*.md' -o -name '*.mdx' | wc -l

Manual checks:

  • Request known unpublished academy URLs, including apps/academy/content/courses/nextjs/api-development/lab-04-database.md and apps/academy/content/courses/svelte/sveltekit-fullstack/lab-02-blog-dynamic-routes.md URL equivalents, and verify they return 404/not found after remediation.
  • Confirm published lessons/docs that should be public are either included in the sitemap or explicitly noindexed by product decision.