Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
8.0 KiB
Teknav Improvement Plan — Performance, SEO depth, Trust
Context
Teknav has a mature, high-performing SEO/content engine (an article ranks #1 on Google above Wikipedia). The remaining growth leverage is not more features — it's (1) shipping far less JavaScript to readers, (2) conservative, additive SEO depth that won't risk current rankings, and (3) light trust/footer polish on top of footer links the user already added.
Investigation confirmed the core problem: the reader SPA ships one undivided app bundle. vite.config.js:42 manualChunks only splits node_modules. There is zero route- or component-level splitting (React.lazy/Suspense appear nowhere). Concretely:
teknav-diagrams.jsximportsthreeand is statically imported by every page (teknav-home.jsx:5,teknav-articles.jsx:9,teknav-pages.jsx:14,teknav-en.jsx:5,teknav-admin.jsx:9) → three.js downloads on the homepage for every visitor.src/components/knowledge-graph.jsximportsd3(also heavy).teknav-admin.jsx(full CMS + editor) is statically imported inteknav-app.jsx:13→ admin code ships to every reader.
Goal: cut reader bundle weight (better LCP on Iranian mobile networks → better engagement and rankings) without touching the zero-CDN rule, RTL, or the static-prerender pipeline.
Sequencing decision (from user): measure first. Web push is out of scope. SEO changes must be additive and non-destructive.
Phase 0 — Baseline measurement (do first, no code changes)
Establish numbers so we fix what hurts and can prove the gain.
- Build prod:
npm run build, thennpm run preview(port 4173). - Bundle baseline: inspect
dist/assets/*.jssizes (sourcemaps already on,vite.config.js:26); record entry size and which chunks containthree/d3/admin. (Optional: add a one-offrollup-plugin-visualizerrun locally — do not commit it.) - Lighthouse (mobile, throttled) on
/, a/article/<slug>, and/again warm — capture LCP, TBT, total JS transfer. - Record results at top of this file as the "before" row.
Gate: the splitting work below targets the chunks Phase 0 proves are largest. If three/admin are not in the entry, re-scope.
Phase 1 — Performance / bundle (primary work)
1a. Lazy-load the heavy visualization components
Create thin lazy wrappers so the 5 existing import sites change minimally and three/d3 move into on-demand chunks:
- New
src/components/lazy-diagram.jsx:(Note:import { lazy, Suspense } from 'react'; const DiagramRenderer = lazy(() => import('./teknav-diagrams.jsx').then(m => ({ default: m.DiagramRenderer }))); export function LazyDiagram(props) { return <Suspense fallback={<div className="tek-diagram-skeleton" aria-hidden="true" />}> <DiagramRenderer {...props} /></Suspense>; }teknav-diagrams.jsxexportsDiagramRendereras a named export — wrapper maps it todefault.) - Replace the 5 static
import { DiagramRenderer }sites (teknav-home.jsx,teknav-articles.jsx,teknav-pages.jsx,teknav-en.jsx,teknav-admin.jsx) withLazyDiagram. - Same pattern for
knowledge-graph.jsx(d3) wherever it's mounted. - Add a small
.tek-diagram-skeletonstyle tosrc/styles/global.cssreusing existing tokens (no new animation primitives — reuse the shared keyframes per CLAUDE.md).
1b. Route-split admin/editor out of the reader path
- In
teknav-app.jsx, convert the heavy, reader-irrelevant routes toReact.lazy:AdminPanel,WriterWorkspace(both fromteknav-admin.jsx:13), andLoginPage. Keep reader-critical pages (HomePage,ArticleDetail) eagerly imported to avoid hydration jank. - Wrap
<main>{renderPage()}</main>(teknav-app.jsx:195) in a single<Suspense fallback={...}>. - This removes the entire CMS/editor graph from the default reader download.
1c. manualChunks hygiene
- Extend
manualChunks(vite.config.js:42) sothreeandd3get their own named chunks (vendor-three,vendor-d3). Once 1a makes them dynamic imports, Rollup splits them automatically; the named chunks just keep caching stable across deploys.
1d. Below-the-fold deferral on article page (measurement-gated)
- In
teknav-articles.jsx, lazy-mount non-critical widgets (comments, Q&A, related, semantic suggestions) so they don't block first render. Only do the ones Phase 0 shows are costly.
1e. Font delivery (small, safe)
- Audit
@font-faceinsrc/styles/global.css: ensurefont-display: swapand<link rel="preload">for the primary Vazirmatn/PelakFA weights only. Self-hosted already (zero-CDN preserved). No subsetting unless Phase 0 flags fonts as a major cost.
Phase 2 — SEO depth (conservative, additive only)
Premise from user: SEO already works — only add what helps and cannot ruin current results. So: no changes to existing article HTML, schema types, canonical/hreflang rules, or sitemap structure (all already correct; stories/compare/directory confirmed present in generate-seo.mjs).
Candidate additive wins (each independently revertible; pick after Phase 0/audit):
- Related-article internal links at the component level (render-time in
teknav-articles.jsx), NOT by mutating stored content HTML — strengthens topic-cluster interlinking with zero risk to existing pages. Verify whether a related block already exists before adding. - Breadcrumb JSON-LD via
src/lib/seo.jsxon article/category/glossary pages if not already emitted (purely additive structured data). - Re-run
npm run check:seoas a gate after any content-adjacent change (CLAUDE.md invariant). - Explicitly NOT extending
link-engine.tsto rewrite published content inline (risk of altering ranked pages). Leave glossary autolinking as-is.
If the audit shows nothing safe to add, Phase 2 is intentionally a no-op — that is an acceptable outcome.
Phase 3 — Trust / footer (light)
User already added the footer links. Scope here is only:
- Verify each footer link resolves in
renderPage(teknav-app.jsx:146) — report any that 404. - Optionally add an Editorial Guidelines (اصول تحریریه) page using the existing
LegalPage/LegalSectionpattern (teknav-pages.jsx) + route, since it's the highest-value missing E-E-A-T page for tech content. Confirm with user before building.
Files to modify (summary)
| File | Change |
|---|---|
src/components/lazy-diagram.jsx (new) |
Lazy wrapper for DiagramRenderer |
teknav-home/articles/pages/en/admin.jsx |
Swap static DiagramRenderer → LazyDiagram |
src/components/knowledge-graph.jsx consumers |
Lazy-mount d3 graph |
teknav-app.jsx |
React.lazy admin/writer/login + <Suspense> wrap |
vite.config.js |
vendor-three / vendor-d3 manualChunks |
teknav-articles.jsx |
Defer below-the-fold widgets; optional related-links block |
src/styles/global.css |
Diagram skeleton style; font-display/preload audit |
src/lib/seo.jsx |
Breadcrumb JSON-LD (if absent) |
teknav-pages.jsx + teknav-app.jsx |
(Optional) Editorial Guidelines page + route |
Constraints honored
Zero CDN (no new external assets); Persian-first RTL; reuse global.css keyframes/utilities; never edit dist/* by hand; static-prerender pipeline unchanged (diagrams are already client-rendered, so lazy-loading is no SEO regression).
Verification (end-to-end)
npm run buildsucceeds;check-no-cdnandcheck-responsivepass.- Confirm split chunks exist:
three/d3/admin no longer in the main entrydist/assets/*.js(compare to Phase 0 baseline). npm run preview: load/and an/article/<slug>— diagrams still render after hydration; no console errors; Suspense fallbacks behave.- Re-run Lighthouse mobile on the same 3 URLs; record "after" row and verify LCP/TBT/JS-transfer improvement.
npm run check:seoclean; spot-check a prerendereddist/article/<slug>/index.htmlstill has its content + schema.npm run compress-distproduces.gz/.brfor the new chunks (runs in pipeline).- Visit each footer link; confirm no 404s.