Services

Public Content Seo Audit 2026 06 17


Public content SEO audit — marketing, docs, blog, academy, design-system, ui-library

Date: 2026-06-17

Scope reviewed: apps/www/app, apps/blog, apps/docs, apps/academy, apps/design-system, and apps/ui-library, focused on page metadata, heading structure, indexable content, internal links, image alt usage where practical, and duplicate/thin metadata patterns.

Ranked findings#

P0 — No critical blocker found#

The six public surfaces all define root metadata in their app layouts, and the highest-value public landing pages have crawlable text, internal links, and explicit titles/descriptions. I did not find a single site-wide robots/noindex misconfiguration in the static app-layout review.

Evidence:

  • apps/www/app/layout.tsx defines root metadata and a skip link to #main-content.
  • apps/blog/app/layout.tsx defines BLOG_NAME title templating and BLOG_DESCRIPTION.
  • apps/docs/app/layout.tsx sets metadataBase, production-aware robots, Open Graph, and Twitter metadata.
  • apps/academy/app/layout.tsx sets templated titles, canonical /academy, Open Graph image alt text, and Twitter metadata.
  • apps/design-system/app/layout.tsx and apps/ui-library/app/layout.tsx define app-level title, description, Open Graph, and Twitter metadata.

P1 — Many public app-router pages rely on inherited metadata or route-specific generators are absent#

Several public pages do not export route-level metadata/generateMetadata. This is not automatically wrong, but it means search snippets can collapse to generic app-level descriptions, especially for high-intent routes.

Representative files flagged by the static page scan:

  • apps/www/app/contact/page.tsx and apps/www/app/contact-sales/page.tsx — contact intent pages with no route-level metadata.
  • apps/www/app/get-quote/page.tsx and apps/www/app/get-quote/[category]/page.tsx — conversion pages with no route-level metadata.
  • apps/www/app/guides/[[...slug]]/page.tsx — guide pages have no local metadata export in the page module.
  • apps/www/app/trainings/[platform]/page.tsx and apps/www/app/trainings/[platform]/[slug]/page.tsx — training detail/category routes have no route-level metadata export in the page module.
  • apps/academy/app/(course)/courses/page.tsx, apps/academy/app/(course)/courses/[category]/page.tsx, apps/academy/app/(trainings)/paths/page.tsx, and apps/academy/app/(trainings)/paths/[slug]/page.tsx — course/path discovery routes lack route-level metadata.
  • apps/design-system/app/(app)/page.tsx and apps/ui-library/app/(app)/page.tsx — app home pages inherit layout metadata only.

Recommendation: add deterministic route-level metadata/generateMetadata for conversion and index pages first, using each app's existing metadata helper patterns (apps/www/lib/metadata.ts, apps/blog/lib/seo.ts, academy buildSeoMetadata usage in apps/academy/app/(trainings)/page.tsx).

P1 — Several page modules have no literal <h1> in the page component#

The static scan found many page modules with no literal <h1> tag. Some may render headings through imported components or MDX, so this requires rendered-page validation before changing code. However, the risk is that route-level content starts with cards/navigation instead of one clear document heading.

Representative files:

  • apps/www/app/page.tsx
  • apps/www/app/about/page.tsx
  • apps/www/app/pricing/page.tsx
  • apps/www/app/case-studies/page.tsx
  • apps/www/app/(pages)/features/page.tsx
  • apps/blog/app/page.tsx
  • apps/blog/app/[slug]/page.tsx
  • apps/docs/app/guides/services/[[...slug]]/page.tsx
  • apps/academy/app/(course)/courses/page.tsx

Counterexamples with clear literal page headings include apps/docs/app/page.tsx, apps/academy/app/(trainings)/page.tsx, apps/design-system/app/(app)/page.tsx, and apps/ui-library/app/(app)/page.tsx.

Recommendation: during the next SEO pass, validate rendered HTML for top landing pages and ensure each indexable route has exactly one meaningful <h1> matching the search intent and title.

P2 — Academy content has missing and duplicate frontmatter descriptions at scale#

A static frontmatter scan across apps/academy/content, apps/blog/_blog, apps/design-system/content, and apps/ui-library/content found 2,484 content files and 202 files missing title/description/frontmatter. The first misses are concentrated in academy course modules/labs, including:

  • apps/academy/content/courses/azure/azure-aks-fundamentals/lab-01-create-your-first-aks-cluster.md
  • apps/academy/content/courses/kubernetes/kubernetes-administrator/module-01-architecture.md
  • apps/academy/content/courses/kubernetes/kubernetes-administrator/lab-02-install-cluster.md

The same scan also found duplicate or templated descriptions, for example:

  • 7 Azure AI Foundry labs share Hands-on lab for the corresponding module in Azure AI Foundry Studio course.
  • 6 Azure AI Fundamentals labs share Hands-on exploration of Azure AI services for the corresponding module topic.
  • 8 Azure Machine Learning labs share Hands-on lab for the corresponding Azure Machine Learning module.
  • 4 UI Library current-user-avatar pages share Assistance Auth-aware avatar across framework variants.
  • 6 UI Library dropzone pages share Displays a control for easier uploading of files directly to Assistance Storage across framework variants.

Recommendation: prioritize unique descriptions for course index pages, course overview pages, and public docs pages before long-tail lab modules. For framework variants, include the framework name in the description to reduce duplicate snippets while preserving consistent component taxonomy.

P2 — Some app-level metadata is thin or semantically mismatched#

The static layout review found a few layout-level descriptions that are too generic for rich snippets or use questionable Open Graph type defaults:

  • apps/ui-library/app/layout.tsx description is Provides a library of components for your project, which is thin and not specific to Assistance, frameworks, auth, realtime, or storage components.
  • apps/ui-library/app/layout.tsx sets root openGraph.type to article and uses current timestamps for publishedTime/modifiedTime in app layout metadata, which can produce misleading article metadata on non-article pages.
  • apps/design-system/app/layout.tsx and apps/ui-library/app/layout.tsx set Open Graph image URLs but no image alt text.

Recommendation: make app layout descriptions specific and evergreen, use website for root app Open Graph types, and add image alt text where layouts define OG images.

The static review found strong internal linking on the docs home page (apps/docs/app/page.tsx), resources page (apps/www/app/resources/page.tsx), consulting/infrastructure/runners pages, and app home pages for design-system/ui-library. Cross-app links often point to absolute URLs such as https://assistance.bg/blog from apps/www/app/resources/page.tsx, which is appropriate if these apps deploy as separate origins/subpaths but should be rendered-checked for canonical consistency.

Recommendation: use a narrow rendered crawl of top pages to confirm canonical URL, navigation links, and breadcrumb links agree with deployment paths.

Suggested next steps#

  1. Add route-level metadata to the high-intent routes listed in P1.
  2. Run rendered HTML checks for top pages to verify one <h1>, canonical URL, and robots behavior.
  3. Batch-generate or hand-author unique academy frontmatter descriptions for missing/duplicated course modules.
  4. Tighten UI Library root metadata (description, OG type, OG image alt).

Static verification commands used#

bash
1
mise exec -- node - <<'JS'
2
const fs=require('fs'), path=require('path');
3
function walk(d){let out=[]; for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); if(e.isDirectory()) out=out.concat(walk(p)); else if(e.name==='page.tsx') out.push(p)} return out}
4
for (const root of ['apps/www/app','apps/blog/app','apps/docs/app','apps/academy/app','apps/design-system/app','apps/ui-library/app']){
5
console.log('\n## '+root); for(const p of walk(root).sort()){const s=fs.readFileSync(p,'utf8'); const meta=s.includes('export const metadata')||s.includes('generateMetadata'); const h1=(s.match(/<h1\\b/g)||[]).length; if(!meta||h1===0) console.log(`${p} metadata=${meta} h1=${h1}`)}
6
}
7
JS
8
9
mise exec -- node - <<'JS'
10
const fs=require('fs'), path=require('path');
11
function walk(d,exts){let out=[]; if(!fs.existsSync(d)) return out; for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); if(e.isDirectory()) out=out.concat(walk(p,exts)); else if(exts.some(x=>e.name.endsWith(x))) out.push(p)} return out}
12
const files=[...walk('apps/blog/_blog',['.mdx']),...walk('apps/academy/content',['.md','.mdx']),...walk('apps/design-system/content',['.mdx']),...walk('apps/ui-library/content',['.mdx'])];
13
const desc=new Map(), missing=[];
14
for(const f of files){const s=fs.readFileSync(f,'utf8'); const fm=s.match(/^---\\n([\\s\\S]*?)\\n---/); if(!fm){missing.push(f); continue} const title=fm[1].match(/^title:\\s*[\"']?(.+?)[\"']?\\s*$/m); const d=fm[1].match(/^description:\\s*[\"']?(.+?)[\"']?\\s*$/m); if(!title||!d) missing.push(f); if(d){const v=d[1].replace(/^[\"']|[\"']$/g,''); (desc.get(v)||desc.set(v,[]).get(v)).push(f)}}
15
console.log('content files',files.length); console.log('missing title/description/frontmatter',missing.length); console.log(missing.slice(0,20).join('\\n'));
16
console.log('duplicate descriptions >1:'); for(const [d,arr] of desc) if(arr.length>1) console.log(arr.length, d, arr.slice(0,5).join(' | '));
17
JS