MaahirSkills · Internal QA · v1.0 · full-site audit

Skill-check platform before the first candidate

A full-surface QA audit of skills.maahir.io — every linked page crawled, every page re-rendered at mobile (390px), tablet (768px) and desktop (1440px), and every form, API and interactive element exercised end to end. The marketing site is genuinely well built. The candidate-facing surface underneath it is not ready: the contact form silently discards every message, the signup flow demands a company name from job seekers, and the assessment engine has no assessment configured for any live job.

Target skills.maahir.io Run v1.0 · 07 Nov 2026 22:40 PKT Method browser + API + source Pages 28 crawled · 84 viewport runs Status 6 critical · 9 significant · 14 working
6 critical — blocks launch 9 significant — fix before launch 14 working — regression-test every deploy
The verdict The marketing site is clean, fast and consistent — zero broken internal links across 48 unique URLs, zero horizontal overflow at any breakpoint, zero console errors, Urdu RTL rendering correct everywhere, and the auth API behaves properly (401 on bad creds, 422 on validation, 302 on unauthenticated dashboard access).

What is broken sits underneath that surface. Six things block a real candidate from completing the product's core promise: the contact form throws messages away, the assessment engine has no assessment configured for any of the 10 live jobs, job seekers are forced to supply a company name, the job board is serving lorem-ipsum test data to the public, the primary JS bundle ships as 0 bytes, and every desktop dropdown is unusable on a touch screen.

None of these are cosmetic. Every one of them sits directly on the path from "candidate sees a job" to "employer gets a ranked list".

01.Scope & coverage

What was actually exercised, and what was deliberately left out.

S1

Covered

Complete
  • 28 pages crawled via HTTP — every page reachable from the homepage footer, header and body, plus 30 additional route probes.
  • 84 page-viewport runs — each page rendered at 390×844, 768×1024 and 1440×900 with Playwright, measuring overflow, broken images, touch-target size, font loading, RTL direction and console errors.
  • 48 unique internal links and 19 static assets individually HTTP-checked for status and content-type.
  • Every form exercised — login, register, contact, and the full apply flow — in both JavaScript-enabled and JavaScript-disabled browsers.
  • Every API endpoint probed with real payloads: /api/auth/login, /api/auth/register, /api/public/jobs, /api/public/apply.
  • Full candidate journey walked: job board → job card → apply form → API submit → assessment token → assessment page → done page.
  • Interactive elements clicked: mobile hamburger, all three dropdown triggers, FAQ accordions, job search + both filters, agency modals.
  • Source read for all 28 pages — inline scripts, meta tags, JSON-LD, heading structure, form markup, and every served asset.
S2

Not covered — and why

Blocked
  • Authenticated employer / HR surfaces. /dashboard redirects to /login with no credentials supplied. Everything behind the login wall — candidate ranking, bulk invites, pipeline kanban, CSV export, override UI, the hiring-AI assistant — is untested.
  • Paywall / subscription enforcement. The pricing page advertises a 14-day Team trial and JazzCash/EasyPaisa payment. No checkout route was reachable from any page, so billing was not exercised.
  • Real assessment grading. Every live job has assessment_template_id: null, so there is no question bank to grade against. The assessment engine's correctness is completely unverified — this is the highest-value untested area remaining.
  • Real email delivery. One registration was created to test the endpoint; the confirmation email was not verified as arriving.
  • Load, concurrency and security penetration testing were out of scope for this pass.

02.Critical findings

Each of these breaks a real user journey. In priority order.

#1

The contact form discards every message and shows a fake success screen

Fix before launch

The /contact form's submit handler does not send anything. It hides the form, reveals a green "Thank you! Your message has been sent. We'll get back to you within 24 hours." panel, and stops there. The source comment says so outright:

// In production, this would POST to /api/contact
document.getElementById('contactForm').classList.add('hidden');
document.getElementById('successMessage').classList.remove('hidden');

There is no fetch call, no action attribute and no method attribute on the form. The matching API route does not exist — POST /api/public/contact returns 404 "The route api/public/contact could not be found."

Verified: filled the form with a valid name, email and message, clicked Send Message, and watched the network panel — zero API requests were made while the success panel appeared instantly. With JavaScript disabled, the form falls back to a GET and dumps the user's message into the URL query string:

https://skills.maahir.io/contact?name=QA&email=qa%40example.com&subject=&message=msg
Why it matters: every inbound lead, sales enquiry, support request and the entire "Talk to sales" CTA on the pricing page silently evaporates. Nothing is logged, nothing is delivered, and the user is told it worked. This is the most damaging class of bug on the site — it destroys data while displaying success. It also puts candidate PII (name, email, message body) into browser history and server access logs via the GET fallback.
#2

No live job has an assessment — the candidate's core promise ends at "Assessment not configured."

Fix before launch

All 10 jobs returned by the public jobs API carry assessment_template_id: null and assessment_required: true simultaneously. The job cards promise "Skill test included" and "≈25 min test". The candidate applies, the API returns 201 with a valid assessment token, and the assessment page renders exactly one line:

Assessment not configured.

Verified: completed a real application for Junior Accountant at Systems Limited. The apply API returned 201, the browser was redirected to /assessment/jX3vIQ6BNow…, and that page displayed "Assessment not configured." — with no question, no progress indicator, no way to continue and no error state.

The one real job with a description — Intern (Finance and Account) at Server4Sale — also has a null template. Separately, the assessment page's own source contains an offline banner ("You appear to be offline. Your answers are saved locally and will sync when you reconnect.") sitting in the default DOM alongside the completion screen, which needs a state check before it can render.

Why it matters: the entire product promise is "every job includes a short skill test" and "your verified score goes straight to the employer." Right now a candidate can apply, gets routed to a dead end, and the employer receives an application with no skill evidence. The marketing copy on /jobs, / and /pricing is actively contradicted by the product.
#3

The job seeker cannot register — signup demands a company name

Fix before launch

Every page in the site repeats one promise, in the footer of all 28 pages: "Free for job seekers". On the homepage it becomes "For every job seeker — free forever" and "Free for job seekers, CNIC never shared, Works on slow internet". The pricing page states "Job seekers never pay" and gives job seekers their own "FREE forever" tier.

The registration form requires company_name as a non-optional field:

$ curl -X POST https://skills.maahir.io/api/auth/register \
    -d '{"name":"…","email":"…","password":"…","password_confirmation":"…"}'
{"message":"The company name field is required.","errors":{"company_name":["The company name field is required."]}} 422

Supplying a company name creates a tenant and registers the account with the role tenant_owner — the employer/organisation role, not a candidate role. There is no candidate signup path anywhere on the site. The only job-seeking CTA on the homepage, "I want a job", points at /register — the employer form.

Why it matters: the platform is free for job seekers precisely because employers pay. Forcing a job seeker to invent a company name to sign up is both a conversion dead-end and a data-integrity problem — every candidate account pollutes the employer tenant table, and every "employer" metric on the site becomes wrong. This is the single most commercially costly defect on the site.
#4

The public job board is serving lorem-ipsum test data

Fix before launch

The job board at /jobs is the most public-facing product surface, and it currently lists test data. Verified against the live /api/public/jobs response — 10 published jobs, of which the first three are:

  • "Stone Sawyer" — Agency T · "North Jamalberg" — "Error officiis iste ea qui. Est est deserunt deleniti quia. Necessitatibus labore laboriosam labore velit."
  • "Mold Maker" — Agency T · "Hackettville" — "Optio debitis praesentium rerum consequatur fugit dicta…"
  • "Anthropologist" — Probe Agency A · "East Jaquan" — "Reiciendis est dolore rem tempora quo…"

The city filter dropdown on /jobs offers East Jaquan, Hackettville, North Jamalberg alongside the real Pakistani cities. These are Faker-generated place names, not Pakistan. The tenants are named "Agency T" and "Probe Agency A".

This directly breaks the "Built for Pakistan" positioning and the "Made for Pakistan" badge, and it is the first thing any employer, candidate or investor will see. It also poisons the /apply funnel — the very first job card links straight into an apply form for a fictional role in a fictional city.

Why it matters: a public job board full of lorem ipsum is the clearest possible "not ready" signal, and it sits on the homepage's primary Jobs nav link. Worse, because is_published: true is set on these records, this is live content — not a staging preview someone forgot to hide.
#5

The primary JavaScript bundle ships as 0 bytes

Fix before launch

Every page loads the same module bundle. It is empty on the wire:

$ curl -I https://skills.maahir.io/build/assets/app-BvRk9kiK.js
HTTP/2 200
content-type: application/javascript
content-length: 0

The CSS bundle beside it is healthy — app-Ckq0k-Cg.css returns 62,019 bytes and renders correctly. The build/manifest.json lists the full asset set including Inter and Noto Nastaliq Urdu webfonts, and those fonts do load correctly, so the build pipeline itself ran — the emitted JS file is the thing that came out empty.

Impact today is contained but fragile. The critical interactive paths — mobile hamburger, contact form, login, register, job board, apply form, assessment, agency workspace — all carry their own inline <script> blocks, so they still work. That is luck, not design: the page is carrying a 7.2 KB inline copy of a browser-logging script on every single page while the intended bundle is empty, and any new component that assumes the bundle will ship will fail silently in production.

Why it matters: an empty entry-point bundle is a broken build that currently happens to be masked. It also means the whole site ships ~7.9 KB of inline script on every page for no user benefit — the browser logger is a dev tool that should not be in production at all.
#6

All desktop navigation dropdowns are unusable on touch devices

Fix before launch

The three header dropdowns — For Organizations, For Individuals FREE and Platform — are opened purely by CSS group-hover. They carry no click handler, no aria-expanded state change, and no keyboard focus target that opens them.

Verified on an emulated iPhone (390×844, touch enabled): tapping For Organizations did not open the panel — measured panel height 0px, computed visibility false, after a 5-second tap attempt that timed out because the element never became interactive. The same markup on desktop opens fine on hover.

This matters more than usual for this product specifically: the site's own homepage feature list advertises "Works on slow internet" and "Built for Pakistan: Urdu, PKR, CNIC-safe identity, WhatsApp, NAVTTC-aligned" and the platform page sells "Built for Pakistan — Urdu, mobile, 3G". A Pakistani candidate or recruiter on a phone cannot reach Hiring, Campus & Workforce, Skill Passport, Govt Job Prep, Certification Prep or Skill Paths from the header. The mobile hamburger does open the same destinations — but the hamburger is the only way in, and every one of those destinations is also a top-level section of the product.

Why it matters: the entire mobile nav depends on one hamburger button that the header dropdowns duplicate. On touch, the primary navigation is a single 44px target, and the two richest navigation surfaces in the site are unreachable from it by hover.

03.Significant findings

Not launch-blocking on their own, but each one costs credibility, reach or conversion.

#7

Applying with empty required fields produces no feedback at all

Fix before launch

The apply form is declared <form id="applyForm" novalidate>. The submit handler calls form.checkValidity() and then form.reportValidity() — but because novalidate is set on the form element, reportValidity() is suppressed and the browser never shows its native bubble.

Verified: clicked Apply with first_name and email empty. Result: no API request fired, no error message anywhere in the DOM, the button stayed enabled and unchanged, and the user was left on the same page with no idea why nothing happened. The inputs were correctly flagged as invalid internally (:invalid, "Please fill out this field.") — the information existed, it just was never surfaced.

Why it matters: a dead-looking button on the highest-intent form on the site. Most candidates will assume the form is broken and leave.
#8

A raw enum key leaks into the job card UI

Fix before launch

The job card renderer maps employment types through a fixed lookup:

const typeLabels = { full_time: 'Full-time', part_time: 'Part-time',
                     contract: 'Contract', internship: 'Internship' };

The API also returns "intern" — a value the map does not cover. The fallback is j.employment_type, so the raw key is rendered verbatim. The live Intern (Finance and Account) card at Server4Sale displays:

Intern (Finance and Account)
Server4Sale · Karachi
intern  Skill test included

A lowercase bare enum key sitting next to a properly-capitalised badge. The same map mismatch means the Contract filter option exists in the dropdown but no job has that type, and Internship and intern are treated as separate types in the filter even though both are internships.

Why it matters: visible build-artefact leakage in the most public surface. A candidate sees "intern" and reasonably concludes the site is half-finished.
#9

Every page is missing canonical, Open Graph and Twitter meta tags

Fix before launch

Checked all 28 pages: 0 of 28 have a rel="canonical". 0 of 28 have og:title, og:description, og:image or og:url. 0 of 28 have twitter:card. Six pages are also missing a meta description entirely — /agency, /login, /register, /contact, /about, /privacy and /terms.

Also missing site-wide: /sitemap.xml returns 404, and /robots.txt contains only User-agent: * / Disallow: with no Sitemap: line.

Why it matters: every share of a MaahirSkills link — on WhatsApp, which the product explicitly targets, on LinkedIn, on X — renders as a bare URL with no title, no description and no preview image. For a product whose growth channel is WhatsApp invitations, this suppresses every shared link.
#10

The FAQ structured data is empty while six FAQs are on screen

Fix before launch

All 20 marketing pages emit a FAQPage JSON-LD block with "mainEntity":[] — a valid but completely empty schema. The homepage renders six real, well-written FAQ items via <details> elements:

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[]}

Visible on the homepage: Is it really free for job seekers? · Is my CNIC safe? · Are the tests available in Urdu? · How do you stop cheating? · Can our university or NGO join? · What does it cost a company? — the exact content Google requires to award FAQ rich results.

Why it matters: the content is written and ready; the structured data that would surface it in Google is an empty array. Zero effort to fix, and it is one of the cheapest rich-result wins available on the site.
#11

Three pages ship no <h1> at all

Fix before launch

/login, /register and /contact each have zero <h1> elements. Every one of them opens with a <h2> instead. All three are conversion-critical entry points.

A second structural issue runs across the site: 22 of 28 pages jump from <h2> straight to <h4>, skipping <h3> entirely. The footer is the usual culprit — its column headings are <h4> — but it occurs on every marketing page including /pricing and /help.

Why it matters: heading hierarchy is how screen readers, search engines and AI crawlers understand page structure. A skipped level is a structural defect, and a missing <h1> on the signup page weakens the page that matters most for conversion.
#12

"Live demo portal" redirects to the login page

Fix before launch

The For Organizations dropdown and the footer list a link labelled "Agency Workspace — Live demo portal" pointing at /agency. That page's script immediately checks for a token and redirects:

const token = localStorage.getItem('token');
if (!token) window.location.href = '/login';

Verified: navigating to /agency as an anonymous visitor lands on https://skills.maahir.io/login with no explanation of why. A "live demo" that requires a login is not a demo — and this link sits in the primary navigation of all 20 marketing pages.

Additionally, /agency renders a different, much thinner layout (92 words vs 400–1,755 on the other pages) with no <h1>, no footer nav and no shared CSS bundle — it is a separate layout that has drifted from the rest of the site.

Why it matters: "Live demo portal" is the product's own sales hook, and it dead-ends at a login wall. Recruiters clicking it from the nav hit a form instead of the product.
#13

A literal </span> is rendered as visible text on the homepage

Fix before launch

Inside the homepage pricing teaser, a closing tag was HTML-escaped into the text content instead of being closed properly:

<span class="font-semibold text-accent-700">Job seekers: always free.&lt;/span&gt; Universities, NGOs &amp; NAVTTC programs: always free.

Verified in the browser — the rendered text of that paragraph is:

Job seekers: always free.</span> Universities, NGOs & NAVTTC programs: always free. Full pricing →

It appears in the pricing section of the homepage only; the standalone /pricing page renders the same sentence correctly.

Why it matters: raw markup visible to users on the highest-traffic page, sitting inside a "Job seekers: always free" claim. It directly undermines the exact pricing statement it is attached to.
#14

Mobile nav relies on a single 44px target while 16 links per page sit below touch-target size

Fix before launch

Measured at 390px, every page reports between 8 and 19 interactive elements below the 44×44px minimum. The recurring offenders are the dropdown-panel links (Hiring at 41×19, Campus & Workforce at 139×19, Growth at 49×19), the inline CTA links ("See how hiring works" at 191×24, "Full pricing →" at 94×19) and the logo lockup (151×32).

There is no horizontal overflow at any breakpoint — the layout itself is sound — but with the desktop dropdowns unreachable on touch (#6), the mobile hamburger is the only route to navigation, and it is a single 44px button at the top-right.

Why it matters: combined with #6, mobile navigation is one small button plus a set of sub-44px links. For a product marketing itself to Pakistan's mobile-first, low-bandwidth users, that is the wrong place to be tight.
#15

The homepage sells statistics that cannot be corroborated

Fix before launch

The homepage hero carries four hard numbers: 52,000+ skill tests taken, 340+ companies hiring, 19 partner institutes and a 96% test completion rate. The same page shows "We screened 340 applicants in one afternoon" in a customer quote, and "20-minute tests" as a product promise while every live job advertises 25 or 30 minutes.

None of these are reachable through any public API. The only public data endpoint returns 10 jobs from 6 tenants — and three of those tenants are Agency T, Probe Agency A and other test records. The internal /about page adds "16 intelligence layers, 379+ job roles, 28 role families" — again with no public corroboration.

Why it matters: unverifiable scale claims on a site that currently serves 10 test jobs are the fastest way for a serious employer to lose confidence. Either these are seeded demo numbers that must be replaced before launch, or they need a source. The 20-vs-30 minute contradiction is separately verifiable and should be reconciled.

04.Marketing vs. reality

What the site promises, checked against what the product actually does.

Site claims
Verified behaviour
"Every job includes a short skill test"
All 10 jobs have assessment_template_id: null. Applying yields "Assessment not configured."
→
"Job seekers never pay" / "FREE forever" / "Free for job seekers" — in every page footer
Signup requires company_name and creates a tenant_owner (employer) account. No candidate signup exists.
→
"Your message has been sent" — contact form success panel
No network request is made. The handler only toggles CSS classes. POST /api/public/contact → 404.
→
"Agency Workspace — Live demo portal"
Redirects to /login for any anonymous visitor.
→
"Built for Pakistan" · "Made for Pakistan" badge
Public job board lists "North Jamalberg", "Hackettville", "East Jaquan" and Latin filler descriptions.
→
"Ranked applicant list" · "Candidate comparison" · "Bulk invites" · "Pipeline kanban" (pricing)
All behind the login wall and untested — no credentials were available for this run.
→
"20-minute skill test"
Every live job advertises 25 or 30 minutes. No job offers 20.
→
"CNIC never shared" · "Encrypted, masked" · "CNIC-safe identity"
Privacy policy confirms the intent, but the CNIC encryption path is inside the authenticated surface and was not verifiable.
→
"Works on slow internet" · "Built for Pakistan — Urdu, mobile, 3G"
All 28 pages carry zero horizontal overflow at 390px, and Urdu text renders with correct dir="rtl" on all 23 Urdu nodes.
→
"No sifarish" · "Ranked by verified score, not keyword luck"
Concept is coherent across /, /solutions/hiring and /platform/skill-passport, with consistent evidence-based language.
→
"Job seekers: always free" · "Universities & NGOs always free"
Consistent across /pricing tiers, the homepage pricing block and all 28 footers.
→

05.What is still unverified

The authenticated half of the product was never reached. These are the open questions, not guesses.

Open items behind the login wall — the largest untested surface on this site

  1. Candidate ranking. Does the ranked applicant list actually order by verified skill score? Is the ordering stable and reproducible, or does it shift on refresh?
  2. Assessment grading quality. No live job has a template, so no question bank, no grader and no explainability output has been exercised end to end. This is the highest-value untested area remaining — it is the product.
  3. 15+ filters advertised on the homepage. "skills, scores, city, CGPA, availability" — none were reachable. The /jobs public filters (search, city, type) do work, but the employer-side filter set is entirely unverified.
  4. "Ask the hiring AI" and the HR decision / override bar, both advertised on /platform/hiring and /about, plus the claim that "every HR decision is auditable".
  5. Tenant isolation and RBAC. Whether one employer's candidates can leak into another's view. This is the single highest-stakes security question on a hiring platform and it needs a deliberate multi-tenant test.
  6. Bulk invites (paste emails, CSV, QR poster) and WhatsApp alerts — both are named product pillars. Neither has an obvious reachable route.
  7. Payment flow. The Team tier advertises a 14-day trial and JazzCash/EasyPaisa. No checkout route was reachable from any page.
  8. Candidate account creation. Since /register only creates employer tenants, there may be an unpublished candidate onboarding path — or there may not be one at all. This needs a product decision before a code fix.

Three ways to unblock: paste a working employer account's credentials, create a seeded tenant with test data, or send screenshots of each screen so the flow can be reviewed without an account. Each unlocks a different slice of the list above.

06.What is working

Regression-test these on every deploy — they are all currently right.

Job board showing the ten published jobs including the lorem-ipsum test records
Evidence — /jobs, desktop. The three top cards are the lorem-ipsum test jobs from finding #4.
Mobile navigation menu opened on a 390px viewport showing the full destination list
Working — mobile hamburger opens the full nav at 390px. This is the only reliable route to navigation on touch.
Apply page rendered with JavaScript disabled, showing Loading job details
Evidence — apply page with JS disabled. Job details never load; the form has no action.
Contact page with JavaScript disabled, submitting puts the message into the URL query string
Evidence — contact page with JS disabled. Submit falls back to GET and leaks the message into the URL.
W1

Internal link integrity is flawless

Working

All 48 unique internal links across 28 pages resolve with a 200. Zero 404s, zero redirects, zero broken references. The footer, header, body CTAs and in-page anchors all point at live pages. This is the single best-maintained aspect of the site.

W2

Zero horizontal overflow at every breakpoint

Working

Across all 84 page-viewport runs, document.scrollWidth equalled the viewport width on every single page. No page forces a horizontal scrollbar at 390px, 768px or 1440px. The Tailwind responsive layout is genuinely well built — this is a real strength for the low-bandwidth market the product targets.

W3

Zero console errors and zero failed requests

Working

Across all 84 runs, no uncaught JavaScript exceptions, no console errors, no failed network requests and no 4xx/5xx triggered by page load. Console output is clean even on the pages with the 0-byte bundle, because the critical logic lives in inline scripts.

W4

Urdu RTL rendering is correct everywhere

Working

All 23 Urdu text nodes across 21 pages render with computed direction: rtl and correct font resolution. The Noto Nastaliq Urdu webfont is served and loads correctly from the build manifest. Bilingual content is present on the homepage, every solution page, /jobs, /learn, /skill-passport, /govt-prep and /cert-prep. For a Pakistan-first product this is exactly right.

W5

The auth API is correctly implemented

Working

Bad credentials → 401 {"errors":[{"message":"Invalid credentials."}]}. Empty body → 422 with per-field messages. Password mismatch → 422 "The password field confirmation does not match." Unauthenticated /dashboard → 302 to /login. CSRF tokens are present and rotate per form. This is production-grade behaviour.

W6

The mobile hamburger works correctly

Working

At 390px the button toggles mobileMenu from hidden (height 0px) to open (height 1065px) and updates aria-expanded. The menu exposes all platform destinations. It is a proper 44×44px target. This is the correct pattern — the header dropdowns in #6 should be rebuilt to match it.

W7

Public job filters genuinely work

Working

Searching "accountant" on /jobs narrowed 10 results to exactly 1 — the Junior Accountant role — and the count line updated to "1 job found". City and type selects are populated from live API data. The filter logic itself is sound; only the underlying data is wrong.

W8

FAQ accordions work and the content is genuinely good

Working

All six homepage FAQ items toggle correctly via native <details> with no JS dependency. The answers are specific and honest — "Yes — forever… We charge employers, never candidates" directly addresses the pricing question. This content deserves to be surfaced via structured data (#10).

W9

Images carry proper dimensions and alt text

Working

All 6 homepage images have descriptive alt text, explicit width/height attributes (preventing layout shift) and loading="lazy" where appropriate. The 3 missing avatar files do 404, but each carries an onerror handler that swaps in an initials avatar — so the degradation is graceful and invisible. Verified: no broken-image icons render anywhere.

W10

The CSS build and font pipeline are healthy

Working

The stylesheet is 62,019 bytes of Tailwind v4.3.5 and renders correctly everywhere. The Vite manifest lists the complete asset set including 5 Inter weights and 4 Noto Nastaliq Urdu weights, all of which resolve and load. The build system works — only the JS entry point came out empty (#5).

W11

The apply API accepts a real application

Working

With valid data, POST /api/public/apply returned 201, minted a real assessment token and redirected the browser correctly. File upload accepts PDF/DOCX with a 6MB cap. The backend apply path is sound — it is the assessment layer behind it that is empty (#2).

W12

Legal pages exist, are dated and are substantively reasonable

Working

/privacy and /terms are both present, both dated "Last updated: November 2026", and both cover the right ground — data categories, 90-day retention, access/delete/export rights, AI vendor processing disclosure (naming MiniMax and OpenAI), and a no-emotion-detection / no-personality-inference guarantee in the terms. A missing privacy page is a common launch blocker; this one exists and reads well.

W13

Page titles are unique and well-formed across the site

Working

All 28 pages have unique, descriptive titles — "Fair PKR pricing", "Help Center — MaahirSkills", "Agency Workspace — MaahirSkills" — with zero duplicates. Length is appropriate throughout (19–71 characters). Only the canonical and OG tags are missing (#9).

W14

The marketing page inventory is complete and coherent

Working

20 marketing pages cover solutions, platform, product and company — and each one is distinct, not a duplicate with a changed headline. The information architecture is genuinely well thought out: /solutions/* by persona, /platform/* by product surface, /product by use case. Nothing here needs restructuring.

07.Page-by-page technical detail

Complete coverage of all 28 pages. Meta columns marked — are missing from every page.

PageH1WordsLinksMetaStatus
/1109378—Stray </span> text
/solutions/hiring144761—ok
/solutions/campus144761—ok
/solutions/growth141961—ok
/solutions/agencies140961—ok
/agency1868—Redirects to /login
/skill-passport145061—ok
/govt-prep143361—ok
/cert-prep144261—ok
/learn142561—ok
/jobs133859—Lorem-ipsum jobs
/partners135262—ok
/platform/hiring1175563—ok
/platform/campus1153063—ok
/platform/skill-development185863—ok
/platform/corporate183261—ok
/platform/agency169861—ok
/platform/skill-passport178261—ok
/platform/technology169760—ok
/platform166869—ok
/pricing144962—ok
/help145475—ok
/login0319—No H1
/register0399—No H1 · needs company
/contact0668—No H1 · discards message
/about138410—ok
/privacy12098—ok
/terms12128—ok
Asset / endpointExpectedActualImpact
/build/assets/app-BvRk9kiK.jsJS bundle200 · 0 bytesBroken build, masked by inline scripts
/favicon.icoicon200 · 0 bytesBlank tab icon sitewide
/sitemap.xmlsitemap404No search-engine sitemap
/api/public/contactPOST404 route not foundContact form has no backend
/images/avatar-*.png3 images404 ×3Graceful — initials fallback
/build/assets/app-Ckq0k-Cg.cssCSS bundle200 · 62,019 bytesHealthy
/build/manifest.jsonmanifest200Healthy — fonts listed
/robots.txtrobots200 · no Sitemap lineMinor
/api/public/jobsJSON200 · 10 jobsWorks — data is test content
/api/auth/loginPOST401 / 422 as appropriateHealthy
/api/auth/registerPOST201 / 422 as appropriateHealthy API — wrong product requirement
/api/public/applyPOST201 · mints tokenHealthy — no template behind it
/dashboard302 → /login302 → /loginHealthy

08.Methodology

How the audit was run, so any result can be re-checked or re-run.

M1

Tooling and method

  • Crawl: all 28 pages fetched with curl -sL, following redirects, recording status, byte size and final URL. All 48 internal links and 19 assets checked individually for status and content-type.
  • Browser walk: Playwright + Chromium at 390×844 (mobile, touch enabled), 768×1024 (tablet) and 1440×900 (desktop), networkidle wait, then a DOM probe measuring scrollWidth vs viewport, elements wider than the viewport, image load state, touch-target geometry, document.fonts.check() for Inter and Noto Nastaliq Urdu, computed direction on every Urdu node, and horizontal-scroll containers.
  • Console capture: every console.error, console.warning, uncaught pageerror and requestfailed event recorded across all 84 runs.
  • Functional: every form filled and submitted with real data, with network requests and dialogs captured. Both JavaScript-enabled and JavaScript-disabled contexts tested to expose missing server-rendered fallbacks.
  • Source: all 28 page sources read directly — inline scripts extracted, JSON-LD parsed, heading levels sequenced, meta tags inventoried, form markup and onerror handlers audited.
  • API: every endpoint probed with valid, invalid and empty payloads to map status-code behaviour and validation messages.

Data hygiene: one test tenant and one test candidate were created to exercise the auth and apply paths. The candidate row may appear in employer reports — it should be purged before the next real run.

Reproduction commands
$ # full page inventory
for p in / /solutions/hiring /solutions/campus /solutions/growth /solutions/agencies \
         /agency /skill-passport /govt-prep /cert-prep /learn /jobs /partners \
         /platform /platform/hiring /platform/campus /platform/skill-development \
         /platform/corporate /platform/agency /platform/skill-passport \
         /platform/technology /pricing /help /login /register /contact \
         /about /privacy /terms; do
  curl -s -o /dev/null -w "%{http_code} $p\n" "https://skills.maahir.io$p"
done

# the empty bundle (#5)
curl -sI https://skills.maahir.io/build/assets/app-BvRk9kiK.js | grep -i content-length
content-length: 0

# contact form discards the message (#1)
curl -s -X POST https://skills.maahir.io/api/public/contact \
  -H 'Content-Type: application/json' -d '{}'
{"message":"The route api/public/contact could not be found."}

# job seekers forced to give a company name (#3)
curl -s -X POST https://skills.maahir.io/api/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"name":"QA","email":"qa@example.com","password":"Test12345!","password_confirmation":"Test12345!"}'
{"message":"The company name field is required.", ...} 422

# no live job has an assessment (#2)
curl -s https://skills.maahir.io/api/public/jobs | \
  python3 -c "import json,sys; d=json.load(sys.stdin); \
    print([(j['title'], j['assessment_template_id']) for j in d['data']])"

# empty FAQ schema (#10)
curl -s https://skills.maahir.io/ | grep -o '"mainEntity":\[[^]]*\]'
"mainEntity":[]
Findings index
  • #1 Contact form discards every message, shows fake success — critical
  • #2 No live job has an assessment — "Assessment not configured." — critical
  • #3 Job seekers forced to supply company_name; no candidate signup — critical
  • #4 Public job board serves lorem-ipsum test data — critical
  • #5 Primary JS bundle ships as 0 bytes — critical
  • #6 Desktop dropdowns unusable on touch devices — critical
  • #7 Empty apply form gives zero feedback (novalidate + reportValidity) — significant
  • #8 Raw enum key "intern" leaks into job card UI — significant
  • #9 All 28 pages missing canonical + OG + Twitter tags; no sitemap — significant
  • #10 FAQ JSON-LD schema is an empty array — significant
  • #11 3 pages missing h1; 22 pages skip h2→h4 — significant
  • #12 "Live demo portal" redirects to /login — significant
  • #13 Literal </span> rendered as visible text on homepage — significant
  • #14 8–19 sub-44px touch targets per page on mobile — significant
  • #15 Unverifiable scale statistics; 20-min vs 25/30-min contradiction — significant