feat(web): overhaul SEO structure and intent landing pages (#210)
* feat(web): overhaul SEO structure and landing pages Add distro-specific install pages and an engine comparison hub to capture high-intent organic queries. Centralize metadata and structured data handling to improve crawl consistency, internal linking, and rich-result eligibility. * feat(web): add subpage navigation and visual polish Make SEO landing pages easier to discover and navigate by adding persistent subpage header/footer links to home and key routes. Refresh install and comparison pages with colorful icon-driven sections while expanding homepage footer links for stronger internal linking. * fix(ci): make web version sync robust to schema location CI assumed softwareVersion lived in layout.tsx, but SEO refactors moved SoftwareApplication JSON-LD to page.tsx. Update workflow sync steps to patch whichever file currently contains softwareVersion so website build and release jobs keep working. * chore(web): remove SEO audit artifact Drop the internal SEO audit markdown from the PR so only production website and workflow changes are included.
This commit is contained in:
@@ -154,16 +154,24 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py)
|
||||
echo "Syncing version: $VERSION"
|
||||
# Update softwareVersion in layout.tsx
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/layout.tsx
|
||||
# Update softwareVersion where JSON-LD currently lives
|
||||
if grep -q '"softwareVersion": "' web/src/app/page.tsx; then
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/page.tsx
|
||||
echo "Updated page.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/page.tsx
|
||||
elif grep -q '"softwareVersion": "' web/src/app/layout.tsx; then
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/layout.tsx
|
||||
echo "Updated layout.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/layout.tsx
|
||||
else
|
||||
echo "::warning::No softwareVersion field found in page.tsx or layout.tsx; skipping sync"
|
||||
fi
|
||||
# Update package.json version (remove -alpha/beta/rc suffix for npm)
|
||||
NPM_VERSION=${VERSION%-*}
|
||||
NPM_VERSION=${NPM_VERSION%-beta*}
|
||||
NPM_VERSION=${NPM_VERSION%-rc*}
|
||||
sed -i 's/"version": "[^"]*"/"version": "'"$NPM_VERSION"'"/' web/package.json
|
||||
# Verify the changes
|
||||
echo "Updated layout.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/layout.tsx
|
||||
echo "Updated package.json version:"
|
||||
grep -oP '"version":\s*"\K[^"]+' web/package.json
|
||||
|
||||
|
||||
@@ -194,14 +194,22 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py)
|
||||
echo "Syncing version: $VERSION"
|
||||
# Update softwareVersion in layout.tsx
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/layout.tsx
|
||||
# Update softwareVersion where JSON-LD currently lives
|
||||
if grep -q '"softwareVersion": "' web/src/app/page.tsx; then
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/page.tsx
|
||||
echo "Updated page.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/page.tsx
|
||||
elif grep -q '"softwareVersion": "' web/src/app/layout.tsx; then
|
||||
sed -i 's/"softwareVersion": "[^"]*"/"softwareVersion": "'"$VERSION"'"/' web/src/app/layout.tsx
|
||||
echo "Updated layout.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/layout.tsx
|
||||
else
|
||||
echo "::warning::No softwareVersion field found in page.tsx or layout.tsx; skipping sync"
|
||||
fi
|
||||
# Update package.json version (remove -alpha suffix for npm)
|
||||
NPM_VERSION=${VERSION%-*}
|
||||
sed -i 's/"version": "[^"]*"/"version": "'"$NPM_VERSION"'"/' web/package.json
|
||||
# Verify the changes
|
||||
echo "Updated layout.tsx version:"
|
||||
grep -oP '"softwareVersion":\s*"\K[^"]+' web/src/app/layout.tsx
|
||||
echo "Updated package.json version:"
|
||||
grep -oP '"version":\s*"\K[^"]+' web/package.json
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /api/
|
||||
Disallow: /_next/
|
||||
Disallow: /out/
|
||||
Disallow: /*?*
|
||||
|
||||
# Sitemap location
|
||||
Sitemap: https://vocalinux.com/sitemap.xml
|
||||
Host: vocalinux.com
|
||||
|
||||
+32
-2
@@ -2,8 +2,38 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://vocalinux.com/</loc>
|
||||
<lastmod>2026-02-05</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/</loc>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/ubuntu/</loc>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/fedora/</loc>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/arch/</loc>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/compare/</loc>
|
||||
<lastmod>2026-02-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
describe("SEO Assets", () => {
|
||||
const robotsPath = path.join(process.cwd(), "public", "robots.txt");
|
||||
const sitemapPath = path.join(process.cwd(), "public", "sitemap.xml");
|
||||
|
||||
let robotsContent: string;
|
||||
let sitemapContent: string;
|
||||
|
||||
beforeAll(() => {
|
||||
robotsContent = fs.readFileSync(robotsPath, "utf-8");
|
||||
sitemapContent = fs.readFileSync(sitemapPath, "utf-8");
|
||||
});
|
||||
|
||||
it("contains sitemap reference in robots.txt", () => {
|
||||
expect(robotsContent).toContain("Sitemap: https://vocalinux.com/sitemap.xml");
|
||||
});
|
||||
|
||||
it("blocks crawler access to technical paths", () => {
|
||||
expect(robotsContent).toContain("Disallow: /api/");
|
||||
expect(robotsContent).toContain("Disallow: /_next/");
|
||||
expect(robotsContent).toContain("Disallow: /out/");
|
||||
});
|
||||
|
||||
it("includes core landing pages in sitemap", () => {
|
||||
const expectedUrls = [
|
||||
"https://vocalinux.com/",
|
||||
"https://vocalinux.com/install/",
|
||||
"https://vocalinux.com/install/ubuntu/",
|
||||
"https://vocalinux.com/install/fedora/",
|
||||
"https://vocalinux.com/install/arch/",
|
||||
"https://vocalinux.com/compare/",
|
||||
];
|
||||
|
||||
expectedUrls.forEach((url) => {
|
||||
expect(sitemapContent).toContain(`<loc>${url}</loc>`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { CheckCircle2, Cpu, Sparkles, Zap } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const engineTable = [
|
||||
{
|
||||
engine: "whisper.cpp",
|
||||
speed: "Fastest startup + low latency",
|
||||
hardware: "CPU + AMD/Intel/NVIDIA GPU",
|
||||
accuracy: "High (best overall balance)",
|
||||
footprint: "Small models available (~39MB tiny)",
|
||||
bestFor: "Most users who want strong speed + quality",
|
||||
icon: Zap,
|
||||
iconColor: "text-amber-500",
|
||||
iconBg: "bg-amber-500/10",
|
||||
},
|
||||
{
|
||||
engine: "Whisper (OpenAI)",
|
||||
speed: "Slower install and startup",
|
||||
hardware: "CPU or NVIDIA CUDA",
|
||||
accuracy: "High",
|
||||
footprint: "Large dependency footprint (~2.3GB)",
|
||||
bestFor: "Users already standardized on PyTorch stack",
|
||||
icon: Sparkles,
|
||||
iconColor: "text-violet-500",
|
||||
iconBg: "bg-violet-500/10",
|
||||
},
|
||||
{
|
||||
engine: "VOSK",
|
||||
speed: "Very fast realtime on low-end systems",
|
||||
hardware: "CPU",
|
||||
accuracy: "Good for lightweight use",
|
||||
footprint: "Very lightweight (~40MB model)",
|
||||
bestFor: "Older hardware and minimal-resource environments",
|
||||
icon: Cpu,
|
||||
iconColor: "text-cyan-500",
|
||||
iconBg: "bg-cyan-500/10",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Whisper.cpp vs Whisper vs VOSK for Linux Voice Dictation",
|
||||
description:
|
||||
"Compare whisper.cpp, Whisper, and VOSK for Linux speech-to-text. See speed, hardware requirements, model size, and which engine is best for your workflow.",
|
||||
path: "/compare",
|
||||
keywords: [
|
||||
"whisper.cpp vs vosk",
|
||||
"linux speech recognition comparison",
|
||||
"best voice dictation engine linux",
|
||||
"whisper vs vosk linux",
|
||||
],
|
||||
});
|
||||
|
||||
export default function CompareEnginesPage() {
|
||||
const articleJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: "Whisper.cpp vs Whisper vs VOSK for Linux Voice Dictation",
|
||||
description:
|
||||
"Technical comparison of speech recognition engines for offline Linux voice typing.",
|
||||
dateModified: "2026-02-12",
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: "Jatin K Malik",
|
||||
url: "https://github.com/jatinkrmalik",
|
||||
},
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "Vocalinux",
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: absoluteUrl("/vocalinux.png"),
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: absoluteUrl("/compare"),
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Speech Engine Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
whisper.cpp vs Whisper vs VOSK on Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
If you are choosing a Linux speech-to-text engine, this page gives a practical side-by-side
|
||||
comparison focused on latency, hardware support, install footprint, and real desktop usage.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="overflow-x-auto rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<table className="w-full min-w-[860px] border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Engine</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Speed</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Hardware</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Accuracy</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Footprint</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Best for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{engineTable.map((row) => {
|
||||
const Icon = row.icon;
|
||||
return (
|
||||
<tr
|
||||
key={row.engine}
|
||||
className="border-b border-zinc-100 align-top dark:border-zinc-700/70"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className={`inline-flex rounded-md p-1.5 ${row.iconBg}`}>
|
||||
<Icon className={`h-4 w-4 ${row.iconColor}`} />
|
||||
</span>
|
||||
{row.engine}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.speed}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.hardware}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.accuracy}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.footprint}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.bestFor}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-3">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick whisper.cpp</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose whisper.cpp when you want the best speed-to-accuracy ratio and broad hardware support.
|
||||
It is the default in Vocalinux for a reason.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick Whisper</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose OpenAI Whisper if your environment already depends on PyTorch/CUDA workflows and you
|
||||
prefer that runtime profile.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick VOSK</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose VOSK on older laptops, low-RAM systems, or lightweight VMs where small model size and
|
||||
minimal overhead matter most.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Next steps</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
Install by distro:
|
||||
<Link href="/install/ubuntu/" className="font-semibold text-primary hover:underline">
|
||||
Ubuntu
|
||||
</Link>
|
||||
,
|
||||
<Link href="/install/fedora/" className="font-semibold text-primary hover:underline">
|
||||
Fedora
|
||||
</Link>
|
||||
,
|
||||
<Link href="/install/arch/" className="font-semibold text-primary hover:underline">
|
||||
Arch Linux
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
Use interactive install to detect your hardware and pick the best engine defaults.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
After install, tune model size for your preferred latency and accuracy level.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Laptop,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { absoluteUrl, buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
type DistroSlug = "ubuntu" | "fedora" | "arch";
|
||||
type DistroParams = { distro: string } | Promise<{ distro: string }>;
|
||||
|
||||
interface DistroConfig {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
intro: string;
|
||||
prerequisites: string[];
|
||||
testedOn: string[];
|
||||
installCommand: string;
|
||||
setupNotes: string[];
|
||||
postInstallChecks: string[];
|
||||
}
|
||||
|
||||
const distroConfig: Record<DistroSlug, DistroConfig> = {
|
||||
ubuntu: {
|
||||
name: "Ubuntu",
|
||||
title: "Install Voice Dictation on Ubuntu",
|
||||
description:
|
||||
"Step-by-step Ubuntu voice dictation setup for offline speech-to-text with whisper.cpp and VOSK. Works on Ubuntu 22.04+, GNOME, KDE, X11, and Wayland.",
|
||||
intro:
|
||||
"This Ubuntu guide is tuned for practical desktop use: terminals, browsers, IDEs, office suites, and chat apps. Vocalinux runs locally so your voice data never leaves your machine.",
|
||||
prerequisites: [
|
||||
"Ubuntu 22.04+ (or Ubuntu-based distro)",
|
||||
"Python 3.8+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo apt update && sudo apt install -y curl)",
|
||||
],
|
||||
testedOn: [
|
||||
"Ubuntu 24.04 LTS (GNOME, Wayland)",
|
||||
"Ubuntu 22.04 LTS (GNOME, X11)",
|
||||
"Kubuntu and Linux Mint derivatives",
|
||||
],
|
||||
installCommand:
|
||||
"curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
|
||||
setupNotes: [
|
||||
"Choose whisper.cpp for the fastest setup and cross-vendor GPU support.",
|
||||
"On lower-RAM systems, pick VOSK for lightweight realtime dictation.",
|
||||
"If your desktop uses Wayland, keep keyboard injection defaults unless you know your compositor specifics.",
|
||||
],
|
||||
postInstallChecks: [
|
||||
"Run vocalinux and confirm the tray indicator appears.",
|
||||
"Double-tap Ctrl in a text field and verify speech-to-text starts.",
|
||||
"Set language and model size in the settings panel for your workload.",
|
||||
],
|
||||
},
|
||||
fedora: {
|
||||
name: "Fedora",
|
||||
title: "Install Voice Dictation on Fedora",
|
||||
description:
|
||||
"Fedora speech-to-text installation guide for Vocalinux. Set up offline Linux voice typing on Fedora Workstation with Wayland or X11.",
|
||||
intro:
|
||||
"Fedora is an excellent match for Vocalinux thanks to current Python stacks and up-to-date system libraries. This guide focuses on reliable setup and minimal friction.",
|
||||
prerequisites: [
|
||||
"Fedora Workstation 39+ (or Fedora-based distro)",
|
||||
"Python 3.8+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo dnf install -y curl)",
|
||||
],
|
||||
testedOn: [
|
||||
"Fedora Workstation 41 (Wayland)",
|
||||
"Fedora Workstation 40 (X11 fallback)",
|
||||
"Fedora KDE spin",
|
||||
],
|
||||
installCommand:
|
||||
"curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
|
||||
setupNotes: [
|
||||
"If prompted for optional dependencies, accept defaults unless your desktop setup requires custom input tooling.",
|
||||
"Keep the default whisper.cpp engine for best speed-to-accuracy tradeoff.",
|
||||
"Use the smallest model first, then scale up after confirming stable dictation latency.",
|
||||
],
|
||||
postInstallChecks: [
|
||||
"Run vocalinux from terminal and test transcription in GNOME Text Editor or VS Code.",
|
||||
"Verify push-to-dictate behavior with your preferred keyboard layout.",
|
||||
"Confirm startup behavior and language settings persist after reboot.",
|
||||
],
|
||||
},
|
||||
arch: {
|
||||
name: "Arch Linux",
|
||||
title: "Install Voice Dictation on Arch Linux",
|
||||
description:
|
||||
"Arch Linux voice dictation guide for offline speech recognition. Install Vocalinux on Arch, Manjaro, and EndeavourOS with distro-aware setup notes.",
|
||||
intro:
|
||||
"Arch users usually care about control and performance. This guide keeps setup lean while preserving compatibility across rolling-release desktop environments.",
|
||||
prerequisites: [
|
||||
"Arch Linux / Manjaro / EndeavourOS",
|
||||
"Python 3.8+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo pacman -Sy --needed curl)",
|
||||
],
|
||||
testedOn: [
|
||||
"Arch Linux (GNOME, Wayland)",
|
||||
"Arch Linux (KDE Plasma)",
|
||||
"Manjaro (X11 and Wayland sessions)",
|
||||
],
|
||||
installCommand:
|
||||
"curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive",
|
||||
setupNotes: [
|
||||
"Prefer whisper.cpp for lower dependency footprint and faster cold start.",
|
||||
"On custom compositor setups, verify text injection behavior in both terminal and browser contexts.",
|
||||
"If you tweak keybinds heavily, set a non-conflicting activation shortcut in Vocalinux settings.",
|
||||
],
|
||||
postInstallChecks: [
|
||||
"Run vocalinux and test dictation in multiple apps (terminal, browser, editor).",
|
||||
"Confirm your chosen speech engine model balances latency and accuracy.",
|
||||
"Back up ~/.config/vocalinux/config.yaml after tuning preferences.",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const distroKeywords: Record<DistroSlug, string[]> = {
|
||||
ubuntu: [
|
||||
"ubuntu voice dictation",
|
||||
"speech to text ubuntu",
|
||||
"ubuntu voice typing",
|
||||
"offline dictation ubuntu",
|
||||
],
|
||||
fedora: [
|
||||
"fedora voice dictation",
|
||||
"speech to text fedora",
|
||||
"fedora voice typing",
|
||||
"offline dictation fedora",
|
||||
],
|
||||
arch: [
|
||||
"arch linux voice dictation",
|
||||
"speech to text arch linux",
|
||||
"manjaro dictation",
|
||||
"offline dictation arch",
|
||||
],
|
||||
};
|
||||
|
||||
const isDistroSlug = (value: string): value is DistroSlug => {
|
||||
return value in distroConfig;
|
||||
};
|
||||
|
||||
const resolveParams = async (params: DistroParams): Promise<{ distro: string }> => {
|
||||
return Promise.resolve(params);
|
||||
};
|
||||
|
||||
const getDistroStyles = (distro: DistroSlug) => {
|
||||
if (distro === "ubuntu") {
|
||||
return {
|
||||
icon: Laptop,
|
||||
iconClass: "text-orange-500",
|
||||
chipClass: "border-orange-500/30 bg-orange-500/10 text-orange-600 dark:text-orange-400",
|
||||
panelClass: "from-orange-500/15 to-amber-500/10",
|
||||
};
|
||||
}
|
||||
|
||||
if (distro === "fedora") {
|
||||
return {
|
||||
icon: Sparkles,
|
||||
iconClass: "text-blue-500",
|
||||
chipClass: "border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
||||
panelClass: "from-blue-500/15 to-cyan-500/10",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: Cpu,
|
||||
iconClass: "text-emerald-500",
|
||||
chipClass: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||
panelClass: "from-emerald-500/15 to-teal-500/10",
|
||||
};
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(distroConfig).map((distro) => ({ distro }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: DistroParams;
|
||||
}): Promise<Metadata> {
|
||||
const { distro } = await resolveParams(params);
|
||||
|
||||
if (!isDistroSlug(distro)) {
|
||||
return buildPageMetadata({
|
||||
title: "Linux Voice Dictation Install Guide",
|
||||
description:
|
||||
"Install Vocalinux on Linux with distro-specific setup instructions for Ubuntu, Fedora, and Arch.",
|
||||
path: "/install",
|
||||
});
|
||||
}
|
||||
|
||||
const config = distroConfig[distro];
|
||||
|
||||
return buildPageMetadata({
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
path: `/install/${distro}`,
|
||||
keywords: distroKeywords[distro],
|
||||
});
|
||||
}
|
||||
|
||||
export default async function DistroInstallPage({
|
||||
params,
|
||||
}: {
|
||||
params: DistroParams;
|
||||
}) {
|
||||
const { distro } = await resolveParams(params);
|
||||
|
||||
if (!isDistroSlug(distro)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const config = distroConfig[distro];
|
||||
const styles = getDistroStyles(distro);
|
||||
const DistroIcon = styles.icon;
|
||||
|
||||
const howToJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
name: `${config.title} in 3 Steps`,
|
||||
description: config.description,
|
||||
inLanguage: "en-US",
|
||||
totalTime: "PT10M",
|
||||
supply: [
|
||||
{
|
||||
"@type": "HowToSupply",
|
||||
name: "Linux computer",
|
||||
},
|
||||
{
|
||||
"@type": "HowToSupply",
|
||||
name: "Microphone",
|
||||
},
|
||||
],
|
||||
tool: [
|
||||
{
|
||||
"@type": "HowToTool",
|
||||
name: "Terminal",
|
||||
},
|
||||
],
|
||||
step: [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Open terminal",
|
||||
text: "Launch your terminal application and ensure curl is available.",
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Run installer",
|
||||
text: `Run: ${config.installCommand}`,
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Start dictating",
|
||||
text: "Launch vocalinux and double-tap Ctrl to begin dictation.",
|
||||
url: absoluteUrl(`/install/${distro}`),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
|
||||
/>
|
||||
|
||||
<nav className="mb-6 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link href="/" className="hover:text-foreground hover:underline">
|
||||
Home
|
||||
</Link>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<Link href="/install/" className="hover:text-foreground hover:underline">
|
||||
Install Guides
|
||||
</Link>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span className="font-medium text-foreground">{config.name}</span>
|
||||
</nav>
|
||||
|
||||
<section>
|
||||
<p
|
||||
className={`mb-4 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm font-medium ${styles.chipClass}`}
|
||||
>
|
||||
<DistroIcon className={`h-4 w-4 ${styles.iconClass}`} />
|
||||
{config.name} Setup Guide
|
||||
</p>
|
||||
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl">{config.title}</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">{config.intro}</p>
|
||||
|
||||
<div
|
||||
className={`rounded-2xl border border-zinc-200 bg-gradient-to-br p-6 dark:border-zinc-700 ${styles.panelClass}`}
|
||||
>
|
||||
<h2 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
Install command
|
||||
</h2>
|
||||
<div className="rounded-xl bg-zinc-950 p-4">
|
||||
<code className="block overflow-x-auto whitespace-pre-wrap break-all text-sm text-green-400 sm:text-base">
|
||||
{config.installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Shield className="h-5 w-5 text-blue-500" />
|
||||
Prerequisites
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{config.prerequisites.map((item) => (
|
||||
<li key={item} className="inline-flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Sparkles className="h-5 w-5 text-violet-500" />
|
||||
Tested environments
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{config.testedOn.map((item) => (
|
||||
<li key={item} className="inline-flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-cyan-500" />
|
||||
Setup notes for {config.name}
|
||||
</h2>
|
||||
<ol className="space-y-3 text-muted-foreground">
|
||||
{config.setupNotes.map((item) => (
|
||||
<li key={item} className="inline-flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/70">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
Post-install checklist
|
||||
</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
{config.postInstallChecks.map((item) => (
|
||||
<li key={item} className="inline-flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p className="mt-6 text-sm text-muted-foreground">
|
||||
Need help selecting an engine? Read the
|
||||
<Link href="/compare/" className="ml-1 font-semibold text-primary hover:underline">
|
||||
whisper.cpp vs Whisper vs VOSK comparison
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { ChevronRight, Cpu, Laptop, Sparkles, Terminal } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const installCommand =
|
||||
"curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive";
|
||||
|
||||
const distroGuides = [
|
||||
{
|
||||
name: "Ubuntu",
|
||||
href: "/install/ubuntu/",
|
||||
description:
|
||||
"Install offline voice dictation on Ubuntu 22.04+ with GNOME, KDE, X11, or Wayland.",
|
||||
icon: Laptop,
|
||||
accent: "from-orange-500/15 to-amber-500/10",
|
||||
iconColor: "text-orange-500",
|
||||
},
|
||||
{
|
||||
name: "Fedora",
|
||||
href: "/install/fedora/",
|
||||
description:
|
||||
"Set up speech-to-text on Fedora Workstation and Silverblue with distro-specific notes.",
|
||||
icon: Sparkles,
|
||||
accent: "from-blue-500/15 to-cyan-500/10",
|
||||
iconColor: "text-blue-500",
|
||||
},
|
||||
{
|
||||
name: "Arch Linux",
|
||||
href: "/install/arch/",
|
||||
description:
|
||||
"Install Vocalinux on Arch, Manjaro, and EndeavourOS with package prerequisites and tips.",
|
||||
icon: Cpu,
|
||||
accent: "from-emerald-500/15 to-teal-500/10",
|
||||
iconColor: "text-emerald-500",
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Install Voice Dictation on Linux (Ubuntu, Fedora, Arch)",
|
||||
description:
|
||||
"Step-by-step Linux voice dictation installation guides for Ubuntu, Fedora, and Arch. Install Vocalinux in minutes and start offline speech-to-text.",
|
||||
path: "/install",
|
||||
keywords: [
|
||||
"install voice dictation linux",
|
||||
"linux speech to text setup",
|
||||
"ubuntu voice typing install",
|
||||
"fedora speech recognition",
|
||||
"arch linux dictation",
|
||||
],
|
||||
});
|
||||
|
||||
export default function InstallGuidesPage() {
|
||||
return (
|
||||
<SeoSubpageShell>
|
||||
<section className="text-center">
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Linux Installation Guides
|
||||
</p>
|
||||
<h1 className="mb-6 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
Install Voice Dictation on Linux
|
||||
</h1>
|
||||
<p className="mx-auto mb-8 max-w-3xl text-lg text-muted-foreground">
|
||||
Pick your distro guide and follow a proven setup flow for offline speech-to-text on Linux.
|
||||
Every guide is optimized for real desktop usage, not just terminal demos.
|
||||
</p>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-950 p-6 text-left shadow-lg dark:border-zinc-700">
|
||||
<p className="mb-3 inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
||||
<Terminal className="h-4 w-4 text-green-400" />
|
||||
Recommended interactive install
|
||||
</p>
|
||||
<code className="block overflow-x-auto whitespace-pre-wrap break-all text-sm text-green-400 sm:text-base">
|
||||
{installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-14 grid gap-6 md:grid-cols-3">
|
||||
{distroGuides.map((guide) => {
|
||||
const Icon = guide.icon;
|
||||
return (
|
||||
<Link
|
||||
key={guide.href}
|
||||
href={guide.href}
|
||||
className="group rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
<div
|
||||
className={`mb-4 inline-flex rounded-xl bg-gradient-to-br p-3 ${guide.accent}`}
|
||||
>
|
||||
<Icon className={`h-6 w-6 ${guide.iconColor}`} />
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-semibold">{guide.name}</h2>
|
||||
<p className="mb-5 text-sm text-muted-foreground">{guide.description}</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Read {guide.name} guide
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-16 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">After Installation</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
Launch with
|
||||
<code className="ml-1.5 rounded bg-zinc-200 px-1.5 py-0.5 dark:bg-zinc-800">vocalinux</code>.
|
||||
</li>
|
||||
<li>Double-tap Ctrl to start or stop dictation in any text field.</li>
|
||||
<li>
|
||||
Choose whisper.cpp for speed, Whisper for maximum model compatibility, or VOSK for
|
||||
low-RAM systems.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-6 text-sm text-muted-foreground">
|
||||
Need help choosing an engine? See the
|
||||
<Link href="/compare/" className="ml-1 font-semibold text-primary hover:underline">
|
||||
speech engine comparison
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
</SeoSubpageShell>
|
||||
);
|
||||
}
|
||||
+99
-221
@@ -4,6 +4,14 @@ import { GeistSans } from "geist/font/sans";
|
||||
import { type Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import {
|
||||
absoluteUrl,
|
||||
DEFAULT_OG_IMAGE_ALT,
|
||||
DEFAULT_OG_IMAGE_PATH,
|
||||
SITE_NAME,
|
||||
SITE_URL,
|
||||
TWITTER_HANDLE,
|
||||
} from "@/lib/seo";
|
||||
|
||||
export const viewport = {
|
||||
width: "device-width",
|
||||
@@ -16,36 +24,31 @@ export const viewport = {
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://vocalinux.com"),
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
|
||||
template: "%s | Vocalinux"
|
||||
default: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
template: "%s | Vocalinux",
|
||||
},
|
||||
description: "Free, open-source voice dictation for Linux. 100% offline with Whisper AI. Double-tap Ctrl to start. Works everywhere - X11 & Wayland. One-click install.",
|
||||
applicationName: "Vocalinux",
|
||||
description:
|
||||
"Free and open-source voice dictation for Linux. Convert speech to text offline with whisper.cpp and VOSK on Ubuntu, Fedora, Arch, X11, and Wayland.",
|
||||
applicationName: SITE_NAME,
|
||||
keywords: [
|
||||
"voice dictation linux",
|
||||
"speech to text linux",
|
||||
"linux voice typing",
|
||||
"whisper linux",
|
||||
"vosk linux",
|
||||
"offline voice dictation",
|
||||
"privacy voice recognition",
|
||||
"speech recognition linux",
|
||||
"dictation software ubuntu",
|
||||
"voice typing fedora",
|
||||
"linux voice typing",
|
||||
"whisper.cpp linux",
|
||||
"vosk linux",
|
||||
"wayland voice dictation",
|
||||
"x11 speech to text",
|
||||
"open source dictation",
|
||||
"free speech recognition",
|
||||
"linux accessibility",
|
||||
"hands free typing linux",
|
||||
"voice commands linux",
|
||||
"GTK voice dictation"
|
||||
"x11 speech recognition",
|
||||
"open source dictation software",
|
||||
"voice typing ubuntu",
|
||||
"voice typing fedora",
|
||||
"voice typing arch linux",
|
||||
],
|
||||
authors: [{ name: "Jatin K Malik", url: "https://github.com/jatinkrmalik" }],
|
||||
creator: "Jatin K Malik",
|
||||
publisher: "Vocalinux",
|
||||
publisher: SITE_NAME,
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/vocalinux.png", type: "image/png" },
|
||||
@@ -54,37 +57,45 @@ export const metadata: Metadata = {
|
||||
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
],
|
||||
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
||||
shortcut: "/vocalinux.png"
|
||||
shortcut: "/vocalinux.png",
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "default",
|
||||
title: "Vocalinux"
|
||||
title: SITE_NAME,
|
||||
},
|
||||
formatDetection: {
|
||||
telephone: false
|
||||
telephone: false,
|
||||
email: false,
|
||||
address: false,
|
||||
},
|
||||
referrer: "strict-origin-when-cross-origin",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://vocalinux.com",
|
||||
siteName: "Vocalinux",
|
||||
title: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
|
||||
description: "Free, open-source voice dictation for Linux. 100% offline & private with Whisper AI and VOSK. Double-tap Ctrl to start dictating in any app.",
|
||||
images: [{
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Vocalinux - Voice Dictation for Linux"
|
||||
}]
|
||||
url: absoluteUrl("/"),
|
||||
siteName: SITE_NAME,
|
||||
title: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
description:
|
||||
"Voice dictation for Linux that runs fully offline. Install in minutes and dictate in any app on Ubuntu, Fedora, Arch, X11, or Wayland.",
|
||||
images: [
|
||||
{
|
||||
url: DEFAULT_OG_IMAGE_PATH,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: DEFAULT_OG_IMAGE_ALT,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Vocalinux - Voice Dictation for Linux, Finally Done Right",
|
||||
description: "Free, open-source voice dictation for Linux. 100% offline & private. Double-tap Ctrl to start dictating in any app.",
|
||||
images: ["/og-image.png"],
|
||||
creator: "@jatinkrmalik"
|
||||
title: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
description:
|
||||
"Speech-to-text for Linux with whisper.cpp and VOSK. 100% offline, privacy-first, and open-source.",
|
||||
images: [DEFAULT_OG_IMAGE_PATH],
|
||||
creator: TWITTER_HANDLE,
|
||||
site: TWITTER_HANDLE,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@@ -98,206 +109,73 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
alternates: {
|
||||
canonical: "https://vocalinux.com",
|
||||
canonical: absoluteUrl("/"),
|
||||
languages: {
|
||||
"en-US": absoluteUrl("/"),
|
||||
},
|
||||
},
|
||||
category: "Technology",
|
||||
};
|
||||
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"@id": `${absoluteUrl("/")}#organization`,
|
||||
name: SITE_NAME,
|
||||
url: absoluteUrl("/"),
|
||||
logo: absoluteUrl("/vocalinux.png"),
|
||||
description: "Open-source, offline voice dictation software for Linux.",
|
||||
sameAs: ["https://github.com/jatinkrmalik/vocalinux"],
|
||||
founder: {
|
||||
"@type": "Person",
|
||||
name: "Jatin K Malik",
|
||||
url: "https://github.com/jatinkrmalik",
|
||||
},
|
||||
};
|
||||
|
||||
const webSiteJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": `${absoluteUrl("/")}#website`,
|
||||
name: SITE_NAME,
|
||||
url: absoluteUrl("/"),
|
||||
inLanguage: "en-US",
|
||||
description:
|
||||
"Vocalinux provides offline speech-to-text and voice dictation for Linux desktops.",
|
||||
publisher: {
|
||||
"@id": `${absoluteUrl("/")}#organization`,
|
||||
},
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate: "https://github.com/jatinkrmalik/vocalinux/issues?q={search_term_string}",
|
||||
},
|
||||
"query-input": {
|
||||
"@type": "PropertyValueSpecification",
|
||||
valueRequired: true,
|
||||
valueName: "search_term_string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const siteWideJsonLd = [organizationJsonLd, webSiteJsonLd];
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
// SoftwareApplication schema
|
||||
const softwareAppJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Vocalinux",
|
||||
"applicationCategory": "UtilitiesApplication",
|
||||
"operatingSystem": "Linux",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"description": "Free, open-source voice dictation for Linux. 100% offline with Whisper AI and VOSK. Works with X11 and Wayland.",
|
||||
"softwareVersion": "0.6.1-beta",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Jatin K Malik",
|
||||
"url": "https://github.com/jatinkrmalik"
|
||||
},
|
||||
"url": "https://vocalinux.com",
|
||||
"downloadUrl": "https://github.com/jatinkrmalik/vocalinux",
|
||||
"screenshot": "https://vocalinux.com/og-image.png",
|
||||
"featureList": [
|
||||
"100% offline voice recognition",
|
||||
"Privacy-focused - no cloud dependencies",
|
||||
"Works with all Linux applications",
|
||||
"X11 and Wayland support",
|
||||
"Double-tap Ctrl activation",
|
||||
"Whisper AI and VOSK engines",
|
||||
"Voice commands for punctuation",
|
||||
"System tray integration"
|
||||
]
|
||||
};
|
||||
|
||||
// Organization schema
|
||||
const organizationJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Vocalinux",
|
||||
"url": "https://vocalinux.com",
|
||||
"logo": "https://vocalinux.com/vocalinux.png",
|
||||
"description": "Free, open-source voice dictation for Linux",
|
||||
"sameAs": [
|
||||
"https://github.com/jatinkrmalik/vocalinux"
|
||||
],
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Jatin K Malik",
|
||||
"url": "https://github.com/jatinkrmalik"
|
||||
}
|
||||
};
|
||||
|
||||
// WebSite schema
|
||||
const webSiteJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "Vocalinux",
|
||||
"url": "https://vocalinux.com",
|
||||
"description": "Voice dictation for Linux, finally done right. 100% offline and privacy-focused.",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "https://github.com/jatinkrmalik/vocalinux/issues?q={search_term_string}"
|
||||
},
|
||||
"query-input": {
|
||||
"@type": "PropertyValueSpecification",
|
||||
"valueRequired": true,
|
||||
"valueName": "search_term_string"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FAQPage schema for rich results
|
||||
const faqJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is Vocalinux really 100% offline?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Yes! All speech recognition processing happens locally on your machine. Your voice data never leaves your computer. No internet connection is required after installation."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Which Linux distributions are supported?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Vocalinux works on most modern Linux distributions including Ubuntu 22.04+, Fedora, Debian, Arch Linux, Linux Mint, Pop!_OS, and more. It supports both X11 and Wayland display servers."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How do I switch between Whisper and VOSK?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "You can switch engines via the settings GUI or command line. Use 'vocalinux --engine whisper' or 'vocalinux --engine vosk'. Whisper is recommended for accuracy, VOSK for speed."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What are the system requirements?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Minimum: 4GB RAM, dual-core CPU, Python 3.8+. For Whisper large models: 8GB+ RAM recommended. The tiny/base Whisper models and VOSK work great on modest hardware."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Can I use it in languages other than English?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Yes! Whisper supports 99+ languages with varying accuracy levels. VOSK has models for 20+ languages. Download additional language models as needed."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How do I customize the activation shortcut?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "The default is double-tap Ctrl. You can customize this via the GUI settings dialog or by editing ~/.config/vocalinux/config.yaml. Any key combination is supported."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is Vocalinux free?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Yes, Vocalinux is completely free and open-source, licensed under GPL-3.0. No premium tiers, no subscriptions, no tracking — just free software."
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// HowTo schema for installation guide
|
||||
const howToJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
"name": "How to Install Vocalinux on Linux",
|
||||
"description": "Step-by-step guide to install Vocalinux voice dictation software on Linux",
|
||||
"totalTime": "PT10M",
|
||||
"estimatedCost": {
|
||||
"@type": "MonetaryAmount",
|
||||
"currency": "USD",
|
||||
"value": "0"
|
||||
},
|
||||
"step": [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"name": "Run the installation command",
|
||||
"text": "Open your terminal and run the one-liner installation command to download and install Vocalinux.",
|
||||
"url": "https://vocalinux.com/#install"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"name": "Wait for installation to complete",
|
||||
"text": "The installer will download dependencies, set up the environment, and download speech recognition models. This takes approximately 5-10 minutes.",
|
||||
"url": "https://vocalinux.com/#install"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"name": "Launch Vocalinux",
|
||||
"text": "After installation, launch Vocalinux from your terminal by typing 'vocalinux' or find it in your application menu.",
|
||||
"url": "https://vocalinux.com/#install"
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
"name": "Start dictating",
|
||||
"text": "Double-tap Ctrl to start voice dictation. Speak clearly into your microphone. Double-tap Ctrl again to stop.",
|
||||
"url": "https://vocalinux.com/#install"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const jsonLd = [softwareAppJsonLd, organizationJsonLd, webSiteJsonLd, faqJsonLd, howToJsonLd];
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${GeistSans.variable} scroll-smooth`} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Preconnect to GitHub API for faster fetch of stars and release info (~300ms improvement) */}
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(siteWideJsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{/* Google tag (gtag.js) - deferred to prioritize page interactivity */}
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-7NBBNJNNQ7"
|
||||
strategy="lazyOnload"
|
||||
|
||||
+205
-7
@@ -5,7 +5,6 @@ import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { LiveDemo } from "@/components/live-demo";
|
||||
import {
|
||||
Mic,
|
||||
Terminal,
|
||||
Lock,
|
||||
Zap,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
Settings,
|
||||
Code,
|
||||
Github,
|
||||
Edit,
|
||||
Star,
|
||||
Download,
|
||||
ChevronRight,
|
||||
@@ -30,7 +28,6 @@ import {
|
||||
Volume2,
|
||||
Sparkles,
|
||||
Heart,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { VocalinuxLogo } from "@/components/optimized-image";
|
||||
@@ -51,6 +48,99 @@ const getInstallCommands = (latestRelease: string) => ({
|
||||
uninstallCommand: `curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/${latestRelease}/uninstall.sh -o /tmp/vul.sh && bash /tmp/vul.sh`,
|
||||
});
|
||||
|
||||
const homeJsonLd = [
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"@id": "https://vocalinux.com/#softwareapplication",
|
||||
name: "Vocalinux",
|
||||
applicationCategory: "UtilitiesApplication",
|
||||
operatingSystem: "Linux",
|
||||
isAccessibleForFree: true,
|
||||
license: "https://github.com/jatinkrmalik/vocalinux/blob/main/LICENSE",
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: "0",
|
||||
priceCurrency: "USD",
|
||||
},
|
||||
description:
|
||||
"Offline voice dictation and speech-to-text for Linux with whisper.cpp and VOSK.",
|
||||
"softwareVersion": "0.6.1-beta",
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: "Jatin K Malik",
|
||||
url: "https://github.com/jatinkrmalik",
|
||||
},
|
||||
url: "https://vocalinux.com/",
|
||||
downloadUrl: "https://github.com/jatinkrmalik/vocalinux",
|
||||
screenshot: "https://vocalinux.com/og-image.png",
|
||||
featureList: [
|
||||
"100% offline speech recognition",
|
||||
"Works with X11 and Wayland",
|
||||
"Double-tap Ctrl activation",
|
||||
"whisper.cpp, Whisper, and VOSK support",
|
||||
"Linux desktop integration",
|
||||
],
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: [
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Is Vocalinux really 100% offline?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Yes. All speech recognition runs locally on your Linux machine. Voice data does not leave your computer.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Which Linux distributions are supported?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Vocalinux supports Ubuntu, Fedora, Debian, Arch, Linux Mint, Pop!_OS, and other modern Linux distributions across X11 and Wayland.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "How do I switch between speech engines?",
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Use the settings GUI or the CLI flags: vocalinux --engine whisper_cpp, vocalinux --engine whisper, or vocalinux --engine vosk.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "HowTo",
|
||||
name: "Install Vocalinux on Linux",
|
||||
description: "Install offline voice dictation on Linux in a few minutes.",
|
||||
totalTime: "PT10M",
|
||||
step: [
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Run the install command",
|
||||
text: "Copy the install command from vocalinux.com and run it in terminal.",
|
||||
url: "https://vocalinux.com/#install",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Complete the guided setup",
|
||||
text: "Pick your speech engine and model size in the interactive installer.",
|
||||
url: "https://vocalinux.com/#install",
|
||||
},
|
||||
{
|
||||
"@type": "HowToStep",
|
||||
name: "Start dictating",
|
||||
text: "Launch vocalinux and double-tap Ctrl to begin voice dictation.",
|
||||
url: "https://vocalinux.com/#install",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const FeatureCard = ({
|
||||
icon,
|
||||
title,
|
||||
@@ -173,6 +263,7 @@ export default function HomePage() {
|
||||
{ href: "#demo", label: "Demo" },
|
||||
{ href: "#features", label: "Features" },
|
||||
{ href: "#install", label: "Install" },
|
||||
{ href: "#guides", label: "Guides" },
|
||||
{ href: "#faq", label: "FAQ" },
|
||||
];
|
||||
|
||||
@@ -182,6 +273,11 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-zinc-50 to-white dark:from-zinc-900 dark:to-zinc-950">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(homeJsonLd) }}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-md border-b border-border">
|
||||
<nav className="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
|
||||
@@ -439,7 +535,7 @@ export default function HomePage() {
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
Try It Yourself
|
||||
Try Linux Speech-to-Text in Your Browser
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Experience voice-to-text right here in your browser. Click the microphone and start speaking!
|
||||
@@ -493,7 +589,7 @@ export default function HomePage() {
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
Why Choose Vocalinux?
|
||||
Offline Voice Dictation Features for Linux
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl mx-auto">
|
||||
Finally, Linux users get the voice dictation experience they deserve —
|
||||
@@ -598,7 +694,7 @@ export default function HomePage() {
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
Get Started in Seconds
|
||||
How to Install Voice Dictation on Linux
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
One command. That's all it takes.
|
||||
@@ -769,13 +865,80 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SEO Guide Hub Section */}
|
||||
<section id="guides" className="py-16 sm:py-24 px-4 sm:px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
Linux Voice Dictation Guides by Distribution
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl mx-auto">
|
||||
Follow distro-specific setup instructions for Ubuntu, Fedora, and Arch Linux.
|
||||
These pages are written for real desktop workflows and include post-install checks.
|
||||
</p>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
href: "/install/ubuntu/",
|
||||
title: "Ubuntu Voice Typing Guide",
|
||||
description:
|
||||
"Install offline speech-to-text on Ubuntu 22.04+ for GNOME, KDE, X11, and Wayland.",
|
||||
},
|
||||
{
|
||||
href: "/install/fedora/",
|
||||
title: "Fedora Speech-to-Text Guide",
|
||||
description:
|
||||
"Set up Vocalinux on Fedora Workstation with distro-specific notes for a stable dictation flow.",
|
||||
},
|
||||
{
|
||||
href: "/install/arch/",
|
||||
title: "Arch Linux Dictation Guide",
|
||||
description:
|
||||
"Lean setup for Arch, Manjaro, and EndeavourOS with hardware and injection sanity checks.",
|
||||
},
|
||||
].map((guide) => (
|
||||
<Link
|
||||
key={guide.href}
|
||||
href={guide.href}
|
||||
className="group rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
<h3 className="mb-3 text-xl font-semibold">{guide.title}</h3>
|
||||
<p className="mb-4 text-sm text-muted-foreground">{guide.description}</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
Read guide <ChevronRight className="h-4 w-4" />
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FadeInSection delay={0.15}>
|
||||
<div className="mt-8 rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8">
|
||||
<h3 className="text-2xl font-bold mb-3">Need help choosing an engine?</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Compare whisper.cpp, Whisper, and VOSK for speed, hardware support, and model size.
|
||||
</p>
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline"
|
||||
>
|
||||
View speech engine comparison <ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Speech Engines Section */}
|
||||
<section className="py-16 sm:py-24 px-4 sm:px-6 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<FadeInSection>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
|
||||
Choose Your Engine
|
||||
Choose Your Linux Speech Recognition Engine
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Vocalinux supports multiple speech recognition engines. Pick the one that suits your needs.
|
||||
@@ -1023,6 +1186,31 @@ export default function HomePage() {
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/install/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Linux Install Guides
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/install/ubuntu/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Ubuntu Guide
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/install/fedora/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Fedora Guide
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/install/arch/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Arch Guide
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/compare/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Engine Comparison
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1059,6 +1247,16 @@ export default function HomePage() {
|
||||
Discussions
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/install/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
Setup by Distribution
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/compare/" className="text-zinc-400 hover:text-white transition-colors">
|
||||
whisper.cpp vs Whisper vs VOSK
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/jatinkrmalik/vocalinux/blob/main/CONTRIBUTING.md"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { BookOpen, ChevronRight, Cpu, Home } from "lucide-react";
|
||||
import { VocalinuxLogo } from "@/components/optimized-image";
|
||||
|
||||
interface SeoSubpageShellProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
href: "/",
|
||||
label: "Home",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
href: "/install/",
|
||||
label: "Install Guides",
|
||||
icon: BookOpen,
|
||||
},
|
||||
{
|
||||
href: "/compare/",
|
||||
label: "Engine Compare",
|
||||
icon: Cpu,
|
||||
},
|
||||
];
|
||||
|
||||
export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-zinc-50 to-white dark:from-zinc-900 dark:to-zinc-950">
|
||||
<header className="border-b border-zinc-200/80 bg-background/80 backdrop-blur-md dark:border-zinc-800/80">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4 sm:px-6">
|
||||
<Link href="/" className="group flex items-center gap-2">
|
||||
<VocalinuxLogo width={30} height={30} className="h-7 w-7 transition-transform group-hover:scale-110" />
|
||||
<span className="text-base font-bold sm:text-lg">Vocalinux</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-2 sm:flex">
|
||||
{navLinks.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex max-w-6xl gap-2 px-4 pb-4 sm:hidden">
|
||||
{navLinks.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-muted-foreground dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto w-full max-w-6xl px-4 pb-16 pt-10 sm:px-6 sm:pt-12">{children}</div>
|
||||
|
||||
<footer className="border-t border-zinc-200 bg-white/70 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-start justify-between gap-5 px-4 py-8 sm:flex-row sm:items-center sm:px-6">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Want the full product overview?</p>
|
||||
<p className="text-sm text-muted-foreground">Go back to the homepage for the live demo and one-command installer.</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Home Page
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { type Metadata } from "next";
|
||||
|
||||
export const SITE_URL = "https://vocalinux.com";
|
||||
export const SITE_NAME = "Vocalinux";
|
||||
export const DEFAULT_OG_IMAGE_PATH = "/og-image.png";
|
||||
export const DEFAULT_OG_IMAGE_ALT = "Vocalinux - Voice Dictation for Linux";
|
||||
export const TWITTER_HANDLE = "@jatinkrmalik";
|
||||
|
||||
interface PageMetadataOptions {
|
||||
title: string;
|
||||
description: string;
|
||||
path?: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
const normalizePath = (path: string): string => {
|
||||
if (path === "/") {
|
||||
return "/";
|
||||
}
|
||||
|
||||
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
||||
return `/${trimmed}/`;
|
||||
};
|
||||
|
||||
export const absoluteUrl = (path = "/"): string => {
|
||||
return new URL(normalizePath(path), SITE_URL).toString();
|
||||
};
|
||||
|
||||
export const buildPageMetadata = ({
|
||||
title,
|
||||
description,
|
||||
path = "/",
|
||||
keywords = [],
|
||||
}: PageMetadataOptions): Metadata => {
|
||||
const canonical = absoluteUrl(path);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
alternates: {
|
||||
canonical,
|
||||
languages: {
|
||||
"en-US": canonical,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: SITE_NAME,
|
||||
url: canonical,
|
||||
title,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: DEFAULT_OG_IMAGE_PATH,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: DEFAULT_OG_IMAGE_ALT,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: [DEFAULT_OG_IMAGE_PATH],
|
||||
creator: TWITTER_HANDLE,
|
||||
site: TWITTER_HANDLE,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user