Kigumo TVC — SEO Audit & Implementation Report

Produced against codebase at kigumo-TVC/ · Domain: kigumotvc.ac.ke · Hosting: cPanel / Passenger (Node.js)

STEP 1 — Audit Findings (before changes)

sitemap.xml & robots.txt

FileStatusDetail
public/robots.txtMISSINGNo file existed. No crawl guidance, no Sitemap pointer.
public/sitemap.xmlMISSINGNo static or dynamic sitemap anywhere in the codebase.

Meta tags on public HTML pages

PagetitledescriptioncanonicalOG tagsviewport
index.html✓Weak — wrong county (Kirinyaga vs Murang'a)✗✗✓
about.html✓✓✗✗✓
courses.html✓✓✗✗✓
departments.html✓Too short / generic✗✗✓
news.html✓✓✗✗✓
admissions.html✓✓✗✗✓
contact.html✓✓✗✗✓
downloads.html✓✓✗✗✓
portals.html✓Vague — "and more"✗✗✓
apply.html✓✓✗✗✓
course-details.htmlGenericGeneric "Course details"✗✗✓
news-article.htmlGenericGeneric "News Article"✗✗✓

Dynamic pages (course-details.html, news-article.html)

Both pages use static HTML + client-side fetch — there is no server-side rendering. The HTML file is served with a generic title/description. Google's crawler does execute JavaScript, but the delay before JS runs means:

Favicon

Present — assets/images/logo.jpeg used as <link rel="icon"> on all pages. Not an ICO or PNG but JPEG is accepted by all modern browsers.

Heading structure (h1/h2)

Correct on all pages audited. Each page has exactly one visible <h1> in the body (injected via JS on dynamic pages once data loads). No skipped heading levels observed.

Image alt text

Mostly good — static images in index.html and about.html have descriptive alt text (e.g. "Kigumo TVC Logo", "Kenya Government Coat of Arms"). Dynamic images (news cards, department images, staff photos) correctly use the item's real name as alt text via escapeHtml(article.title), escapeHtml(dept.name), etc. No alt="" or missing alt attributes found on content images.

HTTPS / www vs non-www

Unresolved in code — The domain is kigumotvc.ac.ke (non-www, from docs). HTTPS is present (cPanel + SSL). However server/index.js had no redirect logic: a crawler reaching www.kigumotvc.ac.ke would get duplicate content. HTTP→HTTPS redirect is handled by cPanel/Apache at the webserver level (not visible in Node.js code), so it is assumed correct but cannot be verified from the codebase alone.

Structured data (JSON-LD)

None — No schema.org markup existed anywhere on the site.

Duplicate script tag bug

Bug — course-details.html loaded assets/js/navbar.js twice, which caused buildNavbar() to be called twice and the navbar to be rendered twice in the DOM.

STEP 2 — Changes Implemented

FileWhat changed & why
public/robots.txt CREATED New file. Allows all public pages. Disallows /portal/, /innovation/admin/, /innovation/coordinator/, /api/, /uploads/. Points to /sitemap.xml.
server/index.js EDITED (1) Sitemap route — GET /sitemap.xml added. Queries the live database for courses WHERE is_active=TRUE, all departments, and news_articles WHERE is_published=TRUE. Merges with 10 static pages. Returns valid XML with lastmod, changefreq, and priority. Non-fatal try/catch per DB query so static pages always serve even if DB is down. 1-hour cache header.

(2) www→non-www redirect — Middleware added at the very top (before CORS). Sends 301 redirect from www.kigumotvc.ac.ke to kigumotvc.ac.ke in production only.
public/index.html EDITED Fixed description (wrong county corrected to Murang'a). Added canonical, all OG tags, and EducationalOrganization JSON-LD schema with real address, phone, email, and Facebook URL from the site's footer.
public/about.html EDITED Improved description. Added canonical and OG tags.
public/courses.html EDITED Improved title and description to name the actual departments. Added canonical and OG tags.
public/departments.html EDITED Improved description to list actual department names. Added canonical and OG tags.
public/news.html EDITED Improved description. Added canonical and OG tags.
public/admissions.html EDITED Improved description. Added canonical and OG tags.
public/contact.html EDITED Improved description with real phone number and email from the site footer. Added canonical and OG tags.
public/downloads.html EDITED Improved description. Added canonical and OG tags.
public/portals.html EDITED Improved description to name actual portal types. Added canonical and OG tags.
public/apply.html EDITED Improved description. Added canonical and OG tags.
public/course-details.html EDITED Dynamic SEO injection: once loadCourseDetails() receives the course from /api/v1/courses/:id, it writes a unique document.title, <meta description>, <link rel="canonical">, all OG meta tags, and a Course JSON-LD block — each using the real course name, description, level, and department from the API response.

Default canonical set to /courses.html in HTML so pages without a valid ?id don't get indexed with a useless generic title.

Duplicate navbar.js script removed.
public/news-article.html EDITED Dynamic SEO injection: once loadArticle() receives the article, it writes a unique title, description (stripped from article body HTML, first 155 chars), canonical URL, all OG tags (including the article's own image if present, else fallback to logo), and a NewsArticle JSON-LD block with real datePublished.

Default canonical set to /news.html in HTML for the same fallback reason.
Architecture note on dynamic pages: This is a static HTML + client-side fetch site — there is no template engine or SSR. Google's crawler does execute JavaScript, so the injected tags will be seen on the second crawl pass. This is the correct approach for this architecture. A future improvement would be a prerendering proxy (e.g. Rendertron / SSR via Puppeteer), but that requires infrastructure changes outside this task scope.

STEP 3 — Outstanding Items & Decisions Needed

Decisions needed from you

www vs non-www canonical choice. The code now redirects www. → non-www (matching the docs which consistently use kigumotvc.ac.ke). If cPanel is actually configured the other way (non-www → www), the redirect in Node.js will fight it and cause redirect loops. Verify your cPanel "Redirects" or .htaccess settings and confirm which is the canonical version you want. If you prefer www, swap the redirect direction and update all canonical URLs in the HTML files.

Cannot verify from codebase alone

ItemWhy it needs manual action
Google Search Console domain verification You must add the site to Google Search Console, verify domain ownership (DNS TXT record via your registrar, or HTML file upload via cPanel), then submit https://kigumotvc.ac.ke/sitemap.xml under Indexing → Sitemaps.
HTTP → HTTPS redirect This must be enforced at the Apache/cPanel level (via .htaccess or cPanel's "Force HTTPS" toggle). Node.js cannot enforce it because the TLS termination happens at Apache before requests reach the Node app. Verify it is already on.
Favicon format The favicon is a JPEG (logo.jpeg). Modern browsers accept this, but for maximum compatibility (especially older Android and Windows) consider converting to a PNG or ICO and adding a 180×180 Apple Touch icon. No change was made as it works today.
admissions@kigumotvc.ac.ke Found in admissions.html source. This email does not appear in the official contact information (only kigumotvc@gmail.com is in the footer). Verify whether this address is active before it gets indexed.
Google Analytics / Search Console integration No analytics tag was found in the codebase. You cannot measure organic search traffic without it. Add a GA4 snippet or a Google Tag Manager container to assets/js/main.js or directly to each HTML <head>.
Crawler JS rendering for course/news pages Google will see the injected title and meta on its second crawl pass (after JS executes). Bing and other crawlers may not execute JS and will only see the fallback static title. There is no way to fix this without SSR or a prerendering proxy.
Social share image dimensions The og:image points to assets/images/logo.jpeg. Facebook and LinkedIn recommend 1200×630px. The current logo JPEG's actual dimensions were not checked. If it is small (e.g. a square logo), social share cards will look poor. Consider uploading a proper banner image and updating the og:image URL.

Not changed (already correct)