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-8is a client component and does not exportmetadataorgenerateMetadata.apps/academy/app/(trainings)/paths/[slug]/page.tsx:1-58is 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/pathsatapps/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:9readsNEXT_PUBLIC_BASE_PATH, andapps/academy/next.config.mjs:42-43applies it as bothbasePathandassetPrefix.- Root metadata assumes the production base path is
/academyatapps/academy/app/layout.tsx:21-35. apps/academy/app/robots.ts:8-17disallows/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-24imports onlyallCoursesand builds entries only for course index pages plus training categories.apps/academy/app/sitemap.ts:26-47returns the academy root,/trainings,/paths, categories, and course index pages; it excludesallDocs,allLearnings, and individual course module/lab pages that are rendered byapps/academy/app/(course)/[...slug]/page.tsx:71-85andapps/academy/app/(course)/[...slug]/page.tsx:287-419.apps/academy/app/sitemap.ts:5hard-codesBUILD_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
publishedfor docs, courses, and learnings atapps/academy/contentlayer.config.js:116-119,apps/academy/contentlayer.config.js:187, andapps/academy/contentlayer.config.js:236. - The sitemap/catalog paths filter published course indexes, e.g.
apps/academy/app/sitemap.ts:8-10andapps/academy/app/(trainings)/page.tsx:31-38. - The catch-all renderer does not filter by
published:apps/academy/app/(course)/[...slug]/page.tsx:71-85returns 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
CourseJsonLdatapps/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.pngfor Open Graph and Twitter images atapps/academy/app/layout.tsx:43-59. - The shared SEO helper uses the same 1200×630 image at
apps/academy/lib/seo.ts:3-5andapps/academy/lib/seo.ts:52-66. - The asset exists at
apps/academy/public/img/assistance-og-image.png, and favicon assets exist underapps/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. buildSeoMetadatacentralizes per-route title, description, canonical, OG, and Twitter metadata atapps/academy/lib/seo.ts:16-67./trainingsuses route-specific metadata and ItemList JSON-LD atapps/academy/app/(trainings)/page.tsx:16-30andapps/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-31andapps/academy/app/(trainings)/[category]/page.tsx:56-73. - Course/detail renderers use single visible
h1headers for course lessons and docs atapps/academy/app/(course)/[...slug]/page.tsx:344andapps/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.