wacrm/src/lib/seo/structured-data.ts
Arnas Donauskas c2587f7466
feat(seo): full SEO pass — metadata, structured data, sitemap, robots
Brings the site from bare-bones metadata to production-ready SEO.
Covers technical crawlability, rich-result eligibility, social
sharing, and duplicate-content prevention.

Added:
- src/lib/seo/site-config.ts — single source of truth for base URL,
  brand, description, keywords. Referenced by every consumer so a
  domain / rename is a one-file edit.
- src/lib/seo/faq-data.ts — FAQ Q&A extracted from the component so
  the UI and the FAQPage JSON-LD render the same content.
- src/lib/seo/structured-data.ts — builders for WebSite,
  Organization, SoftwareApplication, and FAQPage JSON-LD nodes.
  FAQPage is eligible for Google's FAQ rich result; SoftwareApplication
  with a free Offer can earn a "Free" badge on SERP.
- src/components/seo/json-ld.tsx — emits one <script
  type="application/ld+json"> per node (Google's preferred shape).
- src/app/opengraph-image.tsx — Next auto-generates a 1200×630 PNG
  for the landing page's OG / Twitter previews.
- src/app/sitemap.ts — lists only public, indexable pages.
- src/app/robots.ts — Allow /, Disallow every app / auth route,
  references the sitemap.

Updated:
- src/app/layout.tsx — metadataBase, title template (%s — WaCRM),
  default OpenGraph + Twitter Cards (inherited everywhere), canonical,
  icons config, viewport (themeColor + colorScheme=dark for browser
  chrome on mobile).
- src/app/page.tsx — injects the 4 JSON-LD blocks, sets explicit
  canonical for /, overrides the title.
- src/app/(auth)/layout.tsx — noindex+nofollow for login / signup /
  forgot-password (no SEO value; would compete with landing page).
- src/app/(dashboard)/layout.tsx — split into thin server layout
  (exports noindex metadata) + DashboardShell client component. App
  routes won't be indexed even if a URL leaks.
- src/components/landing/faq.tsx — consumes the shared FAQ data
  module (no more drift between UI and JSON-LD).

Verified end-to-end against the running dev server:
- GET / HTML contains: canonical, description, keywords, robots,
  og:title/url/image/type, twitter:card/title, theme-color, + 4
  JSON-LD blocks (WebSite, Organization, SoftwareApplication,
  FAQPage).
- GET /opengraph-image → 200 OK, content-type image/png.
- GET /login HTML contains <meta name="robots" content="noindex,
  nofollow, nocache">.
- GET /sitemap.xml serves valid XML with /.
- GET /robots.txt serves Allow /, Disallow all app + auth paths,
  sitemap reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:17:48 +03:00

111 lines
2.9 KiB
TypeScript

import {
absoluteUrl,
ORG_INFO,
SITE_DESCRIPTION,
SITE_NAME,
SITE_URL,
} from './site-config'
import { FAQ_ITEMS } from './faq-data'
/**
* Builders for the JSON-LD blobs we inject on the landing page. Each
* returns a plain object — the consumer wraps it in <script
* type="application/ld+json">. Keeping them typed as `Record<string,
* unknown>` so schema.org shapes don't leak into the rest of the app.
*/
type Ld = Record<string, unknown>
/** Generic site-level node. Helps search engines map the domain. */
export function websiteLd(): Ld {
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_NAME,
url: SITE_URL,
description: SITE_DESCRIPTION,
inLanguage: 'en',
}
}
/** Publisher / brand. Paired with the WebSite node. */
export function organizationLd(): Ld {
return {
'@context': 'https://schema.org',
'@type': 'Organization',
name: ORG_INFO.name,
legalName: ORG_INFO.legalName,
url: ORG_INFO.url,
logo: ORG_INFO.logo,
}
}
/**
* Core product node. SoftwareApplication is the recommended type for
* web-based SaaS in Google's Rich Results guidelines. The free-tier
* `offers` block enables a "Free" badge in SERPs where eligible.
*/
export function softwareApplicationLd(): Ld {
return {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: SITE_NAME,
description: SITE_DESCRIPTION,
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
url: SITE_URL,
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
featureList: [
'Shared WhatsApp inbox',
'Contact management with tags and custom fields',
'Sales pipelines with kanban board',
'Broadcast campaigns via approved WhatsApp templates',
'No-code workflow automations',
'Real-time analytics dashboard',
],
}
}
/**
* FAQPage — eligible for the FAQ rich result on Google.
* We build this from the same source the UI renders, so answers
* in SERP match answers on the page.
*/
export function faqPageLd(): Ld {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: FAQ_ITEMS.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: {
'@type': 'Answer',
text: item.a,
},
})),
}
}
/**
* Convenience: an array of the nodes we want on the landing page.
* The consumer component iterates and emits one <script> per node
* (Google prefers one schema per script tag).
*/
export function landingPageLd(): Ld[] {
return [
websiteLd(),
organizationLd(),
softwareApplicationLd(),
faqPageLd(),
]
}
// Kept exported for discoverability; unused by runtime code but handy
// for anyone building additional pages that need an absolute URL in
// their JSON-LD.
export { absoluteUrl }