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
1 change: 1 addition & 0 deletions scripts/sync-brand.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const logos = [
"wavekat-tight-light.svg",
"wavekat-tight-dark.svg",
"wavekat-icon-light.svg",
"wavekat-icon-dark.svg",
];

// Initialise submodule if vendor directory is empty (Cloudflare Pages shallow clone)
Expand Down
19 changes: 19 additions & 0 deletions src/components/VoiceDownload.astro
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,22 @@ const primaryCls =
document.addEventListener('DOMContentLoaded', resolve);
})();
</script>

<script is:inline>
// Native <details> only toggles via its <summary>, so the "other platforms"
// dropdown stays open when you click elsewhere. Close any open one on an
// outside click or the Escape key, the way a menu is expected to behave.
(function () {
document.addEventListener('click', function (e) {
document.querySelectorAll('[data-dl-more][open]').forEach(function (d) {
if (!d.contains(e.target)) d.removeAttribute('open');
});
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
document.querySelectorAll('[data-dl-more][open]').forEach(function (d) {
d.removeAttribute('open');
});
});
})();
</script>
62 changes: 61 additions & 1 deletion src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,58 @@ interface Props {
title?: string;
description?: string;
ogImage?: string;
ogImageAlt?: string;
ogType?: 'website' | 'article';
noindex?: boolean;
article?: {
publishedTime?: Date;
modifiedTime?: Date;
author?: string;
tags?: string[];
};
}

const {
title = 'WaveKat — Open-Source AI Voice Tools for Small Business',
description = 'WaveKat builds open-source, AI-powered tools that give small businesses enterprise-grade voice capabilities — phone answering, conversations, and 24/7 presence.',
ogImage = '/og.png',
ogImageAlt = 'WaveKat — open-source AI voice tools for small business',
ogType = 'website',
noindex = false,
article,
} = Astro.props;

const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const ogImageURL = new URL(ogImage, Astro.site);

// Sitewide Organization + WebSite structured data. Emitted on every page so
// search engines can build a consistent entity for WaveKat (logo, social
// profiles) and surface the site name in results.
const orgSchema = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': 'https://wavekat.com/#organization',
name: 'WaveKat',
url: 'https://wavekat.com',
logo: 'https://wavekat.com/og.png',
description:
'WaveKat builds open-source, AI-powered voice tools that give small businesses enterprise-grade phone capabilities.',
sameAs: [
'https://github.com/wavekat',
'https://www.linkedin.com/company/wavekat/',
],
},
{
'@type': 'WebSite',
'@id': 'https://wavekat.com/#website',
name: 'WaveKat',
url: 'https://wavekat.com',
publisher: { '@id': 'https://wavekat.com/#organization' },
},
],
};
---

<!doctype html>
Expand All @@ -24,6 +66,9 @@ const ogImageURL = new URL(ogImage, Astro.site);
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<meta name="robots" content={noindex ? 'noindex, nofollow' : 'index, follow'} />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0a0e14" media="(prefers-color-scheme: dark)" />
<link rel="canonical" href={canonicalURL} />
<link rel="icon" type="image/svg+xml" href="/logos/wavekat-icon-light.svg" />
<link rel="icon" type="image/svg+xml" href="/logos/wavekat-icon-light.svg" media="(prefers-color-scheme: light)" />
Expand All @@ -32,19 +77,34 @@ const ogImageURL = new URL(ogImage, Astro.site);
<link rel="alternate" type="application/rss+xml" title="WaveKat Blog" href="/rss.xml" />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content="WaveKat" />
<meta property="og:locale" content="en_US" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImageURL} />
<meta property="og:image:alt" content={ogImageAlt} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
{article?.publishedTime && (
<meta property="article:published_time" content={article.publishedTime.toISOString()} />
)}
{article?.modifiedTime && (
<meta property="article:modified_time" content={article.modifiedTime.toISOString()} />
)}
{article?.author && <meta property="article:author" content={article.author} />}
{article?.tags?.map((tag) => <meta property="article:tag" content={tag} />)}

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageURL} />
<meta name="twitter:image:alt" content={ogImageAlt} />

<!-- Organization + WebSite structured data (sitewide) -->
<script type="application/ld+json" set:html={JSON.stringify(orgSchema)} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet" />
Expand Down
23 changes: 20 additions & 3 deletions src/layouts/Post.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ const { title, description, date, updated, author, tags = [], ogImage } = Astro.

const formatDate = (d: Date) =>
d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });

const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---

<Base title={`${title} — WaveKat`} description={description} ogImage={ogImage}>
<Base
title={`${title} — WaveKat`}
description={description}
ogImage={ogImage}
ogImageAlt={title}
ogType="article"
article={{ publishedTime: date, modifiedTime: updated, author: author || 'WaveKat', tags }}
>
<div class="max-w-3xl mx-auto px-6 py-12">

<Header active="blog" />
Expand Down Expand Up @@ -80,11 +89,15 @@ const formatDate = (d: Date) =>
type="application/ld+json"
set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
'@type': 'BlogPosting',
headline: title,
description,
mainEntityOfPage: { '@type': 'WebPage', '@id': canonicalURL.href },
url: canonicalURL.href,
...(ogImage && { image: new URL(ogImage, Astro.site).href }),
...(tags.length && { keywords: tags.join(', ') }),
datePublished: date.toISOString(),
...(updated && { dateModified: updated.toISOString() }),
dateModified: (updated || date).toISOString(),
author: {
'@type': 'Organization',
name: author || 'WaveKat',
Expand All @@ -94,6 +107,10 @@ const formatDate = (d: Date) =>
'@type': 'Organization',
name: 'WaveKat',
url: 'https://wavekat.com',
logo: {
'@type': 'ImageObject',
url: 'https://wavekat.com/og.png',
},
},
})}
/>
Expand Down
35 changes: 35 additions & 0 deletions src/pages/voice/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,38 @@ const faqs = [
a: 'It\'s a lightweight menu-bar app that stays out of your way until a call comes in, and it updates itself quietly in the background. You install it once and forget it\'s there.',
},
];

// Structured data: the app itself (eligible for software rich results) and the
// on-page FAQ. Strip apostrophe-escapes from the FAQ copy is unnecessary — the
// strings are already plain text.
const appSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'WaveKat Voice',
applicationCategory: 'BusinessApplication',
operatingSystem: 'macOS, Linux',
url: 'https://wavekat.com/voice/',
softwareVersion: macDownload.version,
downloadUrl: 'https://wavekat.com/voice/download/',
description:
'A desktop app that turns your computer into your business phone — answer and place calls, with every call recorded and transcribed. Works with the phone provider you already have.',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
publisher: { '@id': 'https://wavekat.com/#organization' },
};

const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((f) => ({
'@type': 'Question',
name: f.q,
acceptedAnswer: { '@type': 'Answer', text: f.a },
})),
};
---

<Voice
Expand Down Expand Up @@ -247,4 +279,7 @@ const faqs = [
</section>

<TalkCTA />

<script type="application/ld+json" set:html={JSON.stringify(appSchema)} />
<script type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
</Voice>
Loading