Compare commits

...
94 Commits
Author SHA1 Message Date
platform-mcpandClaude Opus 4.8 fccc0115c4 feat: render WRNexus bot widget globally via shared layout
Deploy to Production / Build & Verify (push) Failing after 16m44s
E2E Test Suite / Security Header Tests (push) Failing after 10m27s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m28s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m21s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m13s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 13m58s
E2E Test Suite / Form Interaction Tests (push) Failing after 14m51s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m43s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m59s
E2E Test Suite / Test Report Summary (push) Failing after 14m58s
Deploy to Production / Deploy to Fly.io (push) Failing after 13m21s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 13m48s
Deploy to Production / Deploy to Render (push) Failing after 14m14s
Deploy to Production / Deploy to Railway (push) Failing after 14m40s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m10s
Deploy to Production / Notify on Failure (push) Successful in 1s
Inject the WRNexus bot widget loader (bots/widget.js) once into
BaseLayout.astro's <head> so the chat widget appears on every route
(pages + blog posts) without per-page duplication. Uses is:inline so
Astro does not bundle/transform the external script, preserving the
data-bot-id and async attributes.

Update CSP in middleware.ts to permit the widget: script-src and
connect-src already allowed https://api.wrnexus.com (web-push SDK);
added it to frame-src for the widget's iframe. No other directives
weakened.

Verified: build is clean, widget tag served on /, /about, /contact,
and a blog post; widget.js loads 200; zero console CSP violations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:29:32 +05:30
platform-mcp 189260bbb3 pre-agent: Add WRNexus chat widget script to layout 2026-06-26 00:27:14 +05:30
platform-mcpandClaude Opus 4.8 8e7070cbaf feat(footer): make push notification button reflect subscription state
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 10m42s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m35s
E2E Test Suite / Security Header Tests (push) Failing after 11m20s
E2E Test Suite / Mobile Device Tests (push) Failing after 12m13s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 13m5s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 13m57s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 14m50s
E2E Test Suite / Test Report Summary (push) Failing after 12m50s
Deploy to Production / Build & Verify (push) Failing after 19m19s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m59s
Deploy to Production / Deploy to Fly.io (push) Failing after 12m23s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 12m26s
Deploy to Production / Deploy to Render (push) Failing after 13m16s
Deploy to Production / Deploy to Railway (push) Failing after 14m8s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m57s
Deploy to Production / Notify on Failure (push) Failing after 14m58s
The WRNexus web-push button always showed "Enable notifications". It now
reads WRNexusPush.isSubscribed() on load and renders the correct state:
- hides entirely when SDK/browser push support is missing
- "Enable notifications" -> calls prompt() (user gesture)
- "Unsubscribe" when already subscribed -> calls unsubscribe()
- disabled "Notifications blocked" when permission is denied
- in-progress disabled state to prevent double-clicks; re-reads state after
  each action and re-renders the label

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 02:39:37 +05:30
platform-mcp a8d7db562d pre-agent: Make push notification button reflect subscription 2026-06-24 02:37:20 +05:30
platform-mcp e220f46d35 agent: Host WRNexus push service worker at site root 2026-06-24 01:45:37 +05:30
platform-mcp 93460ece19 pre-agent: Host WRNexus push service worker at site root 2026-06-24 01:42:48 +05:30
platform-mcpandClaude Opus 4.8 7b67897612 fix(csp): allow WRNexus push SDK origin in script-src and connect-src
E2E Test Suite / Form Interaction Tests (push) Failing after 10m29s
E2E Test Suite / Security Header Tests (push) Failing after 10m15s
E2E Test Suite / Mobile Device Tests (push) Failing after 11m8s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 12m0s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m52s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m44s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m37s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m17s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Failing after 12m14s
Deploy to Production / Notify on Failure (push) Successful in 1s
Deploy to Production / Build & Verify (push) Failing after 5m47s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m54s
Deploy to Production / Deploy to Railway (push) Failing after 14m58s
Deploy to Production / Deploy to Render (push) Failing after 14m35s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m6s
Deploy to Production / Deploy to Fly.io (push) Failing after 13m43s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m11s
The web-push SDK (https://api.wrnexus.com/push/sdk.js) and its runtime
config fetch were blocked by the Content-Security-Policy set in
src/middleware.ts. Added https://api.wrnexus.com to script-src (prod and
dev) and connect-src. No wildcards introduced; existing analytics
origins (GTM, GA, Plausible) unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:38:51 +05:30
platform-mcp c5f79f1fd2 pre-agent: Allow WRNexus push SDK in CSP script-src 2026-06-24 01:35:55 +05:30
platform-mcpandClaude Opus 4.8 41abcaa7a4 feat: integrate WRNexus web-push SDK and subscribe control
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Failing after 12m56s
Deploy to Production / Build & Verify (push) Failing after 17m52s
Deploy to Production / Deploy to Fly.io (push) Failing after 12m36s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 13m3s
Deploy to Production / Deploy to Render (push) Failing after 13m30s
Deploy to Production / Deploy to Railway (push) Failing after 13m55s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m57s
Deploy to Production / Notify on Failure (push) Successful in 1s
- Add WRNexus push SDK (is:inline) to BaseLayout <head> so it loads on
  every page that should collect subscribers (api.wrnexus.com/push/sdk.js)
- Add "Enable notifications" subscribe button in the footer wired via
  addEventListener to window.WRNexusPush.subscribe()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:33:20 +05:30
platform-mcp 966f3b23fa pre-agent: Add WRNexus push notification SDK and subscribe bu 2026-06-24 01:30:45 +05:30
platform-mcp 45b30918a0 pre-agent: Add WRNexus push notification SDK and subscribe bu 2026-06-24 01:30:12 +05:30
platform-mcp 7eeec7e781 pre-agent: Draft PR and earned-mentions plan 2026-06-19 09:38:43 +05:30
platform-mcp 527131a462 pre-agent: Create directory listing and Google Business Profi 2026-06-19 09:36:05 +05:30
platform-mcpandClaude Opus 4.8 6b07b83583 chore(seo): open robots.txt to all major AI crawlers for GEO
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 10m48s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m43s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 12m35s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 13m27s
E2E Test Suite / Form Interaction Tests (push) Failing after 14m20s
E2E Test Suite / Security Header Tests (push) Failing after 14m1s
E2E Test Suite / Mobile Device Tests (push) Failing after 14m56s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m57s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 17m16s
E2E Test Suite / Test Report Summary (push) Failing after 13m13s
Deploy to Production / Pre-Deploy Tests (push) Failing after 12m22s
Deploy to Production / Deploy to Fly.io (push) Failing after 13m13s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m5s
Deploy to Production / Deploy to Render (push) Failing after 14m5s
Deploy to Production / Deploy to Railway (push) Failing after 14m57s
Deploy to Production / Post-Deploy Verification (push) Failing after 13h5m7s
Deploy to Production / Notify on Failure (push) Failing after 15m4s
Repo robots.txt already matched the deployed file (verified via diff
against https://workroot.in/robots.txt). Added commonly-cited AI agents
that were still missing, each with Allow: /:
- OpenAI: OAI-SearchBot
- Anthropic: ClaudeBot, Claude-SearchBot, Claude-User
- Perplexity: Perplexity-User
- Meta: Meta-ExternalAgent / meta-externalagent
- Apple: Applebot, Applebot-Extended
- Amazon: Amazonbot
- ByteDance: Bytespider
- Cohere: cohere-ai
- DuckDuckGo: DuckAssistBot
- You.com: YouBot
- Diffbot

Disallow list unchanged (/admin/, /private/, /_astro/) so no content
directory needed for citation is blocked. Sitemap line preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:35:47 +05:30
platform-mcp c7e130ce4d pre-agent: Verify robots.txt keeps AI crawlers open 2026-06-19 09:34:50 +05:30
platform-mcpandClaude Opus 4.8 5a2b68763f chore(content): unify company facts for NAP consistency
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 14m45s
Reconcile inconsistent company facts to the canonical set (single
source of truth):

- Structured-data Organization/LocalBusiness/publisher/provider names
  now use the legal entity name "WorkRoot IT Solutions LLP"
  (SEO.astro, utils/seo.ts) for NAP consistency. Brand short forms
  ("WorkRoot", "WorkRoot IT Solutions") retained only as display brand
  and alternateName.
- Headline government-reach proof figure unified to "11+ districts"
  across all company-level proof bands and marketing/SEO copy
  (ProofNumbers, /about, /portfolio, homepage, /services, SEO desc).
  Specific Bihar Police Management System case studies / service-page
  stat tiles keep the factual "11 districts" — documented rationale.
- Verified phone (+91 9561417403), email (admin@workroot.in),
  founded (2022), 20+ projects, 50,000+ records, 99.9% uptime and
  IIT Patna partnership are consistent site-wide.

Added .agent/scratch/FACTS-CANONICAL.md as the single source of truth
plus an off-site checklist (LinkedIn, Clutch, GoodFirms, Crunchbase,
GeM) to keep external listings identical. Build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:34:30 +05:30
platform-mcp 84624976a8 pre-agent: Audit and unify company facts (NAP consistency) 2026-06-19 09:23:55 +05:30
platform-mcpandClaude Opus 4.8 a3dd4b9ffa content: add quotable, extractable company facts + comparison table to homepage
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 11m34s
Deploy to Production / Pre-Deploy Tests (push) Failing after 13m8s
Deploy to Production / Build & Verify (push) Failing after 16m0s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m5s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m6s
Deploy to Production / Deploy to Render (push) Failing after 14m57s
Deploy to Production / Deploy to Railway (push) Failing after 14m58s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m57s
Deploy to Production / Notify on Failure (push) Failing after 13h10m32s
Add an LLM-citable "WorkRoot at a glance" section to the homepage with 7
standalone factual sentences that each bundle the company name + a concrete
claim + a number (founded 2022, 20+ projects, 11 districts, 50,000+ records,
99.9% uptime, IIT Patna partnership, GeM empanelment, 15,000+ officers, 8+
years), so each line stays accurate when quoted out of context.

- New companyFacts list + "Custom software vs off-the-shelf ERP" comparison
  table in src/pages/index.astro (reuses the table pattern from the custom-ERP
  service page; final row carries the canonical 11-districts / 99.9% proof).
- Rewrite vague HomepageFaq answers into definitive, self-contained one-
  paragraph answers; visible text still drives the single FAQPage JSON-LD.
- Normalize stray "11+ districts" in the services array to the canonical
  "11 districts" for exact sitewide numerical consistency.

Verified: build passes; rendered homepage shows all 7 facts, the comparison
table, definitive FAQ answers, exactly one FAQPage JSON-LD, and no "11+".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:23:30 +05:30
platform-mcp 0121641163 pre-agent: Add quotable, extractable facts for LLM citation 2026-06-19 09:17:58 +05:30
platform-mcpandClaude Opus 4.8 8d35785a4c content: add buyer-question blog posts on custom ERP cost & GeM empanelment
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m33s
Add two long-form (1,200+ word) blog posts targeting buyer search
questions, with valid content-collection frontmatter, comparison
tables, India-specific rupee figures, and definitive FAQ blocks:

- how-much-custom-erp-cost-india-2026.md (hero: erp-systems.jpg)
- gem-empanelment-process-software-vendors.md (hero: govt-systems.jpg)

Both use category "Web Development" (valid enum), author "Ajay", and
reuse existing hero images. Verified: astro build passes schema
validation and both render at /blog/<slug> with working hero images
and appear in the blog index.

Also record the 1-2 posts/month editorial cadence in
.agent/scratch/SEO-NOTES.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:17:34 +05:30
platform-mcp c1585570c6 pre-agent: Expand blog into a buyer-question content engine 2026-06-19 09:12:13 +05:30
platform-mcpandClaude Opus 4.8 22251ea82e feat(services): add keyword-focused service landing pages
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 13m25s
Add five SEO landing pages under /services that reuse existing section
components (GuaranteeBand, InlineLeadForm) and a new prop-driven ServiceFaq:

- /services/custom-erp-development
- /services/university-erp
- /services/government-it-solutions-gem-vendor
- /services/police-management-system
- /services/react-native-app-development

Each page has 1,000+ words of specific copy, a real portfolio-backed case
study (IIT Patna, PSCL Bihar, Police MS, Pophealth), a comparison table /
stat block, a page-specific FAQ that feeds the FAQPage schema, and a CTA into
the existing inline lead form (/api/contact). Service + FAQ + BreadcrumbList
schema are passed into SEO.astro; the visible FAQ accordion does not duplicate
the JSON-LD. All company stats match the canonical site-wide set.

Wire the pages into navigation: deep-dive links from the /services detail
sections, a new "Solutions" group on the HTML sitemap, and entries in both the
dynamic sitemap.xml.ts and the served static public/sitemap.xml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:11:40 +05:30
platform-mcp a21aa99e90 pre-agent: Build keyword-focused service and location landing 2026-06-19 08:58:06 +05:30
platform-mcp e697750e22 agent: Set unique titles and meta descriptions per page 2026-06-19 08:57:40 +05:30
platform-mcp 23acac5c4d pre-agent: Set unique titles and meta descriptions per page 2026-06-19 08:48:19 +05:30
platform-mcpandClaude Opus 4.8 dccc5d3b7f feat(seo): centralize sitewide JSON-LD in SEO.astro with Organization reviews
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m27s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Security Header Tests (push) Failing after 10m21s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m27s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m13s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m5s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 12m58s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 13m50s
E2E Test Suite / Form Interaction Tests (push) Failing after 14m43s
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
Move the base Organization + LocalBusiness and WebSite structured data out of
BaseLayout's inline scripts into SEO.astro behind a new `base` prop, rendered
once from BaseLayout's <head> so every route (incl. pages without a
page-specific <SEO>) carries the canonical company schema exactly once.

- Organization/LocalBusiness now includes aggregateRating + Review nodes built
  from the three real homepage testimonials (Ajay Pruthi, Dhirendra Singh,
  Dilip Raj — all 5-star, reviewCount 3, rating 5.0).
- foundingDate 2022, telephone +91-9561417403, email admin@workroot.in,
  areaServed Bihar (AdministrativeArea) + India (Country).
- Description keeps the canonical proof numbers identical to the site:
  20+ projects, 11 districts, 50,000+ records, 99.9% uptime.
- Reusable Service node now links provider via @id to #organization and
  serves Bihar/India; page-specific schema (Service/FAQ/WebPage/...) stays
  opt-in and is gated off the base instance to prevent duplication.
- Homepage still emits a single FAQPage (from HomepageFaq) — no duplicate.
- Add jsdom devDependency required by scripts/validate-schema.js.

Verified: build succeeds; scripts/validate-schema.js passes (24 schemas, 0
issues, 0 warnings); rendered ld+json inspected on homepage, services, and
schema-less pages (privacy/terms/cookies/404).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:47:50 +05:30
platform-mcp be80d63f04 pre-agent: Add sitewide JSON-LD structured data 2026-06-19 08:40:21 +05:30
platform-agentandClaude Opus 4.8 95a5af1d9a feat(home): strengthen Government IT card with live proof points
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 10m28s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m20s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m13s
Ping Search Engines / Notify Search Engines (push) Failing after 11m6s
E2E Test Suite / Security Header Tests (push) Failing after 11m59s
E2E Test Suite / Mobile Device Tests (push) Failing after 12m51s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 13m43s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 14m36s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m13s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 20m11s
E2E Test Suite / Test Report Summary (push) Failing after 13m11s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m58s
Deploy to Production / Deploy to Fly.io (push) Failing after 13m48s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m4s
Deploy to Production / Deploy to Render (push) Failing after 14m40s
Deploy to Production / Deploy to Railway (push) Failing after 14m56s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m10s
Deploy to Production / Notify on Failure (push) Failing after 14m58s
Surface the existing, already-published Government IT credibility signals
on the homepage service card — 11+ districts live, 99.9% uptime, and GeM
empanelment (all already stated on /services and in the homepage FAQ) —
so the card matches the depth of the per-service quote CTA beside it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:49:55 +05:30
WorkRoot AgentandClaude Opus 4.8 5a6db9c20f feat(homepage): expand inline lead form to name/email/phone/type/message
Ping Search Engines / Notify Search Engines (push) Successful in 3s
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Failing after 10m58s
Deploy to Production / Build & Verify (push) Failing after 16m37s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m3s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m3s
Deploy to Production / Deploy to Render (push) Failing after 14m55s
Deploy to Production / Deploy to Railway (push) Failing after 14m55s
Deploy to Production / Post-Deploy Verification (push) Failing after 13m32s
Deploy to Production / Notify on Failure (push) Failing after 13m51s
Rework the homepage InlineLeadForm from a single combined "email or phone"
contact field to an explicit 5-field layout — name, email (type=email),
phone (type=tel), project type and an optional message — so each field gets
the correct mobile keyboard and leads carry a real email + phone separately.

- Required fields kept to name + email + project type for a low-friction
  mobile flow; phone and message are optional.
- Email uses type=email + inputmode=email; phone uses type=tel + inputmode=tel
  for the numeric keypad. Message is an optional textarea; when blank the
  client synthesizes a >=10 char summary so the /api/contact validator passes.
- Still posts to the same /api/contact endpoint tagged source=homepage-inline,
  delivering leads exactly the same way the /contact page does (no page nav).
- Update inline-lead-form.spec.ts to the new field set and keyboards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:33:26 +05:30
platform-mcp 870075b33f pre-agent: 🔧 Auto-fix: frontend
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 11m51s
2026-06-17 20:32:16 +05:30
platform-mcp 8b16729ab4 pre-agent: 🔧 Auto-fix: frontend 2026-06-17 20:32:15 +05:30
platform-mcp 1ab4dc1033 agent: Make header and hero section full width 2026-06-17 20:31:31 +05:30
platform-mcp a064260c55 agent: Fix and replace header logo with correct asset 2026-06-17 20:31:30 +05:30
platform-mcp a7b1ae4800 agent: Fix mobile menu full-screen overlay 2026-06-17 20:30:40 +05:30
platform-mcpandClaude Opus 4.8 4b7cfc4ab5 feat(header): place WRNexus nav item directly after Home
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 13m26s
Reorder the shared navLinks array so 'WRNexus' renders as the second
item (right after Home) in both the desktop menubar and the mobile menu
panel. The mobile menu panel already includes a top-right cross close
icon (#mobile-menu-close, aria-label="Close menu") wired to closeMenu().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:27:03 +05:30
platform-mcp a79c734fe9 agent: Convert home hero to left-right layout 2026-06-17 20:25:21 +05:30
platform-mcp 8ac0b8a760 pre-agent: Make header and hero section full width 2026-06-17 20:25:05 +05:30
platform-mcpandClaude Opus 4.8 52b6392f74 feat(home): convert hero to two-column left/right layout
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 13m57s
Restructure the homepage hero from a single centered block into a
responsive two-column grid: left column holds the left-aligned
headline, subtext, CTAs, click-to-call and trust line; right column
holds the framed product dashboard preview.

- Replace `max-w-5xl mx-auto text-center` with
  `grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center`.
- Scale headline down from text-5xl/8xl to a responsive
  text-4xl md:text-5xl lg:text-6xl so it no longer overflows.
- Left-align CTAs/links on desktop (justify-start, lg:text-left)
  while keeping centered stacking on mobile (grid-cols-1).
- Swap section min-h-screen for lg:min-h-[85vh] and py-24 lg:py-32
  so the hero fits the viewport instead of dominating it.
- Preserve existing background gradient and decorative blur elements,
  consistent with the about/portfolio heroes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:24:38 +05:30
platform-mcp 14aefa4a71 pre-agent: Add WRNexus nav item with corner close icon 2026-06-17 20:21:28 +05:30
platform-mcp d64a191c12 agent: Add WRNexus external link to navbar 2026-06-17 20:20:18 +05:30
platform-mcpandClaude Opus 4.8 4b2e96c681 feat(header): add WRNexus external nav link
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 11m2s
Add a top-level "WRNexus" navigation item linking to https://wrnexus.com/
that opens in a new tab. Implemented via the data-driven navLinks array
with an `external` flag; desktop and mobile renderers apply
target="_blank" and rel="noopener noreferrer" and skip prefetch/active-state
for external links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:20:01 +05:30
platform-mcp 1b922ddda3 pre-agent: Convert home hero to left-right layout 2026-06-17 20:19:15 +05:30
platform-mcp bc10d5fe4b pre-agent: Fix mobile menu full-screen overlay 2026-06-17 20:18:23 +05:30
platform-mcp a5c6e32734 pre-agent: Add WRNexus external link to navbar 2026-06-17 20:17:39 +05:30
platform-mcp 35feade8e7 pre-agent: 🔧 Auto-fix: frontend 2026-06-17 20:16:23 +05:30
platform-mcp c86fce6125 pre-agent: Fix and replace header logo with correct asset 2026-06-17 20:16:23 +05:30
platform-mcp 7f3d94178d agent: Fix repeated 404 requests to malformed '/)' path 2026-06-17 20:15:35 +05:30
platform-mcp 9a523c038c pre-agent: Fix repeated 404 requests to malformed '/)' path
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
2026-06-17 20:12:24 +05:30
platform-mcp 8e60268caf agent: Set missing CONTACT_EMAIL_TO environment variable 2026-06-17 20:12:04 +05:30
platform-mcp 2291336300 pre-agent: Set missing CONTACT_EMAIL_TO environment variable 2026-06-17 20:09:12 +05:30
platform-mcp ff2870ba2a pre-agent: check what isseue here on log 2026-06-17 20:08:40 +05:30
platform-mcp ad90b92dcd pre-agent: check what isseue here on log 2026-06-17 17:16:37 +05:30
platform-mcpandClaude Opus 4.8 5a6847c774 feat: apply WorkRoot logo site-wide and as favicon
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 10m2s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 10m54s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 11m46s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m39s
E2E Test Suite / Form Interaction Tests (push) Failing after 13m31s
E2E Test Suite / Security Header Tests (push) Failing after 13m17s
E2E Test Suite / Mobile Device Tests (push) Failing after 14m9s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 23m32s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 19m34s
E2E Test Suite / Test Report Summary (push) Failing after 13m11s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m51s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m4s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m5s
Deploy to Production / Deploy to Render (push) Failing after 14m56s
Deploy to Production / Deploy to Railway (push) Failing after 14m57s
Deploy to Production / Post-Deploy Verification (push) Failing after 14m58s
Deploy to Production / Notify on Failure (push) Successful in 3s
- Generate crisp icon set from WorkRoot SVG: favicon.ico (16/32/48),
  favicon-32x32.png, apple-touch-icon.png (180), logo.png (512),
  and PWA icon-192.png / icon-512.png
- Add favicon.ico link with recommended <head> tag order in BaseLayout
- Reference icon-192/icon-512 in manifest.json for PWA installs
- JSON-LD Organization logo now resolves to the real 512x512 mark
- Header/Footer already render the new mark via /favicon.svg; dark-mode
  wordmark contrast (BUG-5) preserved and verified

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:27:34 +05:30
platform-mcp e6feb36523 pre-agent: Apply WorkRoot logo site-wide and as favicon 2026-06-17 16:19:31 +05:30
platform-mcpandClaude Opus 4.8 01cafba6c1 feat: apply WorkRoot logo sitewide and as favicon
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 10m48s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m40s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m32s
Ping Search Engines / Notify Search Engines (push) Failing after 11m26s
E2E Test Suite / Security Header Tests (push) Failing after 12m18s
E2E Test Suite / Mobile Device Tests (push) Failing after 13m10s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 14m3s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 14m55s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 22m34s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Failing after 14m58s
Deploy to Production / Post-Deploy Verification (push) Failing after 10m26s
Deploy to Production / Build & Verify (push) Failing after 14m49s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m58s
Deploy to Production / Deploy to Railway (push) Failing after 14m5s
Deploy to Production / Deploy to Render (push) Failing after 14m5s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 13m13s
Deploy to Production / Deploy to Fly.io (push) Failing after 13m13s
Deploy to Production / Notify on Failure (push) Failing after 13m53s
- Add WorkRoot Icon as public/favicon.svg, regenerate logo.png (512x512)
  and apple-touch-icon.png (180x180) from the brand vector; add
  favicon-32x32.png raster
- Register 32x32 PNG favicon link in BaseLayout <head> alongside the
  SVG and apple-touch-icon
- Replace placeholder "W" logo boxes with the WorkRoot mark in the
  header, mobile menu, and footer (preserving "WorkRoot" text spans
  required by the dark-mode contrast tests and the home link wrapper)

JSON-LD/SEO logo (/logo.png) and PWA manifest icons now resolve to the
real brand assets. Build passes; BUG-5 dark-mode and navigation tests
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:08:31 +05:30
platform-mcp 19a0720fe8 pre-agent: Apply WorkRoot logo sitewide and as favicon 2026-06-17 15:03:27 +05:30
platform-mcp 5c05779541 agent: Fix mobile responsiveness and conversion across pa 2026-06-17 14:51:43 +05:30
platform-mcp ce67f90f06 pre-agent: Fix mobile responsiveness and conversion across pa 2026-06-17 14:42:24 +05:30
platform-mcp fb5e1ea281 agent: Improve blog content and lead funnel 2026-06-17 14:41:57 +05:30
platform-mcp 03c37a623f pre-agent: Improve blog content and lead funnel 2026-06-17 14:34:26 +05:30
platform-mcpandClaude Opus 4.8 721d683eb9 feat(contact): CRO improvements — align services dropdown, value-prop H1, response promise above form, tighter message field
E2E Test Suite / Form Interaction Tests (push) Failing after 10m53s
E2E Test Suite / Security Header Tests (push) Failing after 10m39s
E2E Test Suite / Mobile Device Tests (push) Failing after 11m31s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 12m23s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 13m16s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 14m8s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 15m0s
Ping Search Engines / Notify Search Engines (push) Failing after 14m47s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m30s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Failing after 14m58s
Deploy to Production / Build & Verify (push) Failing after 19m22s
Deploy to Production / Pre-Deploy Tests (push) Failing after 14m59s
Deploy to Production / Deploy to Railway (push) Failing after 14m58s
Deploy to Production / Deploy to Render (push) Failing after 14m57s
Deploy to Production / Deploy to VPS (PM2) (push) Failing after 14m6s
Deploy to Production / Deploy to Fly.io (push) Failing after 14m5s
Deploy to Production / Notify on Failure (push) Failing after 14m58s
Addresses contact-page audit points 22–27:
- Confirmed no '200+ companies' credibility claim; canonical stat set retained
- Service dropdown now matches real services, led by ERP/Enterprise + Government IT;
  removed Cloud Services and AI & ML, added UI/UX Design
- Backend allowlist + labels updated to accept 'ui-ux' and reflect real service names
- H1 paired with value prop: free 1-page proposal in 24 hours
- '< 24h response' promise badge moved above the form
- Clarified contradictory hours to 'We reply within 24 business hours, Mon–Fri'
- Reduced message textarea height (rows 5→4) and max length (5000→2000)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:33:37 +05:30
platform-mcp 0b8c2163f3 pre-agent: Apply contact page CRO improvements 2026-06-17 14:28:14 +05:30
platform-mcpandClaude Opus 4.8 72a11cfcbc feat(portfolio): add outcome headlines, drop dead AI&ML filter
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m48s
Apply portfolio CRO audit points 17-21:
- Add a one-line outcome headline above each project card description
  (grid + featured cards), styled with the primary accent per the
  design system (e.g. "Cut crematorium slot booking from hours to
  minutes across 10+ facilities.").
- Remove the "AI & ML" category filter chip — no project genuinely
  fits it (Pophealth is health/mobile) — and correct the ItemList
  structured-data description to drop the inaccurate AI/ML claim.
- Stats bar already surfaces the canonical proof set (11 Districts
  Deployed, 50,000+ Records Managed) shared with the homepage/services
  via ProofNumbers; the weak "4 Specializations" stat is gone.
- "View Case Study" CTAs open the inline case-study modal with full
  problem -> solution -> outcome (Challenge / Solution / Key Results),
  verified working via headless browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:27:08 +05:30
platform-mcp 3da9c04dce pre-agent: Apply portfolio page CRO improvements 2026-06-17 14:19:26 +05:30
platform-mcp 58c0cb5165 agent: Apply about page CRO improvements 2026-06-17 14:18:59 +05:30
platform-mcp 387433cd26 pre-agent: Apply about page CRO improvements 2026-06-17 14:11:18 +05:30
platform-mcpandClaude Opus 4.8 4929a1c906 feat(services): apply CRO improvements (audit points 7–11)
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m26s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 10m9s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 11m1s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m53s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m45s
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 11m38s
- Replace generic H1 with outcome-led headline ("Custom software for
  enterprises and governments — shipped in weeks, not quarters.")
- Rewrite each service description to lead with the outcome and end
  with the tech stack
- Reorder service cards so Government IT and ERP/Enterprise lead,
  before Web/Mobile
- Add "What's included" label above each pricing tier's bullet list
  (Most Popular badge on Professional retained)
- Unify CTAs to one primary ("Get my free proposal") + one secondary
  ("See our work" → /portfolio)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:10:50 +05:30
platform-mcp 8192ed189a pre-agent: Apply services page CRO improvements 2026-06-17 14:06:10 +05:30
platform-mcp 5e81c4bab5 agent: Apply homepage CRO improvements 2026-06-17 14:05:47 +05:30
platform-mcp de0426cad9 pre-agent: Apply homepage CRO improvements 2026-06-17 13:58:15 +05:30
platform-mcpandClaude Opus 4.8 b1a8fa4167 fix: standardize stats, currency, and CTAs site-wide
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m31s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m9s
Deploy to Production / Build & Verify (push) Blocked by required conditions
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Blocked by required conditions
Deploy to Production / Deploy to Render (push) Blocked by required conditions
Deploy to Production / Deploy to VPS (PM2) (push) Blocked by required conditions
Deploy to Production / Deploy to Fly.io (push) Blocked by required conditions
Deploy to Production / Post-Deploy Verification (push) Blocked by required conditions
Deploy to Production / Notify on Failure (push) Blocked by required conditions
Resolve site-wide credibility/consistency bugs (audit A–E):

- Bug A: replace homepage pricing placeholder "Projects from $X,XXX"
  with the real "₹1,00,000" floor (matches Services Starter tier) in
  PricingTimelineBand.
- Bug C: standardize currency on ₹ — convert contact budget radios
  from $ to ₹ ranges (< ₹1L / ₹1L–₹5L / ₹5L–₹15L / ₹15L+).
- Bug B: adopt one canonical stat set everywhere — 20+ Projects ·
  11 Districts Deployed · 50,000+ Records Managed · 8+ Years Avg
  Experience. Applied to homepage stats counter + ProofNumbers band,
  /portfolio stats bar (removes "4 Specializations"), and /about grid.
  Counter now formats with en-IN grouping so 50,000+ renders correctly.
- Contact: delete the conflicting "200+ companies" claim.
- Bug E: standardize CTA wording — primary "Get my free proposal",
  secondary "See our work". Replaced "Get a free consultation",
  "Book a discovery call", "Schedule a Free Call", "Get Started",
  "Start a Project", "Get Quote", "Get my free consultation",
  "See case studies" across Home, Header, Services, Portfolio, Contact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:57:51 +05:30
platform-mcp 3b8e86d81e pre-agent: Standardize stats, currency, and CTAs site-wide 2026-06-17 13:50:13 +05:30
platform-mcp 1b7824b265 polish(mobile-sticky-bar): tighten labels, 24px icons, 64px tap targets
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m19s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Blocked by required conditions
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Blocked by required conditions
Deploy to Production / Deploy to Render (push) Blocked by required conditions
Deploy to Production / Deploy to VPS (PM2) (push) Blocked by required conditions
Deploy to Production / Deploy to Fly.io (push) Blocked by required conditions
Deploy to Production / Post-Deploy Verification (push) Blocked by required conditions
Deploy to Production / Notify on Failure (push) Blocked by required conditions
- Rename WhatsApp label from "WhatsApp" (8 chars) to "Chat" (4) so every
  visible label fits the ≤7-char action-verb policy and never triggers
  the new text-overflow:ellipsis safety net even at 320px (iPhone SE).
- Bump inline SVG icons to 24×24 (was 22), set min-height 64px and
  min-width 44px per Apple HIG / WCAG 2.5.5 tap-target requirements.
- Add white-space:nowrap + text-overflow:ellipsis as defensive safety
  net, plus a 320-359px breakpoint that nudges label to 11px / padding
  to 4px-horizontal so glyph-width drift can't cause overflow.
- Rewrite aria-labels to be action-led + descriptive even when visible
  text is terse: "Call us", "Chat with us on WhatsApp", "Get a free
  quote" (WCAG 2.5.3 name-in-name).
- Bump BaseLayout bottom-spacer from 64px → 72px so the new
  PricingTimelineBand and GuaranteeBand always have an 8px visual
  buffer above the fixed sticky bar (no overlap).

Verified: build green, all 6 tests in tests/sticky-cta.spec.ts pass
against the restarted frontend service. Probed live at 320 / 375 /
414 px — every button is 64px tall, ≥103px wide, icons 24×24, labels
never truncate. Footer fully clears the bar at scroll-bottom.
2026-06-17 13:42:51 +05:30
platform-mcp 96ea8fc0d1 pre-agent: Polish mobile sticky bar labels 2026-06-17 13:35:51 +05:30
platform-mcp 210117e73e feat(homepage): add TrustStrip + GuaranteeBand for trust + risk-reversal
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 13m20s
- TrustStrip.astro: single-row, grayscale-on-default / colour-on-hover
  client logo strip under the hero. 6 logos via <img> with explicit
  width/height + loading=lazy (CLS-safe), wraps to 3+3 on mobile.
  Includes sr-only "Trusted by" heading and dark-mode invert filter
  vetted against THEME_ACCESSIBILITY_AUDIT.md. Replaces the older
  ClientLogos placement on the homepage.

- GuaranteeBand.astro: 3-up risk-reversal band wedged between
  PricingTimelineBand and InlineLeadForm. Three claims (30-day
  satisfaction guarantee, no long-term contracts, fixed-price quotes)
  with icon + headline + supporting body. Stacks vertically on mobile.

- public/images/logos/: 6 placeholder SVG wordmarks (plnr, rentec,
  iit-patna, moc-soft, gov-bihar, pscl). Replace at same path with
  real client-supplied artwork when handed off.

- index.astro: wires the two new components, repoints the hero
  scroll indicator to #trust-strip, preserves legacy #client-logos
  anchor for any external deep-links.
2026-06-17 13:35:26 +05:30
platform-mcp 35c8cc23a9 pre-agent: Add trust strip and risk-reversal guarantee 2026-06-17 13:26:12 +05:30
platform-mcp 0e37dab05b feat(homepage): swap "3 Team Members" stat for "8+ Years Avg Experience"
Ping Search Engines / Notify Search Engines (push) Successful in 8s
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
The team-member count read as a small-shop signal next to the new ProofNumbers band. Swap it for a stronger value-prop (team seniority) on both
index.astro and about.astro. Rename the about-page "Years of Experience" stat
to "Years in Business" to disambiguate from the new team-experience metric, and
refresh the about-page SEO description accordingly. The new label doesn't
collide with any ProofNumbers entry (Projects Delivered / Client Rating /
Districts Deployed / Avg Page Load), so the two bands stay non-redundant.
2026-06-17 13:25:52 +05:30
platform-mcp 1707a246f7 pre-agent: Swap the 3-team-members stat 2026-06-17 13:22:21 +05:30
platform-mcp 8ae947b1ae feat(home): add pricing + timeline band above inline lead form
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 10m12s
Adds a new PricingTimelineBand component that sits directly above the
homepage InlineLeadForm with two side-by-side cards answering the two
final pre-conversion questions every buyer has:

  1. "Projects from $X,XXX"  — includes discovery, design, build, QA,
     and post-launch support.
  2. "Typical timeline: 4–8 weeks"  — discovery week 1, weekly demos,
     launch by week 8.

A subtle "Custom quote available — tell us about your project" line
with a downward chevron visually bridges the band into the form below.

Implementation notes:
- Anchor id="pricing-timeline" so the mobile sticky CTA bar / nav can
  deep-link to it.
- grid-cols-1 md:grid-cols-2 → stacks vertically on mobile, side-by-
  side on md+ (>=768px).
- Each card has an explicit min-h-[260px] sm:min-h-[280px] so the
  band reserves its own vertical space at first paint, keeping
  Lighthouse CLS unchanged.
- Uses existing design tokens (primary-* / accent-* / secondary-*,
  rounded-3xl, container-wrapper, gradient-br) so the band feels
  native to the page.
- Placeholder copy ($X,XXX and 4–8 weeks) is flagged with
  data-stakeholder-confirm markers + HTML comments so the marketing/
  leadership team knows what still needs sign-off.
- Reduced-motion-respecting CSS animation on the bridging chevron.
2026-06-17 13:21:58 +05:30
platform-mcp dc84e3ebe8 pre-agent: Add pricing and timeline band above lead form 2026-06-17 13:15:15 +05:30
platform-mcp 624f8532a5 feat(home): replace "Why Choose Us" benefits grid with ProofNumbers band
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Successful in 3s
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
Swaps the soft-adjective benefits section (Rapid Development, Enterprise
Security, Built to Scale, 24/7 Support) for a compact horizontal stat
band so the first below-the-fold surface leads with hard, scannable
numbers instead of generic copy.

- New src/components/ProofNumbers.astro renders 4 large gradient numbers
  with one-line captions: 20+ Projects Delivered, 5★ Client Rating,
  11 Districts Deployed, < 2s Avg Page Load. Numbers carry a TODO for
  stakeholder confirmation; defaults mirror existing hero/stat copy.
- Responsive grid: 2×2 on mobile (<md), 4×1 horizontal band on desktop,
  with subtle vertical dividers between columns on md+.
- Matches site typography tokens (text-secondary-* / text-primary-*)
  with full dark-mode parity (gradients shift to primary-400/300).
- Removes now-unused `benefits` data array and inline SVG icons from
  src/pages/index.astro along with the surrounding section markup.
2026-06-17 13:14:51 +05:30
platform-mcp 5e943584d9 pre-agent: Replace why-choose-us with proof numbers 2026-06-17 13:09:02 +05:30
platform-mcp e418650757 feat(homepage): add testimonial photos + outcome metric badges
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 11m13s
- Add `image`/`imageAlt`/`initials`/`metric`/`metricLabel` fields to the
  homepage testimonials data so each card surfaces a concrete client
  outcome (e.g. "3× faster loan processing", "+42% organic traffic")
  next to the quote.
- Render a 56–64px rounded avatar with lazy-loaded `<img>` (width/height
  set to prevent CLS) and a brand-tinted metric badge in the card
  header. Initials sit behind the image as an automatic fallback if
  the avatar 404s or fails to decode.
- Ship 3 neutral SVG initial-avatars under public/images/testimonials/
  (ajay-pruthi, dhirendra-singh, dilip-raj) as placeholders until real
  headshots are licensed — flagged in the data-array comment so future
  edits know to swap in JPG/WebP at the same path.
- Tighten responsive sizing: card padding scales 8→14, quote text
  scales 20→30, avatar 56→64, and the star+metric row now wraps on
  narrow viewports so the layout reads cleanly at 360/768/1280.
2026-06-17 13:08:38 +05:30
platform-mcp 3394c246b2 pre-agent: Add testimonial photos and metrics 2026-06-17 13:03:51 +05:30
platform-mcp 8dbbad4277 refactor(home): remove duplicate FAQ section, merge unique Q&As into HomepageFaq
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Successful in 3s
The homepage rendered two FAQ blocks — an inline "Common Questions" section
plus the <HomepageFaq /> component above the final CTA — which produced two
FAQPage JSON-LD entries and a redundant accordion. Consolidate on
HomepageFaq (self-contained, polished accordion, better a11y) and:

- delete the inline FAQ <section>, the `faqs` array, the `faq={faqs}` prop
  on <SEO>, the orphan .faq-question/.faq-answer/.faq-icon CSS, and the
  initFAQ() boot script + its call in index.astro
- merge the 4 unique inline questions (support, process, team augmentation,
  security) into HomepageFaq's moneyFaqs; the 2 duplicates (technologies,
  timeline) were already covered there
- refresh HomepageFaq's lead-in copy and comments to reflect it as the
  single canonical homepage FAQ

Result: one FAQ heading + one accordion in the DOM, exactly one FAQPage
JSON-LD with 10 Q&A pairs, no broken anchor links.
2026-06-17 13:03:29 +05:30
platform-mcp 5395a529dd pre-agent: Remove duplicate FAQ section 2026-06-17 12:58:09 +05:30
platform-mcp 462d2058e1 fix(forms): harden honeypot hiding on contact + inline lead forms
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m31s
Both honeypots now use off-screen positioning (position:absolute; left:-9999px;
width:1px; height:1px; overflow:hidden) instead of relying on a parent's
aria-hidden alone, and the input itself carries tabindex="-1",
autocomplete="off", and aria-hidden="true". Labels are sr-only and sit as
siblings of the input (no nested label/input). Avoid display:none and
visibility:hidden so bots that skip those still trip the trap.

Verified: rendered honeypot HTML matches spec on both /contact and the
homepage inline lead form, and /api/contact still 200s a populated `website`
silently (bot path) while accepting empty `website` payloads as real leads.
2026-06-17 12:57:47 +05:30
platform-mcp 16c5f79f2b pre-agent: Hide honeypot field on contact forms 2026-06-17 12:54:32 +05:30
platform-mcp e4b962bb1b feat(api/contact): wire env-driven SMTP for contact + project inquiry forms
Ping Search Engines / Notify Search Engines (push) Successful in 5s
E2E Test Suite / Form Interaction Tests (push) Failing after 10m11s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m49s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m42s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m34s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m26s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m19s
E2E Test Suite / Security Header Tests (push) Failing after 14m55s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 20m54s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Waiting to run
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
Both the contact form (/contact) and the homepage "Tell us about your project"
inline lead form post to /api/contact. Updated that single handler to build
the nodemailer transport from a clean set of SMTP_* env vars per spec:

  SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS
  SMTP_FROM + SMTP_FROM_NAME  -> "From: Ajay Ghanwat <ghanwat.ajay@workroot.in>"
  MAIL_ADMIN_TO               -> admin notification recipient
                                  (falls back to SMTP_FROM, then legacy
                                   CONTACT_EMAIL_TO / CONTACT_EMAIL / SMTP_USER)
  SMTP_REQUIRE_TLS            -> force STARTTLS upgrade on port 587
  SMTP_CONNECTION_TIMEOUT_MS  -> defaults to 10000
  SMTP_GREETING_TIMEOUT_MS    -> defaults to 10000
  SMTP_SOCKET_TIMEOUT_MS      -> defaults to 10000

Behaviour preserved:
- Existing user-facing auto-reply (the block previously at L270) kept.
  Subject + body now adapt for project-inquiry sources (homepage-inline,
  homepage-hero, homepage-final-cta, mobile-sticky) so the admin inbox
  can triage [Project Inquiry] vs [Contact Form] at a glance.
- Graceful degradation per FORM_INTEGRATION.md: missing env vars or a
  failed nodemailer import logs a structured warning and returns 200 —
  the site never 500s on email config.
- Rate limiting, honeypot, CORS, validation, and fire-and-forget
  background dispatch are untouched.

Observability:
- Structured console logs at: transport created, admin accepted (with
  messageId), user auto-reply accepted (with messageId), and send-failure
  (with smtp code/command/response). SMTP_PASS is never logged.

Misc:
- .env.example updated to document the new variables and the legacy
  aliases that are still honored.
2026-06-17 12:25:08 +05:30
platform-mcp 0893078d73 pre-agent: Wire SMTP into contact and project inquiry forms 2026-06-17 12:16:51 +05:30
platform-mcp 8fb25ded08 docs(devops): document SMTP env vars for Hostinger mail integration
Deploy to Production / Build & Verify (push) Has been cancelled
Deploy to Production / Pre-Deploy Tests (push) Has been cancelled
Deploy to Production / Deploy to Railway (push) Has been cancelled
Deploy to Production / Deploy to Render (push) Has been cancelled
Deploy to Production / Deploy to VPS (PM2) (push) Has been cancelled
Deploy to Production / Deploy to Fly.io (push) Has been cancelled
Deploy to Production / Post-Deploy Verification (push) Has been cancelled
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Failing after 12m59s
Expand the CI/CD env-var table to list all SMTP_*, MAIL_ADMIN_TO,
SMTP_FROM, SMTP_FROM_NAME and STARTTLS/timeout knobs that the contact
and "Tell us about your project" forms now rely on. Adds a note that
the production secrets live in the Default environment's platform
env-var store and that the frontend service must be restarted after
any change.
2026-06-17 12:16:17 +05:30
platform-mcp 9057ec8d11 pre-agent: Configure SMTP environment variables for email 2026-06-17 12:14:12 +05:30
platform-mcp f0e755ef87 agent: Add Money-Focused FAQ Section to Homepage 2026-06-17 11:55:49 +05:30
platform-mcp 3e276375bf pre-agent: Add Money-Focused FAQ Section to Homepage 2026-06-17 11:50:11 +05:30
141 changed files with 9989 additions and 2578 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

@@ -35,8 +35,8 @@ Both endpoints include server-side validation, rate limiting, spam protection, a
- `name` — required, 2100 characters
- `email` — required, valid format, max 254 chars
- `phone` — optional; if present, must match phone regex
- `subject` — required, allowlist enum (web-development, mobile-development, cloud-services, ai-ml, consulting, support, other)
- `message` — required, 105000 characters
- `subject` — required, allowlist enum (erp-solutions, government-systems, web-development, mobile-development, ui-ux, consulting, other; legacy values cloud-services, ai-ml, support still accepted by the API but no longer offered in the contact-page dropdown)
- `message` — required, 102000 characters
- `website` — honeypot, must be empty
**Rate limit:** 5 requests per IP per hour
+17 -4
View File
@@ -312,13 +312,26 @@ All env vars must be configured in your hosting platform, not in the workflow.
| `NODE_ENV` | Yes | — | Must be `production` |
| `HOST` | Yes | — | `0.0.0.0` |
| `PORT` | Yes | — | `10000` |
| `CONTACT_EMAIL` | No | — | Where contact form sends emails |
| `SMTP_HOST` | No | — | Email server host |
| `SMTP_USER` | No | — | Email server username |
| `SMTP_PASS` | No | — | Email server password (use platform secrets) |
| `CONTACT_EMAIL` | Yes (legacy) | — | Legacy admin recipient for contact form; kept for backward compatibility with older code paths. Mirror of `MAIL_ADMIN_TO`. |
| `MAIL_ADMIN_TO` | Yes | — | Address that receives "Tell us about your project" and contact form submissions. |
| `SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.hostinger.com`). |
| `SMTP_PORT` | Yes | `587` | SMTP server port. Use `587` for STARTTLS, `465` for implicit TLS. |
| `SMTP_USER` | Yes | — | SMTP auth username (full email address). |
| `SMTP_PASS` | Yes | — | SMTP auth password — MUST be stored as a platform secret, never plaintext. |
| `SMTP_FROM` | Yes | — | `From:` address used by outbound mail (should match `SMTP_USER` or an allowed alias). |
| `SMTP_FROM_NAME` | Yes | — | Human-readable display name shown alongside `SMTP_FROM`. |
| `SMTP_SECURE` | Yes | `false` | `true` only for implicit-TLS ports (465). For port 587 keep `false` and rely on STARTTLS. |
| `SMTP_STARTTLS` | Yes | `true` | Negotiate STARTTLS on the SMTP connection. |
| `SMTP_REQUIRE_TLS` | Yes | `true` | Refuse to send if the server does not advertise STARTTLS — prevents silent plaintext fallback. |
| `SMTP_AUTH` | Yes | `true` | Authenticate with `SMTP_USER` / `SMTP_PASS`. |
| `SMTP_CONNECTION_TIMEOUT_MS` | No | `10000` | Socket connect timeout in milliseconds. |
| `SMTP_GREETING_TIMEOUT_MS` | No | `10000` | Wait time for the server greeting (EHLO/HELO) in milliseconds. |
| `SMTP_SOCKET_TIMEOUT_MS` | No | `10000` | Idle socket timeout in milliseconds (covers read/write while transferring the message). |
| `SENTRY_DSN` | No | — | Sentry error tracking |
| `LOG_LEVEL` | No | `info` | Logging verbosity |
> Production SMTP is provisioned on Hostinger and configured in the **Default** environment via the platform's env-var store (`SMTP_PASS` flagged as secret). After changing any `SMTP_*` value you MUST restart the `frontend` service so the new variables are picked up by the Astro SSR process.
---
## Rollback Procedures
+26 -6
View File
@@ -37,12 +37,32 @@ NODE_ENV=production
# ==================================
# Contact Form & Email (SMTP)
# ==================================
# Used by /api/contact for sending contact form submissions
# If not set, submissions are logged to console (dev/staging mode)
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_USER=your_email@example.com
# SMTP_PASS=your_app_password_here
# Used by /api/contact (powers both the contact page form AND the
# homepage "Tell us about your project" inline lead form).
# If unset, submissions are logged to console (dev/staging mode).
#
# --- Required for live email delivery ---
# SMTP_HOST=smtp.hostinger.com
# SMTP_PORT=587 # 587 = STARTTLS (recommended)
# SMTP_USER=ghanwat.ajay@workroot.in
# SMTP_PASS=your_app_password_here # MARK AS SECRET in platform UI
#
# --- From header (RFC 5322) ---
# SMTP_FROM=ghanwat.ajay@workroot.in
# SMTP_FROM_NAME=Ajay Ghanwat
#
# --- Admin recipient for inbound submissions ---
# Falls back to SMTP_FROM, then legacy CONTACT_EMAIL_TO/CONTACT_EMAIL.
# MAIL_ADMIN_TO=ghanwat.ajay@workroot.in
#
# --- TLS & timeouts (optional, sensible defaults applied) ---
# SMTP_REQUIRE_TLS=true # set to "false" to disable STARTTLS upgrade
# SMTP_CONNECTION_TIMEOUT_MS=10000
# SMTP_GREETING_TIMEOUT_MS=10000
# SMTP_SOCKET_TIMEOUT_MS=10000
#
# --- Legacy aliases (still honored for backwards compatibility) ---
# CONTACT_EMAIL_TO=hello@workroot.in
# CONTACT_EMAIL=hello@workroot.in
# ==================================
+87 -52
View File
@@ -1,11 +1,11 @@
# Graph Report - 2e709500-79ab-403f-9ca0-3f027ac9f36e (2026-06-17)
# Graph Report - 2e709500-79ab-403f-9ca0-3f027ac9f36e (2026-06-24)
## Corpus Check
- 56 files · ~324,046 words
- 64 files · ~615,668 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 518 nodes · 563 edges · 91 communities (84 shown, 7 thin omitted)
- 619 nodes · 640 edges · 117 communities (90 shown, 27 thin omitted)
- Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 4 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
@@ -23,9 +23,9 @@
- [[_COMMUNITY_Community 5|Community 5]]
- [[_COMMUNITY_Community 6|Community 6]]
- [[_COMMUNITY_Community 7|Community 7]]
- [[_COMMUNITY_Community 9|Community 9]]
- [[_COMMUNITY_Community 8|Community 8]]
- [[_COMMUNITY_Community 10|Community 10]]
- [[_COMMUNITY_Community 12|Community 12]]
- [[_COMMUNITY_Community 11|Community 11]]
- [[_COMMUNITY_Community 13|Community 13]]
- [[_COMMUNITY_Community 14|Community 14]]
- [[_COMMUNITY_Community 15|Community 15]]
@@ -49,7 +49,30 @@
- [[_COMMUNITY_Community 33|Community 33]]
- [[_COMMUNITY_Community 34|Community 34]]
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 46|Community 46]]
- [[_COMMUNITY_Community 47|Community 47]]
- [[_COMMUNITY_Community 48|Community 48]]
- [[_COMMUNITY_Community 49|Community 49]]
- [[_COMMUNITY_Community 50|Community 50]]
- [[_COMMUNITY_Community 51|Community 51]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 53|Community 53]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
- [[_COMMUNITY_Community 56|Community 56]]
- [[_COMMUNITY_Community 57|Community 57]]
- [[_COMMUNITY_Community 58|Community 58]]
- [[_COMMUNITY_Community 59|Community 59]]
- [[_COMMUNITY_Community 60|Community 60]]
- [[_COMMUNITY_Community 61|Community 61]]
- [[_COMMUNITY_Community 62|Community 62]]
- [[_COMMUNITY_Community 63|Community 63]]
- [[_COMMUNITY_Community 64|Community 64]]
## God Nodes (most connected - your core abstractions)
1. `Performance Testing Scripts` - 12 edges
@@ -73,7 +96,7 @@
- `POST()` --calls--> `logApiRequest()` [INFERRED]
src/pages/api/newsletter.ts → src/utils/logger.ts
## Communities (91 total, 7 thin omitted)
## Communities (117 total, 27 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
@@ -84,8 +107,8 @@ Cohesion: 0.05
Nodes (37): 1. `performance_test.py`, 2. `quick-perf-check.sh`, 3. `lighthouse_audit.py`, Available Scripts, Basic Response Time, Best Practices, CI/CD Integration, code:bash (# Quick check (recommended)) (+29 more)
### Community 2 - "Community 2"
Cohesion: 0.14
Nodes (27): checkRateLimit(), corsHeaders(), escapeHtml(), OPTIONS(), POST(), sendContactEmail(), validateContactForm(), checkRateLimit() (+19 more)
Cohesion: 0.13
Nodes (29): checkRateLimit(), corsHeaders(), escapeHtml(), getTransporter(), OPTIONS(), POST(), readSmtpConfig(), sendContactEmail() (+21 more)
### Community 3 - "Community 3"
Cohesion: 0.16
@@ -105,92 +128,104 @@ Nodes (15): 1. Modular Monolith Over Premature Microservices, 2. Role-Based Acce
### Community 7 - "Community 7"
Cohesion: 0.14
Nodes (13): 1. Multi-District Deployment Complexity, 2. Security and Compliance, 3. Reliability Under Constraint, Architecture Decisions That Paid Off, code:java (@Configuration), code:java (@Service), code:java (@Component), code:yaml (# District-specific feature configuration) (+5 more)
Nodes (13): 1. Database-per-district with central aggregation, 2. Offline-first for field-critical modules, 3. Audit logging baked into the data layer, Building something similar?, code:yaml (# Per-district deployment descriptor), code:java (@Scheduled(fixedDelay = 300_000) // every 5 minutes), code:yaml (features:), The Architecture We Committed To (+5 more)
### Community 9 - "Community 9"
Cohesion: 0.17
Nodes (11): 1. Customer Service Automation, 2. Predictive Analytics, 3. Process Automation, 4. Intelligent Document Processing, code:python (from transformers import pipeline), code:python (import pandas as pd), code:python (# Example: Extracting structured data from forms), Implementation Considerations (+3 more)
### Community 8 - "Community 8"
Cohesion: 0.14
Nodes (13): 1. Multi-District Deployment Complexity, 2. Security and Compliance, 3. Reliability Under Constraint, Architecture Decisions That Paid Off, code:java (@Configuration), code:java (@Service), code:java (@Component), code:yaml (# District-specific feature configuration) (+5 more)
### Community 10 - "Community 10"
Cohesion: 0.17
Nodes (11): 1. Customer Service Automation, 2. Predictive Analytics, 3. Process Automation, 4. Intelligent Document Processing, code:python (from transformers import pipeline), code:python (import pandas as pd), code:python (# Example: Extracting structured data from forms), Implementation Considerations (+3 more)
### Community 11 - "Community 11"
Cohesion: 0.17
Nodes (11): Code Example, code:astro (---), code:javascript (// Traditional SPA: Everything loads upfront), code:typescript (// src/content/config.ts), code:bash (npm create astro@latest), Content Collections: Type-Safe Content, Getting Started, How Astro Differs from Traditional Frameworks (+3 more)
### Community 13 - "Community 13"
### Community 14 - "Community 14"
Cohesion: 0.18
Nodes (10): code:hcl (# Define AWS EC2 instance), code:dockerfile (# Multi-stage build for a Go application), code:yaml (# AWS Cost allocation tags), Common Pitfalls, Conclusion, Containerization as a Migration Strategy, Cost Optimization Strategies, Infrastructure as Code (+2 more)
### Community 17 - "Community 17"
### Community 16 - "Community 16"
Cohesion: 0.2
Nodes (9): Catalogue Listing vs Bidding: How You Actually Win, Common Reasons Software Vendors Get Rejected or Delayed, Document Checklist, Fees, Caution Money, and Timelines, Frequently Asked Questions, MSME and Startup Benefits Worth Claiming, Step-by-Step Registration Process, Turning GeM Access Into Government Contracts (+1 more)
### Community 18 - "Community 18"
Cohesion: 0.22
Nodes (8): 2026 Cost Ranges by Project Tier, A Simple Way to Estimate Your Own Cost, Build vs Buy: When Custom Actually Pays Off, Frequently Asked Questions, Getting an Accurate Number for Your Project, Hidden Costs Buyers Forget to Budget, The Biggest Cost Driver: The Team, What "Custom ERP" Actually Means for Pricing
### Community 20 - "Community 20"
Cohesion: 0.43
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
### Community 18 - "Community 18"
### Community 21 - "Community 21"
Cohesion: 0.36
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
### Community 19 - "Community 19"
### Community 22 - "Community 22"
Cohesion: 0.46
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
### Community 20 - "Community 20"
Cohesion: 0.36
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
### Community 21 - "Community 21"
Cohesion: 0.46
Nodes (7): checkRateLimit(), corsHeaders(), escapeHtml(), OPTIONS(), POST(), sendContactEmail(), validateContactForm()
### Community 22 - "Community 22"
Cohesion: 0.43
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
### Community 23 - "Community 23"
Cohesion: 0.36
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
### Community 24 - "Community 24"
Cohesion: 0.46
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
Nodes (7): checkRateLimit(), corsHeaders(), escapeHtml(), OPTIONS(), POST(), sendContactEmail(), validateContactForm()
### Community 25 - "Community 25"
Cohesion: 0.43
Nodes (6): trackCaseStudyView(), trackEvent(), trackExternalLink(), trackFormSubmit(), trackNewsletterSignup(), trackPortfolioFilter()
### Community 26 - "Community 26"
Cohesion: 0.36
Nodes (7): extract_scores(), get_issues(), main(), Run Lighthouse audit on a URL, Extract key scores from Lighthouse data, Extract issues from Lighthouse audit, run_lighthouse()
### Community 27 - "Community 27"
Cohesion: 0.46
Nodes (7): captureScreenshot(), ensureDir(), generateIndex(), main(), screenshotPath(), setTheme(), waitForPageReady()
### Community 28 - "Community 28"
Cohesion: 0.36
Nodes (4): deleteFromDB(), getAllFromDB(), openDB(), processSyncQueue()
### Community 26 - "Community 26"
Cohesion: 0.48
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
### Community 27 - "Community 27"
Cohesion: 0.48
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
### Community 28 - "Community 28"
Cohesion: 0.48
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
### Community 29 - "Community 29"
Cohesion: 0.48
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
### Community 30 - "Community 30"
Cohesion: 0.47
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
Cohesion: 0.48
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
### Community 31 - "Community 31"
Cohesion: 0.48
Nodes (5): initAllAnimations(), initButtonRipple(), initCounters(), initProgressBars(), initScrollReveal()
### Community 32 - "Community 32"
Cohesion: 0.48
Nodes (5): extractJsonLd(), main(), validateOpenGraph(), validatePage(), validateTwitterCard()
### Community 33 - "Community 33"
Cohesion: 0.47
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
### Community 34 - "Community 34"
### Community 35 - "Community 35"
Cohesion: 0.47
Nodes (5): main(), measure_response_time(), print_stats(), Measure response time for a URL over multiple iterations, Print statistics for response times
### Community 37 - "Community 37"
Cohesion: 0.6
Nodes (3): formatUptime(), GET(), percentile()
### Community 35 - "Community 35"
### Community 38 - "Community 38"
Cohesion: 0.6
Nodes (3): formatUptime(), GET(), percentile()
## Knowledge Gaps
- **111 isolated node(s):** `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit`, `Measure response time for a URL over multiple iterations`, `Print statistics for response times` (+106 more)
- **156 isolated node(s):** `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit`, `Measure response time for a URL over multiple iterations`, `Print statistics for response times` (+151 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **7 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **27 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
@@ -198,13 +233,13 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 2 inferred relationships involving `captureException()` (e.g. with `sendContactEmail()` and `subscribeToNewsletter()`) actually correct?**
_`captureException()` has 2 INFERRED edges - model-reasoned connections that need verification._
- **What connects `Run Lighthouse audit on a URL`, `Extract key scores from Lighthouse data`, `Extract issues from Lighthouse audit` to the rest of the system?**
_111 weakly-connected nodes found - possible documentation gaps or missing edges._
_156 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
- **Should `Community 2` be split into smaller, more focused modules?**
_Cohesion score 0.14 - nodes in this community are weakly interconnected._
_Cohesion score 0.13 - nodes in this community are weakly interconnected._
- **Should `Community 4` be split into smaller, more focused modules?**
_Cohesion score 0.09 - nodes in this community are weakly interconnected._
- **Should `Community 5` be split into smaller, more focused modules?**
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "label": "middleware.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1"}, {"id": "src_middleware_validatecsrforigin", "label": "validateCsrfOrigin()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "astro_middleware", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_logger_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_logger_logger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_sentry_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_sentry_captureexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "src_middleware_validatecsrforigin", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "src_middleware_validatecsrforigin", "callee": "includes", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L24"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L25"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L36"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L37"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_inline_lead_form_spec_ts", "label": "inline-lead-form.spec.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/inline-lead-form.spec.ts", "source_location": "L1"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_inline_lead_form_spec_ts", "target": "test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/inline-lead-form.spec.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_config_ts", "label": "playwright.config.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright.config.ts", "source_location": "L1"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_config_ts", "target": "test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright.config.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_md", "label": "error-context.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-6ed4a--has-proper-structured-data-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-6ed4a--has-proper-structured-data-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-6ed4a--has-proper-structured-data-chromium/error-context.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_md", "target": "e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-6ed4a--has-proper-structured-data-chromium/error-context.md", "source_location": "L1", "weight": 1.0}, {"source": "e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_page_snapshot", "target": "e2e_smoke_suite_smoke_suit_6ed4a_has_proper_structured_data_chromium_error_context_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-6ed4a--has-proper-structured-data-chromium/error-context.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "label": "middleware.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1"}, {"id": "src_middleware_validatecsrforigin", "label": "validateCsrfOrigin()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "astro_middleware", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_logger_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_logger_logger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_sentry_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_sentry_captureexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "src_middleware_validatecsrforigin", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "src_middleware_validatecsrforigin", "callee": "includes", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L24"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L25"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L36"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L37"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_1639575cca1fe4171917a09f8f7cc59c33654911_md", "label": "1639575cca1fe4171917a09f8f7cc59c33654911.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/1639575cca1fe4171917a09f8f7cc59c33654911.md", "source_location": "L1"}, {"id": "data_1639575cca1fe4171917a09f8f7cc59c33654911_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/1639575cca1fe4171917a09f8f7cc59c33654911.md", "source_location": "L1"}, {"id": "data_1639575cca1fe4171917a09f8f7cc59c33654911_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/1639575cca1fe4171917a09f8f7cc59c33654911.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_1639575cca1fe4171917a09f8f7cc59c33654911_md", "target": "data_1639575cca1fe4171917a09f8f7cc59c33654911_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/1639575cca1fe4171917a09f8f7cc59c33654911.md", "source_location": "L1", "weight": 1.0}, {"source": "data_1639575cca1fe4171917a09f8f7cc59c33654911_page_snapshot", "target": "data_1639575cca1fe4171917a09f8f7cc59c33654911_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/1639575cca1fe4171917a09f8f7cc59c33654911.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_md", "label": "error-context.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-8282a-mepage-has-proper-meta-tags-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-8282a-mepage-has-proper-meta-tags-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-8282a-mepage-has-proper-meta-tags-chromium/error-context.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_md", "target": "e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-8282a-mepage-has-proper-meta-tags-chromium/error-context.md", "source_location": "L1", "weight": 1.0}, {"source": "e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_page_snapshot", "target": "e2e_smoke_suite_smoke_suit_8282a_mepage_has_proper_meta_tags_chromium_error_context_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-8282a-mepage-has-proper-meta-tags-chromium/error-context.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_a1c157260391dca389fbe38c6259c4efd286b319_md", "label": "a1c157260391dca389fbe38c6259c4efd286b319.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/a1c157260391dca389fbe38c6259c4efd286b319.md", "source_location": "L1"}, {"id": "data_a1c157260391dca389fbe38c6259c4efd286b319_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/a1c157260391dca389fbe38c6259c4efd286b319.md", "source_location": "L1"}, {"id": "data_a1c157260391dca389fbe38c6259c4efd286b319_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/a1c157260391dca389fbe38c6259c4efd286b319.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_a1c157260391dca389fbe38c6259c4efd286b319_md", "target": "data_a1c157260391dca389fbe38c6259c4efd286b319_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/a1c157260391dca389fbe38c6259c4efd286b319.md", "source_location": "L1", "weight": 1.0}, {"source": "data_a1c157260391dca389fbe38c6259c4efd286b319_page_snapshot", "target": "data_a1c157260391dca389fbe38c6259c4efd286b319_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/a1c157260391dca389fbe38c6259c4efd286b319.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_md", "label": "448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "source_location": "L1"}, {"id": "data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "source_location": "L1"}, {"id": "data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_md", "target": "data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "source_location": "L1", "weight": 1.0}, {"source": "data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_page_snapshot", "target": "data_448763de83b442e1e9a6c6e4e70870f8a7246e0a_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/448763de83b442e1e9a6c6e4e70870f8a7246e0a.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_md", "label": "error-context.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/theme-Theme-ARIA-Accessibi-8c42a-ria-checked-desktop-mobile--chromium/error-context.md", "source_location": "L1"}, {"id": "theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/theme-Theme-ARIA-Accessibi-8c42a-ria-checked-desktop-mobile--chromium/error-context.md", "source_location": "L1"}, {"id": "theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/theme-Theme-ARIA-Accessibi-8c42a-ria-checked-desktop-mobile--chromium/error-context.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_md", "target": "theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/theme-Theme-ARIA-Accessibi-8c42a-ria-checked-desktop-mobile--chromium/error-context.md", "source_location": "L1", "weight": 1.0}, {"source": "theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_page_snapshot", "target": "theme_theme_aria_accessibi_8c42a_ria_checked_desktop_mobile_chromium_error_context_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/theme-Theme-ARIA-Accessibi-8c42a-ria-checked-desktop-mobile--chromium/error-context.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_content_config_ts", "label": "config.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/content/config.ts", "source_location": "L1"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_content_config_ts", "target": "astro_content", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/content/config.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_md", "label": "3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "source_location": "L1"}, {"id": "data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "source_location": "L1"}, {"id": "data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_codeblock_1", "label": "code:yaml (- generic [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_md", "target": "data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "source_location": "L1", "weight": 1.0}, {"source": "data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_page_snapshot", "target": "data_3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/3e3d231801ce8a68ec884e518f6e46e7f3a8d6ff.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_md", "label": "6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "source_location": "L1"}, {"id": "data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "source_location": "L1"}, {"id": "data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_md", "target": "data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "source_location": "L1", "weight": 1.0}, {"source": "data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_page_snapshot", "target": "data_6a86fc05ec2c2ce701392e4df2db4d19695f2488_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6a86fc05ec2c2ce701392e4df2db4d19695f2488.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_md", "label": "error-context.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-7fcfd-ntact-form-submission-works-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-7fcfd-ntact-form-submission-works-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_codeblock_1", "label": "code:yaml (- generic [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-7fcfd-ntact-form-submission-works-chromium/error-context.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_md", "target": "e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-7fcfd-ntact-form-submission-works-chromium/error-context.md", "source_location": "L1", "weight": 1.0}, {"source": "e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_page_snapshot", "target": "e2e_smoke_suite_smoke_suit_7fcfd_ntact_form_submission_works_chromium_error_context_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-7fcfd-ntact-form-submission-works-chromium/error-context.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_md", "label": "e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "source_location": "L1"}, {"id": "data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "source_location": "L1"}, {"id": "data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_md", "target": "data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "source_location": "L1", "weight": 1.0}, {"source": "data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_page_snapshot", "target": "data_e913a96525d005a5baae8ddb2fbea15bbc53abb1_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/e913a96525d005a5baae8ddb2fbea15bbc53abb1.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_md", "label": "cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "source_location": "L1"}, {"id": "data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "source_location": "L1"}, {"id": "data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_codeblock_1", "label": "code:yaml (- generic [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_md", "target": "data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "source_location": "L1", "weight": 1.0}, {"source": "data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_page_snapshot", "target": "data_cc0ed5549c339ad261436d0f63d13eb3259a9dde_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/cc0ed5549c339ad261436d0f63d13eb3259a9dde.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_sticky_cta_spec_ts", "label": "sticky-cta.spec.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/sticky-cta.spec.ts", "source_location": "L1"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_sticky_cta_spec_ts", "target": "test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/sticky-cta.spec.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_690bd8f0a073eceeb87b8e46e681123950f08f93_md", "label": "690bd8f0a073eceeb87b8e46e681123950f08f93.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/690bd8f0a073eceeb87b8e46e681123950f08f93.md", "source_location": "L1"}, {"id": "data_690bd8f0a073eceeb87b8e46e681123950f08f93_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/690bd8f0a073eceeb87b8e46e681123950f08f93.md", "source_location": "L1"}, {"id": "data_690bd8f0a073eceeb87b8e46e681123950f08f93_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/690bd8f0a073eceeb87b8e46e681123950f08f93.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_690bd8f0a073eceeb87b8e46e681123950f08f93_md", "target": "data_690bd8f0a073eceeb87b8e46e681123950f08f93_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/690bd8f0a073eceeb87b8e46e681123950f08f93.md", "source_location": "L1", "weight": 1.0}, {"source": "data_690bd8f0a073eceeb87b8e46e681123950f08f93_page_snapshot", "target": "data_690bd8f0a073eceeb87b8e46e681123950f08f93_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/690bd8f0a073eceeb87b8e46e681123950f08f93.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_806b690efd1ee745a1991725b2d2aea1b5c435f2_md", "label": "806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "source_location": "L1"}, {"id": "data_806b690efd1ee745a1991725b2d2aea1b5c435f2_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "source_location": "L1"}, {"id": "data_806b690efd1ee745a1991725b2d2aea1b5c435f2_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_806b690efd1ee745a1991725b2d2aea1b5c435f2_md", "target": "data_806b690efd1ee745a1991725b2d2aea1b5c435f2_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "source_location": "L1", "weight": 1.0}, {"source": "data_806b690efd1ee745a1991725b2d2aea1b5c435f2_page_snapshot", "target": "data_806b690efd1ee745a1991725b2d2aea1b5c435f2_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/806b690efd1ee745a1991725b2d2aea1b5c435f2.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_md", "label": "32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "source_location": "L1"}, {"id": "data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "source_location": "L1"}, {"id": "data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_md", "target": "data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "source_location": "L1", "weight": 1.0}, {"source": "data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_page_snapshot", "target": "data_32e7238c826ab5be125e270cc69efc4a37a8ffb3_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/32e7238c826ab5be125e270cc69efc4a37a8ffb3.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_inline_lead_form_spec_ts", "label": "inline-lead-form.spec.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/inline-lead-form.spec.ts", "source_location": "L1"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_tests_inline_lead_form_spec_ts", "target": "test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/tests/inline-lead-form.spec.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_md", "label": "c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "source_location": "L1"}, {"id": "data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "source_location": "L1"}, {"id": "data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_md", "target": "data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "source_location": "L1", "weight": 1.0}, {"source": "data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_page_snapshot", "target": "data_c0712711f6dffbdf3e61da69d7bf618fa7d12cce_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/c0712711f6dffbdf3e61da69d7bf618fa7d12cce.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "label": "middleware.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1"}, {"id": "src_middleware_validatecsrforigin", "label": "validateCsrfOrigin()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "astro_middleware", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_logger_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_logger_logger", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_utils_sentry_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "utils_sentry_captureexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L3", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_middleware_ts", "target": "src_middleware_validatecsrforigin", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "src_middleware_validatecsrforigin", "callee": "includes", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L24"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L25"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L36"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "get", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L37"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}, {"caller_nid": "src_middleware_validatecsrforigin", "callee": "startsWith", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/middleware.ts", "source_location": "L52"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_md", "label": "0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "source_location": "L1"}, {"id": "data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "source_location": "L1"}, {"id": "data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_md", "target": "data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "source_location": "L1", "weight": 1.0}, {"source": "data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_page_snapshot", "target": "data_0f6bc059f79105264bd8619a97b50f0c07ebb8d8_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/0f6bc059f79105264bd8619a97b50f0c07ebb8d8.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "label": "sitemap.xml.ts", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L1"}, {"id": "pages_sitemap_xml_get", "label": "GET()", "file_type": "code", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L13"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "astro", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L1", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "astro_content", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L2", "weight": 1.0}, {"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_src_pages_sitemap_xml_ts", "target": "pages_sitemap_xml_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L13", "weight": 1.0}], "raw_calls": [{"caller_nid": "pages_sitemap_xml_get", "callee": "getCollection", "is_member_call": false, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L15"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "getCollection", "is_member_call": false, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L20"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L43"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L52"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "join", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L69"}, {"caller_nid": "pages_sitemap_xml_get", "callee": "map", "is_member_call": true, "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/src/pages/sitemap.xml.ts", "source_location": "L69"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_md", "label": "error-context.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-742f0-enders-correctly-on-Desktop-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-742f0-enders-correctly-on-Desktop-chromium/error-context.md", "source_location": "L1"}, {"id": "e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-742f0-enders-correctly-on-Desktop-chromium/error-context.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_test_results_e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_md", "target": "e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-742f0-enders-correctly-on-Desktop-chromium/error-context.md", "source_location": "L1", "weight": 1.0}, {"source": "e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_page_snapshot", "target": "e2e_smoke_suite_smoke_suit_742f0_enders_correctly_on_desktop_chromium_error_context_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/test-results/e2e-smoke-suite-Smoke-Suit-742f0-enders-correctly-on-Desktop-chromium/error-context.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_md", "label": "bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "source_location": "L1"}, {"id": "data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "source_location": "L1"}, {"id": "data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_md", "target": "data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "source_location": "L1", "weight": 1.0}, {"source": "data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_page_snapshot", "target": "data_bbfcf94d298e196a738e1c4aa9fcbebf09850749_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/bbfcf94d298e196a738e1c4aa9fcbebf09850749.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_6604b3783e14b0fe2e620b288e8828b6f55be330_md", "label": "6604b3783e14b0fe2e620b288e8828b6f55be330.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6604b3783e14b0fe2e620b288e8828b6f55be330.md", "source_location": "L1"}, {"id": "data_6604b3783e14b0fe2e620b288e8828b6f55be330_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6604b3783e14b0fe2e620b288e8828b6f55be330.md", "source_location": "L1"}, {"id": "data_6604b3783e14b0fe2e620b288e8828b6f55be330_codeblock_1", "label": "code:yaml (- generic [active] [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6604b3783e14b0fe2e620b288e8828b6f55be330.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_6604b3783e14b0fe2e620b288e8828b6f55be330_md", "target": "data_6604b3783e14b0fe2e620b288e8828b6f55be330_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6604b3783e14b0fe2e620b288e8828b6f55be330.md", "source_location": "L1", "weight": 1.0}, {"source": "data_6604b3783e14b0fe2e620b288e8828b6f55be330_page_snapshot", "target": "data_6604b3783e14b0fe2e620b288e8828b6f55be330_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/6604b3783e14b0fe2e620b288e8828b6f55be330.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_9b3c49237ac0c6da58c2e4aee5df00459559a271_md", "label": "9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "source_location": "L1"}, {"id": "data_9b3c49237ac0c6da58c2e4aee5df00459559a271_page_snapshot", "label": "Page snapshot", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "source_location": "L1"}, {"id": "data_9b3c49237ac0c6da58c2e4aee5df00459559a271_codeblock_1", "label": "code:yaml (- generic [ref=e1]:)", "file_type": "document", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "source_location": "L3"}], "edges": [{"source": "home_agent_ai_agent_backend_workspace_2e709500_79ab_403f_9ca0_3f027ac9f36e_playwright_report_data_9b3c49237ac0c6da58c2e4aee5df00459559a271_md", "target": "data_9b3c49237ac0c6da58c2e4aee5df00459559a271_page_snapshot", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "source_location": "L1", "weight": 1.0}, {"source": "data_9b3c49237ac0c6da58c2e4aee5df00459559a271_page_snapshot", "target": "data_9b3c49237ac0c6da58c2e4aee5df00459559a271_codeblock_1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/home/agent/ai-agent/backend/workspace/2e709500-79ab-403f-9ca0-3f027ac9f36e/playwright-report/data/9b3c49237ac0c6da58c2e4aee5df00459559a271.md", "source_location": "L3", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
+3090 -1409
View File
File diff suppressed because it is too large Load Diff
+1473 -15
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -45,10 +45,13 @@
"nodemailer": "^8.0.3"
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/tailwind": "^5.1.0",
"@playwright/test": "^1.58.2",
"@types/nodemailer": "^7.0.11",
"jsdom": "^29.1.1",
"playwright": "^1.58.2",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
['list']
],
use: {
baseURL: 'http://localhost:10000',
baseURL: process.env.BASE_URL || 'http://localhost:10000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -6
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="Government of Bihar logo placeholder">
<title>Govt. of Bihar</title>
<circle cx="20" cy="24" r="13" fill="#15803d"/>
<path d="M20 13 L21.8 19 L28 19 L23 22.6 L24.8 28.8 L20 25 L15.2 28.8 L17 22.6 L12 19 L18.2 19 Z" fill="#fef3c7"/>
<text x="40" y="22" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="13" fill="#0f172a" letter-spacing="0.3">GOVT. OF BIHAR</text>
<text x="40" y="36" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="500" font-size="9" fill="#475569" letter-spacing="0.8">11 DISTRICTS · PSCL</text>
</svg>

After

Width:  |  Height:  |  Size: 718 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="IIT Patna logo placeholder">
<title>IIT Patna</title>
<path d="M20 8 L34 18 L34 38 L6 38 L6 18 Z" fill="#b91c1c"/>
<rect x="12" y="24" width="4" height="12" fill="#fef3c7"/>
<rect x="18" y="24" width="4" height="12" fill="#fef3c7"/>
<rect x="24" y="24" width="4" height="12" fill="#fef3c7"/>
<rect x="6" y="22" width="28" height="2" fill="#fef3c7"/>
<text x="42" y="22" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="14" fill="#0f172a" letter-spacing="0.5">IIT PATNA</text>
<text x="42" y="36" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="500" font-size="9" fill="#475569" letter-spacing="1">UNIVERSITY SYSTEM</text>
</svg>

After

Width:  |  Height:  |  Size: 832 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="MOC Soft Technologies logo placeholder">
<title>MOC Soft</title>
<rect x="6" y="12" width="24" height="24" rx="4" fill="#7c3aed"/>
<text x="18" y="30" text-anchor="middle" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="13" fill="#ffffff" letter-spacing="-0.5">MOC</text>
<text x="38" y="24" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="14" fill="#0f172a" letter-spacing="0">MOC Soft</text>
<text x="38" y="38" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="500" font-size="9" fill="#475569" letter-spacing="0.8">TECHNOLOGIES</text>
</svg>

After

Width:  |  Height:  |  Size: 790 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="PLNR logo placeholder">
<title>PLNR</title>
<rect x="6" y="10" width="28" height="28" rx="6" fill="#1d4ed8"/>
<text x="20" y="29" text-anchor="middle" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="14" fill="#ffffff" letter-spacing="-0.5">P</text>
<text x="44" y="31" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="20" fill="#1e293b" letter-spacing="-0.5">PLNR</text>
</svg>

After

Width:  |  Height:  |  Size: 589 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="PSCL logo placeholder">
<title>PSCL</title>
<rect x="6" y="10" width="28" height="28" rx="14" fill="#0891b2"/>
<text x="20" y="29" text-anchor="middle" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="13" fill="#ffffff" letter-spacing="-0.3">PS</text>
<text x="44" y="22" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="18" fill="#0f172a" letter-spacing="0">PSCL</text>
<text x="44" y="36" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="500" font-size="9" fill="#475569" letter-spacing="0.8">BIHAR · SCMS</text>
</svg>

After

Width:  |  Height:  |  Size: 766 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" width="160" height="48" role="img" aria-label="RENTEC logo placeholder">
<title>RENTEC</title>
<circle cx="20" cy="24" r="12" fill="#0d9488"/>
<path d="M14 20h8a3 3 0 0 1 0 6h-3l4 4h-3l-4-4h-2v4h-2V20zm2 2v2h6a1 1 0 0 0 0-2h-6z" fill="#ffffff"/>
<text x="40" y="31" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="800" font-size="18" fill="#0f172a" letter-spacing="0.5">RENTEC</text>
</svg>

After

Width:  |  Height:  |  Size: 492 B

+113
View File
@@ -0,0 +1,113 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 720" width="1200" height="720" role="img" aria-label="WorkRoot project dashboard preview">
<defs>
<linearGradient id="wr-accent" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#22d3ee"/>
<stop offset="1" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="wr-bar" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#0891b2"/>
<stop offset="1" stop-color="#22d3ee"/>
</linearGradient>
<linearGradient id="wr-area" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0891b2" stop-opacity="0.35"/>
<stop offset="1" stop-color="#0891b2" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- App canvas -->
<rect width="1200" height="720" fill="#f1f5f9"/>
<!-- Sidebar -->
<rect width="240" height="720" fill="#0f172a"/>
<circle cx="44" cy="52" r="14" fill="url(#wr-accent)"/>
<rect x="68" y="44" width="104" height="16" rx="6" fill="#e2e8f0"/>
<!-- Nav items -->
<rect x="20" y="108" width="200" height="40" rx="10" fill="#0e7490"/>
<rect x="40" y="120" width="18" height="16" rx="4" fill="#67e8f9"/>
<rect x="70" y="121" width="120" height="14" rx="5" fill="#e2e8f0"/>
<g fill="#334155">
<rect x="40" y="172" width="18" height="16" rx="4"/>
<rect x="40" y="224" width="18" height="16" rx="4"/>
<rect x="40" y="276" width="18" height="16" rx="4"/>
<rect x="40" y="328" width="18" height="16" rx="4"/>
</g>
<g fill="#475569">
<rect x="70" y="173" width="110" height="14" rx="5"/>
<rect x="70" y="225" width="96" height="14" rx="5"/>
<rect x="70" y="277" width="124" height="14" rx="5"/>
<rect x="70" y="329" width="84" height="14" rx="5"/>
</g>
<!-- Top header -->
<rect x="240" width="960" height="80" fill="#ffffff"/>
<line x1="240" y1="80" x2="1200" y2="80" stroke="#e2e8f0" stroke-width="2"/>
<rect x="276" y="30" width="220" height="20" rx="7" fill="#0f172a"/>
<rect x="980" y="26" width="150" height="28" rx="14" fill="#f1f5f9"/>
<circle cx="1150" cy="40" r="18" fill="url(#wr-accent)"/>
<!-- Stat cards -->
<g>
<rect x="276" y="116" width="280" height="120" rx="16" fill="#ffffff" stroke="#e2e8f0" stroke-width="2"/>
<rect x="300" y="140" width="100" height="12" rx="5" fill="#94a3b8"/>
<text x="300" y="196" font-family="Plus Jakarta Sans, Segoe UI, sans-serif" font-size="44" font-weight="700" fill="#0f172a">20+</text>
<rect x="476" y="160" width="56" height="56" rx="14" fill="#ecfeff"/>
<rect x="492" y="180" width="24" height="16" rx="4" fill="#0891b2"/>
</g>
<g>
<rect x="588" y="116" width="280" height="120" rx="16" fill="#ffffff" stroke="#e2e8f0" stroke-width="2"/>
<rect x="612" y="140" width="120" height="12" rx="5" fill="#94a3b8"/>
<text x="612" y="196" font-family="Plus Jakarta Sans, Segoe UI, sans-serif" font-size="44" font-weight="700" fill="#0f172a">11</text>
<rect x="788" y="160" width="56" height="56" rx="14" fill="#fffbeb"/>
<rect x="804" y="180" width="24" height="16" rx="4" fill="#f59e0b"/>
</g>
<g>
<rect x="900" y="116" width="280" height="120" rx="16" fill="#ffffff" stroke="#e2e8f0" stroke-width="2"/>
<rect x="924" y="140" width="140" height="12" rx="5" fill="#94a3b8"/>
<text x="924" y="196" font-family="Plus Jakarta Sans, Segoe UI, sans-serif" font-size="44" font-weight="700" fill="#0f172a">50,000+</text>
<rect x="1100" y="160" width="56" height="56" rx="14" fill="#ecfeff"/>
<rect x="1116" y="180" width="24" height="16" rx="4" fill="#0891b2"/>
</g>
<!-- Chart card -->
<rect x="276" y="268" width="556" height="416" rx="16" fill="#ffffff" stroke="#e2e8f0" stroke-width="2"/>
<rect x="300" y="296" width="160" height="16" rx="6" fill="#0f172a"/>
<rect x="300" y="324" width="100" height="12" rx="5" fill="#94a3b8"/>
<!-- Area line -->
<path d="M300 560 L370 520 L440 540 L510 470 L580 500 L650 430 L720 450 L808 380"
fill="none" stroke="url(#wr-bar)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M300 560 L370 520 L440 540 L510 470 L580 500 L650 430 L720 450 L808 380 L808 650 L300 650 Z"
fill="url(#wr-area)"/>
<g fill="#0891b2">
<circle cx="370" cy="520" r="6"/>
<circle cx="510" cy="470" r="6"/>
<circle cx="650" cy="430" r="6"/>
<circle cx="808" cy="380" r="6"/>
</g>
<line x1="300" y1="650" x2="808" y2="650" stroke="#e2e8f0" stroke-width="2"/>
<!-- Activity / table card -->
<rect x="860" y="268" width="320" height="416" rx="16" fill="#ffffff" stroke="#e2e8f0" stroke-width="2"/>
<rect x="884" y="296" width="150" height="16" rx="6" fill="#0f172a"/>
<g>
<circle cx="900" cy="356" r="12" fill="#ecfeff"/>
<rect x="924" y="344" width="180" height="12" rx="5" fill="#334155"/>
<rect x="924" y="364" width="110" height="10" rx="5" fill="#94a3b8"/>
</g>
<g>
<circle cx="900" cy="412" r="12" fill="#fffbeb"/>
<rect x="924" y="400" width="160" height="12" rx="5" fill="#334155"/>
<rect x="924" y="420" width="130" height="10" rx="5" fill="#94a3b8"/>
</g>
<g>
<circle cx="900" cy="468" r="12" fill="#ecfeff"/>
<rect x="924" y="456" width="190" height="12" rx="5" fill="#334155"/>
<rect x="924" y="476" width="96" height="10" rx="5" fill="#94a3b8"/>
</g>
<g>
<circle cx="900" cy="524" r="12" fill="#fffbeb"/>
<rect x="924" y="512" width="150" height="12" rx="5" fill="#334155"/>
<rect x="924" y="532" width="120" height="10" rx="5" fill="#94a3b8"/>
</g>
<rect x="884" y="600" width="272" height="48" rx="12" fill="url(#wr-accent)"/>
<rect x="980" y="617" width="80" height="14" rx="6" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600" role="img" aria-label="Placeholder portrait for Ajay Ghanwat — replace with real headshot">
<defs>
<linearGradient id="ag-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0891b2"/>
<stop offset="100%" stop-color="#155e75"/>
</linearGradient>
</defs>
<rect width="800" height="600" fill="url(#ag-grad)"/>
<text x="400" y="300" text-anchor="middle" dominant-baseline="central" font-family="'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="220" fill="#ffffff" letter-spacing="-6">AG</text>
</svg>

After

Width:  |  Height:  |  Size: 690 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600" role="img" aria-label="Placeholder portrait for Ashok Jamdade — replace with real headshot">
<defs>
<linearGradient id="aj-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0891b2"/>
<stop offset="100%" stop-color="#155e75"/>
</linearGradient>
</defs>
<rect width="800" height="600" fill="url(#aj-grad)"/>
<text x="400" y="300" text-anchor="middle" dominant-baseline="central" font-family="'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="220" fill="#ffffff" letter-spacing="-6">AJ</text>
</svg>

After

Width:  |  Height:  |  Size: 691 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600" role="img" aria-label="Placeholder portrait for Swapnali Vachkal — replace with real headshot">
<defs>
<linearGradient id="sv-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0891b2"/>
<stop offset="100%" stop-color="#155e75"/>
</linearGradient>
</defs>
<rect width="800" height="600" fill="url(#sv-grad)"/>
<text x="400" y="300" text-anchor="middle" dominant-baseline="central" font-family="'Plus Jakarta Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="220" fill="#ffffff" letter-spacing="-6">SV</text>
</svg>

After

Width:  |  Height:  |  Size: 694 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Avatar placeholder for Ajay Pruthi">
<defs>
<linearGradient id="ap-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#1d4ed8"/>
</linearGradient>
</defs>
<rect width="200" height="200" rx="100" fill="url(#ap-grad)"/>
<text x="100" y="100" text-anchor="middle" dominant-baseline="central" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="78" fill="#ffffff" letter-spacing="-2">AP</text>
</svg>

After

Width:  |  Height:  |  Size: 644 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Avatar placeholder for Dhirendra Singh">
<defs>
<linearGradient id="ds-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#b45309"/>
</linearGradient>
</defs>
<rect width="200" height="200" rx="100" fill="url(#ds-grad)"/>
<text x="100" y="100" text-anchor="middle" dominant-baseline="central" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="78" fill="#ffffff" letter-spacing="-2">DS</text>
</svg>

After

Width:  |  Height:  |  Size: 648 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="Avatar placeholder for Dilip Raj">
<defs>
<linearGradient id="dr-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#10b981"/>
<stop offset="100%" stop-color="#047857"/>
</linearGradient>
</defs>
<rect width="200" height="200" rx="100" fill="url(#dr-grad)"/>
<text x="100" y="100" text-anchor="middle" dominant-baseline="central" font-family="system-ui,-apple-system,'Segoe UI',Roboto,sans-serif" font-weight="700" font-size="78" fill="#ffffff" letter-spacing="-2">DR</text>
</svg>

After

Width:  |  Height:  |  Size: 642 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+12
View File
@@ -11,6 +11,18 @@
"lang": "en-US",
"categories": ["business", "productivity"],
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
+52
View File
@@ -58,6 +58,9 @@ Allow: /
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
# Anthropic/Claude
User-agent: anthropic-ai
Allow: /
@@ -65,10 +68,22 @@ Allow: /
User-agent: Claude-Web
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: Claude-User
Allow: /
# Perplexity AI
User-agent: PerplexityBot
Allow: /
User-agent: Perplexity-User
Allow: /
# Google Gemini/Bard
User-agent: Google-Extended
Allow: /
@@ -80,3 +95,40 @@ Allow: /
# Meta AI
User-agent: FacebookBot
Allow: /
User-agent: Meta-ExternalAgent
Allow: /
User-agent: meta-externalagent
Allow: /
# Apple Intelligence
User-agent: Applebot
Allow: /
User-agent: Applebot-Extended
Allow: /
# Amazon AI
User-agent: Amazonbot
Allow: /
# ByteDance / TikTok AI
User-agent: Bytespider
Allow: /
# Cohere AI
User-agent: cohere-ai
Allow: /
# DuckDuckGo AI (DuckAssist)
User-agent: DuckAssistBot
Allow: /
# You.com AI
User-agent: YouBot
Allow: /
# Diffbot (knowledge graph / AI training)
User-agent: Diffbot
Allow: /
+25
View File
@@ -15,6 +15,31 @@
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://workroot.in/services/custom-erp-development/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://workroot.in/services/university-erp/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://workroot.in/services/government-it-solutions-gem-vendor/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://workroot.in/services/police-management-system/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://workroot.in/services/react-native-app-development/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://workroot.in/portfolio/</loc>
<changefreq>weekly</changefreq>
+166
View File
@@ -0,0 +1,166 @@
/*!
* WRNexus Web Push — service worker.
*
* ┌─────────────────────────────────────────────────────────────────────────┐
* │ HOST THIS FILE ON YOUR OWN ORIGIN. │
* │ │
* │ A service worker can only control the origin it is served from, so the │
* │ browser will NOT let wrnexus.com register a worker for your site. Copy │
* │ this file to the ROOT of your site and serve it at: │
* │ │
* │ https://your-domain.example/wrnexus-push-sw.js │
* │ │
* │ It must be served same-origin, over HTTPS, with │
* │ `Content-Type: text/javascript`. To control the whole site from a │
* │ non-root path, also send `Service-Worker-Allowed: /`. │
* └─────────────────────────────────────────────────────────────────────────┘
*
* Core behaviour needs NO configuration — the click/delivery tracking URLs are
* carried inside each push payload built by the WRNexus API. The optional
* CONFIG block below is only used to silently re-subscribe after the browser
* rotates a subscription (`pushsubscriptionchange`); leave it blank to skip
* that.
*/
/* eslint-disable no-restricted-globals */
// ── OPTIONAL: only needed for automatic re-subscription on key rotation ──────
var WRNEXUS_PUSH_CONFIG = {
// Your site's publicKey (the same value as data-site-id in the snippet).
siteKey: '',
// The WRNexus API origin (e.g. 'https://api.wrnexus.com').
apiBase: '',
// Your site's VAPID public key (shown in Push → Settings).
vapidPublicKey: '',
};
self.addEventListener('install', function () {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
// CORS-simple POST (text/plain, no custom headers, no credentials) so the
// receipt reaches the WRNexus API cross-origin without a preflight.
function postReceipt(url) {
if (!url) return Promise.resolve();
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
credentials: 'omit',
keepalive: true,
mode: 'cors',
}).catch(function () {});
}
function urlBase64ToUint8Array(base64String) {
var padding = '='.repeat((4 - (base64String.length % 4)) % 4);
var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
var raw = self.atob(base64);
var out = new Uint8Array(raw.length);
for (var i = 0; i < raw.length; ++i) out[i] = raw.charCodeAt(i);
return out;
}
function keyToB64(buf) {
if (!buf) return '';
var bytes = new Uint8Array(buf);
var binary = '';
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return self
.btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// ── Incoming notification ─────────────────────────────────────────────────
self.addEventListener('push', function (event) {
var data = {};
try {
data = event.data ? event.data.json() : {};
} catch (_e) {
try {
data = { title: event.data && event.data.text() };
} catch (_e2) {
data = {};
}
}
var title = data.title || 'Notification';
var options = {
body: data.body || '',
icon: data.icon || undefined,
image: data.image || undefined,
badge: data.badge || undefined,
tag: data.tag || data.campaignId || undefined,
requireInteraction: !!data.requireInteraction,
data: {
clickUrl: data.clickUrl || data.url || '/',
url: data.url || '/',
openUrl: data.openUrl || '',
campaignId: data.campaignId || '',
subscriptionId: data.subscriptionId || '',
},
};
event.waitUntil(
Promise.all([
self.registration.showNotification(title, options),
// Delivery receipt — records a `delivered` event for analytics.
postReceipt(data.deliveredUrl),
])
);
});
// ── Click → record + open the destination ─────────────────────────────────
self.addEventListener('notificationclick', function (event) {
var d = (event.notification && event.notification.data) || {};
event.notification.close();
// `clickUrl` is a signed WRNexus redirect: hitting it records the `clicked`
// event and 302s to the real destination, so tracking fires from the open.
var target = d.clickUrl || d.url || '/';
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
// Reuse an existing tab on the destination when possible.
for (var i = 0; i < list.length; i++) {
var client = list[i];
if (d.url && client.url === d.url && 'focus' in client) return client.focus();
}
if (self.clients.openWindow) return self.clients.openWindow(target);
return undefined;
})
);
});
// ── Optional: re-subscribe after the browser rotates the subscription ──────
self.addEventListener('pushsubscriptionchange', function (event) {
var cfg = WRNEXUS_PUSH_CONFIG || {};
if (!cfg.siteKey || !cfg.apiBase || !cfg.vapidPublicKey) return;
event.waitUntil(
self.registration.pushManager
.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(cfg.vapidPublicKey) })
.then(function (sub) {
return fetch(cfg.apiBase.replace(/\/+$/, '') + '/api/push/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
credentials: 'omit',
mode: 'cors',
body: JSON.stringify({
key: cfg.siteKey,
endpoint: sub.endpoint,
keys: {
p256dh: keyToB64(sub.getKey('p256dh')),
auth: keyToB64(sub.getKey('auth')),
},
metadata: { source: 'pushsubscriptionchange' },
}),
}).catch(function () {});
})
.catch(function () {})
);
});
+118 -3
View File
@@ -44,9 +44,7 @@ const currentYear = new Date().getFullYear();
<!-- Company Info -->
<div class="lg:col-span-1">
<a href="/" class="inline-flex items-center gap-2 mb-4 group" aria-label="WorkRoot IT Solutions LLP - Home">
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center transition-transform group-hover:scale-105" aria-hidden="true">
<span class="text-white font-bold text-lg">W</span>
</div>
<img src="/favicon.svg" alt="WorkRoot" width="40" height="40" class="w-10 h-10 transition-transform group-hover:scale-105" />
<span class="text-xl font-bold text-white">WorkRoot</span>
</a>
<p class="text-secondary-400 text-sm leading-relaxed mb-6">
@@ -168,6 +166,23 @@ const currentYear = new Date().getFullYear();
By subscribing, you agree to our
<a href="/privacy" class="text-primary hover:underline">Privacy Policy</a>.
</p>
<!-- Web push opt-in (WRNexus). User-gesture subscribe control.
Hidden by default; the client script below reveals it only when the
WRNexus SDK + browser push support are available, and sets the
correct label based on the visitor's current subscription state. -->
<button
id="wrnexus-subscribe"
type="button"
hidden
aria-label="Enable notifications"
class="mt-6 w-full px-4 py-3 bg-secondary-800 border border-secondary-700 text-white font-medium rounded-lg hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-secondary-900 transition-all duration-200 text-sm flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span id="wrnexus-subscribe-label">Enable notifications</span>
</button>
</div>
</div>
</div>
@@ -195,6 +210,106 @@ const currentYear = new Date().getFullYear();
</div>
</footer>
<!-- WRNexus push subscribe: state-aware control wired via addEventListener
(Astro strips inline on* handlers). The button reflects whether the
visitor is already subscribed and lets them toggle the subscription. -->
<script is:inline>
(function () {
function init() {
var btn = document.getElementById('wrnexus-subscribe');
var label = document.getElementById('wrnexus-subscribe-label');
if (!btn || !label) return;
var push = window.WRNexusPush;
var supported =
push &&
'Notification' in window &&
'serviceWorker' in navigator &&
'PushManager' in window;
// No SDK or no browser push support: keep the button hidden entirely.
if (!supported) {
btn.hidden = true;
return;
}
// Permission permanently blocked by the user: the SDK can't re-prompt,
// so surface a clear, non-actionable disabled state instead of a button
// that silently does nothing.
if (window.Notification && Notification.permission === 'denied') {
label.textContent = 'Notifications blocked';
btn.setAttribute('aria-label', 'Notifications are blocked in your browser settings');
btn.disabled = true;
btn.hidden = false;
return;
}
// Render the button for a given subscription state.
// Default UX: subscribed users get an actionable "Unsubscribe" toggle
// rather than a dead "Already subscribed" label.
// (Alternative: render a disabled "Already subscribed" indicator instead
// of the Unsubscribe toggle if product prefers a non-actionable state.)
function render(subscribed) {
if (subscribed) {
label.textContent = 'Unsubscribe';
btn.setAttribute('aria-label', 'Unsubscribe from notifications');
} else {
label.textContent = 'Enable notifications';
btn.setAttribute('aria-label', 'Enable notifications');
}
btn.dataset.subscribed = subscribed ? 'true' : 'false';
btn.disabled = false;
btn.hidden = false;
}
async function refresh() {
try {
var subscribed = await push.isSubscribed();
render(!!subscribed);
} catch (err) {
// On error, fall back to the unsubscribed label so the control stays usable.
console.error('[wrnexus-push] isSubscribed() failed:', err);
render(false);
}
}
btn.addEventListener('click', async function () {
if (btn.disabled) return;
var wasSubscribed = btn.dataset.subscribed === 'true';
var busyLabel = wasSubscribed ? 'Unsubscribing…' : 'Enabling…';
// In-progress state to prevent double-clicks while the promise is pending.
btn.disabled = true;
label.textContent = busyLabel;
try {
if (wasSubscribed) {
await push.unsubscribe();
} else {
// prompt() must run from this real user gesture.
await push.prompt();
}
} catch (err) {
console.error('[wrnexus-push] toggle failed:', err);
} finally {
// Re-read the authoritative state and re-render the label.
await refresh();
}
});
// Determine and render the initial state.
refresh();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
<script>
import { trackNewsletterSignup } from '../utils/analytics';
+140
View File
@@ -0,0 +1,140 @@
---
/**
* GuaranteeBand Component
*
* Risk-reversal band that sits BETWEEN the PricingTimelineBand and the
* InlineLeadForm on the homepage. Its single job is to remove last-mile
* objections ("how do I know this won't blow up in my face?") right before
* the visitor fills in the lead form below.
*
* Layout:
* - 3-up icon + claim row on md+
* - Items stack vertically (1 column) on mobile (<md)
* - Each item is a single visual unit: icon → headline → short subtext
*
* Design tokens:
* - Uses existing tailwind primitives (`primary-*`, `secondary-*`,
* `accent-*`, `rounded-2xl`). No new colours introduced.
* - Dark-mode pairs are taken from the .agents/security-auditor/
* THEME_ACCESSIBILITY_AUDIT.md verified-passing list (e.g.
* `primary-300` on `primary-900/30`, `secondary-200` on
* `secondary-800` — all ≥ AA 4.5:1).
*
* CLS guard:
* - Each item card has an explicit `min-h-[...]` so the band reserves
* its vertical footprint at first paint, even before icons render.
*
* The component is intentionally prop-less. The three claims are part of
* the brand promise and shouldn't vary across pages; if a future variant
* needs different copy, refactor to a `claims` prop then.
*/
interface ClaimEntry {
title: string;
body: string;
/**
* Inline SVG path data (already wrapped in <path>). Kept inline so the
* component has no extra HTTP requests and the icons can ride along with
* the document's currentColor — no FOUC.
*/
icon: string;
}
const claims: ClaimEntry[] = [
{
title: '30-day satisfaction guarantee',
body: 'Not happy in the first 30 days post-launch? We rework it on us or refund the engagement fee.',
// Shield-check icon
icon:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 3 4.5 6v6c0 4.5 3 8.5 7.5 9.5 4.5-1 7.5-5 7.5-9.5V6L12 3Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m9 12 2 2 4-4"/>',
},
{
title: 'No long-term contracts',
body: 'Month-to-month after delivery. Pause, extend, or wind down — you stay in control of the engagement.',
// Document-minus icon
icon:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 12h6M7 21h10a2 2 0 0 0 2-2V7l-5-5H7a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M13 2v5h5"/>',
},
{
title: 'Fixed-price quotes',
body: 'Scope-locked proposals with a clear deliverable and a clear number — no surprise change-orders.',
// Currency / receipt icon
icon:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 6v12M8 9h6.5a2.5 2.5 0 0 1 0 5h-5a2.5 2.5 0 0 0 0 5H16"/><circle cx="12" cy="12" r="9" stroke-width="1.8" fill="none"/>',
},
];
---
<!--
Risk-reversal band. Anchor `#guarantees` lets nav / sticky CTA deep-link
visitors straight to the promises right above the lead form.
-->
<section
id="guarantees"
class="pt-6 sm:pt-8 pb-14 sm:pb-20 bg-white dark:bg-secondary-900"
aria-labelledby="guarantees-heading"
>
<div class="container-wrapper">
<div class="max-w-5xl mx-auto">
<!-- Visually hidden heading. The three claim titles each read as their
own heading inside the cards, so a wrapper heading would be
redundant in the visible flow but is still useful for screen
readers to anchor the landmark. -->
<h2 id="guarantees-heading" class="sr-only">Our guarantees</h2>
<!--
3-up grid.
Mobile (<md): single column, items stack with consistent vertical
rhythm via `gap-4`.
md+ : three equal columns, evenly spaced.
-->
<ul
class="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-6"
role="list"
>
{claims.map((claim, index) => (
<li class="h-full">
<article
class="h-full min-h-[140px] sm:min-h-[160px] flex md:flex-col items-start gap-4 sm:gap-5 rounded-2xl border border-secondary-100 dark:border-secondary-700 bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-900 p-5 sm:p-6 shadow-sm hover:shadow-md transition-shadow duration-300"
data-animate="fade-up"
data-delay={String(index * 80)}
aria-labelledby={`guarantee-${index}-title`}
>
<!-- Icon chip -->
<div
class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 flex items-center justify-center"
aria-hidden="true"
>
<svg
class="w-6 h-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
set:html={claim.icon}
/>
</div>
<!-- Copy block. The leading checkmark prefix communicates the
"✓ benefit" rhythm the brief asks for ("'✓ 30-day satisfaction
guarantee', '✓ No long-term contracts', '✓ Fixed-price
quotes'") without relying on an emoji glyph that varies
across platforms. -->
<div class="min-w-0">
<h3
id={`guarantee-${index}-title`}
class="text-base sm:text-lg font-bold text-secondary-900 dark:text-secondary-100 leading-snug"
>
<span class="text-primary-600 dark:text-primary-300" aria-hidden="true">✓</span>
<span class="ml-1">{claim.title}</span>
</h3>
<p class="mt-2 text-sm sm:text-base text-secondary-600 dark:text-secondary-300 leading-relaxed">
{claim.body}
</p>
</div>
</article>
</li>
))}
</ul>
</div>
</div>
</section>
+69 -34
View File
@@ -12,6 +12,7 @@ const { class: className } = Astro.props;
const navLinks = [
{ name: 'Home', href: '/' },
{ name: 'WRNexus', href: 'https://wrnexus.com/', external: true },
{ name: 'About', href: '/about' },
{ name: 'Services', href: '/services' },
{ name: 'Portfolio', href: '/portfolio' },
@@ -29,13 +30,21 @@ const currentPath = Astro.url.pathname;
]}
id="main-header"
>
<div class="container-wrapper">
<!-- Full-bleed inner wrapper: the header background already spans the full
viewport (the <header> is fixed left-0 right-0); this inner row stretches
edge-to-edge too, keeping only responsive horizontal padding so the logo
and nav never touch the screen edges and stay legible down to 360px. -->
<div class="w-full px-4 sm:px-6 lg:px-8 xl:px-12">
<nav class="flex items-center justify-between h-16 lg:h-20" aria-label="Main navigation">
<!-- Logo -->
<a href="/" class="flex items-center gap-2 group" aria-label="WorkRoot IT Solutions LLP - Home">
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center transition-transform group-hover:scale-105">
<span class="text-white font-bold text-xl">W</span>
</div>
<img
src="/favicon.svg"
alt="WorkRoot"
width="40"
height="40"
class="w-10 h-10 transition-transform group-hover:scale-105"
/>
<div class="hidden sm:block">
<span class="font-bold text-xl text-secondary dark:text-white">WorkRoot</span>
<span class="text-primary font-semibold text-sm block -mt-1">IT Solutions</span>
@@ -45,14 +54,16 @@ const currentPath = Astro.url.pathname;
<!-- Desktop Navigation -->
<ul class="hidden lg:flex items-center gap-1" role="menubar">
{navLinks.map((link) => {
const isActive = currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href));
const isActive = !link.external && (currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href)));
return (
<li role="none">
<a
href={link.href}
role="menuitem"
data-astro-prefetch="hover"
data-astro-prefetch={link.external ? undefined : 'hover'}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
class:list={[
'relative px-4 py-2 text-sm font-medium transition-colors rounded-lg',
isActive
@@ -78,44 +89,68 @@ const currentPath = Astro.url.pathname;
href="/contact"
class="btn-primary text-sm"
>
Get Started
Get my free proposal
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<button
type="button"
class="lg:hidden relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
id="mobile-menu-toggle"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<div class="hamburger-icon" aria-hidden="true">
<span class="hamburger-line hamburger-line-1"></span>
<span class="hamburger-line hamburger-line-2"></span>
<span class="hamburger-line hamburger-line-3"></span>
</div>
</button>
<!-- Mobile actions: prominent click-to-call + hamburger (shown <lg on every page) -->
<div class="flex items-center gap-1 lg:hidden">
<!-- Click-to-call — always visible in the mobile header so visitors on
any page can reach us in one tap without opening the menu. -->
<a
href="tel:+919561417403"
data-cta="header-mobile-call"
aria-label="Call WorkRoot at +91 95614 17403"
class="inline-flex items-center gap-1.5 h-10 px-3 rounded-lg bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 font-semibold text-sm hover:bg-primary-100 dark:hover:bg-primary-900/50 active:scale-95 transition-all focus:outline-none focus:ring-2 focus:ring-primary-400"
>
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span class="hidden xs:inline">Call</span>
</a>
<!-- Mobile Menu Button -->
<button
type="button"
class="relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
id="mobile-menu-toggle"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<div class="hamburger-icon" aria-hidden="true">
<span class="hamburger-line hamburger-line-1"></span>
<span class="hamburger-line hamburger-line-2"></span>
<span class="hamburger-line hamburger-line-3"></span>
</div>
</button>
</div>
</nav>
</div>
<!-- Mobile Menu Overlay -->
<!-- Explicit viewport sizing: the header's `backdrop-blur` makes it the
containing block for fixed descendants, so `inset-0` alone would only
cover the header. w-screen + h-[100dvh] force true full-viewport cover. -->
<div
id="mobile-menu-overlay"
class="fixed inset-0 bg-secondary-900/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 lg:hidden"
class="fixed top-0 left-0 w-screen h-[100dvh] bg-secondary-900/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 lg:hidden"
aria-hidden="true"
></div>
<!-- Mobile Menu Panel -->
<!-- Full-screen overlay panel. Uses explicit viewport units (w-screen /
h-[100dvh]) rather than h-full because the header's backdrop-filter is
the containing block for this fixed element, so `h-full` would collapse
to the header's ~64px height (the original "small box" bug). -->
<div
id="mobile-menu"
class="fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-white dark:bg-secondary-900 shadow-2xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
class="fixed top-0 left-0 w-screen h-[100dvh] min-h-screen bg-white dark:bg-secondary-900 shadow-2xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
role="dialog"
aria-modal="true"
aria-label="Mobile navigation menu"
@@ -124,9 +159,7 @@ const currentPath = Astro.url.pathname;
<!-- Mobile Menu Header -->
<div class="flex items-center justify-between p-4 border-b border-secondary-100 dark:border-secondary-800">
<a href="/" class="flex items-center gap-2" aria-label="WorkRoot IT Solutions LLP - Home">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center" aria-hidden="true">
<span class="text-white font-bold text-lg">W</span>
</div>
<img src="/favicon.svg" alt="WorkRoot" width="32" height="32" class="w-8 h-8" />
<span class="font-bold text-lg text-secondary dark:text-white">WorkRoot</span>
</a>
<button
@@ -145,14 +178,16 @@ const currentPath = Astro.url.pathname;
<nav class="flex-1 overflow-y-auto py-4">
<ul class="space-y-1 px-3" role="menu">
{navLinks.map((link, index) => {
const isActive = currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href));
const isActive = !link.external && (currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href)));
return (
<li role="none" style={`--delay: ${index * 50}ms`}>
<a
href={link.href}
role="menuitem"
data-astro-prefetch="hover"
data-astro-prefetch={link.external ? undefined : 'hover'}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
class:list={[
'mobile-nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200',
isActive
@@ -182,7 +217,7 @@ const currentPath = Astro.url.pathname;
href="/contact"
class="btn-primary w-full justify-center"
>
Get Started
Get my free proposal
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
+250
View File
@@ -0,0 +1,250 @@
---
// HomepageFaq — single homepage FAQ section rendered just above the final
// CTA band. Covers both buyer-stage objections (cost, timeline, NDA, IP, GeM,
// stack) and the process/support/security questions that used to live in the
// older "Common Questions" block higher up the page. That earlier block was
// removed to avoid a duplicate FAQ rendering and a duplicate FAQPage JSON-LD
// emission — this component is now the canonical homepage FAQ.
//
// Emits a single FAQPage JSON-LD inline (below) for rich-result eligibility.
//
// Accessibility:
// - Each question is a real <button> with aria-expanded toggled in JS.
// - aria-controls links to the answer region; the region has role="region"
// and aria-labelledby pointing back to the question.
// - Max-height transition has no layout shift on load — answers are
// collapsed (max-height:0) server-side and only the rotating chevron
// animates.
// - Keyboard: native <button> handles Enter & Space; Tab moves between
// items. No custom key handling needed.
// Each answer is written as a definitive, self-contained paragraph: it can be
// read (or quoted) in isolation without the question for context, and the
// buyer-stage answers bundle the WorkRoot name with the canonical numbers
// (founded 2022 · 20+ projects · 11+ districts · 50,000+ records · 99.9%
// uptime · GeM · IIT Patna). Keep every figure identical to <ProofNumbers />
// and the homepage `companyFacts` set — the visible text below is also the
// source for the FAQPage JSON-LD emitted at the bottom of this component.
const moneyFaqs = [
{
question: 'How much does a project cost? Do you work with fixed budgets?',
answer:
'WorkRoot IT Solutions projects start from ₹1,00,000, and most web, mobile, and ERP builds land between ₹2,00,000 and ₹15,00,000 depending on scope. Yes — we work with fixed budgets: tell us the number you have in mind and we will scope exactly what we can deliver within it. Every quote is free, itemised, and carries no obligation.',
},
{
question: 'How much does a custom ERP cost?',
answer:
'A custom ERP from WorkRoot IT Solutions typically runs ₹2,00,000–₹15,00,000, with a genuinely production-grade single-department system starting around ₹6,00,000. We offer fixed-price quotes for well-defined scopes and time-and-materials pricing for evolving ones, and every estimate is free and itemised.',
},
{
question: 'What is your typical timeline?',
answer:
'Most WorkRoot IT Solutions engagements run 416 weeks from kickoff to launch, and a typical MVP ships in about 6 weeks. We work in 2-week sprints with a live staging environment from day one, so you see and steer working software early rather than waiting for a single big-bang release.',
},
{
question: 'Do you sign NDAs?',
answer:
'Yes. WorkRoot IT Solutions signs a mutual NDA before any detailed discussion of your idea, data, or codebase — at no cost and with no conditions attached. Your concept and commercial details stay confidential from the very first call.',
},
{
question: 'Who owns the source code?',
answer:
'You do. Every WorkRoot IT Solutions engagement includes full intellectual-property transfer on delivery: you receive the complete source repositories, deployment access, and documentation, with no per-seat licence fees and no vendor lock-in. You are free to host, extend, or hand the system to another team at any time.',
},
{
question: 'Do you work with government tenders / GeM?',
answer:
'Yes. WorkRoot IT Solutions is empanelled on the Government e-Marketplace (GeM) for relevant IT service categories and can respond to government tenders. We have delivered public-service platforms live across 11+ districts of Bihar at 99.9% uptime, including a Police Management System and a Smart Crematorium Management System for the Bihar Government.',
},
{
question: 'What technologies do you work with?',
answer:
'WorkRoot IT Solutions builds on Astro, Next.js, Angular, and React Native on the front end; Spring Boot, Go, and Node.js on the back end; and PostgreSQL or MySQL for data — the same stack behind the IIT Patna University Management System and our multi-district government deployments, all deployed and operated on the AWS and GCP cloud.',
},
{
question: 'Do you offer ongoing support after launch?',
answer:
'Yes. WorkRoot IT Solutions offers maintenance packages covering bug fixes, security patches, performance monitoring, and feature enhancements, each with a response SLA matched to your uptime needs — the same operational discipline that keeps our government systems running at 99.9% uptime.',
},
{
question: 'What is your development process?',
answer:
'WorkRoot IT Solutions works in Agile 2-week sprints with weekly check-ins and a live staging environment from day one. You see working software early and steer the roadmap as it ships, rather than waiting for a single final handover at the end of the project.',
},
{
question: 'Can you work with our existing team?',
answer:
'Absolutely. WorkRoot IT Solutions augments in-house teams as readily as it delivers fully managed builds, adapting to your repository, branch strategy, ticketing system, and code-review process so we slot into the way your team already works.',
},
{
question: 'How do you ensure project security?',
answer:
'Security is built into every phase at WorkRoot IT Solutions: OWASP-aligned code reviews, encrypted data at rest and in transit, role-based access control, regular dependency audits, and GDPR- and SOC 2-style controls where a project demands them. The same architecture secures a Bihar government police platform deployed across 11 districts.',
},
];
// FAQPage JSON-LD — emitted inline so the schema travels with the component.
// This is the ONLY FAQPage emitted on the homepage; the site-wide <SEO>
// no longer receives a `faq` prop on / so Google sees exactly one valid
// FAQPage with every visible Q&A pair.
const faqPageSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'@id': 'https://workroot.in/#homepage-money-faq',
mainEntity: moneyFaqs.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
---
<section
id="homepage-money-faq"
class="py-20 sm:py-24 bg-secondary-50 dark:bg-secondary-950 border-t border-secondary-100 dark:border-secondary-800"
aria-labelledby="homepage-money-faq-heading"
>
<div class="container-wrapper">
<div class="max-w-3xl mx-auto">
<!-- Section heading -->
<div class="text-center mb-12 sm:mb-16" data-animate="fade-up">
<span
class="inline-block px-4 py-2 rounded-full bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 text-xs sm:text-sm font-bold uppercase tracking-wider mb-4"
>
Before you reach out
</span>
<h2
id="homepage-money-faq-heading"
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-4 leading-tight"
>
Frequently asked questions
</h2>
<p class="text-base sm:text-lg text-secondary-600 dark:text-secondary-300">
Quick answers on cost, timelines, ownership, our process, support, security, and the stack we ship.
</p>
</div>
<!-- Accordion list -->
<ul class="space-y-3 sm:space-y-4" role="list">
{
moneyFaqs.map((item, index) => (
<li
class="bg-white dark:bg-secondary-800 rounded-2xl border border-secondary-200 dark:border-secondary-700 hover:border-primary-300 dark:hover:border-primary-500/60 shadow-sm transition-colors duration-200 overflow-hidden"
>
<button
type="button"
class="money-faq-question w-full text-left px-5 sm:px-7 py-5 sm:py-6 flex items-start justify-between gap-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-secondary-50 dark:focus-visible:ring-offset-secondary-950 rounded-2xl"
aria-expanded="false"
aria-controls={`money-faq-answer-${index}`}
id={`money-faq-question-${index}`}
data-money-faq-index={index}
>
<span class="font-semibold text-base sm:text-lg text-secondary-900 dark:text-secondary-100 leading-snug">
{item.question}
</span>
<svg
class="money-faq-icon w-5 h-5 sm:w-6 sm:h-6 mt-0.5 sm:mt-1 text-primary-500 dark:text-primary-400 flex-shrink-0 transition-transform duration-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div
class="money-faq-answer grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out"
id={`money-faq-answer-${index}`}
role="region"
aria-labelledby={`money-faq-question-${index}`}
>
<div class="overflow-hidden">
<p class="px-5 sm:px-7 pb-5 sm:pb-6 -mt-1 text-sm sm:text-base text-secondary-600 dark:text-secondary-300 leading-relaxed">
{item.answer}
</p>
</div>
</div>
</li>
))
}
</ul>
<!-- Soft secondary CTA — links to the contact page for visitors whose
question wasn't covered. Avoids competing with the final CTA band
below. -->
<p class="text-center text-sm sm:text-base text-secondary-600 dark:text-secondary-400 mt-10 sm:mt-12">
Have a different question?{' '}
<a
href="/contact?source=homepage-money-faq"
class="font-semibold text-primary-600 dark:text-primary-300 hover:underline"
>
Ask us directly &rarr;
</a>
</p>
</div>
</div>
</section>
<!-- FAQPage structured data for rich-result eligibility -->
<script type="application/ld+json" set:html={JSON.stringify(faqPageSchema)} />
<style>
/* The grid-rows trick gives a true intrinsic-height expand/collapse with
no max-height guesswork and no layout shift on load (answers start at
0fr). It animates smoothly in every modern browser; older browsers
simply snap, which is still accessible. */
.money-faq-answer[data-expanded='true'] {
grid-template-rows: 1fr;
}
.money-faq-question[aria-expanded='true'] .money-faq-icon {
transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
.money-faq-answer,
.money-faq-icon {
transition: none !important;
}
}
</style>
<script is:inline>
(function () {
var buttons = document.querySelectorAll('.money-faq-question');
if (!buttons.length) return;
function setExpanded(button, expanded) {
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
var answerId = button.getAttribute('aria-controls');
if (!answerId) return;
var answer = document.getElementById(answerId);
if (!answer) return;
if (expanded) {
answer.setAttribute('data-expanded', 'true');
} else {
answer.removeAttribute('data-expanded');
}
}
buttons.forEach(function (button) {
button.addEventListener('click', function () {
var isOpen = button.getAttribute('aria-expanded') === 'true';
// Single-open accordion — collapse the rest so only one answer is
// visible at a time. Matches the existing FAQ block's UX.
buttons.forEach(function (other) {
if (other !== button) setExpanded(other, false);
});
setExpanded(button, !isOpen);
});
});
})();
</script>
+177 -82
View File
@@ -1,20 +1,25 @@
---
// InlineLeadForm — Three-field lead capture rendered inline on the homepage.
// InlineLeadForm — Compact lead capture rendered inline on the homepage.
//
// The goal is to cut the friction step of routing every CTA through /contact.
// Visitors who reach the bottom of the homepage can submit name + contact
// (email OR phone) + project type without a navigation. The submission reuses
// /api/contact under the hood and is tagged `source: 'homepage-inline'` so the
// backend / analytics layer can segment these leads cleanly.
// Goal: let visitors who reach the bottom of the homepage submit a project
// enquiry without the friction of navigating to /contact. The submission
// reuses the SAME /api/contact endpoint as the full contact page and is tagged
// `source: 'homepage-inline'` so the backend / analytics layer can segment
// these leads cleanly (it also routes them to the `[Project Inquiry]` admin
// subject prefix).
//
// Why a single "contact" field instead of separate email + phone:
// - 3 fields keeps the form psychologically tiny and dramatically lifts
// completion. Real-world conversion testing repeatedly shows 3 → 5+ fields
// is the biggest single-step drop-off on B2B lead forms.
// - We sniff the value on submit. If it has an "@" we treat it as email.
// Otherwise we treat it as a phone and synthesize a placeholder email to
// satisfy the backend's strict email validation. The phone is also sent
// through, so the admin email body shows the real reachable value.
// Fields (kept to 5, mobile-first):
// 1. Full name — required
// 2. Email — required, type="email" (email keyboard on mobile)
// 3. Phone — optional, type="tel" (numeric keypad on mobile)
// 4. Project type — required, <select> mapped to the backend `subject`
// 5. Message — optional <textarea>; synthesized when left blank so
// the backend's message>=10 char rule always passes
//
// Only name + email + project type are *required* — phone and message stay
// optional to keep completion friction low on mobile, which is where most of
// these submissions originate. The backend already treats `phone` as optional
// and only validates its format when present.
---
<section
@@ -54,15 +59,26 @@
data-testid="inline-lead-form"
novalidate
>
<!-- Honeypot (visually & SR-hidden — bots fill it, real users do not). -->
<div class="absolute -left-[9999px] top-auto w-px h-px overflow-hidden" aria-hidden="true">
<label for="inline-lead-website">Website</label>
<!--
Honeypot — off-screen positioned, NOT display:none / visibility:hidden
(some bots skip those entirely). aria-hidden + sr-only label keep it
silent for screen readers; tabindex="-1" + autocomplete="off" keep it
out of the tab order and browser autofill. Bots that blindly fill
every field will populate `website`, and /api/contact silently drops
the submission.
-->
<div
class="absolute -left-[9999px] top-auto w-px h-px overflow-hidden"
aria-hidden="true"
>
<label for="inline-lead-website" class="sr-only">Website</label>
<input
type="text"
id="inline-lead-website"
name="website"
tabindex="-1"
autocomplete="off"
aria-hidden="true"
/>
</div>
@@ -97,40 +113,68 @@
</p>
</div>
<!-- Contact (email or phone) -->
<div>
<label
for="inline-lead-contact"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Email or phone <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input
type="text"
id="inline-lead-contact"
name="contact"
required
autocomplete="email tel"
inputmode="email"
aria-required="true"
aria-describedby="inline-lead-contact-hint inline-lead-contact-error"
placeholder="jane@company.com or +91 95614 17403"
class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"
/>
<p
id="inline-lead-contact-hint"
class="mt-1.5 text-xs text-secondary-500 dark:text-secondary-400"
>
Drop your best email or phone number — whichever is easiest.
</p>
<p
id="inline-lead-contact-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please enter a valid email address or phone number.
</p>
<!-- Email + Phone row -->
<div class="grid sm:grid-cols-2 gap-5">
<!-- Email (required) -->
<div>
<label
for="inline-lead-email"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Email <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input
type="email"
id="inline-lead-email"
name="email"
required
maxlength="254"
autocomplete="email"
inputmode="email"
aria-required="true"
aria-describedby="inline-lead-email-error"
placeholder="jane@company.com"
class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"
/>
<p
id="inline-lead-email-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please enter a valid email address.
</p>
</div>
<!-- Phone (optional) -->
<div>
<label
for="inline-lead-phone"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Phone
<span class="font-normal text-secondary-400 dark:text-secondary-500">(optional)</span>
</label>
<input
type="tel"
id="inline-lead-phone"
name="phone"
autocomplete="tel"
inputmode="tel"
pattern="[+()0-9.\s\-]{7,}"
maxlength="32"
aria-describedby="inline-lead-phone-error"
placeholder="+91 95614 17403"
class="block w-full min-h-[48px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"
/>
<p
id="inline-lead-phone-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please enter a valid phone number, or leave it blank.
</p>
</div>
</div>
<!-- Project type -->
@@ -173,6 +217,33 @@
</p>
</div>
<!-- Message (optional) -->
<div>
<label
for="inline-lead-message"
class="block text-sm font-semibold text-secondary-700 dark:text-secondary-200 mb-1.5"
>
Project details
<span class="font-normal text-secondary-400 dark:text-secondary-500">(optional)</span>
</label>
<textarea
id="inline-lead-message"
name="message"
rows="3"
maxlength="5000"
aria-describedby="inline-lead-message-error"
placeholder="A sentence or two about what you want to build, your timeline, or budget…"
class="block w-full min-h-[96px] px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors resize-y"
></textarea>
<p
id="inline-lead-message-error"
class="mt-1.5 text-xs text-red-600 dark:text-red-400 hidden"
role="alert"
>
Please add a little more detail (at least 10 characters), or leave it blank.
</p>
</div>
<!-- General form error (server-side / network) -->
<div
id="inline-lead-form-error"
@@ -256,13 +327,17 @@
if (!form) return;
var nameInput = document.getElementById('inline-lead-name');
var contactInput = document.getElementById('inline-lead-contact');
var emailInput = document.getElementById('inline-lead-email');
var phoneInput = document.getElementById('inline-lead-phone');
var projectTypeInput = document.getElementById('inline-lead-project-type');
var messageInput = document.getElementById('inline-lead-message');
var websiteInput = document.getElementById('inline-lead-website');
var nameError = document.getElementById('inline-lead-name-error');
var contactError = document.getElementById('inline-lead-contact-error');
var emailError = document.getElementById('inline-lead-email-error');
var phoneError = document.getElementById('inline-lead-phone-error');
var projectTypeError = document.getElementById('inline-lead-project-type-error');
var messageError = document.getElementById('inline-lead-message-error');
var formError = document.getElementById('inline-lead-form-error');
var submitBtn = document.getElementById('inline-lead-submit');
@@ -272,7 +347,7 @@
var successPanel = document.getElementById('inline-lead-success');
// Tolerant phone test: allow +, digits, spaces, dashes, parens, dots.
// We require at least 7 digits so single-digit / accidental presses fail.
// Require at least 7 digits so single-digit / accidental presses fail.
var PHONE_REGEX = /^[+]?[()\d\s.\-]{7,}$/;
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -300,6 +375,10 @@
}
}
function phoneIsValid(val) {
return PHONE_REGEX.test(val) && digitsOnly(val).length >= 7;
}
function validate() {
var ok = true;
@@ -311,14 +390,21 @@
showError(nameError, false);
}
var contactVal = (contactInput.value || '').trim();
var isEmail = EMAIL_REGEX.test(contactVal);
var isPhone = PHONE_REGEX.test(contactVal) && digitsOnly(contactVal).length >= 7;
if (!contactVal || (!isEmail && !isPhone)) {
showError(contactError, true);
var emailVal = (emailInput.value || '').trim();
if (!EMAIL_REGEX.test(emailVal)) {
showError(emailError, true);
ok = false;
} else {
showError(contactError, false);
showError(emailError, false);
}
// Phone is optional — only validate format when something was typed.
var phoneVal = (phoneInput.value || '').trim();
if (phoneVal && !phoneIsValid(phoneVal)) {
showError(phoneError, true);
ok = false;
} else {
showError(phoneError, false);
}
var projectVal = projectTypeInput.value || '';
@@ -329,13 +415,24 @@
showError(projectTypeError, false);
}
// Message is optional — only enforce the min length when something was typed.
var messageVal = (messageInput.value || '').trim();
if (messageVal && messageVal.length < 10) {
showError(messageError, true);
ok = false;
} else {
showError(messageError, false);
}
return ok;
}
// Live-validate on input/change so the error UI doesn't linger after fix.
// Live-validate on blur/change so the error UI clears once a field is fixed.
nameInput.addEventListener('blur', validate);
contactInput.addEventListener('blur', validate);
emailInput.addEventListener('blur', validate);
phoneInput.addEventListener('blur', validate);
projectTypeInput.addEventListener('change', validate);
messageInput.addEventListener('blur', validate);
// Map the user-friendly project type label to the backend subject code so
// the existing /api/contact validator passes. Anything unmapped → 'other'.
@@ -348,41 +445,39 @@
};
function buildPayload() {
var contactVal = (contactInput.value || '').trim();
var isEmail = EMAIL_REGEX.test(contactVal);
var nameVal = (nameInput.value || '').trim();
var emailVal = (emailInput.value || '').trim();
var phoneVal = (phoneInput.value || '').trim();
var projectType = projectTypeInput.value || 'Other';
var subject = SUBJECT_BY_PROJECT_TYPE[projectType] || 'other';
var messageVal = (messageInput.value || '').trim();
var email, phone;
if (isEmail) {
email = contactVal;
phone = undefined;
// The backend requires a message of at least 10 characters. When the
// visitor leaves the (optional) details blank, synthesize a concise
// summary so the submission still goes through and the admin email
// carries the useful context.
var message;
if (messageVal.length >= 10) {
message = messageVal;
} else {
// Treat as phone. Synthesize a stable placeholder email so the
// backend's strict email validator passes — the admin notification
// surfaces the real phone in both Phone: and Message: fields so the
// team always has a real way to reach the lead.
var digits = digitsOnly(contactVal) || 'unknown';
email = 'lead-' + digits + '@homepage.workroot.in';
phone = contactVal;
message =
'New project enquiry from the homepage form.\n\n' +
'Project type: ' + projectType + '\n' +
'Name: ' + nameVal +
(phoneVal ? '\nPhone: ' + phoneVal : '') +
(messageVal ? '\n\nNote: ' + messageVal : '');
}
var message =
'New inline lead from the homepage.\n\n' +
'Project type: ' + projectType + '\n' +
'Preferred contact: ' + contactVal;
var payload = {
name: nameVal,
email: email,
email: emailVal,
subject: subject,
message: message,
source: 'homepage-inline',
projectType: projectType,
website: (websiteInput && websiteInput.value) || '',
};
if (phone) payload.phone = phone;
if (phoneVal) payload.phone = phoneVal;
return payload;
}
+49 -11
View File
@@ -1,11 +1,19 @@
---
// MobileStickyCtaBar — Sticky bottom action bar for mobile/tablet (<768px)
// 3 equal columns: Call, WhatsApp, Quote
// 3 equal columns: Call, Chat (WhatsApp), Quote
// - Fixed to bottom of viewport
// - Respects safe-area-inset-bottom on iOS
// - Hidden at md+ breakpoints (>=768px)
// - Hidden on print
// - Light + dark theme aware
//
// Label policy (post-polish):
// - Short, action-led verbs (max ~7 chars) so they never truncate even at 320px.
// - Visible text intentionally terse; aria-label carries the full descriptive
// phrasing for screen readers (Apple HIG / WCAG SC 2.5.3 name-in-name).
// - 24px inline SVG icons sit above each label. Tap target is ~64px tall
// and >=44px wide per column at 320px (320 / 3 ≈ 106px), satisfying
// Apple HIG (44x44) and WCAG 2.5.5 (Target Size).
---
<nav
@@ -15,13 +23,15 @@
>
<a
href="tel:+919561417403"
aria-label="Call WorkRoot"
aria-label="Call us"
data-testid="mobile-cta-call"
class="mobile-sticky-cta__btn"
>
<svg
class="mobile-sticky-cta__icon"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -40,13 +50,15 @@
href="https://wa.me/919561417403?text=Hi%20WorkRoot%2C%20I%27d%20like%20to%20discuss%20a%20project."
target="_blank"
rel="noopener"
aria-label="Chat on WhatsApp"
aria-label="Chat with us on WhatsApp"
data-testid="mobile-cta-whatsapp"
class="mobile-sticky-cta__btn mobile-sticky-cta__btn--whatsapp"
>
<svg
class="mobile-sticky-cta__icon"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
@@ -54,18 +66,20 @@
>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.464 3.488"/>
</svg>
<span class="mobile-sticky-cta__label">WhatsApp</span>
<span class="mobile-sticky-cta__label">Chat</span>
</a>
<a
href="/contact"
aria-label="Request a quote"
aria-label="Get a free quote"
data-testid="mobile-cta-quote"
class="mobile-sticky-cta__btn mobile-sticky-cta__btn--accent"
>
<svg
class="mobile-sticky-cta__icon"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -96,7 +110,7 @@
border-top: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 -8px 20px -8px rgba(15, 23, 42, 0.18);
padding-bottom: env(safe-area-inset-bottom, 0px);
/* Ensure no horizontal overflow on very narrow viewports */
/* Ensure no horizontal overflow on very narrow viewports (down to 320px) */
max-width: 100vw;
overflow: hidden;
}
@@ -112,8 +126,12 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
min-height: 56px;
gap: 4px;
/* 44x44 minimum tap target per Apple HIG / WCAG 2.5.5.
64px gives comfortable thumb-zone clearance for the 24px icon
+ 12px label without ever clipping. */
min-height: 64px;
min-width: 44px;
padding: 8px 6px;
text-decoration: none;
color: #0f172a;
@@ -157,15 +175,35 @@
}
.mobile-sticky-cta__icon {
width: 22px;
height: 22px;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.mobile-sticky-cta__label {
display: block;
font-size: 11px;
/* Font tuned so labels (Call/Chat/Quote, ≤5 chars) never overflow
even at 320px (column ≈ 106px 12px padding = 94px usable).
The nowrap + ellipsis pair below is a defensive safety net only —
at the tuned sizes it should never visibly trigger. */
font-size: 12px;
letter-spacing: 0.01em;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Drop label font a hair on the smallest phones (iPhone SE: 320px).
This keeps the ellipsis safety net from ever triggering even if the
system font swaps in slightly wider glyphs. */
@media (max-width: 359px) {
.mobile-sticky-cta__label {
font-size: 11px;
}
.mobile-sticky-cta__btn {
padding: 8px 4px;
}
}
/* Hide on tablets/desktops (>=768px = Tailwind md breakpoint).
+215
View File
@@ -0,0 +1,215 @@
---
/**
* PricingTimelineBand Component
*
* Sits directly above the homepage InlineLeadForm and answers the two
* questions every late-stage buyer has BEFORE they fill in a contact form:
* 1. "Roughly how much does this cost?"
* 2. "Roughly how long does it take?"
*
* Layout:
* - 2 cards side-by-side on md+ (Pricing, Timeline)
* - Stacked vertically on mobile (<md)
* - A subtle "Custom quote available — tell us about your project" line
* visually bridges the band into the form below it.
*
* Anchor: `id="pricing-timeline"` so the mobile sticky CTA bar / nav /
* external deep-links can scroll directly to this band.
*
* CLS guard: each card has an explicit `min-h-[...]` so the band reserves
* its own vertical space at first paint, regardless of font loading or
* icon-fetch delay. Lighthouse CLS should be unaffected.
*
* Design tokens: uses the same `text-secondary-*`, `text-primary-*`,
* `rounded-3xl`, container-wrapper, and gradient conventions established
* by InlineLeadForm and ProofNumbers so the band feels native to the page.
*
* NOTE on copy: the headline values below are PLACEHOLDERS pending
* stakeholder (marketing / leadership) confirmation. See the inline
* `data-stakeholder-confirm` markers + HTML comments at each value.
*/
---
<!--
Pricing + Timeline band.
ANCHOR: #pricing-timeline — referenced by the mobile sticky CTA bar
and any nav deep-links. Do not rename without updating those callers.
-->
<section
id="pricing-timeline"
class="pt-20 sm:pt-24 pb-10 sm:pb-12 bg-gradient-to-b from-white to-white dark:from-secondary-900 dark:to-secondary-900"
aria-labelledby="pricing-timeline-heading"
>
<div class="container-wrapper">
<div class="max-w-5xl mx-auto">
<!-- Eyebrow + section heading. Kept short so the cards below are the
visual focal point. -->
<div class="text-center mb-10 sm:mb-12" data-animate="fade-up">
<p
class="text-xs sm:text-sm font-semibold uppercase tracking-[0.18em] text-primary-600 dark:text-primary-400"
>
What to expect
</p>
<h2
id="pricing-timeline-heading"
class="mt-3 text-3xl sm:text-4xl lg:text-5xl font-bold text-secondary-900 dark:text-secondary-100 leading-tight"
>
Transparent pricing &amp; predictable timelines.
</h2>
</div>
<!-- Two-card grid: stacks on mobile, side-by-side on md+. -->
<div
class="grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-8"
data-animate="fade-up"
>
<!-- Card 1: Pricing -->
<article
class="relative bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-900 rounded-3xl border border-secondary-100 dark:border-secondary-700 shadow-lg hover:shadow-xl transition-shadow duration-300 p-8 sm:p-10 min-h-[260px] sm:min-h-[280px] flex flex-col"
aria-labelledby="pricing-card-heading"
>
<!-- Icon chip -->
<div
class="w-12 h-12 rounded-2xl bg-primary-100 dark:bg-primary-500/20 text-primary-600 dark:text-primary-300 flex items-center justify-center mb-5 flex-shrink-0"
aria-hidden="true"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p
class="text-xs sm:text-sm font-semibold uppercase tracking-wider text-secondary-500 dark:text-secondary-400"
>
Starting at
</p>
<!--
Pricing floor — mirrors the Services page "Starter" tier
(₹1,00,000). Keep this value in sync with the Starter price in
src/pages/services.astro so the homepage band and the pricing
table never contradict each other.
-->
<h3
id="pricing-card-heading"
class="mt-2 text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 leading-none tracking-tight"
>
Projects from
<span
class="block mt-1 bg-gradient-to-br from-primary-600 to-primary-500 dark:from-primary-400 dark:to-primary-300 bg-clip-text text-transparent"
>
₹1,00,000
</span>
</h3>
<p
class="mt-5 text-base sm:text-lg text-secondary-600 dark:text-secondary-300 leading-relaxed"
>
<!-- 1-line "what's included" copy — keep to ~14 words so it
sits in one comfortable line at md+ widths. -->
Includes discovery, design, build, QA, and post-launch support.
</p>
</article>
<!-- Card 2: Timeline -->
<article
class="relative bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-900 rounded-3xl border border-secondary-100 dark:border-secondary-700 shadow-lg hover:shadow-xl transition-shadow duration-300 p-8 sm:p-10 min-h-[260px] sm:min-h-[280px] flex flex-col"
aria-labelledby="timeline-card-heading"
>
<!-- Icon chip -->
<div
class="w-12 h-12 rounded-2xl bg-accent-100 dark:bg-accent-500/20 text-accent-600 dark:text-accent-300 flex items-center justify-center mb-5 flex-shrink-0"
aria-hidden="true"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p
class="text-xs sm:text-sm font-semibold uppercase tracking-wider text-secondary-500 dark:text-secondary-400"
>
Typical timeline
</p>
<!--
STAKEHOLDER-CONFIRM: the "48 weeks" range is the current
default placeholder. Update after delivery-lead review if the
average shifts (e.g. 610 weeks for ERP-heavy scopes).
-->
<h3
id="timeline-card-heading"
data-stakeholder-confirm="timeline-range"
class="mt-2 text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 leading-none tracking-tight"
>
<span
class="block bg-gradient-to-br from-accent-600 to-accent-500 dark:from-accent-400 dark:to-accent-300 bg-clip-text text-transparent"
>
4&ndash;8 weeks
</span>
</h3>
<p
class="mt-5 text-base sm:text-lg text-secondary-600 dark:text-secondary-300 leading-relaxed"
>
<!-- 1-line "process" copy — discovery → sprints → launch. -->
Discovery in week 1, weekly demos through build, launch by week 8.
</p>
</article>
</div>
<!-- Bridging line into the form below. Visually subtle: small text,
muted color, centered, with a chevron pointing down to imply
"fill the form below to get the custom number". -->
<p
class="mt-10 sm:mt-12 text-center text-sm sm:text-base text-secondary-600 dark:text-secondary-300"
data-animate="fade-up"
>
<span class="inline-flex items-center gap-2">
<span>
<span class="font-semibold text-secondary-800 dark:text-secondary-100">Custom quote available</span>
&mdash; tell us about your project
</span>
<svg
class="w-4 h-4 text-primary-500 dark:text-primary-300 animate-bounce-slow"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</span>
</p>
</div>
</div>
</section>
<style>
/* Slow bounce for the bridging chevron — pure CSS, no JS, no layout
impact (transform only, so it never triggers reflow / CLS). */
@keyframes pricing-timeline-bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(4px); }
}
.animate-bounce-slow {
animation: pricing-timeline-bounce-slow 2.4s ease-in-out infinite;
}
/* Respect reduced-motion preference. */
@media (prefers-reduced-motion: reduce) {
.animate-bounce-slow {
animation: none;
}
}
</style>
+98
View File
@@ -0,0 +1,98 @@
---
/**
* ProofNumbers Component
*
* A compact horizontal "stat band" rendered between the social-proof logo
* strip and the services grid on the homepage. Replaces the older
* "Why Choose Us" / "Built for Your Success" benefits grid — the goal is
* to lead with hard, scannable proof points instead of soft adjectives.
*
* Layout:
* - 2 × 2 grid on mobile (<md)
* - 4 × 1 horizontal band on desktop (md+)
*
* Typography matches the site's existing tokens (text-secondary-* /
* text-primary-*) and supports dark mode via Tailwind's `dark:` variants
* so the band stays legible under the theme toggle.
*
* @prop {Array<{ value: string; label: string }>} [items] - override the
* default set of proof points. Each `value` is rendered as the large
* headline number, `label` is the one-line caption underneath.
*
* NOTE on the default copy: these numbers are placeholder defaults pending
* stakeholder confirmation — update them against the latest delivery log
* / Google Analytics / Lighthouse run before each release.
*/
interface ProofItem {
value: string;
label: string;
}
interface Props {
items?: ProofItem[];
}
// Canonical site-wide proof set. These four numbers are the single source of
// truth for WorkRoot's headline stats and MUST stay identical to the homepage
// stats band (src/pages/index.astro), the /portfolio stats bar, and the
// /about stats grid. Changing one without the others reintroduces the
// credibility bug this set was created to fix.
//
// - "20+ Projects" — total delivered engagements
// - "11+ Districts Deployed" — cumulative Bihar government district reach
// - "50,000+ Records Managed" — records under management across deployments
// - "8+ Years Avg Experience" — average team experience
const defaultItems: ProofItem[] = [
{ value: '20+', label: 'Projects' },
{ value: '11+', label: 'Districts Deployed' },
{ value: '50,000+', label: 'Records Managed' },
{ value: '8+', label: 'Years Avg Experience' },
];
const { items = defaultItems } = Astro.props;
// Cap at 4 — the band is designed as a 4×1 / 2×2 layout and extra entries
// would break the responsive grid. Trim silently rather than overflowing.
const proofItems = items.slice(0, 4);
---
<section
id="proof-numbers"
class="py-16 sm:py-20 bg-white dark:bg-secondary-900 border-y border-secondary-100 dark:border-secondary-800"
aria-label="WorkRoot by the numbers"
>
<div class="container-wrapper">
<dl
class="grid grid-cols-2 md:grid-cols-4 gap-y-10 gap-x-6 sm:gap-x-10 max-w-6xl mx-auto"
data-animate="fade-up"
>
{proofItems.map((item, index) => (
<div
class:list={[
'text-center',
// Subtle vertical divider between columns on desktop — keeps the
// band feeling like one continuous strip rather than four cards.
index > 0
? 'md:border-l md:border-secondary-100 md:dark:border-secondary-800'
: '',
]}
data-animate="fade-up"
data-delay={String(index * 100)}
>
<dt
class="text-4xl sm:text-5xl lg:text-6xl font-bold text-secondary-900 dark:text-secondary-100 tracking-tight leading-none"
>
<span class="bg-gradient-to-br from-primary-600 to-primary-500 dark:from-primary-400 dark:to-primary-300 bg-clip-text text-transparent">
{item.value}
</span>
</dt>
<dd
class="mt-3 text-sm sm:text-base font-semibold text-secondary-600 dark:text-secondary-400 uppercase tracking-wider"
>
{item.label}
</dd>
</div>
))}
</dl>
</div>
</section>
+207 -16
View File
@@ -9,6 +9,11 @@ interface BreadcrumbItem {
interface Props {
type?: 'WebPage' | 'BlogPosting' | 'Article' | 'Service' | 'AboutPage' | 'ContactPage' | 'FAQPage';
// When true this instance emits ONLY the site-wide base schema
// (Organization + LocalBusiness and WebSite). Rendered once from
// BaseLayout's <head> so every route carries the canonical company data,
// independently of whether the page adds its own page-specific <SEO>.
base?: boolean;
breadcrumbs?: BreadcrumbItem[];
article?: {
author: string;
@@ -29,11 +34,173 @@ interface Props {
}>;
}
const { type = 'WebPage', breadcrumbs, article, service, faq } = Astro.props;
const { type = 'WebPage', base = false, breadcrumbs, article, service, faq } = Astro.props;
const baseUrl = 'https://workroot.in';
const currentUrl = Astro.url.href;
const orgName = 'WorkRoot IT Solutions LLP';
const orgId = `${baseUrl}/#organization`;
// ---------------------------------------------------------------------------
// Site-wide base schema — Organization + LocalBusiness and WebSite.
//
// Emitted once per page via `<SEO base />` rendered from BaseLayout's <head>,
// so every route (including pages without their own page-specific <SEO>)
// carries WorkRoot's canonical structured data.
//
// `review` / `aggregateRating` are built from the three real, attributable
// client testimonials shown on the homepage (see the `testimonials` array in
// src/pages/index.astro). All three are 5★, so the aggregate rating is 5.0
// over reviewCount = 3.
//
// Headline proof numbers in the description (20+ projects · 11+ districts ·
// 50,000+ records · 99.9% uptime) mirror the canonical set in
// ProofNumbers.astro and the /services stats — keep them identical site-wide.
// ---------------------------------------------------------------------------
const testimonialReviews = [
{
author: 'Ajay Pruthi',
body: 'WorkRoot IT Solutions is very professional and responsive. They built PLNR Financing platform with great attention to detail. The Angular + Spring Boot stack works flawlessly and their ongoing support is excellent.',
},
{
author: 'Dhirendra Singh',
body: 'The FPC Cart e-commerce platform delivered by WorkRoot exceeded our expectations. Built with Astro and Golang, it loads incredibly fast and handles our product catalog seamlessly. Their team understood our business needs perfectly.',
},
{
author: 'Dilip Raj',
body: 'WorkRoot delivered our University Management System for IIT Patna on time and within budget. The Next.js + Spring Boot architecture is robust and scalable. They are a reliable technology partner for complex enterprise projects.',
},
];
const reviewNodes = testimonialReviews.map((review) => ({
"@type": "Review",
"author": {
"@type": "Person",
"name": review.author
},
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5",
"worstRating": "1"
},
"reviewBody": review.body
}));
const organizationSchema = {
"@context": "https://schema.org",
"@type": ["Organization", "LocalBusiness"],
"@id": orgId,
"name": orgName,
"alternateName": ["WorkRoot", "WorkRoot IT Solutions"],
"url": baseUrl,
"logo": {
"@type": "ImageObject",
"url": `${baseUrl}/logo.png`,
"width": "512",
"height": "512"
},
"image": `${baseUrl}/og-image.jpg`,
"description": "WorkRoot IT Solutions LLP builds production-grade web, mobile, ERP, and government IT systems — 20+ projects delivered across 11+ districts, 50,000+ records managed, and 99.9% uptime on mission-critical government deployments.",
"foundingDate": "2022",
"founder": {
"@type": "Person",
"name": "Ajay Ghanwat"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
"addressLocality": "Satara",
"addressRegion": "MH",
"postalCode": "415503",
"addressCountry": "IN"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "17.7733632",
"longitude": "74.3309312"
},
"hasMap": "https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5",
"telephone": "+91-9561417403",
"email": "admin@workroot.in",
"contactPoint": [{
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "customer service",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "IN"
}, {
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "sales",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "IN"
}],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00",
"closes": "18:00"
}
],
"priceRange": "$$",
"currenciesAccepted": "INR",
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
"sameAs": [
"https://x.com/GhanwatAjay",
"https://www.linkedin.com/company/workroot",
"https://git.workroot.in/WorkRoot/",
"https://www.facebook.com/profile.php?id=61576503897897"
],
"areaServed": [
{
"@type": "AdministrativeArea",
"name": "Bihar"
},
{
"@type": "Country",
"name": "India"
}
],
"serviceType": ["Web Development", "Mobile App Development", "ERP & Enterprise Systems", "Government IT Solutions"],
"knowsAbout": ["Web Development", "Software Engineering", "Cloud Computing", "Artificial Intelligence", "Machine Learning", "Mobile Development"],
"slogan": "Transforming Ideas Into Digital Reality",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5.0",
"reviewCount": String(reviewNodes.length),
"bestRating": "5",
"worstRating": "1"
},
"review": reviewNodes
};
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": orgName,
"url": baseUrl,
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"publisher": {
"@type": "Organization",
"@id": orgId,
"name": orgName
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://workroot.in/blog?search={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "en-US"
};
// Get page title and description from parent layout props
const pageTitle = (Astro.props as any).title || 'WorkRoot IT Solutions';
const pageDescription = (Astro.props as any).description || 'Professional IT services including web development, cloud solutions, and digital transformation for businesses.';
@@ -71,7 +238,7 @@ const blogPostingSchema = article && type === 'BlogPosting' ? {
"dateModified": article.dateModified || article.datePublished,
"publisher": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"name": "WorkRoot IT Solutions LLP",
"url": baseUrl,
"logo": {
"@type": "ImageObject",
@@ -90,7 +257,9 @@ const blogPostingSchema = article && type === 'BlogPosting' ? {
"inLanguage": "en-US"
} : null;
// Generate Service schema
// Generate Service schema — reusable node; each service page passes its own
// name/description via the `service` prop. Provider is linked by @id to the
// site-wide Organization node so the structured-data graph stays connected.
const serviceSchema = service ? {
"@context": "https://schema.org",
"@type": "Service",
@@ -98,10 +267,20 @@ const serviceSchema = service ? {
"description": service.description,
"provider": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"@id": orgId,
"name": orgName,
"url": baseUrl
},
"areaServed": "Worldwide",
"areaServed": [
{
"@type": "AdministrativeArea",
"name": "Bihar"
},
{
"@type": "Country",
"name": "India"
}
],
"serviceType": service.name
} : null;
@@ -119,7 +298,7 @@ const webPageSchema = type === 'WebPage' && Astro.url.pathname !== '/' ? {
},
"about": {
"@type": "Organization",
"name": "WorkRoot IT Solutions"
"name": "WorkRoot IT Solutions LLP"
},
"primaryImageOfPage": {
"@type": "ImageObject",
@@ -151,7 +330,7 @@ const aboutPageSchema = type === 'AboutPage' ? {
"inLanguage": "en-US",
"mainEntity": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"name": "WorkRoot IT Solutions LLP",
"url": baseUrl
}
} : null;
@@ -166,8 +345,8 @@ const contactPageSchema = type === 'ContactPage' ? {
"inLanguage": "en-US",
"mainEntity": {
"@type": "LocalBusiness",
"name": "WorkRoot IT Solutions",
"alternateName": "WorkRoot IT Solutions LLP",
"name": "WorkRoot IT Solutions LLP",
"alternateName": "WorkRoot IT Solutions",
"url": baseUrl,
"telephone": "+91-9561417403",
"email": "admin@workroot.in",
@@ -214,30 +393,42 @@ const contactPageSchema = type === 'ContactPage' ? {
} : null;
---
{breadcrumbSchema && (
{/* Site-wide base schema — emitted only by the single `<SEO base />` rendered
from BaseLayout's <head>, so it appears exactly once on every page. */}
{base && (
<script type="application/ld+json" set:html={JSON.stringify(organizationSchema)} />
)}
{base && (
<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />
)}
{/* Page-specific schema — never emitted by the base instance, so there is no
duplication with the site-wide Organization/WebSite nodes above. */}
{!base && breadcrumbSchema && (
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
)}
{blogPostingSchema && (
{!base && blogPostingSchema && (
<script type="application/ld+json" set:html={JSON.stringify(blogPostingSchema)} />
)}
{serviceSchema && (
{!base && serviceSchema && (
<script type="application/ld+json" set:html={JSON.stringify(serviceSchema)} />
)}
{webPageSchema && (
{!base && webPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(webPageSchema)} />
)}
{faqPageSchema && (
{!base && faqPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(faqPageSchema)} />
)}
{aboutPageSchema && (
{!base && aboutPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(aboutPageSchema)} />
)}
{contactPageSchema && (
{!base && contactPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(contactPageSchema)} />
)}
+160
View File
@@ -0,0 +1,160 @@
---
// ServiceFaq — reusable, prop-driven FAQ accordion for the keyword-focused
// service landing pages (e.g. /services/custom-erp-development).
//
// IMPORTANT: this component renders only the VISIBLE accordion. It does NOT
// emit FAQPage JSON-LD. The owning page is expected to pass the same `faqs`
// array into <SEO faq={faqs} />, which emits exactly one FAQPage node. Keeping
// schema emission in SEO.astro (and out of here) guarantees a single FAQPage
// per page even when several of these accordions could theoretically render.
//
// Visual + a11y behaviour mirrors HomepageFaq.astro (single-open accordion,
// real <button> questions, aria-expanded, grid-rows expand with no layout
// shift) but uses a `svc-faq-*` class namespace so it never collides with the
// homepage FAQ script if both ever appear together.
interface FaqItem {
question: string;
answer: string;
}
interface Props {
faqs: FaqItem[];
eyebrow?: string;
heading?: string;
subheading?: string;
}
const {
faqs,
eyebrow = 'Before you reach out',
heading = 'Frequently asked questions',
subheading = 'Straight answers on scope, cost, timelines, ownership, and how we work.',
} = Astro.props;
---
<section
class="py-20 sm:py-24 bg-secondary-50 dark:bg-secondary-950 border-t border-secondary-100 dark:border-secondary-800"
aria-labelledby="svc-faq-heading"
>
<div class="container-wrapper">
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12 sm:mb-16" data-animate="fade-up">
<span
class="inline-block px-4 py-2 rounded-full bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 text-xs sm:text-sm font-bold uppercase tracking-wider mb-4"
>
{eyebrow}
</span>
<h2
id="svc-faq-heading"
class="text-3xl sm:text-4xl lg:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-4 leading-tight"
>
{heading}
</h2>
<p class="text-base sm:text-lg text-secondary-600 dark:text-secondary-300">
{subheading}
</p>
</div>
<ul class="space-y-3 sm:space-y-4" role="list">
{
faqs.map((item, index) => (
<li
class="bg-white dark:bg-secondary-800 rounded-2xl border border-secondary-200 dark:border-secondary-700 hover:border-primary-300 dark:hover:border-primary-500/60 shadow-sm transition-colors duration-200 overflow-hidden"
>
<button
type="button"
class="svc-faq-question w-full text-left px-5 sm:px-7 py-5 sm:py-6 flex items-start justify-between gap-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-2 focus-visible:ring-offset-secondary-50 dark:focus-visible:ring-offset-secondary-950 rounded-2xl"
aria-expanded="false"
aria-controls={`svc-faq-answer-${index}`}
id={`svc-faq-question-${index}`}
>
<span class="font-semibold text-base sm:text-lg text-secondary-900 dark:text-secondary-100 leading-snug">
{item.question}
</span>
<svg
class="svc-faq-icon w-5 h-5 sm:w-6 sm:h-6 mt-0.5 sm:mt-1 text-primary-500 dark:text-primary-400 flex-shrink-0 transition-transform duration-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div
class="svc-faq-answer grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out"
id={`svc-faq-answer-${index}`}
role="region"
aria-labelledby={`svc-faq-question-${index}`}
>
<div class="overflow-hidden">
<p class="px-5 sm:px-7 pb-5 sm:pb-6 -mt-1 text-sm sm:text-base text-secondary-600 dark:text-secondary-300 leading-relaxed">
{item.answer}
</p>
</div>
</div>
</li>
))
}
</ul>
<p class="text-center text-sm sm:text-base text-secondary-600 dark:text-secondary-400 mt-10 sm:mt-12">
Have a different question?{' '}
<a
href="#inline-lead-form"
class="font-semibold text-primary-600 dark:text-primary-300 hover:underline"
>
Ask us directly &rarr;
</a>
</p>
</div>
</div>
</section>
<style>
.svc-faq-answer[data-expanded='true'] {
grid-template-rows: 1fr;
}
.svc-faq-question[aria-expanded='true'] .svc-faq-icon {
transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
.svc-faq-answer,
.svc-faq-icon {
transition: none !important;
}
}
</style>
<script is:inline>
(function () {
var buttons = document.querySelectorAll('.svc-faq-question');
if (!buttons.length) return;
function setExpanded(button, expanded) {
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
var answerId = button.getAttribute('aria-controls');
if (!answerId) return;
var answer = document.getElementById(answerId);
if (!answer) return;
if (expanded) {
answer.setAttribute('data-expanded', 'true');
} else {
answer.removeAttribute('data-expanded');
}
}
buttons.forEach(function (button) {
button.addEventListener('click', function () {
var isOpen = button.getAttribute('aria-expanded') === 'true';
buttons.forEach(function (other) {
if (other !== button) setExpanded(other, false);
});
setExpanded(button, !isOpen);
});
});
})();
</script>
+194
View File
@@ -0,0 +1,194 @@
---
/**
* TrustStrip Component
*
* A compact, single-row "Trusted by" strip of client / partner logos that sits
* directly under the homepage hero. Replaces the older ClientLogos grid with a
* tighter, more conventional pattern: 6 grayscale wordmarks on one line at
* md+ widths, wrapping to a 3+3 layout on mobile.
*
* Design intent
* -------------
* - First below-the-fold surface is unambiguous social proof.
* - Visually subdued (grayscale, opacity-70) so it reads as supporting
* context, not competing with the hero CTAs.
* - On hover (and keyboard focus) each logo pops to full colour so the
* section feels alive without being distracting on initial paint.
* - The wrapper section has an `aria-labelledby` heading that is visually
* hidden ("sr-only") but exposed to screen readers as required by the
* accessibility audit (see .agents/security-auditor/THEME_ACCESSIBILITY_AUDIT.md).
*
* CLS guard
* ---------
* - Every `<img>` has explicit `width` and `height` attributes (160 × 48)
* so the browser reserves the slot before the SVG decodes. No layout
* shift on logo load.
* - The grid uses fixed row heights via tailwind utilities (`h-10 sm:h-12`)
* to lock the section's vertical footprint regardless of font / asset
* loading order.
*
* Asset notes
* -----------
* - Logo SVGs live in `public/images/logos/<slug>.svg`. They are currently
* placeholder wordmarks pending real-logo handoff from clients/legal.
* Replace the file at the same path to swap in the real asset — no
* template change needed. See the README note in the PR.
* - Astro's `astro:assets` `<Image>` component only processes files under
* `src/assets/`. Public-folder static SVGs are best served with a plain
* `<img>` (no transform pipeline needed for already-vector assets);
* this still satisfies the Image-component goals — explicit dimensions,
* `loading="lazy"`, `decoding="async"` — which are the real CLS / perf
* levers.
*
* Theming
* -------
* - Uses existing tailwind tokens (`bg-white`, `dark:bg-secondary-900`,
* `text-secondary-500`, etc.) — no new colour primitives introduced.
* - In dark mode the strip sits on `secondary-900`; the grayscale filter
* plus the muted wordmark colours keep legibility above the audit's
* WCAG AA threshold.
*
* Props
* -----
* @prop {Array<{ name: string; logo: string; href?: string }>} [clients]
* Optional override list. Defaults to the WorkRoot client roster.
* @prop {string} [class] Extra utility classes merged onto the section.
*/
interface LogoEntry {
name: string;
logo: string;
href?: string;
}
interface Props {
clients?: LogoEntry[];
class?: string;
}
const { clients, class: extraClass = '' } = Astro.props;
// -----------------------------------------------------------------------------
// Default logo roster. Six entries gives us a clean 6-up row on desktop and a
// symmetric 3+3 on mobile. Names map to placeholder SVGs under public/images/
// logos/<slug>.svg. Drop a real logo at the same path to swap.
// -----------------------------------------------------------------------------
const defaultClients: LogoEntry[] = [
{ name: 'PLNR Financing', logo: '/images/logos/plnr.svg' },
{ name: 'RENTEC / FPC Cart', logo: '/images/logos/rentec.svg' },
{ name: 'IIT Patna', logo: '/images/logos/iit-patna.svg' },
{ name: 'MOC Soft Technologies', logo: '/images/logos/moc-soft.svg' },
{ name: 'Government of Bihar', logo: '/images/logos/gov-bihar.svg' },
{ name: 'PSCL Bihar', logo: '/images/logos/pscl.svg' },
];
const items: LogoEntry[] = clients && clients.length > 0 ? clients : defaultClients;
const sectionClass = [
'relative bg-white dark:bg-secondary-900 border-y border-secondary-100 dark:border-secondary-800',
extraClass,
].filter(Boolean).join(' ');
---
<!--
Trust strip. Anchor `#trust-strip` lets the hero scroll-indicator (and any
external deep-link) jump straight to the social-proof row.
-->
<section
id="trust-strip"
class={sectionClass}
aria-labelledby="trust-strip-heading"
>
<div class="container-wrapper py-10 sm:py-12">
<!--
Visually hidden but screen-reader announced. The audit calls for every
labelled landmark to have an accessible name; we don't want a heavy
visible header competing with the hero, hence sr-only.
-->
<h2 id="trust-strip-heading" class="sr-only">Trusted by</h2>
<!--
Logo row.
Mobile (<sm): 3-up grid → wraps to 3+3 with 6 items.
sm+ : 6-up flex row, evenly spaced.
Items use min-w-0 + object-contain so wordmarks never overflow.
-->
<ul
class="grid grid-cols-3 sm:grid-cols-6 items-center justify-items-center gap-x-6 gap-y-8 sm:gap-x-8"
role="list"
aria-label="Selected clients and partners"
>
{items.map((client) => {
const Wrapper = client.href ? 'a' : 'div';
const wrapperProps = client.href
? { href: client.href, 'aria-label': `Learn more about ${client.name}` }
: { 'aria-label': client.name, role: 'img' };
return (
<li class="flex justify-center w-full">
<Wrapper
{...wrapperProps}
class={`trust-logo group flex items-center justify-center w-full max-w-[160px] h-10 sm:h-12 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-400 ${client.href ? 'cursor-pointer' : ''}`}
>
<img
src={client.logo}
alt={`${client.name} logo`}
width="160"
height="48"
loading="lazy"
decoding="async"
class="max-h-10 sm:max-h-12 w-auto object-contain"
/>
</Wrapper>
</li>
);
})}
</ul>
</div>
</section>
<style>
/*
Grayscale → colour transition.
Default state: full grayscale + slightly reduced opacity so the strip reads
as a calm, unified band of marks rather than a noisy rainbow of brand
palettes.
Hover / focus-within state: drop the filter so the original brand colours
return, signalling interactivity without being startling.
Dark mode: invert is applied AFTER grayscale so the (now monochrome) marks
flip to a light tone that's legible on the dark surface. The invert is
removed on hover/focus so the original colours are once again visible.
*/
.trust-logo img {
filter: grayscale(100%);
opacity: 0.75;
transition: filter 300ms ease, opacity 300ms ease;
}
.trust-logo:hover img,
.trust-logo:focus-visible img {
filter: grayscale(0%);
opacity: 1;
}
:global(.dark) .trust-logo img {
/* Invert + slight brightness lift so the (grayscaled) marks read as a
light-on-dark wordmark group. Tested against secondary-900 surface; the
resulting luminance comfortably exceeds the WCAG AA 4.5:1 floor for the
largest text-equivalent (the dominant wordmark glyphs). */
filter: grayscale(100%) invert(1) brightness(1.1) contrast(0.95);
}
:global(.dark) .trust-logo:hover img,
:global(.dark) .trust-logo:focus-visible img {
/* Restore original colour on interaction. */
filter: none;
opacity: 1;
}
/* Honour reduced-motion preference — instant swap, no fade. */
@media (prefers-reduced-motion: reduce) {
.trust-logo img {
transition: none;
}
}
</style>
@@ -1,6 +1,8 @@
---
title: "How AI is Transforming Modern Business Operations"
description: "Explore the practical applications of artificial intelligence in streamlining business processes and driving innovation."
seoTitle: "How AI Is Transforming Business Operations"
seoDescription: "How AI is reshaping business operations — practical use cases in customer service, predictive analytics, process automation, and intelligent document handling."
pubDate: 2024-01-10
heroImage: "/images/blog/ai-business.jpg"
category: "AI/ML"
@@ -1,6 +1,8 @@
---
title: "Building Scalable Government IT Systems: Lessons from 11-District Deployments"
description: "Practical insights from building and deploying large-scale government systems across multiple districts, including Police Management Systems and SCMS projects."
seoTitle: "Scaling Government IT Across 11 Districts"
seoDescription: "Lessons from deploying government IT systems across 11 districts — database-per-district architecture, offline-first modules, and audit logging baked in."
pubDate: 2025-12-15
heroImage: "/images/blog/govt-systems.jpg"
category: "Cloud"
@@ -1,6 +1,8 @@
---
title: "Building Scalable ERP Systems: Lessons from Government & Enterprise Projects"
description: "Practical insights on designing and deploying large-scale ERP and management systems for government bodies and enterprises, drawn from real-world project experience."
seoTitle: "Building Scalable ERP Systems That Last"
seoDescription: "How we design and deploy scalable ERP systems for government and enterprise — modular architecture, role-based access control, and offline-resilient patterns."
pubDate: 2024-02-20
heroImage: "/images/blog/erp-systems.jpg"
category: "Web Development"
@@ -1,6 +1,8 @@
---
title: "The Complete Guide to Cloud Migration in 2024"
description: "A step-by-step approach to migrating your infrastructure to the cloud while minimizing downtime and maximizing ROI."
seoTitle: "A Complete Guide to Cloud Migration in 2024"
seoDescription: "A step-by-step cloud migration guide for 2024 — assess workloads, containerize apps, adopt infrastructure as code, and cut downtime while maximizing your ROI."
pubDate: 2024-01-05
heroImage: "/images/blog/cloud-migration.jpg"
category: "Cloud"
@@ -0,0 +1,125 @@
---
title: "How We Deployed a Police Management System Across 11 Bihar Districts"
description: "A field-tested account of rolling out a Police Management System to 15,000+ officers across 11 districts in Bihar — the architecture, the rollout plan, the failures, and the numbers that came out the other side."
seoTitle: "Deploying a Police System Across 11 Districts"
seoDescription: "A field account of rolling out a Police Management System to 15,000+ officers across 11 Bihar districts — the architecture, rollout plan, failures, and results."
pubDate: 2026-06-10
heroImage: "/images/blog/govt-systems.jpg"
category: "DevOps"
tags: ["Government IT", "Case Study", "Deployment", "Spring Boot", "Police Management System", "Bihar"]
author:
name: "Ajay"
avatar: "/images/team/ajay.jpg"
draft: false
---
When a state police department asks you to digitise case management, personnel tracking, and resource allocation across **11 districts** and put it in front of **15,000+ officers**, the hard part is not the code. It is the rollout. This is the story of how we took a Police Management System (PMS) from a single pilot district in Bihar to a statewide deployment without taking public-safety operations offline — what worked, what broke, and the numbers we measured along the way.
> **The headline:** 11 districts live in 14 weeks, zero unplanned downtime during cutover, and FIR registration time cut from an average of 38 minutes (paper) to under 6 minutes (digital).
## The Starting Point
Before the project, every district ran on paper and a patchwork of disconnected spreadsheets. A case opened in one district was invisible to the next. Personnel rosters lived in filing cabinets. Resource allocation — vehicles, equipment, duty assignments — was reconciled by hand at the end of each shift.
The brief was deceptively simple: one system, every district, no disruption to live policing. The constraints were not:
- **Heterogeneous infrastructure.** Some district HQs had fibre; rural outposts shared a single intermittent mobile connection.
- **Zero tolerance for data leakage between jurisdictions.** District A must never see District B's open cases.
- **Officers with mixed digital literacy.** The system had to be usable by someone who had never filed anything on a computer.
- **A non-negotiable audit trail.** Every read and write on sensitive records had to be traceable.
## The Architecture We Committed To
We built the backend on **Spring Boot** as a modular monolith — clean module boundaries (cases, personnel, resources, reporting, auth) without the operational tax of running dozens of microservices across 11 sites. The frontend was an **Angular** SPA with an offline-resilient shell so officers could keep working through connectivity drops.
Three decisions did most of the heavy lifting:
### 1. Database-per-district with central aggregation
Each district ran its own operational database, with a central read-only warehouse for state-level analytics. This gave us hard data isolation *for free* — a query in one district physically cannot reach another district's records — and meant one district's database incident never cascaded.
```yaml
# Per-district deployment descriptor
district:
code: "D04"
deployment-mode: "standard" # high-load | standard | minimal
database:
isolation: dedicated # no cross-district connection strings exist
pool-size: 20
sync:
central-warehouse: nightly
```
### 2. Offline-first for field-critical modules
FIR registration and duty logging had to work when the network did not. We queued writes locally and synced on a fixed cadence, with conflict resolution on the server. An officer in a rural outpost never sees a spinner waiting on a flaky uplink.
```java
@Scheduled(fixedDelay = 300_000) // every 5 minutes
public void syncPendingRecords() {
for (PendingRecord record : localStore.getPending()) {
try {
centralServer.submit(record);
localStore.markSynced(record.getId());
} catch (ConnectException e) {
log.warn("Uplink down — {} records still queued", localStore.pendingCount());
break; // stop and retry next cycle; never block the officer
}
}
}
```
### 3. Audit logging baked into the data layer
Every access to a sensitive record writes an immutable audit entry with user, action, district, timestamp, and source IP. Critical actions trigger real-time alerts. This was not a reporting feature bolted on at the end — it was a cross-cutting concern in the shared library every module depended on.
## The Rollout Plan That Actually Held Up
We did **not** big-bang 11 districts. We sequenced them.
1. **Pilot (Weeks 14): one district.** We picked a mid-sized district with reasonable connectivity and a station house officer who was enthusiastic about the project. Everything we learned here rewrote the playbook for the rest.
2. **Hardening (Weeks 57): two adjacent districts.** This proved the multi-district data isolation under real concurrent load and exposed our first nasty bug (see below).
3. **Wave rollout (Weeks 814): the remaining eight, two at a time.** Each wave used **feature flags** so a district could go live with the core modules first and switch on biometric auth or mobile reporting only once its officers were trained.
```yaml
features:
biometric-auth:
enabled: true
districts: ["D01", "D02", "D05"]
mobile-reporting:
enabled: true
districts: ["D01", "D02", "D03", "D04", "D05",
"D06", "D07", "D08", "D09", "D10", "D11"]
```
Cutover for each district used **blue-green deployment**: stand up the new version alongside the old, run smoke tests, switch the load balancer, and keep the previous environment warm for 24 hours as an instant rollback path. No district ever experienced a maintenance window during working hours.
## What Broke (Because Something Always Does)
**The sync storm.** When we brought districts three and four online, a power restoration after an outage caused hundreds of field devices to flush their queues simultaneously. The central server buckled under the thundering herd. The fix: jittered, exponentially backed-off sync windows so devices never resync in lockstep. It is now standard in every offline module we build.
**The "helpful" copy-paste.** During training we discovered officers pasting entire case narratives that included pasted formatting payloads. Our early sanitisation was too permissive. We tightened input handling and added server-side normalisation before this ever reached production data.
**Training was half the project.** We budgeted for software and under-budgeted for people. The districts that went smoothly were the ones where we ran hands-on sessions with real (anonymised) cases, not slide decks. We rebuilt our rollout checklist to make on-site training a gating requirement, not an afterthought.
## The Numbers
After all 11 districts were live for one quarter:
- **38 min → under 6 min** average FIR registration time.
- **11/11 districts** on a single, consistent platform with strict jurisdiction isolation.
- **15,000+ officers** onboarded.
- **Zero** unplanned downtime during any district cutover.
- **100%** of sensitive-record access captured in a tamper-evident audit trail.
## What We'd Tell Anyone Attempting This
1. **Sequence, never big-bang.** A pilot that teaches you something is worth more than a launch that impresses someone.
2. **Design for the worst network in the state, not the best.** If it breaks at 500ms latency, it will break in the field.
3. **Make isolation structural, not procedural.** If a query *can* cross jurisdictions, eventually it will. Remove the possibility at the schema and connection layer.
4. **Automate every deployment.** With 11+ environments, manual steps are how outages happen. We used Ansible so every cutover was reproducible.
5. **Treat training as engineering work.** Budget for it, measure it, and gate go-live on it.
## Building something similar?
We have shipped systems like this across multiple Indian states and institutions — police, universities, and government departments — under exactly these constraints. If you are scoping a multi-district government platform, a custom ERP, or any system where downtime is not an option, [get a free proposal](/contact) and we will share what we learned the expensive way so you do not have to.
@@ -0,0 +1,125 @@
---
title: "GeM Empanelment Process for Software Vendors: A Step-by-Step 2026 Guide"
description: "A complete, India-specific walkthrough of how software and IT-services vendors get listed and sell on the Government e-Marketplace (GeM) — registration steps, document checklist, fees, timelines, and how the bidding process actually works."
seoTitle: "GeM Empanelment for Software Vendors (2026)"
seoDescription: "Step-by-step guide to GeM registration and empanelment for software vendors in India — documents, fees, timelines, OEM panel, bids vs catalogue, and common rejection reasons."
pubDate: 2026-06-19
heroImage: "/images/blog/govt-systems.jpg"
category: "Web Development"
tags: ["GeM", "Government IT", "Vendor Registration", "Public Procurement", "India", "Software Services"]
author:
name: "Ajay"
avatar: "/images/team/ajay.jpg"
draft: false
---
The Government e-Marketplace (GeM) is the single largest channel for selling software and IT services to Indian government bodies — central ministries, state departments, PSUs, and autonomous institutions all procure through it. If you build custom software, mobile apps, or IT services and you are not on GeM, you are invisible to a buyer base that spends tens of thousands of crores a year. This guide explains exactly how a software vendor gets onto GeM, what it costs, how long it takes, and how to actually win business once you are listed.
One clarification up front: GeM does not use the word "empanelment" the way traditional government tenders do. On GeM you **register as a Seller** and then **publish offerings** (products or, for software companies, *services*). When people say "GeM empanelment for software vendors," they mean getting your company registered, verified, and listed so government buyers can find and buy from you. We use both terms interchangeably below.
## Who Should Register: Product Vendor vs Service Provider
Software companies usually fit one or both of these seller types, and the distinction matters because the catalogue, documents, and bidding flow differ.
| Seller Type | What You Sell on GeM | Typical Software Company Fit |
|-------------|----------------------|------------------------------|
| **Product Seller** | Listed goods with a catalogue (e.g., software licences, devices) | Off-the-shelf software, SaaS licences, hardware bundles |
| **Service Provider** | Service categories with rate-card / bid-based pricing | Custom development, IT manpower, AMC, cloud, web design |
| **OEM** | Original manufacturer with an OEM panel | Companies whose own software product is resold by others |
Most custom-software firms register primarily as **Service Providers** under categories like "Software Development Services," "Web Site Design & Development," "IT Professional Outsourcing," and "Custom Bid for Services."
## Step-by-Step Registration Process
The end-to-end flow from sign-up to your first live listing typically looks like this:
1. **Create a Primary User account** on gem.gov.in using your business email and the Aadhaar or PAN of the authorised signatory. The primary user is legally the organisation's representative.
2. **Select your organisation type** — Proprietorship, Partnership, LLP, Private Limited, etc. This determines which constitution documents you upload.
3. **Complete organisation verification** by linking your **Udyam (MSME) registration** or **CIN**, and validating your **PAN** and **GSTIN**.
4. **Add your business profile** — office address, bank account (for payments), and authorised signatory details.
5. **Complete the GeM seller assessment** where applicable (especially for OEMs and certain service categories).
6. **List your offerings** — choose the right service category, fill the technical parameters, upload your rate card / documents, and publish.
7. **Set up the Caution Money / e-PBG** mechanism if you intend to bid on higher-value tenders (more below).
### Document Checklist
Have these ready before you start — missing or mismatched documents are the number-one cause of delay:
- PAN of the organisation and the authorised signatory
- GSTIN certificate
- Udyam (MSME) registration certificate (strongly recommended — unlocks exemptions)
- Constitution proof (Certificate of Incorporation / Partnership Deed / GST for proprietorship)
- Cancelled cheque or bank account proof matching the registered name
- Authorised signatory's Aadhaar (for e-sign / OTP verification)
- Income Tax Return acknowledgements (often needed to clear turnover-based bid criteria)
- Relevant certifications (ISO, CMMI, CERT-In) if your target tenders demand them
## Fees, Caution Money, and Timelines
A frequent question: *is GeM registration free?* Basic seller registration on GeM is **free**. The costs come later, when you transact.
| Item | 2026 Cost (Indicative) | Notes |
|------|------------------------|-------|
| Seller registration | ₹0 | No registration fee |
| Caution Money Deposit (CMD) | ₹5,000 ₹25,000 | Slab-based on annual GeM turnover; refundable |
| Transaction charge | ~0.30% 0.45% of order value | Levied on the seller above a threshold per order |
| e-PBG / EMD for bids | 2%3% of bid value (often exempt for MSME) | Tender-specific; bid security |
**Timeline:** with documents in order, organisation verification typically completes in **37 working days**, and you can publish your first listing the same week. In practice, factoring in document corrections and category approvals, most software vendors are live and bid-ready in **24 weeks**.
## Catalogue Listing vs Bidding: How You Actually Win
Getting listed is necessary but not sufficient. There are three ways government buyers actually buy from you on GeM, and your strategy should target the right one.
| Mode | How It Works | Best For |
|------|-------------|----------|
| **Direct Purchase** | Buyer purchases directly up to ₹25,000 (or category limit) | Small, fast, low-value services |
| **L1 / Comparison** | Buyer compares listed sellers and picks lowest qualifying price | Standard, well-defined service offerings |
| **Bid / Reverse Auction** | Formal tender with eligibility criteria; sellers bid | High-value custom projects and AMCs |
For custom software and ERP work, the **Bid / Reverse Auction** route is where the meaningful contracts live. A buyer publishes a bid with eligibility criteria (turnover, past performance, certifications), and registered sellers submit technical and financial bids. Reverse auctions then drive the price down to the lowest qualifying bidder, so your win depends on meeting eligibility *and* pricing intelligently — not just being cheapest.
## MSME and Startup Benefits Worth Claiming
If you are an MSME or a DPIIT-recognised startup, GeM gives you real, money-saving advantages that you should activate during registration:
- **Exemption from prior turnover and prior experience** criteria in many bids (under the Public Procurement Policy for MSEs).
- **EMD / bid-security exemption** in a large share of tenders.
- **Reserved procurement** — a mandated share of government purchases is earmarked for MSEs.
- **No registration fee** and reduced caution-money slabs.
Linking your Udyam registration during onboarding is what unlocks these — skipping it leaves money and eligibility on the table.
## Common Reasons Software Vendors Get Rejected or Delayed
Avoid these and you will clear verification on the first attempt:
- **Name mismatches** across PAN, GST, bank account, and Udyam — every record must match exactly.
- **Wrong service category** — listing under a category that does not match your offering causes buyer-side rejection.
- **Incomplete technical parameters** in the service listing, so buyers cannot find or compare you.
- **Not meeting turnover/experience eligibility** in a bid (and not claiming MSME exemption when eligible).
- **Outdated documents** — an expired GST or unrenewed Udyam certificate stalls everything.
## Frequently Asked Questions
**Is GeM registration mandatory to sell to the government?**
For most central and state government procurement of available goods and services, yes — buyers are directed to procure through GeM. If your offering exists on GeM, being registered is effectively required to compete.
**How long does GeM registration take for a software company?**
Organisation verification typically completes in 37 working days. Allowing for document corrections and category approvals, most software vendors are live and bid-ready within 24 weeks.
**Does it cost money to register on GeM?**
Basic seller registration is free. Costs arise later: a refundable caution-money deposit (₹5,000–₹25,000 by turnover slab), a small per-order transaction charge, and bid-specific securities (often waived for MSMEs).
**Can a startup or MSME sell on GeM without prior government experience?**
Yes. Under the MSE procurement policy and startup provisions, many bids exempt MSMEs and DPIIT-recognised startups from prior turnover and experience requirements — but you must link your Udyam/DPIIT registration to claim it.
**What's the difference between being listed and winning a contract?**
Listing makes you visible. Winning happens through Direct Purchase (small value), L1 comparison, or Bid/Reverse Auction (high value). For custom software and ERP contracts, you win by qualifying on eligibility criteria and competing in the bid — listing alone generates very few orders.
**Which categories should a custom software firm register under?**
Common fits include Software Development Services, Web Site Design & Development, IT Professional Outsourcing / Manpower, Cloud Services, and "Custom Bid for Services" for non-standard scopes. Register under every category that genuinely matches your delivery capability.
## Turning GeM Access Into Government Contracts
Registration is the entry ticket; winning is a discipline. The vendors who succeed on GeM keep their documents current, list under precise categories, claim every MSME benefit they qualify for, and price their bids with the reverse-auction dynamic in mind. WorkRoot has delivered software for Indian government bodies — including police and university systems across multiple states — and we understand both the procurement process and the engineering bar it demands. If you want help positioning for government IT work, [get in touch](/contact).
@@ -1,6 +1,8 @@
---
title: "Getting Started with Astro: The Modern Static Site Generator"
description: "Learn how Astro combines the best of static site generation with modern component frameworks for blazing-fast websites."
seoTitle: "Getting Started with Astro for Fast Sites"
seoDescription: "Get started with Astro, the modern site generator — learn islands architecture, type-safe content collections, and how it ships fast, low-JavaScript sites."
pubDate: 2024-01-15
heroImage: "/images/blog/astro-intro.jpg"
category: "Web Development"
@@ -0,0 +1,119 @@
---
title: "How Much Does Custom ERP Cost in India? (2026 Pricing Guide)"
description: "A transparent, India-specific breakdown of what a custom ERP actually costs in 2026 — by scope, team, and deployment model — with real rupee ranges, a cost calculator framework, and hidden costs to budget for."
seoTitle: "Custom ERP Cost in India (2026 Guide)"
seoDescription: "What does a custom ERP cost in India in 2026? Real rupee ranges by scope and team, a cost-driver breakdown, build-vs-buy maths, and hidden costs to plan for."
pubDate: 2026-06-19
heroImage: "/images/blog/erp-systems.jpg"
category: "Web Development"
tags: ["ERP", "Custom Software", "Pricing", "India", "Enterprise", "Cost Estimation"]
author:
name: "Ajay"
avatar: "/images/team/ajay.jpg"
draft: false
---
"How much will it cost?" is the first question every buyer asks about custom ERP — and the honest answer is "it depends." But "it depends" is not a number you can put in a budget. So in this guide we turn the question into something usable: what a custom ERP actually costs in India in 2026, what drives those numbers up or down, and how to estimate your own project before you talk to a single vendor.
The short version: a focused single-department custom ERP in India typically lands between **₹6,00,000 and ₹18,00,000**, a multi-module mid-market system between **₹18,00,000 and ₹60,00,000**, and a large multi-tenant or government-grade platform between **₹60,00,000 and ₹2,50,00,000+**. The rest of this article explains how to find *your* number inside those ranges.
## What "Custom ERP" Actually Means for Pricing
Before any number makes sense, you have to define scope. "ERP" is an umbrella — your price depends entirely on which modules you actually build. A finance-and-inventory system for a trading firm is a completely different project from a 14-module university ERP with admissions, examinations, hostel, and payroll.
Custom ERP cost in India is driven by five things, in roughly this order of impact:
1. **Number and complexity of modules** — each module is a mini-application with its own data model, screens, and rules.
2. **Number of distinct user roles and permission rules** — RBAC complexity scales non-linearly.
3. **Integrations** — payment gateways, GST/e-invoicing, SMS/WhatsApp, biometric, Tally, government APIs.
4. **Deployment model** — single-tenant vs multi-tenant, cloud vs on-premise, offline resilience.
5. **Non-functional requirements** — audit logging, data residency, uptime SLA, concurrent-user load.
## 2026 Cost Ranges by Project Tier
These are realistic, all-in development ranges for an India-based engagement (Indian team rates, INR), covering design, development, testing, and first deployment. They exclude ongoing maintenance, which we cover separately below.
| Tier | Typical Use Case | Modules | Timeline | Cost Range (INR) |
|------|------------------|---------|----------|------------------|
| **Starter** | Single-department tool (inventory, billing, or HR) | 12 | 610 weeks | ₹6L ₹18L |
| **Growth** | Mid-market multi-department ERP | 47 | 47 months | ₹18L ₹60L |
| **Enterprise** | Multi-entity / multi-location ERP, complex RBAC | 814 | 814 months | ₹60L ₹1.5Cr |
| **Government / Platform** | Multi-tenant, offline-resilient, audit-heavy | 10+ | 1224 months | ₹1.5Cr ₹2.5Cr+ |
A useful rule of thumb in 2026: a well-defined, production-grade ERP module costs roughly **₹4,00,000 to ₹9,00,000** to build, depending on how many screens, workflows, and integrations it carries.
## The Biggest Cost Driver: The Team
Custom software is labour. The single largest line item in any ERP quote is engineering time, so blended day-rates dominate the total. Here is what Indian ERP development talent typically costs in 2026.
| Role | Junior (₹/month) | Mid (₹/month) | Senior (₹/month) |
|------|------------------|---------------|------------------|
| Backend Engineer | ₹45,000 ₹70,000 | ₹80,000 ₹1,40,000 | ₹1,60,000 ₹2,80,000 |
| Frontend Engineer | ₹40,000 ₹65,000 | ₹75,000 ₹1,30,000 | ₹1,50,000 ₹2,50,000 |
| QA / Test Engineer | ₹35,000 ₹55,000 | ₹60,000 ₹1,00,000 | ₹1,10,000 ₹1,80,000 |
| Project Manager | — | ₹90,000 ₹1,50,000 | ₹1,60,000 ₹3,00,000 |
| UI/UX Designer | ₹40,000 ₹60,000 | ₹70,000 ₹1,20,000 | ₹1,30,000 ₹2,20,000 |
A typical Growth-tier project runs a squad of 46 people for 5 months. At blended mid-level rates, that alone accounts for ₹25L–₹40L of the budget — which is why the Growth range sits where it does.
## Build vs Buy: When Custom Actually Pays Off
Custom is not always the right call. Off-the-shelf and SaaS ERPs (Zoho, TallyPrime, SAP Business One, Odoo) have real advantages for standard workflows. Here is the honest comparison for an Indian mid-market buyer.
| Factor | Custom ERP | SaaS / Off-the-Shelf |
|--------|-----------|----------------------|
| Upfront cost | ₹18L ₹60L+ | ₹0 ₹5L setup |
| Recurring cost | ~1520% of build/year (AMC) | ₹500 ₹3,000 per user / month |
| Fit to your process | Exact | Adapt your process to the tool |
| Time to first value | 37 months | Days to weeks |
| Data ownership | Full | Vendor-hosted |
| Scaling cost | Predictable (you own it) | Grows with every seat |
The crossover maths matters. A 100-user SaaS ERP at ₹1,500/user/month costs **₹18,00,000 per year** — recurring, forever. A custom build at ₹40,00,000 with ₹7,00,000/year AMC breaks even against that SaaS bill in roughly **2.5 years**, and is cheaper every year after. Custom wins when your headcount is large, your processes are genuinely non-standard, or data sovereignty is non-negotiable (common in government and BFSI).
## Hidden Costs Buyers Forget to Budget
The build quote is not the total cost of ownership. Plan for these:
- **Annual Maintenance Contract (AMC):** budget **1520% of build cost per year** for bug fixes, security patches, and minor enhancements.
- **Cloud hosting:** a mid-size ERP on AWS/Azure in the Mumbai region typically costs **₹15,000 ₹60,000/month** depending on load and redundancy.
- **Third-party APIs:** SMS (₹0.12–₹0.25/message), WhatsApp Business, payment gateway (~2% per transaction), GST e-invoicing add-ons.
- **Data migration:** cleaning and importing legacy data is often underestimated — budget **₹1,50,000 ₹6,00,000**.
- **Training and change management:** for 50+ users, this is a real line item, not an afterthought.
- **Post-launch enhancements:** the first three months after go-live always surface new requirements.
## A Simple Way to Estimate Your Own Cost
You can get within ±25% of a real quote with this back-of-envelope method:
1. **List your modules.** Count them honestly.
2. **Multiply by ₹4L–₹9L per module** depending on complexity (simple CRUD = lower; workflow + integrations = higher).
3. **Add 20% for integrations** if you need GST, payments, or government APIs.
4. **Add 15% for multi-tenant or offline-resilient** architecture.
5. **Add the first year of AMC** (1520%) for a true year-one budget.
Example: a 6-module mid-market ERP with payment + GST integration → (6 × ₹6L) = ₹36L, +20% integrations = ₹43.2L, +18% AMC = roughly **₹51L for year one**. That lands squarely in the Growth tier, which is the reality check you want before any vendor conversation.
## Frequently Asked Questions
**What is the minimum realistic budget for a custom ERP in India in 2026?**
For a genuinely custom, production-grade single-department system, expect a floor of around **₹6,00,000**. Below that you are buying a configured off-the-shelf tool or a prototype, not a custom ERP.
**How long does a custom ERP take to build?**
A Starter system ships in 610 weeks. A Growth-tier multi-module ERP takes 47 months. Enterprise and government platforms run 824 months. Timeline scales with module count and integration depth, not just budget.
**Is custom ERP cheaper than SaaS over time?**
Often, yes — above roughly 75100 users or 2.5 years of use. A custom build has a high upfront cost but a flat recurring cost (AMC), while SaaS charges per seat forever. The more you grow, the better custom looks.
**Why do ERP quotes vary so much between vendors?**
Because "ERP" is undefined until you fix scope. A ₹15L and a ₹60L quote are usually answering two different questions about modules, roles, and integrations. Always compare on a fixed, written scope — never on the headline number alone.
**What ongoing costs should I plan for after launch?**
Budget 1520% of build cost per year for AMC, plus cloud hosting (₹15K–₹60K/month for mid-size systems), third-party API usage, and periodic enhancements. Total cost of ownership over five years is typically 1.72x the initial build.
**Should I pay fixed-price or time-and-materials?**
Fixed-price works when scope is locked and well-documented. Time-and-materials fits evolving requirements and longer builds. For most mid-market ERPs, a hybrid — fixed-price per milestone with a change-request process — gives the best balance of predictability and flexibility.
## Getting an Accurate Number for Your Project
The ranges above will get your budget into the right order of magnitude, but every project has specifics — a single complex integration or a strict uptime SLA can move the number significantly. At WorkRoot we have delivered ERP and management systems across Indian government and enterprise clients, and we are happy to turn your requirements into a fixed, written estimate. [Tell us about your project](/contact) and we will give you a real number, not a vague "it depends."
+7
View File
@@ -5,6 +5,13 @@ const blog = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
// Optional SEO overrides. When set they drive the <title> tag and
// <meta name="description"> without changing the on-page article
// heading/excerpt, letting us keep descriptive H1s while tuning the
// search snippet to the 5060 / 150160 character sweet spots.
// `seoTitle` is the text BEFORE the " | WorkRoot" brand suffix.
seoTitle: z.string().optional(),
seoDescription: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
+36 -108
View File
@@ -4,6 +4,7 @@
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import SEO from '../components/SEO.astro';
import Analytics from '../components/Analytics.astro';
import PWAInstallPrompt from '../components/PWAInstallPrompt.astro';
import WhatsAppFab from '../components/WhatsAppFab.astro';
@@ -26,8 +27,15 @@ const {
noIndex = false,
} = Astro.props;
// Full brand name — used for og:site_name and structured data.
const siteName = 'WorkRoot IT Solutions LLP';
const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
// Short brand suffix for the <title> tag so titles stay within the
// 5060 character SEO sweet spot (the full LLP name eats 25+ chars).
const brand = 'WorkRoot';
// The homepage gets a dedicated, query-focused title instead of the bare
// brand name. Pages pass `title="Home"` to opt into it.
const homeTitle = 'Custom Software & Government IT Development | WorkRoot';
const fullTitle = title === 'Home' ? homeTitle : `${title} | ${brand}`;
---
<!doctype html>
@@ -48,7 +56,9 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
<meta name="rating" content="general" />
<!-- Favicon -->
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- PWA Manifest -->
@@ -101,111 +111,14 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#0891b2" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f172a" />
<!-- JSON-LD Structured Data - Organization + LocalBusiness Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": ["Organization", "LocalBusiness"],
"name": "WorkRoot IT Solutions LLP",
"alternateName": ["WorkRoot", "WorkRoot IT Solutions"],
"url": "https://workroot.in",
"logo": {
"@type": "ImageObject",
"url": "https://workroot.in/logo.png",
"width": "512",
"height": "512"
},
"image": "https://workroot.in/og-image.jpg",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"foundingDate": "2022",
"founder": {
"@type": "Person",
"name": "Ajay Ghanwat"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
"addressLocality": "Satara",
"addressRegion": "MH",
"postalCode": "415503",
"addressCountry": "IN"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "17.7733632",
"longitude": "74.3309312"
},
"hasMap": "https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5",
"telephone": "+91-9561417403",
"email": "admin@workroot.in",
"contactPoint": [{
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "customer service",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "Worldwide"
}, {
"@type": "ContactPoint",
"telephone": "+91-9561417403",
"contactType": "sales",
"email": "admin@workroot.in",
"availableLanguage": ["English", "Hindi", "Marathi"],
"areaServed": "Worldwide"
}],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00",
"closes": "18:00"
}
],
"priceRange": "$$",
"currenciesAccepted": "INR",
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
"sameAs": [
"https://x.com/GhanwatAjay",
"https://www.linkedin.com/company/workroot",
"https://git.workroot.in/WorkRoot/",
"https://www.facebook.com/profile.php?id=61576503897897"
],
"areaServed": {
"@type": "GeoShape",
"name": "Worldwide"
},
"serviceType": ["Web Development", "Mobile App Development", "AI & Machine Learning", "Cloud Services"],
"knowsAbout": ["Web Development", "Software Engineering", "Cloud Computing", "Artificial Intelligence", "Machine Learning", "Mobile Development"],
"slogan": "Transforming Ideas Into Digital Reality",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5.0",
"reviewCount": "20",
"bestRating": "5",
"worstRating": "1"
}
})} />
<!-- JSON-LD Structured Data - WebSite Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "WorkRoot IT Solutions LLP",
"url": "https://workroot.in",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"publisher": {
"@type": "Organization",
"name": "WorkRoot IT Solutions LLP"
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://workroot.in/blog?search={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "en-US"
})} />
<!-- JSON-LD Structured Data — site-wide base schema (Organization +
LocalBusiness and WebSite). Centralised in SEO.astro and emitted here
in `base` mode so it appears exactly once on EVERY route, including
pages that don't add their own page-specific <SEO> (404, privacy,
terms, cookies, offline). Page-specific schema (Service, FAQ,
BlogPosting, breadcrumbs, …) is opt-in via the slotted <SEO> each
page renders into the head. -->
<SEO base />
<!-- Preconnect to Google Fonts (must come before font stylesheet) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -268,6 +181,17 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
<!-- Analytics (GA4 or Plausible) -->
<Analytics />
<!-- WRNexus Web Push SDK — loaded once globally so it runs on every page
that should collect push subscribers. is:inline keeps Astro's bundler
from rewriting the external src and preserves data-site-id / defer. -->
<script is:inline src="https://api.wrnexus.com/push/sdk.js" data-site-id="pk_rP51XN8tZnud5HdX9UqidjHk" defer></script>
<!-- WRNexus Bot Widget — loaded once globally here so the chat widget
renders on every route (pages + blog posts) via this shared layout.
is:inline keeps Astro's bundler from rewriting the external src and
preserves data-bot-id / async. -->
<script is:inline src="https://api.wrnexus.com/bots/widget.js" data-bot-id="d3998f1b-51d7-4f7c-a57e-3fa1af4394a0" async></script>
<!-- Additional head content slot -->
<slot name="head" />
</head>
@@ -292,8 +216,12 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
<!-- Footer -->
<Footer />
<!-- Spacer to ensure footer/content clears the mobile sticky CTA bar (<md only) -->
<div aria-hidden="true" class="md:hidden h-[calc(64px+env(safe-area-inset-bottom,0px))] w-full"></div>
<!-- Spacer to ensure footer/content (incl. PricingTimelineBand, GuaranteeBand,
and any other bottom-of-page sections) clears the mobile sticky CTA bar.
Bar min-height is 64px; we use 72px here so there is always an 8px
visual breathing strip between the last band and the sticky bar.
<md only — bar is hidden at md+. -->
<div aria-hidden="true" class="md:hidden h-[calc(72px+env(safe-area-inset-bottom,0px))] w-full"></div>
<!-- Mobile sticky bottom CTA bar (visible <md) -->
<MobileStickyCtaBar />
+38 -5
View File
@@ -61,6 +61,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
const method = context.request.method;
const path = context.url.pathname;
// Normalize paths corrupted by a trailing stray ")" before routing.
// These come from monitors/links that captured a URL together with a
// closing parenthesis — e.g. "https://workroot.in/)" copied out of the
// markdown/shell form "(https://workroot.in/)". Without this, every such
// probe hits a non-existent route and 404s on a loop (recurring "GET /)").
// Stripping the trailing paren(s) lets the request resolve to the intended
// route (e.g. "/)" -> "/"), so the monitor gets a 301 -> 200 instead.
const normalizedPath = path.replace(/\)+$/, '');
if (normalizedPath !== path && normalizedPath.length > 0) {
return context.redirect(`${normalizedPath}${context.url.search}`, 301);
}
// CSRF origin validation for API mutations
if (!validateCsrfOrigin(context.request, context.url)) {
logger.warn('middleware', 'CSRF origin check failed', {
@@ -106,7 +118,26 @@ export const onRequest = defineMiddleware(async (context, next) => {
if (status >= 500) {
logger.error('middleware', `SSR error ${status}`, { method, path, status, durationMs });
} else if (status === 404) {
logger.warn('middleware', `Not found`, { method, path, status, durationMs });
// 404s are dominated by bots/scanners probing malformed or garbage paths
// (e.g. "/)"), which would otherwise flood warn-level logs and trip alerts.
// Treat a 404 as a warning only when it likely originated from our own site
// (same-origin referer) — that indicates a real broken internal link worth
// fixing. Everything else (no referer / external referer) is logged at info.
const referer = context.request.headers.get('referer');
let internalNavigation = false;
if (referer) {
try {
internalNavigation = new URL(referer).hostname === context.url.hostname;
} catch {
// Malformed referer header → treat as external/untrusted
}
}
const notFoundMeta = { method, path, status, durationMs, referer: referer ?? undefined };
if (internalNavigation) {
logger.warn('middleware', `Not found (broken internal link)`, notFoundMeta);
} else {
logger.info('middleware', `Not found`, notFoundMeta);
}
} else if (status >= 400) {
logger.warn('middleware', `Client error ${status}`, { method, path, status, durationMs });
} else {
@@ -124,11 +155,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
// - inline scripts/styles: Required for Astro framework
// - https://www.googletagmanager.com & https://www.google-analytics.com: GA4
// - https://plausible.io: Plausible Analytics
// - https://api.wrnexus.com: WRNexus web-push SDK (sdk.js), bot widget
// (bots/widget.js) + runtime config/iframe served from the same origin
// Note: unsafe-eval is removed in production (only needed during Astro HMR dev mode)
const isProduction = import.meta.env.PROD;
const scriptSrc = isProduction
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io"
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io";
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io https://api.wrnexus.com"
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io https://api.wrnexus.com";
const csp = [
"default-src 'self'",
@@ -136,11 +169,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com https://maps.googleapis.com https://maps.gstatic.com",
"font-src 'self' data: https://fonts.gstatic.com",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io https://api.wrnexus.com",
"worker-src 'self'",
"manifest-src 'self'",
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
"frame-src https://www.google.com", // Allow Google Maps iframe embed
"frame-src https://www.google.com https://api.wrnexus.com", // Google Maps iframe + WRNexus bot widget iframe
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
+2 -2
View File
@@ -6,8 +6,8 @@ export const prerender = false;
---
<BaseLayout
title="Page Not Found"
description="The page you're looking for doesn't exist. Return to WorkRoot IT Solutions homepage."
title="Page Not Found — Let's Get You Back on Track"
description="The page you're looking for doesn't exist or has moved. Head back to the WorkRoot IT Solutions homepage, or explore our services, portfolio, and blog instead."
noIndex={true}
>
<section class="min-h-[70vh] flex items-center justify-center py-20 px-4">
+92 -100
View File
@@ -6,38 +6,52 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import SEO from '../components/SEO.astro';
// Team member data
// NOTE FOR MAINTAINERS: `photo` points to a placeholder avatar in
// /public/images/team/. Replace each file with a real headshot (same
// path, JPG/WebP at ~800×600) and fill in the real `linkedin` URL —
// the markup renders the photo and LinkedIn icon automatically.
const teamMembers = [
{
name: 'Ajay Ghanwat',
role: 'Director',
bio: 'A visionary technology leader with deep expertise in enterprise software architecture and government IT systems. Ajay founded WorkRoot in 2022 with a mission to deliver world-class software solutions from India. He has led the delivery of 20+ projects including large-scale government deployments across multiple districts in Bihar and enterprise platforms for IIT Patna.',
role: 'Director & Lead Engineer',
bio: 'Founder and hands-on technical lead. Ajay has architected and shipped 20+ production systems — including the Bihar Police Management System deployed across 11 districts and the University platform for IIT Patna. When you work with WorkRoot, he is in the room reviewing your architecture.',
avatar: 'AG',
photo: '/images/team/ajay-ghanwat.svg',
social: {
twitter: 'https://x.com/GhanwatAjay',
linkedin: '',
},
},
{
name: 'Ashok Jamdade',
role: 'Director',
bio: 'An experienced business strategist and operations leader who brings strong client relationship management and project governance expertise to WorkRoot. Ashok ensures every project meets the highest standards of quality and is delivered on time, driving the company\'s growth and operational excellence.',
role: 'Director & Delivery Lead',
bio: 'Runs project governance and client delivery end to end. Ashok is your single point of accountability — scope, timeline, and quality — so nothing gets lost in handoffs between teams.',
avatar: 'AJ',
social: {},
photo: '/images/team/ashok-jamdade.svg',
social: {
linkedin: '',
},
},
{
name: 'Swapnali Vachkal',
role: 'Full Stack Developer',
bio: 'A skilled full-stack developer proficient in Angular, React, Spring Boot, and modern web technologies. Swapnali plays a key role in building and maintaining WorkRoot\'s client projects, from government management systems to e-commerce platforms, delivering clean, performant, and maintainable code.',
role: 'Full Stack Engineer',
bio: 'Builds the systems you depend on, in Angular, React, and Spring Boot. Swapnali writes the production code for your project directly — from government management systems to e-commerce platforms clean, performant, and maintainable.',
avatar: 'SV',
social: {},
photo: '/images/team/swapnali-vachkal.svg',
social: {
linkedin: '',
},
},
];
// Company stats
// Company stats — canonical site-wide proof set (kept identical to the
// homepage ProofNumbers band, the /portfolio stats bar, and the homepage
// stats counters) so the numbers never contradict across pages.
const stats = [
{ value: '3+', label: 'Years of Experience' },
{ value: '20+', label: 'Projects Delivered' },
{ value: '3', label: 'Team Members' },
{ value: '100%', label: 'Client Satisfaction' },
{ value: '20+', label: 'Projects' },
{ value: '11+', label: 'Districts Deployed' },
{ value: '50,000+', label: 'Records Managed' },
{ value: '8+', label: 'Years Avg Experience' },
];
// Company timeline
@@ -74,34 +88,34 @@ const timeline = [
},
];
// Core values
const values = [
// How we work — concrete commitments, not generic values
const howWeWork = [
{
title: 'Innovation',
description: 'We embrace cutting-edge technologies to deliver future-proof solutions.',
icon: 'lightbulb',
title: 'You talk to the engineers',
description: 'No account managers, no offshore relay. The people who design and write your system are the people you meet, email, and call.',
icon: 'users',
},
{
title: 'Integrity',
description: 'Transparency and honesty are the foundation of every client relationship.',
title: 'Fixed scope, price, and timeline',
description: 'We agree on exactly what gets built, what it costs, and when it ships — before a line of code is written. No surprise invoices.',
icon: 'shield',
},
{
title: 'Excellence',
description: 'We pursue quality in every line of code and every interaction.',
title: 'Built to pass an audit',
description: 'Role-based access, data isolation, and offline resilience are designed in from day one — the same standards our 11-district government deployment runs on.',
icon: 'star',
},
{
title: 'Collaboration',
description: 'Great solutions emerge from diverse perspectives working together.',
icon: 'users',
title: 'We stay after launch',
description: 'Shipping is the start, not the finish. We maintain, monitor, and extend the systems we build — some have run in production for years.',
icon: 'lightbulb',
},
];
---
<BaseLayout
title="About Us — Our Story & Team"
description="WorkRoot IT Solutions LLP: founded 2022, 3 team members, 20+ projects delivered. Meet the technologists behind your digital transformation. Innovation, integrity, excellence."
title="About Our Software Engineering Team & Story"
description="Meet WorkRoot IT Solutions: founded 2022, a senior in-house engineering team averaging 8+ years' experience across 20+ web, mobile, ERP, and govtech projects."
>
<SEO
slot="head"
@@ -128,13 +142,13 @@ const values = [
About WorkRoot IT Solutions LLP
</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold text-white leading-tight mb-6">
Building the Future of
The team behind Bihar's 11-district Police Management System
<span class="block text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-400 mt-2">
Digital Innovation
and IIT Patna's University platform.
</span>
</h1>
<p class="text-lg sm:text-xl text-secondary-200 max-w-2xl mb-8">
We're a team of passionate technologists dedicated to transforming businesses through innovative IT solutions.
A small, senior engineering team in India that has shipped 20+ production systems for governments and enterprises. You work directly with the people who build yours.
</p>
<!-- Quick stats in hero -->
@@ -239,59 +253,8 @@ const values = [
</div>
</section>
<!-- Mission & Vision Section -->
<section class="py-16 lg:py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900">
<div class="container-wrapper">
<div class="text-center mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">Our Purpose</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2">Mission & Vision</h2>
</div>
<div class="grid lg:grid-cols-2 gap-8 lg:gap-12">
<!-- Mission -->
<div class="relative group" data-animate="fade-right" data-delay="100">
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700 rounded-3xl transform transition-transform group-hover:scale-105"></div>
<div class="relative p-8 lg:p-12 bg-gradient-to-br from-primary-500 to-primary-700 rounded-3xl text-white overflow-hidden">
<div class="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<div class="relative">
<div class="w-16 h-16 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 class="text-2xl lg:text-3xl font-bold mb-4">Our Mission</h3>
<p class="text-primary-50 leading-relaxed text-lg">
To empower businesses and government organizations with innovative, reliable, and scalable software solutions. We combine deep technical expertise with a client-first approach to deliver systems that drive operational efficiency and digital transformation.
</p>
</div>
</div>
</div>
<!-- Vision -->
<div class="relative group" data-animate="fade-left" data-delay="250">
<div class="absolute inset-0 bg-gradient-to-br from-secondary-800 to-secondary-900 rounded-3xl transform transition-transform group-hover:scale-105"></div>
<div class="relative p-8 lg:p-12 bg-gradient-to-br from-secondary-800 to-secondary-900 rounded-3xl text-white overflow-hidden">
<div class="absolute top-0 right-0 w-64 h-64 bg-primary-500/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<div class="relative">
<div class="w-16 h-16 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</div>
<h3 class="text-2xl lg:text-3xl font-bold mb-4">Our Vision</h3>
<p class="text-secondary-200 leading-relaxed text-lg">
To be India's most trusted technology partner for enterprises and government bodies, recognized for delivering world-class software solutions that set new standards in quality, performance, and innovation.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Core Values Section -->
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900 relative overflow-hidden">
<!-- How We Work Section -->
<section class="py-16 lg:py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900 relative overflow-hidden">
<!-- Background decoration -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute top-0 right-0 w-96 h-96 bg-primary-500/5 rounded-full blur-3xl"></div>
@@ -300,15 +263,15 @@ const values = [
<div class="container-wrapper relative">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">What Drives Us</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Our Core Values</h2>
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">How We Work</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">What working with us actually looks like</h2>
<p class="text-secondary-600 dark:text-secondary-400">
These principles guide every decision we make and every solution we deliver.
A three-person senior team is the feature, not the caveat — no layers between you and the people building your system.
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{values.map((value, index) => (
{howWeWork.map((value, index) => (
<div class="group relative" data-animate="fade-up" data-delay={String(index * 100)}>
<!-- Gradient background on hover -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
@@ -351,9 +314,9 @@ const values = [
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">The Team</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Meet Our Leadership</h2>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">The people who'll build your system</h2>
<p class="text-secondary-600 dark:text-secondary-400">
Talented professionals who bring diverse expertise and shared passion for technology excellence.
You work directly with the senior engineers who design and write your software — no account managers, no offshore handoffs, no junior teams learning on your project.
</p>
</div>
@@ -364,20 +327,47 @@ const values = [
<div class="absolute inset-0 bg-gradient-to-br from-primary-400 to-accent-400 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-sm"></div>
<div class="relative bg-white dark:bg-secondary-800 rounded-3xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 transform group-hover:-translate-y-2">
<!-- Avatar container -->
<!-- Avatar / photo container -->
<div class="relative aspect-[4/3] overflow-hidden bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<span class="text-6xl font-bold text-white/90">{member.avatar}</span>
{member.photo ? (
<img
src={member.photo}
alt={`${member.name}, ${member.role} at WorkRoot`}
width="800"
height="600"
class="absolute inset-0 w-full h-full object-cover"
loading="lazy"
decoding="async"
/>
) : (
<span class="text-6xl font-bold text-white/90">{member.avatar}</span>
)}
<!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-secondary-900/40 via-transparent to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-t from-secondary-900/50 via-transparent to-transparent"></div>
<!-- Social links overlay -->
{Object.keys(member.social).length > 0 && (
{(member.social.twitter || member.social.linkedin) && (
<div class="absolute inset-x-0 bottom-0 p-6 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<div class="flex justify-center gap-3">
{member.social.linkedin && (
<a
href={member.social.linkedin}
target="_blank"
rel="noopener noreferrer"
class="w-11 h-11 bg-white/95 hover:bg-white rounded-xl flex items-center justify-center text-secondary-700 hover:text-primary-500 transition-all shadow-lg hover:scale-110"
aria-label={`${member.name}'s LinkedIn profile`}
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"/>
</svg>
</a>
)}
{member.social.twitter && (
<a
href={member.social.twitter}
target="_blank"
rel="noopener noreferrer"
class="w-11 h-11 bg-white/95 hover:bg-white rounded-xl flex items-center justify-center text-secondary-700 hover:text-primary-500 transition-all shadow-lg hover:scale-110"
aria-label={`${member.name}'s Twitter/X profile`}
>
@@ -422,27 +412,29 @@ const values = [
Let's Build Together
</div>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
Ready to Work With Us?
Talk to the engineers who'll build it
</h2>
<p class="text-primary-50 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
Let's discuss how we can help transform your business with innovative technology solutions.
Tell us what you're building. You'll get a fixed-scope, fixed-price proposal from the people who'll actually deliver it — not a sales pitch.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/contact"
href="/contact?source=about-cta"
data-cta="about-primary"
class="group inline-flex items-center justify-center px-8 py-4 bg-white text-primary-600 font-semibold rounded-xl hover:bg-primary-50 transition-all shadow-xl hover:shadow-2xl hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
Get in Touch
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
Get my free proposal
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
<a
href="/services"
href="/portfolio"
data-cta="about-secondary"
class="inline-flex items-center justify-center px-8 py-4 border-2 border-white/40 text-white font-semibold rounded-xl hover:bg-white/10 hover:border-white/60 transition-all backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
View Our Services
See 20+ case studies
</a>
</div>
</div>
+246 -81
View File
@@ -46,6 +46,7 @@ const PHONE_REGEX = /^[\+]?[(]?[0-9]{1,3}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[
const VALID_SUBJECTS = [
'web-development',
'mobile-development',
'ui-ux',
'cloud-services',
'ai-ml',
'consulting',
@@ -109,53 +110,174 @@ function validateContactForm(data: ContactFormData): string | null {
}
// ============================================================
// SMTP transporter (created once at module level for reuse)
// SMTP transporter — env-driven, created once at module level
// ============================================================
//
// Required env vars:
// SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS
// SMTP_FROM, SMTP_FROM_NAME
// MAIL_ADMIN_TO (admin recipient; falls back to SMTP_FROM)
// Optional env vars (with sensible defaults):
// SMTP_REQUIRE_TLS ("false" disables STARTTLS upgrade; default: required)
// SMTP_CONNECTION_TIMEOUT_MS (default 10000)
// SMTP_GREETING_TIMEOUT_MS (default 10000)
// SMTP_SOCKET_TIMEOUT_MS (default 10000)
//
// Graceful degradation: if the nodemailer import fails, or any of the
// required env vars are missing, we log a structured warning and let
// the POST handler return success to the user. The site never 500s
// because of email config — submissions surface in server logs and
// Sentry instead.
// ============================================================
let smtpTransporter: any = null;
let smtpVerified = false;
// Verify SMTP connection once at module initialization
(async () => {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPass = process.env.SMTP_PASS;
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
interface SmtpConfig {
host: string;
port: number;
user: string;
pass: string;
fromAddress: string;
fromName: string;
adminRecipient: string;
requireTLS: boolean;
connectionTimeout: number;
greetingTimeout: number;
socketTimeout: number;
}
if (!smtpHost || !smtpUser || !smtpPass) {
logger.info('api.contact', 'SMTP not configured — skipping connection verification');
return;
function readSmtpConfig(): SmtpConfig | null {
const host = process.env.SMTP_HOST;
const user = process.env.SMTP_USER;
const pass = process.env.SMTP_PASS;
const fromAddress = process.env.SMTP_FROM || user || '';
const fromName = process.env.SMTP_FROM_NAME || 'CompanySite';
// Admin recipient resolution chain:
// MAIL_ADMIN_TO > SMTP_FROM > legacy CONTACT_EMAIL_TO > legacy CONTACT_EMAIL > SMTP_USER
const adminRecipient =
process.env.MAIL_ADMIN_TO ||
process.env.SMTP_FROM ||
process.env.CONTACT_EMAIL_TO ||
process.env.CONTACT_EMAIL ||
user ||
'';
if (!host || !user || !pass || !fromAddress || !adminRecipient) {
return null;
}
try {
return {
host,
port: Number(process.env.SMTP_PORT ?? 587),
user,
pass,
fromAddress,
fromName,
adminRecipient,
// 587 uses STARTTLS — `secure:false` + `requireTLS:true` is the
// correct combination. The flag lets ops disable TLS enforcement
// by setting SMTP_REQUIRE_TLS=false (e.g. local mailcatcher).
requireTLS: process.env.SMTP_REQUIRE_TLS !== 'false',
connectionTimeout: Number(process.env.SMTP_CONNECTION_TIMEOUT_MS ?? 10000),
greetingTimeout: Number(process.env.SMTP_GREETING_TIMEOUT_MS ?? 10000),
socketTimeout: Number(process.env.SMTP_SOCKET_TIMEOUT_MS ?? 10000),
};
}
let smtpTransporter: any = null;
let smtpTransporterPromise: Promise<any> | null = null;
async function getTransporter(cfg: SmtpConfig): Promise<any> {
if (smtpTransporter) return smtpTransporter;
if (smtpTransporterPromise) return smtpTransporterPromise;
smtpTransporterPromise = (async () => {
const nodemailer = await import('nodemailer');
smtpTransporter = nodemailer.default.createTransport({
host: smtpHost,
port: smtpPort,
secure: false, // Port 587 uses STARTTLS, not SSL
auth: { user: smtpUser, pass: smtpPass },
requireTLS: true, // Force STARTTLS upgrade
const transport = nodemailer.default.createTransport({
host: cfg.host,
port: cfg.port,
secure: false, // 587 uses STARTTLS, not implicit TLS
requireTLS: cfg.requireTLS,
auth: {
user: cfg.user,
pass: cfg.pass,
},
connectionTimeout: cfg.connectionTimeout,
greetingTimeout: cfg.greetingTimeout,
socketTimeout: cfg.socketTimeout,
tls: {
rejectUnauthorized: false, // Handle Hostinger certificate issues
// Hostinger occasionally serves an intermediate cert chain that
// node's default verifier rejects. Keep this off for parity with
// the prior working configuration.
rejectUnauthorized: false,
},
});
await smtpTransporter.verify();
smtpVerified = true;
console.log('SMTP connection verified successfully');
// NOTE: we deliberately do NOT log cfg.pass. Logs go to platform
// stdout and are user-visible in the Operations → Logs tab.
console.log('[api.contact] SMTP transport created', {
host: cfg.host,
port: cfg.port,
user: cfg.user,
from: `"${cfg.fromName}" <${cfg.fromAddress}>`,
adminRecipient: cfg.adminRecipient,
requireTLS: cfg.requireTLS,
timeouts: {
connection: cfg.connectionTimeout,
greeting: cfg.greetingTimeout,
socket: cfg.socketTimeout,
},
});
logger.info('api.contact', 'SMTP transport created', {
host: cfg.host,
port: cfg.port,
user: cfg.user,
requireTLS: cfg.requireTLS,
});
smtpTransporter = transport;
return transport;
})();
try {
return await smtpTransporterPromise;
} catch (err) {
// Reset so a subsequent request can retry instead of being permanently
// wedged on a transient nodemailer import or DNS failure.
smtpTransporterPromise = null;
throw err;
}
}
// Background verify — best-effort, never throws. Hits SMTP at boot so the
// first user submission doesn't pay the TCP+STARTTLS handshake latency.
(async () => {
const cfg = readSmtpConfig();
if (!cfg) {
logger.info('api.contact', 'SMTP not configured — skipping boot-time verification');
return;
}
try {
const transport = await getTransporter(cfg);
await transport.verify();
console.log('[api.contact] SMTP connection verified successfully', {
host: cfg.host,
port: cfg.port,
user: cfg.user,
});
logger.info('api.contact', 'SMTP connection verified successfully', {
host: smtpHost,
port: smtpPort,
user: smtpUser,
host: cfg.host,
port: cfg.port,
user: cfg.user,
});
} catch (err: any) {
smtpVerified = false;
console.error('SMTP connection verification failed:', {
console.error('[api.contact] SMTP connection verification failed', {
code: err?.code,
command: err?.command,
message: err?.message,
response: err?.response,
});
logger.error('api.contact', 'SMTP connection verification failed', {
code: err?.code,
command: err?.command,
message: err?.message,
response: err?.response,
});
@@ -165,16 +287,16 @@ let smtpVerified = false;
// ============================================================
// Email sending via SMTP (nodemailer) or no-op in development
// ============================================================
async function sendContactEmail(data: ContactFormData): Promise<void> {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPass = process.env.SMTP_PASS;
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
const smtpFrom = process.env.SMTP_FROM;
const recipientEmail = process.env.CONTACT_EMAIL_TO;
interface SendResult {
adminInfo?: { messageId?: string };
userConfirmInfo?: { messageId?: string };
}
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
// SMTP not configured - log and continue (graceful degradation)
async function sendContactEmail(data: ContactFormData): Promise<SendResult | void> {
const cfg = readSmtpConfig();
if (!cfg) {
// SMTP not configured — log and continue (graceful degradation)
logger.info('api.contact', 'SMTP not configured — form submission logged only', {
email: data.email,
subject: data.subject,
@@ -185,48 +307,49 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
return;
}
// Dynamic import to avoid bundling issues when nodemailer isn't installed
try {
// Reuse module-level transporter if available, otherwise create a new one
if (!smtpTransporter) {
const nodemailer = await import('nodemailer');
smtpTransporter = nodemailer.default.createTransport({
host: smtpHost,
port: smtpPort,
secure: false,
auth: { user: smtpUser, pass: smtpPass },
requireTLS: true,
tls: {
rejectUnauthorized: false,
},
});
}
const transport = await getTransporter(cfg);
const subjectLabels: Record<string, string> = {
'web-development': 'Web Development',
'mobile-development': 'Mobile Development',
'ui-ux': 'UI/UX Design',
'cloud-services': 'Cloud Services',
'ai-ml': 'AI & Machine Learning',
'consulting': 'IT Consulting',
'consulting': 'IT Consulting / DevOps',
'support': 'Technical Support',
'erp-solutions': 'ERP / Custom Software',
'government-systems': 'Government / GeM',
'erp-solutions': 'ERP / Enterprise Systems',
'government-systems': 'Government IT',
'other': 'Other',
};
const sourceLabel = data.source ? data.source : 'contact-page';
const fromAddress = smtpFrom || smtpUser;
// The "Tell us about your project" inline form on the homepage and the
// mobile sticky CTA tag themselves as project-inquiry sources. Use a
// distinct subject prefix so the admin inbox can be triaged easily.
const isProjectInquiry =
data.source === 'homepage-inline' ||
data.source === 'homepage-hero' ||
data.source === 'homepage-final-cta' ||
data.source === 'mobile-sticky';
const adminSubjectPrefix = isProjectInquiry ? '[Project Inquiry]' : '[Contact Form]';
const userSubjectLine = isProjectInquiry
? "We received your project inquiry - CompanySite"
: 'We received your message - CompanySite';
// 1) Admin notification email with BCC to SMTP_USER for delivery confirmation
const adminInfo = await smtpTransporter.sendMail({
from: `"CompanySite Contact Form" <${fromAddress}>`,
to: recipientEmail,
bcc: process.env.SMTP_USER, // BCC to admin inbox as delivery confirmation
const fromHeader = `"${cfg.fromName}" <${cfg.fromAddress}>`;
// 1) Admin notification email — delivered to MAIL_ADMIN_TO (or SMTP_FROM
// fallback). BCC to SMTP_USER as a soft delivery receipt.
const adminInfo = await transport.sendMail({
from: fromHeader,
to: cfg.adminRecipient,
bcc: cfg.user, // BCC to the SMTP mailbox as delivery confirmation
replyTo: data.email,
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
subject: `${adminSubjectPrefix} ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
text: [
`New contact form submission`,
isProjectInquiry ? 'New project inquiry submission' : 'New contact form submission',
``,
`Source: ${sourceLabel}`,
`Name: ${data.name}`,
@@ -242,7 +365,7 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
`Submitted at: ${new Date().toISOString()}`,
].join('\n'),
html: `
<h2>New Contact Form Submission</h2>
<h2>${isProjectInquiry ? 'New Project Inquiry' : 'New Contact Form Submission'}</h2>
<table style="border-collapse:collapse;width:100%;max-width:600px">
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Source</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(sourceLabel)}</td></tr>
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Name</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.name)}</td></tr>
@@ -256,15 +379,24 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
`,
});
// 2) User confirmation email — proves delivery works AND provides good UX
const userConfirmInfo = await smtpTransporter.sendMail({
from: `"CompanySite" <${fromAddress}>`,
console.log('[api.contact] Admin notification accepted by SMTP', {
messageId: adminInfo?.messageId,
recipient: cfg.adminRecipient,
source: sourceLabel,
});
// 2) User confirmation email (auto-reply) — preserves the existing UX
// pattern. Subject line adapts to whether this is a project inquiry.
const userConfirmInfo = await transport.sendMail({
from: fromHeader,
to: data.email,
subject: 'We received your message - CompanySite',
subject: userSubjectLine,
text: [
`Hi ${data.name},`,
``,
`Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
isProjectInquiry
? `Thank you for telling us about your project. We have received your inquiry regarding "${subjectLabels[data.subject] ?? data.subject}".`
: `Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
``,
`Our team will review your inquiry and get back to you as soon as possible, typically within 1-2 business days.`,
``,
@@ -279,9 +411,11 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
].join('\n'),
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px">
<h2 style="color:#333">Thank you for contacting us!</h2>
<h2 style="color:#333">${isProjectInquiry ? 'Thank you for telling us about your project!' : 'Thank you for contacting us!'}</h2>
<p>Hi ${escapeHtml(data.name)},</p>
<p>We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.</p>
<p>${isProjectInquiry
? `Thank you for telling us about your project. We have received your inquiry regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.`
: `We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.`}</p>
<p>Our team will review your inquiry and get back to you as soon as possible, typically within <strong>1-2 business days</strong>.</p>
<div style="background:#f9f9f9;border-left:4px solid #007bff;padding:12px 16px;margin:20px 0">
<p style="margin:4px 0"><strong>Subject:</strong> ${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</p>
@@ -295,9 +429,16 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
`,
});
console.log('[api.contact] User auto-reply accepted by SMTP', {
messageId: userConfirmInfo?.messageId,
recipient: data.email,
source: sourceLabel,
});
return { adminInfo, userConfirmInfo };
} catch (err) {
// If nodemailer isn't installed, treat as graceful degradation (log only)
} catch (err: any) {
// If nodemailer isn't installed, treat as graceful degradation (log only).
// The site keeps working; ops sees the warning and ships the dependency.
if (err instanceof Error && err.message.includes('Cannot find module')) {
logger.warn('api.contact', 'nodemailer not available — form submission logged only', {
email: data.email,
@@ -305,6 +446,17 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
});
return;
}
// Structured send-error log. Deliberately omits SMTP_PASS — only the
// SMTP server's response/code/command. Sentry captures the full error.
console.error('[api.contact] SMTP send failed', {
code: err?.code,
command: err?.command,
response: err?.response,
responseCode: err?.responseCode,
message: err?.message,
recipient: data.email,
adminRecipient: cfg.adminRecipient,
});
await captureException(err, {
scope: 'api.contact',
extra: { email: data.email, subject: data.subject },
@@ -454,19 +606,29 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
// Fire-and-forget: send email in background so the user gets an instant response.
// Errors are logged/reported but never block the HTTP reply.
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
console.log('Sending contact email to: ' + recipientEmail);
const adminRecipient =
process.env.MAIL_ADMIN_TO ||
process.env.SMTP_FROM ||
process.env.CONTACT_EMAIL_TO ||
process.env.CONTACT_EMAIL ||
process.env.SMTP_USER ||
'unknown';
console.log('[api.contact] Dispatching contact email', {
adminRecipient,
userRecipient: formData.email,
source: formData.source,
});
sendContactEmail(formData)
.then((result) => {
console.log('Contact emails delivered successfully:', {
adminRecipient: recipientEmail,
console.log('[api.contact] Contact emails delivered successfully', {
adminRecipient,
adminMessageId: result?.adminInfo?.messageId,
userRecipient: formData.email,
userMessageId: result?.userConfirmInfo?.messageId,
});
logger.info('api.contact', 'Contact emails delivered successfully', {
adminRecipient: recipientEmail,
adminRecipient,
adminMessageId: result?.adminInfo?.messageId,
userRecipient: formData.email,
userMessageId: result?.userConfirmInfo?.messageId,
@@ -476,17 +638,20 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
});
})
.catch(async (err: any) => {
console.error('Background email delivery failed:', {
// SMTP_PASS is NEVER logged — only protocol-level error fields.
console.error('[api.contact] Background email delivery failed', {
code: err?.code,
command: err?.command,
message: err?.message,
response: err?.response,
responseCode: err?.responseCode,
recipient: recipientEmail,
email: formData.email,
adminRecipient,
userRecipient: formData.email,
subject: formData.subject,
});
logger.error('api.contact', 'Background email delivery failed', {
code: err?.code,
command: err?.command,
message: err?.message,
response: err?.response,
responseCode: err?.responseCode,
+10 -4
View File
@@ -32,21 +32,27 @@ function formatDate(date: Date): string {
}
const readingTime = getReadingTime(post.body);
// SEO meta: prefer per-post overrides so the <title>/description land in the
// 5060 / 150160 character ranges, while the on-page heading below keeps the
// full, descriptive post title. `seoTitle` gets the " | WorkRoot" suffix from
// BaseLayout, so it should be the text before that suffix.
const metaTitle = post.data.seoTitle ?? post.data.title;
const metaDescription = post.data.seoDescription ?? post.data.description;
const shareUrl = encodeURIComponent(Astro.url.href);
const shareTitle = encodeURIComponent(post.data.title);
const shareDescription = encodeURIComponent(post.data.description);
---
<BaseLayout
title={post.data.title}
description={post.data.description}
title={metaTitle}
description={metaDescription}
ogImage={post.data.heroImage}
>
<SEO
slot="head"
type="BlogPosting"
title={post.data.title}
description={post.data.description}
title={metaTitle}
description={metaDescription}
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
+2 -2
View File
@@ -24,8 +24,8 @@ function formatDate(date: Date): string {
---
<BaseLayout
title="Blog"
description="Insights and articles on web development, AI, cloud computing, and digital transformation from the WorkRoot team."
title="Software Development & Technology Insights"
description="Insights on web development, ERP, cloud migration, AI, and government IT from the WorkRoot engineering team — field notes from real, large-scale deployments."
>
<SEO
slot="head"
+177 -124
View File
@@ -34,7 +34,7 @@ const contactInfo = [
{
icon: 'clock',
title: 'Business Hours',
details: ['Mon Fri: 10:00 AM 6:00 PM', 'Sat Sun: Closed'],
details: ['Mon Fri: 10:00 AM 6:00 PM', 'We reply within 24 business hours, Mon Fri.'],
link: null,
linkLabel: null,
color: 'violet',
@@ -80,11 +80,28 @@ const trustStats = [
{ value: '< 24h', label: 'Response Time' },
{ value: '3+', label: 'Years Experience' },
];
// Service options — rendered as large, tappable radio tiles (replacing the
// old native <select>) so the choice is thumb-friendly on mobile. `value`
// is the code submitted to /api/contact (unchanged from the previous
// <option> values); `label` is the display text.
const serviceOptions = [
{ value: 'erp-solutions', label: 'ERP / Enterprise Systems' },
{ value: 'government-systems', label: 'Government IT' },
{ value: 'web-development', label: 'Web Development' },
{ value: 'mobile-development', label: 'Mobile Development' },
{ value: 'ui-ux', label: 'UI/UX Design' },
{ value: 'consulting', label: 'IT Consulting / DevOps' },
{ value: 'other', label: 'Other / General Inquiry' },
];
// Budget options — also rendered as large tappable tiles.
const budgetOptions = ['< ₹1L', '₹1L ₹5L', '₹5L ₹15L', '₹15L+', 'Not sure yet'];
---
<BaseLayout
title="Contact Us — Get a Free Quote"
description="Start your project with WorkRoot IT Solutions LLP. 20+ projects delivered, 100% client satisfaction, response within 24 hours. Reach us at admin@workroot.in or call +91 9561417403."
title="Contact Us — Get a Free Project Quote Today"
description="Start your web, mobile, ERP, or government IT project with WorkRoot. 20+ projects delivered, replies within 1 business day. Email admin@workroot.in to begin."
>
<SEO
slot="head"
@@ -125,7 +142,7 @@ const trustStats = [
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-primary-500">Hear From You</span>
</h1>
<p class="text-xl text-secondary-300 max-w-2xl mx-auto reveal-on-scroll reveal-delay-2">
Whether you have a project in mind, a question about our services, or just want to explore the possibilities — our team is ready to help.
Tell us about your project — get a free 1-page proposal in 24 hours.
</p>
<!-- Trust stats strip -->
@@ -151,23 +168,42 @@ const trustStats = [
<div class="bg-white dark:bg-secondary-800 rounded-2xl shadow-card border border-secondary-100 dark:border-secondary-700 p-8 lg:p-10">
<!-- Form Header -->
<div class="mb-8">
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Send Us a Message</h2>
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Fill out the form below and we'll respond within 24 hours.</p>
<div class="flex flex-wrap items-center justify-between gap-3 mb-2">
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100">Send Us a Message</h2>
<!-- Response promise — shown ABOVE the form so the commitment is visible before users invest time -->
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 text-xs font-semibold whitespace-nowrap">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
&lt; 24h response
</span>
</div>
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Fill out the form below and we'll reply within 24 business hours, Mon Fri.</p>
</div>
<form id="contact-form" class="space-y-5" novalidate>
<!-- Honeypot field (anti-spam) - hidden from users -->
<div class="absolute -left-[9999px]" aria-hidden="true">
<label for="website">
Website
<input
type="text"
id="website"
name="website"
tabindex="-1"
autocomplete="off"
/>
</label>
<!--
Honeypot field (anti-spam).
Hidden from sighted users via off-screen positioning (NOT display:none /
visibility:hidden — some bots skip those entirely and never trip the trap).
The wrapper carries aria-hidden so screen-readers don't announce the label
or the input, and tabindex="-1" + autocomplete="off" keep it out of the
tab order and browser autofill. Bots that blindly fill every field will
set a value here, and /api/contact silently discards the submission.
-->
<div
class="absolute -left-[9999px] top-auto w-px h-px overflow-hidden"
aria-hidden="true"
>
<label for="website" class="sr-only">Website</label>
<input
type="text"
id="website"
name="website"
tabindex="-1"
autocomplete="off"
aria-hidden="true"
/>
</div>
<!-- Name + Email row -->
@@ -254,107 +290,84 @@ const trustStats = [
</div>
</div>
<!-- Phone + Subject row -->
<div class="grid sm:grid-cols-2 gap-5">
<!-- Phone Field -->
<div class="form-group">
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Phone Number
<span class="text-secondary-400 font-normal">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
aria-describedby="phone-error"
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
placeholder="+1 (555) 123-4567"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<!-- Phone Field (full width) -->
<div class="form-group">
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Phone Number
<span class="text-secondary-400 font-normal">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please enter a valid phone number.
</p>
</div>
<!-- Subject Field -->
<div class="form-group">
<label for="subject" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<select
id="subject"
name="subject"
required
aria-required="true"
aria-describedby="subject-error"
class="form-input w-full pl-10 pr-8 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 appearance-none cursor-pointer"
>
<option value="">Select a service...</option>
<option value="web-development">Web Development</option>
<option value="mobile-development">Mobile Development</option>
<option value="cloud-services">Cloud Services</option>
<option value="ai-ml">AI & Machine Learning</option>
<option value="consulting">IT Consulting</option>
<option value="support">Technical Support</option>
<option value="other">Other / General Inquiry</option>
</select>
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
aria-describedby="phone-error"
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
placeholder="+1 (555) 123-4567"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please select a service.
</p>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please enter a valid phone number.
</p>
</div>
<!-- Budget range (new field) -->
<!-- Service Needed — large tappable radio tiles (replaces the
old native <select>). Renders 2 columns on mobile (each tile
a comfortable thumb target) and 3 from sm up. The hidden
radios share name="subject" so the form submission and
analytics read the same value the <select> used to provide. -->
<div class="form-group">
<span id="subject-label" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</span>
<div
id="subject-group"
role="radiogroup"
aria-labelledby="subject-label"
aria-describedby="subject-error"
class="grid grid-cols-2 sm:grid-cols-3 gap-2.5"
>
{serviceOptions.map((opt) => (
<label class="subject-option group flex items-center justify-center text-center min-h-[52px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-700 dark:text-secondary-300 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
<input type="radio" name="subject" value={opt.value} class="sr-only" aria-describedby="subject-error" />
<span class="leading-snug">{opt.label}</span>
</label>
))}
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please select a service.
</p>
</div>
<!-- Project Budget — large tappable radio tiles. 2 cols on mobile
so each is a full thumb target; auto-fit row from sm up. -->
<div class="form-group">
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
Project Budget
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
</label>
<div class="flex flex-wrap gap-2" role="group" aria-label="Budget range options">
{['< $5K', '$5K $15K', '$15K $50K', '$50K+', 'Not sure yet'].map((range, i) => (
<label class="budget-option flex items-center gap-1.5 px-3 py-2 rounded-lg border border-secondary-200 dark:border-secondary-600 text-sm text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2.5" role="group" aria-label="Budget range options">
{budgetOptions.map((range) => (
<label class="budget-option flex items-center justify-center text-center min-h-[48px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
<input type="radio" name="budget" value={range} class="sr-only" />
{range}
</label>
@@ -369,7 +382,7 @@ const trustStats = [
Your Message <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 2000</span>
</div>
<div class="relative">
<textarea
@@ -377,8 +390,8 @@ const trustStats = [
name="message"
required
minlength="10"
maxlength="5000"
rows="5"
maxlength="2000"
rows="4"
aria-required="true"
aria-describedby="message-error message-hint"
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
@@ -682,14 +695,14 @@ const trustStats = [
Let's Build Something <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-300">Remarkable</span> Together
</h2>
<p class="text-secondary-400 mb-8 max-w-xl mx-auto">
Join 200+ companies who've transformed their digital presence with WorkRoot.
Tell us about your project and we'll send back a free, no-obligation scope and timeline within one business day.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="#contact-form"
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-primary text-white font-semibold rounded-xl hover:bg-primary-600 transition-colors duration-200 shadow-primary-glow focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-secondary-900"
>
Start a Project
Get my free proposal
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
@@ -787,6 +800,13 @@ const trustStats = [
color: #0e7490;
}
/* Service tiles: invalid-group highlight (shown only after a failed submit
attempt with no service chosen). Subtle red ring around the whole group. */
.subject-group-invalid {
border-radius: 0.75rem;
box-shadow: 0 0 0 2px rgb(220 38 38 / 0.35);
}
/* FAQ item */
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::marker { display: none; }
@@ -885,12 +905,12 @@ const trustStats = [
if (messageField && charCount) {
messageField.addEventListener('input', () => {
const len = messageField.value.length;
charCount.textContent = `${len} / 5000`;
charCount.className = len > 4500 ? 'text-xs text-red-500' : 'text-xs text-secondary-400';
charCount.textContent = `${len} / 2000`;
charCount.className = len > 1800 ? 'text-xs text-red-500' : 'text-xs text-secondary-400';
});
}
// ── Budget toggle (radio-as-chip) ──────────────────────────
// ── Budget toggle (radio-as-tile) ──────────────────────────
const budgetOptions = document.querySelectorAll('.budget-option');
budgetOptions.forEach((opt) => {
const radio = opt.querySelector('input[type="radio"]') as HTMLInputElement;
@@ -902,6 +922,31 @@ const trustStats = [
}
});
// ── Service tiles (radio group replacing the old native <select>) ──
// The hidden radios share name="subject" so `form.elements.subject` resolves
// to a RadioNodeList whose `.value` is the checked option — exactly what the
// submit handler and analytics previously read from the <select>.
const subjectErrorEl = document.querySelector('[data-error="subject"]');
const subjectGroupEl = document.getElementById('subject-group');
const subjectRadios = Array.from(
document.querySelectorAll<HTMLInputElement>('input[name="subject"]')
);
function getSubjectValue(): string {
return subjectRadios.find((r) => r.checked)?.value ?? '';
}
function validateSubject(touched = true): boolean {
const valid = getSubjectValue() !== '';
if (subjectErrorEl) subjectErrorEl.classList.toggle('hidden', valid || !touched);
subjectGroupEl?.classList.toggle('subject-group-invalid', !valid && touched);
return valid;
}
subjectRadios.forEach((radio) => {
radio.addEventListener('change', () => validateSubject());
});
// ── Form Validation ────────────────────────────────────────
const form = document.getElementById('contact-form') as HTMLFormElement;
const successState = document.getElementById('success-state');
@@ -993,18 +1038,25 @@ const trustStats = [
return;
}
// Validate all fields
// Validate all fields. `subject` is now a radio group, so it is checked
// separately from the text/textarea fields handled by validateField.
let isValid = true;
['name', 'email', 'phone', 'subject', 'message'].forEach((fieldName) => {
['name', 'email', 'phone', 'message'].forEach((fieldName) => {
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
if (field && !validateField(field)) isValid = false;
});
if (!validateSubject()) isValid = false;
if (!isValid) {
showToast('Please fix the highlighted fields before submitting.', 'error');
// Focus first invalid field
// Focus the first invalid text field, or the service tiles if that's
// the only thing missing.
const firstInvalid = form.querySelector('.is-invalid') as HTMLElement | null;
firstInvalid?.focus();
if (firstInvalid) {
firstInvalid.focus();
} else if (getSubjectValue() === '') {
subjectRadios[0]?.focus();
}
return;
}
@@ -1038,7 +1090,7 @@ const trustStats = [
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
phone: (form.elements.namedItem('phone') as HTMLInputElement)?.value.trim(),
subject: (form.elements.namedItem('subject') as HTMLSelectElement)?.value,
subject: getSubjectValue(),
message: (form.elements.namedItem('message') as HTMLTextAreaElement)?.value.trim(),
website: honeypot,
}),
@@ -1068,7 +1120,7 @@ const trustStats = [
}
// Track analytics
const subject = (form.elements.namedItem('subject') as HTMLSelectElement)?.value;
const subject = getSubjectValue();
trackFormSubmit('contact', success);
if (success) trackEvent('contact_form_submit', { subject_category: subject });
@@ -1101,8 +1153,9 @@ const trustStats = [
form.querySelectorAll('.valid-check').forEach((el) => el.classList.add('hidden'));
form.querySelectorAll('.invalid-check').forEach((el) => el.classList.add('hidden'));
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
if (charCount) charCount.textContent = '0 / 5000';
if (charCount) charCount.textContent = '0 / 2000';
budgetOptions.forEach((o) => o.classList.remove('selected'));
subjectGroupEl?.classList.remove('subject-group-invalid');
});
}
}
+3 -3
View File
@@ -8,8 +8,8 @@ const lastUpdated = 'March 20, 2026';
---
<BaseLayout
title="Cookie Policy"
description="WorkRoot IT Solutions Cookie Policy - Learn how we use cookies and similar tracking technologies on our website."
title="Cookie Policy — How We Use Cookies & Tracking"
description="Learn how WorkRoot IT Solutions uses cookies and similar tracking technologies, what each cookie does, and how to manage or disable them in your browser."
noIndex={true}
>
<!-- Hero Section -->
@@ -299,7 +299,7 @@ const lastUpdated = 'March 20, 2026';
<div class="bg-secondary-900/50 border border-secondary-800 rounded-xl p-6">
<p class="text-secondary-300"><strong class="text-white">WorkRoot IT Solutions LLP</strong></p>
<p class="text-secondary-300">Email: admin@workroot.in</p>
<p class="text-secondary-300">Phone: +91 9561417403</p>
<p class="text-secondary-300">Phone: <a href="tel:+919561417403" class="text-primary-400 hover:underline">+91 9561417403</a></p>
<p class="text-secondary-300">Address: At Post Rajapur Shantinagar, Taluka Khatav, Dist Satara, MH 415503</p>
</div>
<p class="text-secondary-300 mt-4">

Some files were not shown because too many files have changed in this diff Show More