Services

Apps Academy


SEO review: apps/academy

Scope: Next.js metadata, course/content routes, canonical/OG/Twitter tags, robots/sitemap behavior, ContentLayer content handling, structured data, headings/content semantics, and public assets for apps/academy.

High severity#

H1 — Learning path routes inherit the academy homepage metadata and canonical#

Evidence

  • apps/academy/app/(trainings)/paths/page.tsx:1-8 is a client component and does not export metadata or generateMetadata.
  • apps/academy/app/(trainings)/paths/[slug]/page.tsx:1-58 is also client-only and does not export route metadata.
  • These routes therefore fall back to the root metadata canonical at apps/academy/app/layout.tsx:23-34, which points every inherited page to /academy.
  • The sitemap advertises /academy/paths at apps/academy/app/sitemap.ts:39-43, making this an indexable route with a self-canonical mismatch.

Impact

/academy/paths and /academy/paths/[slug] can be indexed as duplicates of the academy homepage or have their unique titles/descriptions ignored.

Recommended fix

Move client logic into child components and make both pages server components that export metadata/generateMetadata via buildSeoMetadata({ path: '/paths' }) and buildSeoMetadata({ path: '/paths/[slug]' }), using the actual slug at runtime where detail metadata is available. Alternatively, add a server layout.tsx for the paths segment with route-specific metadata.

H2 — Robots disallow rules miss the configured /academy base path#

Evidence

  • apps/academy/next.config.mjs:9 reads NEXT_PUBLIC_BASE_PATH, and apps/academy/next.config.mjs:42-43 applies it as both basePath and assetPrefix.
  • Root metadata assumes the production base path is /academy at apps/academy/app/layout.tsx:21-35.
  • apps/academy/app/robots.ts:8-17 disallows /dashboard, /certificates, /achievements, /groups, /settings, and /instructor, but not their /academy/... production paths.

Impact

When served under https://assistance.bg/academy, crawlers may not treat protected academy URLs such as /academy/dashboard or /academy/certificates as disallowed by robots.txt.

Recommended fix

Generate robots paths with the same base path used by the app, e.g. disallow /academy/dashboard, /academy/certificates, etc. Keep the sitemap URL aligned with that same canonical host/base-path source.

Medium severity#

M1 — Sitemap omits many indexable content routes and uses a fixed last-modified date#

Evidence

  • apps/academy/app/sitemap.ts:1-24 imports only allCourses and builds entries only for course index pages plus training categories.
  • apps/academy/app/sitemap.ts:26-47 returns the academy root, /trainings, /paths, categories, and course index pages; it excludes allDocs, allLearnings, and individual course module/lab pages that are rendered by apps/academy/app/(course)/[...slug]/page.tsx:71-85 and apps/academy/app/(course)/[...slug]/page.tsx:287-419.
  • apps/academy/app/sitemap.ts:5 hard-codes BUILD_DATE = new Date('2026-04-18T00:00:00.000Z') for every URL.

Impact

Search engines receive no sitemap hints for foundations/platform docs, case studies/success stories, or individual lessons/labs, and all submitted URLs claim the same stale modification date.

Recommended fix

Add published allDocs, allLearnings, and appropriate published course lesson/lab URLs to the sitemap, and compute lastModified from content metadata or source file timestamps. If lessons should not be search landing pages, explicitly noindex them instead of leaving them indexable but unsitemapped.

M2 — published: false content is not enforced by the dynamic content route#

Evidence

  • ContentLayer defines published for docs, courses, and learnings at apps/academy/contentlayer.config.js:116-119, apps/academy/contentlayer.config.js:187, and apps/academy/contentlayer.config.js:236.
  • The sitemap/catalog paths filter published course indexes, e.g. apps/academy/app/sitemap.ts:8-10 and apps/academy/app/(trainings)/page.tsx:31-38.
  • The catch-all renderer does not filter by published: apps/academy/app/(course)/[...slug]/page.tsx:71-85 returns the first matching course, learning, or doc regardless of publication state.

Impact

Draft or intentionally unpublished content can still render and be indexed if a crawler or user discovers the URL.

Recommended fix

Make getPageFromParams return only published documents, or return notFound()/noindex metadata for unpublished content consistently across courses, docs, and learnings.

M3 — Course lesson and learning pages have limited structured data#

Evidence

  • Course index pages emit Course JSON-LD via CourseJsonLd at apps/academy/app/(course)/[...slug]/page.tsx:171-183.
  • Individual course lesson pages render as articles with headings/content at apps/academy/app/(course)/[...slug]/page.tsx:287-365, but do not emit Article, LearningResource, VideoObject, or Breadcrumb structured data beyond the shared breadcrumb component.
  • Learning pages render article-style content at apps/academy/app/(course)/[...slug]/page.tsx:394-418, but also lack Article/BlogPosting structured data.

Impact

Important content pages have less eligibility for rich result interpretation than the course landing pages.

Recommended fix

Add JSON-LD for lesson pages (LearningResource or Article, optionally VideoObject when videoId/videoDuration exists) and learning/case-study pages (Article). Include canonical URL, title, description, provider/author, and breadcrumb where available.

Low severity#

L1 — Public OG image and favicon assets are present and correctly referenced#

Evidence

  • Root metadata references /academy/img/assistance-og-image.png for Open Graph and Twitter images at apps/academy/app/layout.tsx:43-59.
  • The shared SEO helper uses the same 1200×630 image at apps/academy/lib/seo.ts:3-5 and apps/academy/lib/seo.ts:52-66.
  • The asset exists at apps/academy/public/img/assistance-og-image.png, and favicon assets exist under apps/academy/public/favicon/.

Recommendation

Keep this as-is. Consider adding a quick asset existence check to SEO tests if one is introduced.

L2 — Core marketing/training pages have good title, canonical, OG, Twitter, and semantic heading coverage#

Evidence

  • The root layout defines title templates, description, canonical, OG, Twitter, and favicon metadata at apps/academy/app/layout.tsx:23-60.
  • buildSeoMetadata centralizes per-route title, description, canonical, OG, and Twitter metadata at apps/academy/lib/seo.ts:16-67.
  • /trainings uses route-specific metadata and ItemList JSON-LD at apps/academy/app/(trainings)/page.tsx:16-30 and apps/academy/app/(trainings)/page.tsx:92-113.
  • Training category pages use route-specific generated metadata and ItemList JSON-LD at apps/academy/app/(trainings)/[category]/page.tsx:13-31 and apps/academy/app/(trainings)/[category]/page.tsx:56-73.
  • Course/detail renderers use single visible h1 headers for course lessons and docs at apps/academy/app/(course)/[...slug]/page.tsx:344 and apps/academy/app/(course)/[...slug]/page.tsx:454.

Recommendation

Preserve the existing helper-based metadata pattern and extend it to the missing paths/content cases above.