Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ This repo uses **release-please**. Since GitHub squash-merges use the PR title a
- **Deployment**: Cloudflare Pages (consistent with rest of org)
- **Domain**: `wavekat.com` — DNS to be pointed at Cloudflare Pages once site is ready

## SEO & GEO — every new page must be both

This site is optimized for classic search (SEO) **and** generative answer engines (GEO — being quoted by ChatGPT, Perplexity, Google AI Overviews, Claude). The two overlap but aren't identical: SEO wants crawlable, well-described, linkable pages; GEO wants self-contained, factual, extractable passages an LLM can lift verbatim. Build for both on every page.

### What's already wired (don't reinvent it)

- **`Base.astro` is the SEO baseline** (established in #81, `feat(seo)`). It emits the canonical URL, `<meta name="description">`, the `robots` directive (with a `noindex` prop to opt a page out of indexing), the full Open Graph + Twitter card set, light/dark `theme-color`, and the sitewide `Organization` + `WebSite` JSON-LD graph (`@id`s `https://wavekat.com/#organization` and `#website`). It also takes `ogType` / `article` props so child layouts can emit article semantics. Every layout (`Voice`, `Post`, `Docs`) wraps `Base`, so every page gets this for free.
- **Sitemap + robots are automatic.** `@astrojs/sitemap` generates `sitemap-index.xml` at build from the `site` in `astro.config.mjs`; `public/robots.txt` allows all crawlers (including AI crawlers — keep it open for GEO) and points at the sitemap. A new page under `src/pages/` is in the sitemap with zero extra work.
- **`og.png`** is generated 1200×630 from the brand SVG by `make sync`. Pages share it unless they pass their own `ogImage`.

### Rules for every new page

1. **Always pass a real `title` and `description`** to the layout — they drive `<title>`, the meta description, OG/Twitter, and (in `Base`) the canonical. Description: one sentence, ~150–160 chars, leads with the concrete value, names the product. Never ship the layout default.
2. **Add page-type JSON-LD** as a `<script type="application/ld+json" set:html={JSON.stringify(...)} />` at the end of the page. Pick the type that fits and reference the org by `@id` (`{ '@id': 'https://wavekat.com/#organization' }`) rather than re-declaring it:
- Product/app page → `SoftwareApplication` (see `voice/index.astro`).
- Any page with a Q&A section → `FAQPage` built from the same array you render, so the on-page text and the schema never drift.
- Comparison / detail pages reached through a hierarchy → `BreadcrumbList`.
- Hub/listing pages → `ItemList` of the child URLs.
- Blog posts → `BlogPosting` (handled by `Post.astro`).
3. **Trailing-slash, absolute internal URLs** (`/voice/alternatives/linphone/`). The sub-nav active-state and the build both assume them.
4. **One `<h1>` per page**, then a descriptive `<h2>` per section. Headings are ranking *and* extraction signals — write them as the thing the section answers, not as labels.

### GEO: write so an LLM can quote you

- **Lead with a self-contained answer.** The first sentence under a heading should stand alone if lifted out of context — name the subject, state the fact. Don't make the reader (or the model) assemble the answer from three paragraphs.
- **Q&A blocks earn their keep twice** — they render as a human FAQ *and* feed `FAQPage` schema *and* are the single most-quoted structure in AI answers. Phrase questions the way a user would type them ("Can WaveKat Voice connect to the same SIP provider as Linphone?"), and answer in 1–3 plain sentences.
- **Comparison tables are extractable gold.** Keep cells short, factual, and parallel across rows; models lift table rows almost verbatim into "X vs Y" answers.
- **Be specific and honest.** Concrete specifics (platforms, versions, prices, "records every call automatically") get quoted; vague superlatives get skipped. On comparison pages, name what the competitor is genuinely good at — fair framing reads as a trustworthy source to both readers and models, and avoids the "marketing fluff" discount.
- **Keep entity naming consistent.** Always "WaveKat Voice" (not "the app", "Voice", "WaveKat" interchangeably) so engines bind the facts to one entity. Same for platform claims — match the truth in `voice/index.astro` (Mac and Linux today; Windows when there's demand).
- **Don't target a single platform in copy.** Voice is Mac + Linux today; write "your computer" / "desktop" in body copy and put the platforms in a table row or a "Mac & Linux" qualifier. Titles may still include "Mac" to catch the high-volume "… for Mac" queries, but never *exclude* Linux.

When you add a page that doesn't fit the patterns above, mirror the closest existing one (`voice/alternatives/[slug].astro` is the current best example: clear `<h1>`, self-contained intro, comparison table, fair "what it is", Q&A, and `FAQPage` + `BreadcrumbList` schema).

## Brand assets

Logo SVGs come from `vendor/wavekat-brand` (git submodule — source of truth, never edit here).
Expand Down
9 changes: 8 additions & 1 deletion src/layouts/Voice.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ const { title, description } = Astro.props;
const subNav = [
{ href: '/voice/', label: 'overview' },
{ href: '/voice/use-cases/', label: 'use cases' },
{ href: '/voice/alternatives/', label: 'alternatives' },
{ href: '/voice/download/', label: 'download' },
{ href: '/voice/talk/', label: 'talk to us' },
];

const currentPath = Astro.url.pathname.replace(/\/$/, '') + '/';

// A nav item is active on its own page and on any page beneath it (e.g. the
// per-competitor /voice/alternatives/<slug>/ pages keep "alternatives" lit).
// The overview href is everyone's prefix, so it only matches exactly.
const isActive = (href: string) =>
currentPath === href || (href !== '/voice/' && currentPath.startsWith(href));
---

<Base title={`${title} — WaveKat`} description={description}>
Expand All @@ -28,7 +35,7 @@ const currentPath = Astro.url.pathname.replace(/\/$/, '') + '/';
<!-- Sub-nav -->
<nav class="flex flex-wrap items-center gap-3 mb-12">
{subNav.map((item) => {
const active = currentPath === item.href;
const active = isActive(item.href);
return (
<a
href={item.href}
Expand Down
145 changes: 145 additions & 0 deletions src/lib/voice-alternatives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Data for the /voice/alternatives/ comparison pages.
//
// Each entry becomes its own page (/voice/alternatives/<slug>/) targeting the
// "<name> alternative for Mac" search intent, plus a card on the hub. Adding a
// competitor is just appending an entry here — the hub and the [slug] route
// pick it up automatically. Keep the framing honest: name what the competitor
// is genuinely good at, then say plainly where WaveKat Voice is different. A
// comparison page that reads as a fair guide ranks and converts better than one
// that reads as a takedown.

export interface ComparisonRow {
/** What capability this row is about — kept in phone-user language. */
label: string;
/** How WaveKat Voice handles it. */
wavekat: string;
/** How the competitor handles it — fair, not disparaging. */
them: string;
}

export interface AltFaq {
q: string;
a: string;
}

export interface Alternative {
/** URL slug — /voice/alternatives/<slug>/. */
slug: string;
/** Competitor's product name, as people search for it. */
name: string;
/** One-line card summary on the hub. */
tagline: string;
/** <title> / meta description for the page. */
seoTitle: string;
seoDescription: string;
/** Hero copy. */
heading: string;
intro: string;
/** Honest "what it is" — what the competitor is genuinely good at. */
whatItIs: { summary: string; strengths: string[] };
/** Side-by-side capability table. */
comparison: ComparisonRow[];
/** "Choose <them> if…" — kept fair so the page reads as a guide. */
chooseThem: string[];
/** "Choose WaveKat Voice if…" */
chooseWavekat: string[];
/** 2–3 Q&A for the on-page FAQ + FAQPage structured data. */
faqs: AltFaq[];
}

export const alternatives: Alternative[] = [
{
slug: 'linphone',
name: 'Linphone',
tagline:
'The free, open-source SIP client. Capable and cross-platform — WaveKat Voice trades breadth for a focused desktop business phone that records and writes down every call.',
seoTitle: 'WaveKat Voice — a Linphone alternative for Mac & Linux',
seoDescription:
'How WaveKat Voice compares to Linphone on Mac and Linux: a focused business phone that records and transcribes every call automatically, with guided provider setup. Free public beta.',
heading: 'A Linphone alternative for Mac & Linux',
intro:
'Linphone is a capable, free SIP client that runs almost everywhere. If what you actually want is a desktop business phone that records and writes down every call — and sets up without hand-editing SIP fields — here is how the two compare on Mac and Linux.',
whatItIs: {
summary:
'Linphone is a long-running, open-source SIP softphone from Belledonne Communications. It runs on Mac, Windows, Linux, iOS, and Android, and covers a lot of ground — voice and video calls, instant messaging, and end-to-end encryption — for free.',
strengths: [
'Free and open source, with no account required to use it',
'Runs on virtually every platform, including mobile',
'Voice, video, and chat in one client',
'End-to-end encryption (ZRTP/SRTP) for the technically inclined',
],
},
comparison: [
{
label: 'Records every call',
wavekat: 'Automatic — every call recorded and saved the moment you hang up.',
them: 'Manual recording per call; not a saved, browsable history by default.',
},
{
label: 'Written transcript',
wavekat: 'Live transcript alongside the call, kept with the recording.',
them: 'No transcription.',
},
{
label: 'Searchable call history',
wavekat: 'Every call lands in one history with its recording and transcript.',
them: 'Call log only — no recordings or transcripts attached.',
},
{
label: 'Setting up your number',
wavekat: 'Pick your provider from a list and the settings are filled in for you.',
them: 'General-purpose SIP fields you configure yourself.',
},
{
label: 'Where your data lives',
wavekat: 'On your computer by default; optional sign-in to sync to the web.',
them: 'On your device — it is a client; nothing is hosted for you.',
},
{
label: 'Platforms',
wavekat: 'Mac and Linux today (Windows when there is demand).',
them: 'Mac, Windows, Linux, iOS, Android.',
},
{
label: 'Video & chat',
wavekat: 'Focused on calls — no video or messaging.',
them: 'Voice, video, and instant messaging.',
},
{
label: 'Price',
wavekat: 'Free during the public beta; paid later.',
them: 'Free and open source.',
},
],
chooseThem: [
'You want a free, open-source client with the source available',
'You need the same app across Mac, Windows, Linux, and mobile',
'You want video calls and chat in the same place as voice',
'You are comfortable configuring SIP settings yourself',
],
chooseWavekat: [
'You want every call recorded and written down automatically, with nothing to switch on',
'You want one searchable history of calls, recordings, and transcripts',
'You would rather pick your provider from a list than fill in SIP fields',
'You want a focused desktop business phone, not a general-purpose VoIP toolkit',
],
faqs: [
{
q: 'Can WaveKat Voice connect to the same SIP provider as Linphone?',
a: 'Yes. Both are SIP softphones, so any provider that works with Linphone works with WaveKat Voice. The difference is setup: WaveKat Voice fills in the settings for common providers like Twilio, Telnyx, and 2talk, and lets you enter the details yourself for anything else.',
},
{
q: 'Does Linphone record and transcribe calls like WaveKat Voice?',
a: 'Linphone can record an individual call when you start it manually, but it does not transcribe calls or keep a browsable history of recordings and transcripts. WaveKat Voice records every call automatically, writes a live transcript alongside it, and saves both to your call history without any setup.',
},
{
q: 'Is WaveKat Voice open source like Linphone?',
a: 'No — WaveKat Voice is a commercial product, free during the public beta. Several of the building blocks underneath it are open source on our GitHub, but the Voice app itself is not.',
},
],
},
];

export function getAlternative(slug: string): Alternative | undefined {
return alternatives.find((a) => a.slug === slug);
}
Loading
Loading