feat(landing): self-host Inter font and promote Android app on Google Play

- Replace next/font/google with next/font/local + vendored Inter variable
  woff2 (latin, @fontsource-variable/inter@5.3.0), so neither visitors nor
  the Docker build contact Google's servers.
- Add PlayStoreBadge (official Google Play artwork, DE/EN by locale, served
  locally from /public/badges) to the hero, a new "PawFeed in your pocket"
  section, and the shared landing footer (also covers /ueber-uns).
- Centralize the Play Store URL in src/lib/app-links.ts (derived from the
  android.package id org.pawfeed.app).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-25 19:54:14 +02:00
1 parent 779f18966d
commit 71ac36b08a
10 files changed
+76 -6

No files matched your search

+5 -1
View File
@@ -742,7 +742,11 @@
"detailPoint2": "Gesundheitsdaten bleiben privat, außer du teilst sie bewusst per Link",
"footerTagline": "Wo Haustiere die Stars sind.",
"footerDesignedBy": "Designed & Powered by PawFeed",
"footerSecurityBadge": "Sicherheits-Check: Note A+"
"footerSecurityBadge": "Sicherheits-Check: Note A+",
"heroAndroidHint": "Auch als App für Android",
"playStoreAlt": "Jetzt bei Google Play",
"appHeading": "PawFeed für die Hosentasche",
"appBody": "Die Android-App bringt Feed, Storys und Nachrichten direkt aufs Handy — mit Push-Benachrichtigungen, wenn dein Tier ein neues Like oder einen neuen Follower bekommt."
},
"AboutPage": {
"metaTitle": "Über uns — PawFeed",
+5 -1
View File
@@ -742,7 +742,11 @@
"detailPoint2": "Health data stays private unless you deliberately share it via link",
"footerTagline": "Where pets are the stars.",
"footerDesignedBy": "Designed & Powered by PawFeed",
"footerSecurityBadge": "Security check: Grade A+"
"footerSecurityBadge": "Security check: Grade A+",
"heroAndroidHint": "Also available as an Android app",
"playStoreAlt": "Get it on Google Play",
"appHeading": "PawFeed in your pocket",
"appBody": "The Android app puts the feed, stories, and messages right on your phone — with push notifications when your pet gets a new like or follower."
},
"AboutPage": {
"metaTitle": "About us — PawFeed",
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.
+8 -3
View File
@@ -1,6 +1,6 @@
import type { Metadata, Viewport } from "next";
import { headers } from "next/headers";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import { ThemeProvider } from "next-themes";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
@@ -9,9 +9,14 @@ import { ActivePetProvider } from "@/context/ActivePetContext";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const inter = Inter({
// Inter (variable weight, latin subset) is vendored in ./fonts rather than
// pulled via next/font/google, so neither visitors nor the Docker build ever
// contact Google's servers. Source: @fontsource-variable/inter@5.3.0.
const inter = localFont({
src: "./fonts/inter-latin-wght-normal.woff2",
variable: "--font-sans",
subsets: ["latin"],
weight: "100 900",
display: "swap",
});
export const metadata: Metadata = {
+20
View File
@@ -7,6 +7,7 @@ import { PawPrint, HeartPulse, MessageCircle, Images, Heart, Sparkles, CheckCirc
import { ScrollReveal } from "@/components/landing/ScrollReveal";
import { LandingHeader } from "@/components/landing/LandingHeader";
import { LandingFooter } from "@/components/landing/LandingFooter";
import { PlayStoreBadge } from "@/components/landing/PlayStoreBadge";
/**
* Root route (/) — public landing page for logged-out visitors, redirects
@@ -74,6 +75,11 @@ export default async function RootPage() {
{t("logIn")}
</Link>
</div>
<div className="mt-8 flex flex-col items-center gap-2 lg:items-start">
<span className="text-sm text-muted-foreground">{t("heroAndroidHint")}</span>
<PlayStoreBadge className="h-11" />
</div>
</div>
{/* Phone mockup — sits inside one soft marigold glow, the hero's
@@ -211,6 +217,20 @@ export default async function RootPage() {
</div>
</ScrollReveal>
</section>
{/* Android app — a closing, left-aligned invitation rather than a
second hero; the official Play badge is the only CTA. */}
<section className="px-6 pb-24 sm:px-10">
<ScrollReveal className="mx-auto flex max-w-5xl flex-col items-center gap-8 text-center sm:flex-row sm:items-center sm:justify-between sm:text-left">
<div className="max-w-xl">
<h2 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
{t("appHeading")}
</h2>
<p className="mt-3 text-base leading-relaxed text-muted-foreground">{t("appBody")}</p>
</div>
<PlayStoreBadge className="h-14 shrink-0" />
</ScrollReveal>
</section>
</main>
{/* Footer — repeats the navbar */}
+5 -1
View File
@@ -1,6 +1,7 @@
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { PawPrint, ShieldCheck } from "lucide-react";
import { PlayStoreBadge } from "./PlayStoreBadge";
/**
* Shared footer for the public landing page and "Über uns" (Gitea #23) —
@@ -19,6 +20,9 @@ export async function LandingFooter() {
<span className="text-base font-bold text-foreground">PawFeed</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">{t("footerTagline")}</p>
<div className="mt-4">
<PlayStoreBadge className="h-10" />
</div>
</div>
<nav className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-muted-foreground sm:justify-end">
@@ -35,7 +39,7 @@ export async function LandingFooter() {
<a href="https://www.cloudflare.com" target="_blank" rel="noopener noreferrer" className="flex items-center">
{/* Official Cloudflare badge asset — brand guidelines require
using their own artwork, not a custom "Protected by
Cloudflare" text label. Plain <img>, matching the existing
Cloudflare" text label. Plain img element, matching the existing
AiDisclosureBadge pattern for small fixed-size assets. */}
<img
src="/badges/cloudflare-protected.png"
+25
View File
@@ -0,0 +1,25 @@
import Image from "next/image";
import { getLocale, getTranslations } from "next-intl/server";
import { PLAY_STORE_URL } from "@/lib/app-links";
/**
* Official "Get it on Google Play" badge — Google's brand guidelines require
* their own artwork (no recolouring, no custom text), vendored locally in
* /public/badges so the page makes no request to Google.
*/
export async function PlayStoreBadge({ className = "h-12" }: { className?: string }) {
const locale = await getLocale();
const t = await getTranslations("Landing");
const src = locale.startsWith("de") ? "/badges/google-play-de.png" : "/badges/google-play-en.png";
return (
<a
href={PLAY_STORE_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex rounded-lg transition-opacity hover:opacity-85 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-500"
>
<Image src={src} alt={t("playStoreAlt")} width={646} height={250} sizes="180px" className={`${className} w-auto`} />
</a>
);
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Store listing URLs for the native app — the single place to change them.
* The Play URL is derived from the Android package name in mobile/app.json
* (`android.package`), so it stays valid as long as that id doesn't change.
*/
export const ANDROID_PACKAGE_ID = "org.pawfeed.app";
export const PLAY_STORE_URL = `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_ID}`;