fix(web): SEO fixes from Search Console and Lighthouse audit (#1039)
* fix(web): SEO fixes from Search Console and Lighthouse audit Technical: - Emit BreadcrumbList JSON-LD from the same trail as the visual breadcrumbs on every SeoSubpageShell page. - absoluteUrl() no longer appends a trailing slash to file paths. The Organization and publisher logo URLs pointed at /vocalinux.png/, a 404. - Fetch the GitHub star count once at build time in next.config.js and drop the client-side api.github.com fetch and preconnect (Lighthouse "third-party code" on every page). Falls back to the "GitHub" label. - Fix h1 -> h3 heading skips on 11 pages by promoting the leading card headings to h2; new .subpage-card-title keeps the old h3 sizing. - Header and footer logo get alt="" next to the visible "Vocalinux" text. - Footer links every guide, distro and feature page, so the pages added in #1038 and /vs-nerd-dictation/ (Discovered, not indexed) and /writers/ (no inbound links) are linked sitewide. Titles and descriptions: - Rewrite titles/descriptions for pages with high impressions and low CTR in Search Console (home, FAQ, compare, languages, open-source, GPU, alternatives, offline, install) to lead with the queries they rank for. - Distro pages use "<Distro> Speech to Text: Offline Voice Typing Setup" as the search title; the on-page h1 is unchanged. - Remove brand repetition and shorten 75+ character titles. - Bump sitemap lastmod for the rewritten pages. * fix(web): keep home install card titles styled The home page's install cards still use h3; only /install/ moved to h2. Style both so the home cards keep their size and spacing.
This commit is contained in:
@@ -576,6 +576,9 @@ jobs:
|
||||
|
||||
- name: Install and build
|
||||
working-directory: ./web
|
||||
env:
|
||||
# next.config.js reads the repo star count at build time
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm ci || npm install
|
||||
npm run deploy
|
||||
|
||||
+29
-1
@@ -1,3 +1,5 @@
|
||||
import { PHASE_PRODUCTION_BUILD } from 'next/constants.js';
|
||||
|
||||
// @ts-ignore
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
@@ -70,4 +72,30 @@ const config = {
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
// The header star count is fetched once per production build and inlined, so
|
||||
// visitors never call api.github.com (Lighthouse flagged it as third-party
|
||||
// code on every page). Any failure falls back to the plain "GitHub" label.
|
||||
async function fetchGitHubStars() {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/VocaHQ/vocalinux', {
|
||||
headers: process.env.GITHUB_TOKEN
|
||||
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
||||
: {},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return '';
|
||||
const data = await res.json();
|
||||
return data.stargazers_count ? String(data.stargazers_count) : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default async function nextConfig(phase) {
|
||||
if (phase !== PHASE_PRODUCTION_BUILD) return config;
|
||||
return {
|
||||
...config,
|
||||
env: { NEXT_PUBLIC_GITHUB_STARS: await fetchGitHubStars() },
|
||||
};
|
||||
}
|
||||
|
||||
+12
-12
@@ -2,37 +2,37 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://vocalinux.com/</loc>
|
||||
<lastmod>2026-06-11</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/ubuntu/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/fedora/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/install/arch/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/compare/</loc>
|
||||
<lastmod>2026-06-11</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
@@ -50,7 +50,7 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/faq/</loc>
|
||||
<lastmod>2026-06-11</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
@@ -80,14 +80,14 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/alternatives/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<!-- New SEO pages -->
|
||||
<url>
|
||||
<loc>https://vocalinux.com/languages/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
@@ -129,13 +129,13 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/gpu-acceleration/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/open-source/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
@@ -153,7 +153,7 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://vocalinux.com/offline/</loc>
|
||||
<lastmod>2026-02-22</lastmod>
|
||||
<lastmod>2026-11-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
@@ -56,9 +56,9 @@ const comparisonTable = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vocalinux Alternatives - Linux Voice Dictation Comparison",
|
||||
title: "Linux Alternatives to Dragon, Otter.ai and Speechnotes",
|
||||
description:
|
||||
"Compare Vocalinux to Dragon, Otter.ai, Google Voice Typing, and other speech-to-text alternatives. See why Vocalinux is the best choice for Linux voice dictation.",
|
||||
"Looking for Dragon NaturallySpeaking, Otter.ai or Google Voice Typing on Linux? Compare offline Linux dictation options, including Vocalinux and Nerd Dictation.",
|
||||
path: "/alternatives",
|
||||
keywords: [
|
||||
"vocalinux alternatives",
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function AutostartPage() {
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<Icon className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{method.method}</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{method.method}</h2>
|
||||
<p className="text-sm text-muted-foreground">{method.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -519,7 +519,7 @@ const getTypeStyles = (type: string) => {
|
||||
};
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vocalinux Changelog - Release History",
|
||||
title: "Changelog and Release Notes",
|
||||
description:
|
||||
"Track Vocalinux release history and version updates. See what's new in each version of our Linux voice dictation software.",
|
||||
path: "/changelog",
|
||||
|
||||
@@ -81,9 +81,9 @@ const engineTable = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Whisper.cpp vs Whisper vs VOSK vs Remote API for Linux",
|
||||
title: "Vosk vs Whisper vs whisper.cpp: Linux Speech Engines",
|
||||
description:
|
||||
"Compare whisper.cpp, Whisper, VOSK, and Remote API for Linux speech-to-text. See speed, hardware requirements, privacy tradeoffs, and best use cases.",
|
||||
"Vosk vs Whisper vs whisper.cpp for offline Linux speech to text, plus Faster Whisper, Parakeet and Remote API: speed, accuracy, hardware needs and privacy.",
|
||||
path: "/compare",
|
||||
keywords: [
|
||||
"whisper.cpp vs vosk",
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function CustomDictionaryPage() {
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<BookOpen className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Custom terms</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Custom terms</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A plain text list, one term per line, passed to the engine as
|
||||
context so it is more likely to spell those words your way.
|
||||
@@ -175,7 +175,7 @@ export default function CustomDictionaryPage() {
|
||||
className="mb-3 h-6 w-6 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h3 className="mb-2 font-semibold">Transcript corrections</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Transcript corrections</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Heard-as and replace-with pairs applied to every finished
|
||||
transcript, whatever the engine.
|
||||
@@ -183,7 +183,7 @@ export default function CustomDictionaryPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Code className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Post-processing script</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Post-processing script</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Any executable that reads text on stdin and writes the replacement
|
||||
on stdout.
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function DictationPadPage() {
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Monitor className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Who it is for</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Who it is for</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Wayland users whose compositor blocks or garbles injected text, and
|
||||
anyone who wants to read and fix a draft before it reaches the real
|
||||
@@ -188,7 +188,7 @@ export default function DictationPadPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Copy className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">How text gets out</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">How text gets out</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Copy All puts the whole pad on the clipboard. You can also select
|
||||
part of the text and copy it like any other text box.
|
||||
@@ -196,7 +196,7 @@ export default function DictationPadPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Keyboard className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">What stays the same</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">What stays the same</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your shortcut, engine, transcript corrections and post-processing
|
||||
script all work as before. Only the destination of the text changes.
|
||||
|
||||
@@ -125,9 +125,9 @@ const faqCategories = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Frequently Asked Questions - Vocalinux Voice Dictation",
|
||||
title: "Linux Dictation FAQ: Offline, Wayland, GPU and Privacy",
|
||||
description:
|
||||
"Get answers to common questions about Vocalinux Linux voice dictation. Learn about installation, privacy, features, and troubleshooting.",
|
||||
"Does Vocalinux work offline or on Wayland? How much RAM does it need, and which engines and languages does it support? Answers on install, privacy and GPU use.",
|
||||
path: "/faq",
|
||||
keywords: [
|
||||
"vocalinux faq",
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function ForDevelopersPage() {
|
||||
key={benefit.title}
|
||||
className="rounded-[12px] border border-border bg-background p-5 text-center"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{benefit.title}</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -130,9 +130,9 @@ const hardwareRequirements = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "GPU Accelerated Voice Dictation - AMD, Intel, NVIDIA | Vocalinux",
|
||||
title: "Vulkan GPU Speech to Text on Linux: AMD, Intel, NVIDIA",
|
||||
description:
|
||||
"GPU-accelerated speech recognition for Linux. Support for AMD, Intel, and NVIDIA GPUs via Vulkan. Compare CPU vs GPU performance and model sizes.",
|
||||
"Run whisper.cpp speech recognition on your GPU with Vulkan on Linux: AMD, Intel and NVIDIA. CPU vs GPU speed, model sizes, and automatic CPU fallback.",
|
||||
path: "/gpu-acceleration",
|
||||
keywords: [
|
||||
"GPU voice dictation",
|
||||
|
||||
@@ -431,7 +431,9 @@ export async function generateMetadata({
|
||||
const config = distroConfig[distro];
|
||||
|
||||
return buildPageMetadata({
|
||||
title: config.title,
|
||||
// Search title leads with the query people type ("ubuntu speech to text");
|
||||
// the on-page h1 stays config.title.
|
||||
title: `${config.name} Speech to Text: Offline Voice Typing Setup`,
|
||||
description: config.description,
|
||||
path: `/install/${distro}`,
|
||||
keywords: distroKeywords[distro],
|
||||
|
||||
@@ -67,7 +67,7 @@ const distroGuides = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Install Voice Dictation on Linux (Ubuntu, Fedora, Arch)",
|
||||
title: "Install Speech to Text on Linux (Ubuntu, Fedora, Arch)",
|
||||
description:
|
||||
"Linux voice dictation install guides for Ubuntu, Debian, Fedora, Arch, openSUSE, Mint, Pop!_OS, NixOS, and Flatpak. Start offline speech-to-text.",
|
||||
path: "/install",
|
||||
@@ -108,7 +108,7 @@ export default function InstallGuidesPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">No installer</p>
|
||||
<h3>AppImage</h3>
|
||||
<h2>AppImage</h2>
|
||||
<p>
|
||||
x86_64 or aarch64 from GitHub Releases. No root. Host
|
||||
text-injection tools are still required.
|
||||
@@ -120,7 +120,7 @@ export default function InstallGuidesPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">New</p>
|
||||
<h3>Native packages</h3>
|
||||
<h2>Native packages</h2>
|
||||
<p>
|
||||
.deb for Debian 12+ and Ubuntu 24.04+. .rpm for Fedora.
|
||||
x86_64 and aarch64 on every release.
|
||||
@@ -131,7 +131,7 @@ export default function InstallGuidesPage() {
|
||||
href="/flatpak/"
|
||||
>
|
||||
<p className="install-alt-kicker">Auto-updates</p>
|
||||
<h3>Flatpak</h3>
|
||||
<h2>Flatpak</h2>
|
||||
<p>
|
||||
Install once from the VocaHQ remote and{" "}
|
||||
<code>flatpak update</code> carries each stable release. Or
|
||||
@@ -144,7 +144,7 @@ export default function InstallGuidesPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">Snap Store</p>
|
||||
<h3>Snap</h3>
|
||||
<h2>Snap</h2>
|
||||
<p>
|
||||
<code>snap install vocalinux --edge</code>. amd64 only for
|
||||
now; a .snap is attached to every release too.
|
||||
@@ -156,7 +156,7 @@ export default function InstallGuidesPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">Arch</p>
|
||||
<h3>AUR</h3>
|
||||
<h2>AUR</h2>
|
||||
<p>
|
||||
<code>yay -S vocalinux</code>, or vocalinux-bin for the
|
||||
prebuilt package.
|
||||
@@ -168,7 +168,7 @@ export default function InstallGuidesPage() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">From source</p>
|
||||
<h3>Build it yourself</h3>
|
||||
<h2>Build it yourself</h2>
|
||||
<p>
|
||||
Clone the repo and run <code>./install.sh</code>. The install
|
||||
guide has the full source path.
|
||||
|
||||
@@ -306,9 +306,9 @@ const features = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Multilingual Voice Dictation - 30+ Languages | Vocalinux",
|
||||
title: "Linux Speech to Text in 30+ Languages, Offline",
|
||||
description:
|
||||
"Dictate in 30+ languages including Spanish, French, German, Hungarian, Japanese, Korean, Arabic, and more. Free offline multilingual voice typing for Linux.",
|
||||
"Offline voice typing on Linux in 30+ languages, including Spanish, French, German, Hungarian, Japanese, Korean and Arabic. Full language list and model picks.",
|
||||
path: "/languages",
|
||||
keywords: [
|
||||
"multilingual voice dictation",
|
||||
@@ -383,7 +383,7 @@ export default function LanguagesPage() {
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{feature.title}</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{feature.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,11 +23,11 @@ export const viewport = {
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
default: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
template: "%s | Vocalinux",
|
||||
},
|
||||
description:
|
||||
"Free and open-source voice dictation for Linux. Convert speech to text with whisper.cpp, VOSK, Remote API servers, Silero VAD, X11, and Wayland.",
|
||||
"Free, open-source speech to text for Linux. Offline voice dictation and voice typing in any app with whisper.cpp, VOSK or Parakeet, on X11 and Wayland.",
|
||||
applicationName: SITE_NAME,
|
||||
keywords: [
|
||||
"voice dictation linux",
|
||||
@@ -80,7 +80,7 @@ export const metadata: Metadata = {
|
||||
locale: "en_US",
|
||||
url: absoluteUrl("/"),
|
||||
siteName: SITE_NAME,
|
||||
title: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
title: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
description:
|
||||
"Voice dictation for Linux with local engines or an optional Remote API. Install in minutes and dictate in any app on Ubuntu, Fedora, Arch, X11, or Wayland.",
|
||||
images: [
|
||||
@@ -94,7 +94,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Vocalinux: Offline Voice Dictation for Linux",
|
||||
title: "Vocalinux: Offline Speech to Text & Voice Dictation for Linux",
|
||||
description:
|
||||
"Speech-to-text for Linux with whisper.cpp and VOSK. Local engines by default, optional Remote API, open source.",
|
||||
images: [DEFAULT_OG_IMAGE_PATH],
|
||||
@@ -178,7 +178,6 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className="scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(siteWideJsonLd) }}
|
||||
|
||||
@@ -225,7 +225,7 @@ export default function MultilingualDictationPage() {
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Globe className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Auto-detect</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Auto-detect</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The engine guesses the language for each utterance. Easiest to set
|
||||
up, but short clips can be guessed wrong.
|
||||
@@ -233,7 +233,7 @@ export default function MultilingualDictationPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Keyboard className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Language shortcuts</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Language shortcuts</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A dedicated key per language. Explicit and reliable, for one
|
||||
dictation at a time.
|
||||
@@ -241,7 +241,7 @@ export default function MultilingualDictationPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Languages className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Follow keyboard layout</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Follow keyboard layout</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Switch layout to switch dictation language. Best on GNOME, where the
|
||||
active layout can be read.
|
||||
|
||||
@@ -76,9 +76,9 @@ const sensitiveUseCases = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Offline Voice Dictation - Local Engines | Vocalinux",
|
||||
title: "Offline Speech to Text for Linux: No Cloud Needed",
|
||||
description:
|
||||
"Local engines run on your Linux machine with no cloud account. Remote API is optional if you want a server you configure.",
|
||||
"Offline speech to text on Linux. whisper.cpp, VOSK and other local engines transcribe on your machine after a one-time model download. No cloud account.",
|
||||
path: "/offline",
|
||||
keywords: [
|
||||
"offline voice dictation",
|
||||
@@ -149,7 +149,7 @@ export default function OfflinePage() {
|
||||
<div className={`mb-3 inline-flex rounded-lg p-2 ${benefit.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${benefit.iconColor}`} />
|
||||
</div>
|
||||
<h3 className="mb-2 font-semibold">{benefit.title}</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -85,9 +85,9 @@ const techStack = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Open Source Voice Dictation for Linux | Vocalinux",
|
||||
title: "Open Source Dictation Software for Linux (AGPL-3.0)",
|
||||
description:
|
||||
"100% free and open source voice dictation for Linux. AGPL-3.0 licensed, no vendor lock-in, transparent code, community-driven development. Your voice data stays private.",
|
||||
"Free, open source dictation software for Linux under AGPL-3.0. Offline speech recognition with whisper.cpp or VOSK, no required account, all code on GitHub.",
|
||||
path: "/open-source",
|
||||
keywords: [
|
||||
"open source voice dictation",
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function PrivacyPage() {
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Lock className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">Local engines</h3>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">Local engines</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local engines do not need the internet after the model is downloaded. Processing stays
|
||||
on your device unless you turn on Remote API.
|
||||
@@ -79,7 +79,7 @@ export default function PrivacyPage() {
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">No Cloud Processing</h3>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">No Cloud Processing</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local engines do not send your voice to our servers. Remote API sends audio only to the
|
||||
URL you configure.
|
||||
@@ -90,7 +90,7 @@ export default function PrivacyPage() {
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Eye className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">Open Source</h3>
|
||||
<h2 className="subpage-card-title mb-2 text-xl font-semibold">Open Source</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fully open source under AGPL-3.0. Inspect the code yourself on GitHub. The installed
|
||||
app does not send usage telemetry.
|
||||
|
||||
@@ -103,7 +103,7 @@ const settingsShots: Screenshot[] = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vocalinux Screenshots | Linux Voice Dictation UI",
|
||||
title: "Screenshots: Linux Voice Dictation App UI",
|
||||
description:
|
||||
"See Vocalinux in action: system tray, log viewer, and settings pages for speech model, dictation, audio, performance, application, advanced options, and about/updates. Flip between light and dark app shots.",
|
||||
path: "/screenshots",
|
||||
|
||||
@@ -100,7 +100,7 @@ function Mic(props: { className?: string }) {
|
||||
}
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Keyboard Shortcuts & Voice Commands - Vocalinux",
|
||||
title: "Keyboard Shortcuts and Voice Commands",
|
||||
description:
|
||||
"Learn Vocalinux keyboard shortcut modes and voice commands. Use toggle mode or push-to-talk, then customize activation and command behavior.",
|
||||
path: "/shortcuts",
|
||||
|
||||
@@ -174,9 +174,9 @@ export default function TranscribeAudioFilesPage() {
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Mic className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">
|
||||
For recordings, not live dictation
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
File transcription is separate from the dictation pipeline. It never
|
||||
types into other apps and does not change your dictation model.
|
||||
@@ -184,7 +184,7 @@ export default function TranscribeAudioFilesPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Users className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Speaker turns, English only</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Speaker turns, English only</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The model is <code>small.en-tdrz</code>. It is English-only, so use
|
||||
it for English recordings.
|
||||
@@ -192,7 +192,7 @@ export default function TranscribeAudioFilesPage() {
|
||||
</div>
|
||||
<div className="rounded-[12px] border border-border bg-background p-5">
|
||||
<Lock className="mb-3 h-6 w-6 text-primary" aria-hidden="true" />
|
||||
<h3 className="mb-2 font-semibold">Local processing</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">Local processing</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Audio is decoded and transcribed on your computer. Nothing is sent
|
||||
to a server.
|
||||
|
||||
@@ -168,7 +168,7 @@ const troubleshootingItems = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Troubleshooting Guide - Vocalinux Voice Dictation",
|
||||
title: "Linux Voice Dictation Troubleshooting Guide",
|
||||
description:
|
||||
"Fix common Vocalinux issues: microphone problems, text injection, installation errors, and performance. Step-by-step solutions for Linux voice dictation.",
|
||||
path: "/troubleshooting",
|
||||
|
||||
@@ -92,7 +92,7 @@ const useCases = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Use Cases - Who Uses Vocalinux Voice Dictation",
|
||||
title: "Use Cases for Linux Voice Dictation",
|
||||
description:
|
||||
"Discover how developers, writers, students, and professionals use Vocalinux for Linux voice dictation. Find your use case and boost productivity.",
|
||||
path: "/use-cases",
|
||||
|
||||
@@ -67,7 +67,7 @@ const comparisonRows = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vocalinux vs Nerd Dictation: Best Offline Voice Dictation for Linux",
|
||||
title: "Nerd Dictation Alternative for Linux | Vocalinux",
|
||||
description:
|
||||
"Comparing Vocalinux and Nerd Dictation for offline Linux voice dictation. CLI vs GUI, GPU vs CPU-only, Wayland support, and more.",
|
||||
path: "/vs-nerd-dictation",
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function WaylandPage() {
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{feature.title}</h3>
|
||||
<h2 className="subpage-card-title mb-2 font-semibold">{feature.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
|
||||
@@ -61,7 +61,7 @@ const modelComparison = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Whisper Model Sizes Explained: Tiny, Base, Small, Medium, Large for Linux",
|
||||
title: "Whisper Model Sizes: Tiny to Large for Linux Dictation",
|
||||
description:
|
||||
"Complete guide to Whisper model sizes for Linux voice dictation. Compare tiny vs base vs small vs medium vs large models - download size, RAM usage, accuracy, GPU requirements, and speed.",
|
||||
path: "/whisper-model-guide",
|
||||
|
||||
@@ -70,7 +70,7 @@ const integrations = [
|
||||
];
|
||||
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Voice Dictation for Writers on Linux: Dictate Novels, Articles & Notes",
|
||||
title: "Voice Dictation for Writers on Linux",
|
||||
description:
|
||||
"Complete guide to voice dictation for writers on Linux. How to dictate novels, blog posts, and notes using Vocalinux. Hands-free writing workflow tips.",
|
||||
path: "/writers",
|
||||
|
||||
@@ -129,4 +129,9 @@ describe('VocalinuxLogo', () => {
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('loading', 'eager');
|
||||
});
|
||||
|
||||
it('uses empty alt text when decorative', () => {
|
||||
const { container } = render(<VocalinuxLogo decorative />);
|
||||
expect(container.querySelector('img')).toHaveAttribute('alt', '');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,13 +53,21 @@ export const VocalinuxLogo: React.FC<{
|
||||
width?: number;
|
||||
height?: number;
|
||||
priority?: boolean;
|
||||
}> = ({ className = "", width = 32, height = 32, priority = false }) => {
|
||||
/** Set when the visible "Vocalinux" text sits next to the mark. */
|
||||
decorative?: boolean;
|
||||
}> = ({
|
||||
className = "",
|
||||
width = 32,
|
||||
height = 32,
|
||||
priority = false,
|
||||
decorative = false,
|
||||
}) => {
|
||||
return (
|
||||
// SVG lockup; next/image is for raster.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src="/brand/vocalinux-mark-circle.svg"
|
||||
alt="Vocalinux"
|
||||
alt={decorative ? "" : "Vocalinux"}
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SiteChrome } from "@/components/site-chrome";
|
||||
import { absoluteUrl } from "@/lib/seo";
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
"/install": "Install Guides",
|
||||
@@ -72,20 +73,40 @@ function Breadcrumbs() {
|
||||
};
|
||||
});
|
||||
|
||||
// Same trail as the visible breadcrumbs, so the markup matches the page.
|
||||
const breadcrumbJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [{ href: "/", label: "Home" }, ...crumbs].map(
|
||||
(crumb, index) => ({
|
||||
"@type": "ListItem",
|
||||
position: index + 1,
|
||||
name: crumb.label,
|
||||
item: absoluteUrl(crumb.href),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
||||
<Link href="/">Home</Link>
|
||||
{crumbs.map((crumb, index) => (
|
||||
<React.Fragment key={crumb.href}>
|
||||
<span aria-hidden="true">/</span>
|
||||
{index === crumbs.length - 1 ? (
|
||||
<span>{crumb.label}</span>
|
||||
) : (
|
||||
<Link href={crumb.href}>{crumb.label}</Link>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
||||
<Link href="/">Home</Link>
|
||||
{crumbs.map((crumb, index) => (
|
||||
<React.Fragment key={crumb.href}>
|
||||
<span aria-hidden="true">/</span>
|
||||
{index === crumbs.length - 1 ? (
|
||||
<span>{crumb.label}</span>
|
||||
) : (
|
||||
<Link href={crumb.href}>{crumb.label}</Link>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
type NavLink,
|
||||
} from "@/lib/site-nav";
|
||||
|
||||
const GITHUB_API_REPO_URL = `https://api.github.com/repos/${GITHUB_REPO_URL.replace("https://github.com/", "")}`;
|
||||
// Star count is fetched once at build time (next.config.js) so visitors never
|
||||
// call api.github.com. Empty or missing falls back to the "GitHub" label.
|
||||
const GITHUB_STARS = Number(process.env.NEXT_PUBLIC_GITHUB_STARS) || null;
|
||||
|
||||
function SmartLink({
|
||||
href,
|
||||
@@ -63,7 +65,7 @@ export function SiteChrome({
|
||||
variant?: "home" | "page";
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [stars, setStars] = useState<number | null>(null);
|
||||
const stars = GITHUB_STARS;
|
||||
const navId = useId();
|
||||
const nav = variant === "home" ? homeNav : pageNav;
|
||||
const ctaHref = variant === "home" ? "#install" : "/#install";
|
||||
@@ -91,15 +93,6 @@ export function SiteChrome({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(GITHUB_API_REPO_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data: { stargazers_count?: number }) => {
|
||||
if (data.stargazers_count) setStars(data.stargazers_count);
|
||||
})
|
||||
.catch(() => setStars(null));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-[100dvh] max-w-[100vw] overflow-x-clip">
|
||||
<a className="skip-link" href="#main">
|
||||
@@ -109,7 +102,13 @@ export function SiteChrome({
|
||||
<header className="site-header">
|
||||
<div className="shell header-inner">
|
||||
<Link href="/" className="brand" aria-label="Vocalinux home">
|
||||
<VocalinuxLogo width={28} height={28} className="h-7 w-7" priority />
|
||||
<VocalinuxLogo
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-7 w-7"
|
||||
priority
|
||||
decorative
|
||||
/>
|
||||
<span>Vocalinux</span>
|
||||
</Link>
|
||||
|
||||
@@ -128,7 +127,7 @@ export function SiteChrome({
|
||||
rel="noopener noreferrer"
|
||||
aria-label={
|
||||
stars !== null
|
||||
? `Vocalinux on GitHub, ${stars.toLocaleString()} stars`
|
||||
? `Vocalinux on GitHub, ${stars.toLocaleString("en-US")} stars`
|
||||
: "Vocalinux on GitHub"
|
||||
}
|
||||
>
|
||||
@@ -138,7 +137,7 @@ export function SiteChrome({
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{stars !== null ? stars.toLocaleString() : "GitHub"}</span>
|
||||
<span>{stars !== null ? stars.toLocaleString("en-US") : "GitHub"}</span>
|
||||
</a>
|
||||
<button
|
||||
className="nav-toggle"
|
||||
@@ -192,7 +191,12 @@ export function SiteChrome({
|
||||
<div className="footer-inner">
|
||||
<div className="footer-brand">
|
||||
<Link href="/" className="brand">
|
||||
<VocalinuxLogo width={28} height={28} className="h-7 w-7" />
|
||||
<VocalinuxLogo
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-7 w-7"
|
||||
decorative
|
||||
/>
|
||||
<span>Vocalinux</span>
|
||||
</Link>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { absoluteUrl } from "../seo";
|
||||
|
||||
describe("absoluteUrl", () => {
|
||||
it("returns the site root for /", () => {
|
||||
expect(absoluteUrl("/")).toBe("https://vocalinux.com/");
|
||||
});
|
||||
|
||||
it("adds the trailing slash pages are served with", () => {
|
||||
expect(absoluteUrl("/install/debian")).toBe(
|
||||
"https://vocalinux.com/install/debian/",
|
||||
);
|
||||
expect(absoluteUrl("/faq/")).toBe("https://vocalinux.com/faq/");
|
||||
});
|
||||
|
||||
it("leaves file URLs without a trailing slash", () => {
|
||||
expect(absoluteUrl("/vocalinux.png")).toBe(
|
||||
"https://vocalinux.com/vocalinux.png",
|
||||
);
|
||||
expect(absoluteUrl("/og-image.png")).toBe(
|
||||
"https://vocalinux.com/og-image.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,13 @@ const normalizePath = (path: string): string => {
|
||||
}
|
||||
|
||||
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
||||
|
||||
// Files such as /vocalinux.png must not get the page trailing slash:
|
||||
// /vocalinux.png/ is a 404 on GitHub Pages.
|
||||
if (/\.[a-z0-9]+$/i.test(trimmed)) {
|
||||
return `/${trimmed}`;
|
||||
}
|
||||
|
||||
return `/${trimmed}/`;
|
||||
};
|
||||
|
||||
|
||||
+18
-3
@@ -48,17 +48,24 @@ export const footerGroups: { title: string; links: NavLink[] }[] = [
|
||||
{ href: "/screenshots/", label: "Screenshots" },
|
||||
{ href: "/#engines", label: "Engines" },
|
||||
{ href: "/compare/", label: "Compare engines" },
|
||||
{ href: "/parakeet/", label: "Parakeet engine" },
|
||||
{ href: "/faster-whisper/", label: "Faster Whisper" },
|
||||
{ href: "/alternatives/", label: "Alternatives" },
|
||||
{ href: "/vs-nerd-dictation/", label: "vs Nerd Dictation" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Install",
|
||||
links: [
|
||||
{ href: "/#install", label: "Installation" },
|
||||
{ href: "/install/", label: "Distro guides" },
|
||||
{ href: "/install/ubuntu/", label: "Ubuntu" },
|
||||
{ href: "/install/debian/", label: "Debian" },
|
||||
{ href: "/install/fedora/", label: "Fedora" },
|
||||
{ href: "/install/arch/", label: "Arch" },
|
||||
{ href: "/install/debian/", label: "Debian" },
|
||||
{ href: "/install/linux-mint/", label: "Linux Mint" },
|
||||
{ href: "/install/pop-os/", label: "Pop!_OS" },
|
||||
{ href: "/install/opensuse/", label: "openSUSE" },
|
||||
{ href: "/install/nixos/", label: "NixOS" },
|
||||
{ href: "/flatpak/", label: "Flatpak" },
|
||||
],
|
||||
},
|
||||
@@ -67,9 +74,14 @@ export const footerGroups: { title: string; links: NavLink[] }[] = [
|
||||
links: [
|
||||
{ href: "/offline/", label: "Offline" },
|
||||
{ href: "/wayland/", label: "Wayland" },
|
||||
{ href: "/hyprland-sway/", label: "Hyprland and Sway" },
|
||||
{ href: "/pipewire/", label: "PipeWire" },
|
||||
{ href: "/gpu-acceleration/", label: "GPU acceleration" },
|
||||
{ href: "/whisper-model-guide/", label: "Whisper models" },
|
||||
{ href: "/desktop-reliability/", label: "Reliability" },
|
||||
{ href: "/remote-api/", label: "Remote API" },
|
||||
{ href: "/voice-typing-vscode/", label: "VS Code" },
|
||||
{ href: "/writers/", label: "For writers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -78,14 +90,17 @@ export const footerGroups: { title: string; links: NavLink[] }[] = [
|
||||
{ href: "/dictation-pad/", label: "Dictation Pad" },
|
||||
{ href: "/transcribe-audio-files/", label: "Transcribe audio files" },
|
||||
{ href: "/custom-dictionary/", label: "Custom dictionary" },
|
||||
{ href: "/multilingual-dictation/", label: "Multilingual dictation" },
|
||||
{ href: "/global-shortcuts/", label: "Global shortcuts" },
|
||||
{ href: "/parakeet/", label: "Parakeet engine" },
|
||||
{ href: "/shortcuts/", label: "Voice commands" },
|
||||
{ href: "/local-gateway/", label: "Local gateway" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Project",
|
||||
links: [
|
||||
{ href: "/faq/", label: "FAQ" },
|
||||
{ href: "/troubleshooting/", label: "Troubleshooting" },
|
||||
{ href: "/privacy/", label: "Privacy" },
|
||||
{ href: "/changelog/", label: "Changelog" },
|
||||
{ href: GITHUB_REPO_URL, label: "Source" },
|
||||
|
||||
@@ -1522,7 +1522,8 @@
|
||||
font-size: clamp(1.5rem, 3vw, 1.85rem);
|
||||
}
|
||||
|
||||
.subpage-content h3 {
|
||||
.subpage-content h3,
|
||||
.subpage-content .subpage-card-title {
|
||||
margin-top: 1.6rem;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: -0.03em;
|
||||
@@ -1726,6 +1727,8 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* /install/ uses h2 (heading order); the home page cards sit under an h2. */
|
||||
.install-alt h2,
|
||||
.install-alt h3 {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 1.2rem;
|
||||
|
||||
Reference in New Issue
Block a user