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
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a21aa99e90
commit
22251ea82e
@@ -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 →
|
||||
</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>
|
||||
Reference in New Issue
Block a user