+
+
+ W
+
+
+ {SITE_NAME}
+
+
+
+
+ Run your WhatsApp business from{' '}
+ one inbox.
+
+
+
+ Shared inbox, sales pipelines, broadcasts, and no-code automations —
+ built on the official WhatsApp Business API.
+
+
+
+ ✓ Shared inbox
+ ✓ Automations
+ ✓ Pipelines
+ ✓ Analytics
+
+
+ ),
+ size,
+ )
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4e4e79b..4de18f9 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -12,11 +12,22 @@ import { InboxMock } from '@/components/landing/mock/inbox-mock'
import { PipelineMock } from '@/components/landing/mock/pipeline-mock'
import { AutomationMock } from '@/components/landing/mock/automation-mock'
import { AnalyticsMock } from '@/components/landing/mock/analytics-mock'
+import { JsonLd } from '@/components/seo/json-ld'
+import { landingPageLd } from '@/lib/seo/structured-data'
+import { SITE_DESCRIPTION, SITE_NAME, SITE_TAGLINE } from '@/lib/seo/site-config'
+// Landing-specific metadata. Most fields are inherited from the root
+// layout's metadata — we override title (so the hero copy shows in
+// SERPs / tab titles) and set an explicit canonical for "/" to avoid
+// trailing-slash duplicate-content signals.
export const metadata: Metadata = {
- title: 'WaCRM — Run your WhatsApp business from one inbox',
- description:
- 'Shared inbox, contact hub, sales pipelines, broadcasts, and no-code automations for WhatsApp. Built on the official WhatsApp Business API.',
+ title: {
+ absolute: `${SITE_NAME} — ${SITE_TAGLINE}`,
+ },
+ description: SITE_DESCRIPTION,
+ alternates: {
+ canonical: '/',
+ },
}
// Marketing landing. Visible at / for everyone (auth redirect that
@@ -25,6 +36,9 @@ export const metadata: Metadata = {
export default function LandingPage() {
return (
+ {/* JSON-LD — WebSite, Organization, SoftwareApplication, FAQPage.
+ Emitted before any visible content so crawlers hit it first. */}
+
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..ad1e62e
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,35 @@
+import type { MetadataRoute } from 'next'
+import { SITE_URL } from '@/lib/seo/site-config'
+
+/**
+ * robots.txt served at /robots.txt. We explicitly disallow every
+ * gated area so crawlers don't spend their budget on pages they'll
+ * bounce off. Auth pages are closed off too — no SEO value, and
+ * we don't want "Sign in to WaCRM" competing with the landing page
+ * in SERPs.
+ */
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: [
+ {
+ userAgent: '*',
+ allow: '/',
+ disallow: [
+ '/api/',
+ '/dashboard',
+ '/inbox',
+ '/contacts',
+ '/pipelines',
+ '/broadcasts',
+ '/automations',
+ '/settings',
+ '/login',
+ '/signup',
+ '/forgot-password',
+ ],
+ },
+ ],
+ sitemap: `${SITE_URL}/sitemap.xml`,
+ host: SITE_URL,
+ }
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..b387557
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,20 @@
+import type { MetadataRoute } from 'next'
+import { SITE_URL } from '@/lib/seo/site-config'
+
+/**
+ * Sitemap served at /sitemap.xml. Only lists public, indexable pages
+ * — auth pages are explicitly noindex, and every route under /app is
+ * gated. The landing anchors (#features, #how-it-works, #faq) don't
+ * belong here; fragments aren't a sitemap concept.
+ */
+export default function sitemap(): MetadataRoute.Sitemap {
+ const now = new Date()
+ return [
+ {
+ url: `${SITE_URL}/`,
+ lastModified: now,
+ changeFrequency: 'weekly',
+ priority: 1,
+ },
+ ]
+}
diff --git a/src/components/landing/faq.tsx b/src/components/landing/faq.tsx
index c58ca00..e964783 100644
--- a/src/components/landing/faq.tsx
+++ b/src/components/landing/faq.tsx
@@ -3,38 +3,9 @@
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { Section, SectionHeader } from './section'
+import { FAQ_ITEMS } from '@/lib/seo/faq-data'
import { cn } from '@/lib/utils'
-// Answering real objections a small-business owner has before signing
-// up. Order matters — setup friction is usually the #1 concern, cost
-// #2, data ownership #3.
-const QA = [
- {
- q: 'Do I need my own WhatsApp Business API access?',
- a: "Yes. WaCRM plugs into your existing Meta WhatsApp Business setup — you bring the phone number and access token, we provide the CRM tooling around it. Any Meta-approved BSP (Business Solution Provider) works.",
- },
- {
- q: 'Can my whole team share one WhatsApp number?',
- a: 'Yes. Assign conversations to specific agents, track who is responding to what, and hand off threads without losing context. All your agents work from a single shared inbox.',
- },
- {
- q: 'How fast is setup?',
- a: 'Most teams are live in under 30 minutes once their WhatsApp Business number has been approved by Meta. Paste your credentials in Settings, import contacts if you have them, and start replying.',
- },
- {
- q: 'Who owns the data?',
- a: 'You do. Everything lives in your own Supabase project — contacts, conversations, deals, automation logs. Export it anytime; there is no lock-in on the data layer.',
- },
- {
- q: 'Can I send bulk messages and automated replies?',
- a: 'Yes. Broadcasts send Meta-approved templates to segmented contact lists with delivery tracking. Automations run no-code flows triggered by new contacts, keywords, tag changes, and more.',
- },
- {
- q: 'What about message templates?',
- a: 'Templates you create in Meta are synced automatically. Use them from the inbox, broadcasts, or inside an automation step.',
- },
-]
-
export function FAQ() {
const [openIdx, setOpenIdx] = useState(0)
return (
@@ -46,7 +17,7 @@ export function FAQ() {
/>
- {QA.map((item, i) => {
+ {FAQ_ITEMS.map((item, i) => {
const isOpen = openIdx === i
return (
diff --git a/src/components/seo/json-ld.tsx b/src/components/seo/json-ld.tsx
new file mode 100644
index 0000000..a544b0f
--- /dev/null
+++ b/src/components/seo/json-ld.tsx
@@ -0,0 +1,22 @@
+/**
+ * Emits one