feat(pwa): als App installierbar
Web-Manifest mit eigenen Symbolen; auf dem Handy laesst sich Nebula zum Startbildschirm hinzufuegen und laeuft dann ohne Adressleiste. Das Manifest entsteht wie die uebrigen Metadaten aus einer Funktion, nicht aus einer Konstanten: nur so wird APP_NAME zur Laufzeit gelesen und das fertige Docker-Image bleibt umbenennbar. Manifest und Symbole muessen an der Zugangsschranke vorbei - der Browser holt sie, bevor sich jemand angemeldet hat. Sonst landeten sie auf der Anmeldeseite und die Anwendung waere nicht installierbar. Die maskierbare Fassung hat mehr Rand: Android schneidet Symbole je nach Geraet rund, eckig oder als Tropfen zu. Die PNGs sind aus dem vorhandenen SVG mit Chrome erzeugt - das spart eine Bildbibliothek als Abhaengigkeit. Beim Ergaenzen des Apple-Symbols ist mir aufgefallen, dass ein eigener icons-Eintrag den Verweis ersetzt, den Next aus app/icon.svg von selbst erzeugt: die Seite hatte danach gar kein Symbol mehr und der Browser fiel auf /favicon.ico zurueck, das es hier nicht gibt. Beide Eintraege stehen jetzt ausdruecklich da, ein Test prueft den Verweis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+16
-1
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
|
import { ChangelogButton } from "@/components/ChangelogButton";
|
||||||
import { CommandPalette } from "@/components/CommandPalette";
|
import { CommandPalette } from "@/components/CommandPalette";
|
||||||
import { TopNav } from "@/components/TopNav";
|
import { TopNav } from "@/components/TopNav";
|
||||||
import { getSessionUser } from "@/lib/auth/session";
|
import { getSessionUser } from "@/lib/auth/session";
|
||||||
@@ -20,6 +21,20 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
"Uebersicht ueber alle Vibe-Coding Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.",
|
"Uebersicht ueber alle Vibe-Coding Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.",
|
||||||
applicationName: APP_NAME,
|
applicationName: APP_NAME,
|
||||||
robots: { index: false, follow: false },
|
robots: { index: false, follow: false },
|
||||||
|
manifest: "/manifest.webmanifest",
|
||||||
|
// iOS liest das Manifest nicht; dort braucht es diese Angaben einzeln.
|
||||||
|
appleWebApp: { capable: true, title: APP_NAME, statusBarStyle: "black-translucent" },
|
||||||
|
// Beide Eintraege ausdruecklich: sobald hier etwas steht, ersetzt es den
|
||||||
|
// Verweis, den Next aus app/icon.svg von selbst erzeugt. Ohne die erste
|
||||||
|
// Zeile haette die Seite gar kein Symbol mehr und der Browser fiele auf
|
||||||
|
// /favicon.ico zurueck – das es hier nicht gibt.
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: "/icon.svg", type: "image/svg+xml" },
|
||||||
|
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||||
|
],
|
||||||
|
apple: "/apple-touch-icon.png",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +71,7 @@ export default async function RootLayout({
|
|||||||
<footer className="mx-auto w-full max-w-[1400px] px-4 pb-10 text-xs text-slate-500 sm:px-6 lg:px-8">
|
<footer className="mx-auto w-full max-w-[1400px] px-4 pb-10 text-xs text-slate-500 sm:px-6 lg:px-8">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-white/5 pt-6">
|
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-white/5 pt-6">
|
||||||
<span>{APP_NAME} · lokal betrieben, Daten bleiben in deiner PostgreSQL.</span>
|
<span>{APP_NAME} · lokal betrieben, Daten bleiben in deiner PostgreSQL.</span>
|
||||||
<span className="font-mono text-slate-600">v{APP_VERSION}</span>
|
<ChangelogButton version={APP_VERSION} />
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { APP_NAME, APP_TAGLINE } from "@/lib/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web-App-Manifest: macht Nebula auf dem Handy installierbar.
|
||||||
|
*
|
||||||
|
* Als Funktion und nicht als Konstante, aus demselben Grund wie bei
|
||||||
|
* generateMetadata: nur so wird APP_NAME zur Laufzeit gelesen. Ein
|
||||||
|
* konstanter Export waere beim Build eingefroren und liesse sich im
|
||||||
|
* fertigen Docker-Image nicht mehr aendern.
|
||||||
|
*/
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: `${APP_NAME} – ${APP_TAGLINE} Dashboard`,
|
||||||
|
short_name: APP_NAME,
|
||||||
|
description:
|
||||||
|
"Uebersicht ueber alle Vibe-Coding-Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.",
|
||||||
|
// Ohne Adressleiste, aber mit den Systemleisten – die Anwendung braucht
|
||||||
|
// keinen Vollbildmodus, wohl aber die Uhrzeit und den Akkustand.
|
||||||
|
display: "standalone",
|
||||||
|
start_url: "/",
|
||||||
|
scope: "/",
|
||||||
|
background_color: "#05060f",
|
||||||
|
theme_color: "#05060f",
|
||||||
|
lang: "de",
|
||||||
|
orientation: "any",
|
||||||
|
icons: [
|
||||||
|
{ src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
|
||||||
|
{ src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
|
||||||
|
// Getrennte Fassung mit mehr Rand: Android schneidet maskierbare
|
||||||
|
// Symbole je nach Geraet rund, eckig oder als Tropfen zu.
|
||||||
|
{ src: "/icon-maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||||
|
],
|
||||||
|
shortcuts: [
|
||||||
|
{ name: "Dashboard", url: "/" },
|
||||||
|
{ name: "Mein Konto", url: "/account" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-2
@@ -20,12 +20,21 @@ const SESSION_COOKIE = "nebula_session";
|
|||||||
/** Ohne Anmeldung erreichbar. */
|
/** Ohne Anmeldung erreichbar. */
|
||||||
const PUBLIC_PATHS = ["/login", "/setup", "/register"];
|
const PUBLIC_PATHS = ["/login", "/setup", "/register"];
|
||||||
const PUBLIC_API_PREFIXES = ["/api/auth/"];
|
const PUBLIC_API_PREFIXES = ["/api/auth/"];
|
||||||
/** Der Docker-Healthcheck hat kein Cookie. */
|
/**
|
||||||
const PUBLIC_API_EXACT = ["/api/health"];
|
* Der Docker-Healthcheck hat kein Cookie. Manifest und Symbole braucht der
|
||||||
|
* Browser, bevor sich jemand angemeldet hat – wuerden sie zur Anmeldeseite
|
||||||
|
* umgeleitet, liesse sich die Anwendung nicht installieren und die
|
||||||
|
* Anmeldeseite haette kein Symbol.
|
||||||
|
*/
|
||||||
|
const PUBLIC_API_EXACT = ["/api/health", "/manifest.webmanifest"];
|
||||||
|
|
||||||
|
/** Bilder und aehnliches im Wurzelverzeichnis verraten nichts. */
|
||||||
|
const PUBLIC_FILE = /\.(png|svg|ico|webmanifest|txt|webp)$/;
|
||||||
|
|
||||||
function isPublicPath(pathname: string): boolean {
|
function isPublicPath(pathname: string): boolean {
|
||||||
if (PUBLIC_PATHS.includes(pathname)) return true;
|
if (PUBLIC_PATHS.includes(pathname)) return true;
|
||||||
if (PUBLIC_API_EXACT.includes(pathname)) return true;
|
if (PUBLIC_API_EXACT.includes(pathname)) return true;
|
||||||
|
if (PUBLIC_FILE.test(pathname)) return true;
|
||||||
return PUBLIC_API_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
return PUBLIC_API_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user