fix(web): UX/a11y polish + middleware metadata route fix

- footer.tsx: Discord link → canonical discord.gg/PUwSMR9XNk
  (was outdated invite that 404'd).
- [locale]/layout.tsx: hooks count 40 → 54 (matches AGENTS.md).
- lib/stats.ts:88: drop '!' non-null assertion; use '??' fallback.
  Removes the only '!' in packages/web (project rule).
- nav-header.tsx: hamburger Button size='icon' → explicit h-11 w-11;
  mobile drawer links min-h-11 px-3 rounded-md. WCAG 2.5.5 tap
  target 44x44 minimum.
- install-command.tsx: copy button h-8 w-8 → h-11 w-11 (was 32px,
  below WCAG). Icon stays 16x16; padding fills to 44.
- middleware.ts: matcher excludes opengraph-image, twitter-image,
  icon, apple-icon, manifest.webmanifest, robots.txt, sitemap.xml.
  Previously next-intl redirected /opengraph-image → /en/opengraph-image
  breaking OG previews for crawlers that hit the unlocalized path.
This commit is contained in:
YeonGyu-Kim
2026-05-20 14:26:09 +09:00
parent 68b80e0343
commit 7e0406f4ec
6 changed files with 38 additions and 20 deletions
+4 -4
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next"
import type { JSX } from "react"
import type { JSX, ReactNode } from "react"
import { notFound } from "next/navigation"
import { hasLocale } from "next-intl"
import { setRequestLocale } from "next-intl/server"
@@ -8,10 +8,10 @@ import { routing } from "@/i18n/routing"
export const metadata: Metadata = {
description:
"Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 40+ lifecycle hooks.",
"Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 54+ lifecycle hooks.",
}
export function generateStaticParams() {
export function generateStaticParams(): Array<{ readonly locale: string }> {
return routing.locales.map((locale) => ({ locale }))
}
@@ -19,7 +19,7 @@ export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
children: ReactNode
params: Promise<{ locale: string }>
}): Promise<JSX.Element> {
const { locale } = await params
+3 -2
View File
@@ -1,7 +1,8 @@
import type { JSX } from "react"
import { getTranslations } from "next-intl/server"
import { Link } from "@/i18n/routing"
export async function Footer({ locale }: { locale?: string } = {}) {
export async function Footer({ locale }: { readonly locale?: string } = {}): Promise<JSX.Element> {
const t = locale
? await getTranslations({ locale, namespace: "footer" })
: await getTranslations("footer")
@@ -27,7 +28,7 @@ export async function Footer({ locale }: { locale?: string } = {}) {
{t("github")}
</a>
<a
href="https://discord.gg/indentcorp"
href="https://discord.gg/PUwSMR9XNk"
target="_blank"
rel="noopener noreferrer"
className="transition-colors hover:text-cyan-400"
@@ -27,7 +27,7 @@ export function InstallCommand({ command }: { command: string }) {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-zinc-400 hover:text-white"
className="h-11 w-11 text-zinc-400 hover:text-white"
onClick={copyCommand}
aria-label="Copy install command"
>
+10 -9
View File
@@ -1,5 +1,6 @@
"use client"
import type { JSX } from "react"
import { useState } from "react"
import { useTranslations } from "next-intl"
import { Menu, X } from "lucide-react"
@@ -7,7 +8,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Link } from "@/i18n/routing"
function GitHubMark({ className }: { readonly className?: string }) {
function GitHubMark({ className }: { readonly className?: string }): JSX.Element {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className={className} fill="currentColor">
<path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49v-1.9c-2.78.62-3.37-1.22-3.37-1.22-.46-1.2-1.11-1.52-1.11-1.52-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.35 1.12 2.92.85.09-.67.35-1.12.63-1.38-2.22-.26-4.55-1.14-4.55-5.06 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05A9.3 9.3 0 0 1 12 6.94c.85 0 1.7.12 2.5.34 1.9-1.33 2.74-1.05 2.74-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.93-2.34 4.8-4.57 5.05.36.32.68.95.68 1.91v2.84c0 .27.18.59.69.49A10.18 10.18 0 0 0 22 12.25C22 6.58 17.52 2 12 2" />
@@ -15,7 +16,7 @@ function GitHubMark({ className }: { readonly className?: string }) {
)
}
export function NavHeader() {
export function NavHeader(): JSX.Element {
const t = useTranslations("nav")
const [isOpen, setIsOpen] = useState(false)
@@ -59,7 +60,7 @@ export function NavHeader() {
<Button
variant="ghost"
size="icon"
className="text-zinc-400 hover:bg-zinc-800 hover:text-white md:hidden"
className="h-11 w-11 text-zinc-400 hover:bg-zinc-800 hover:text-white md:hidden"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? "Close menu" : "Open menu"}
aria-expanded={isOpen}
@@ -80,31 +81,31 @@ export function NavHeader() {
}
aria-hidden={!isOpen}
>
<nav className="flex flex-col gap-4 p-4 text-sm font-medium text-zinc-400">
<nav className="flex flex-col gap-1 p-3 text-sm font-medium text-zinc-400">
<Link
href="/#features"
className="transition-colors hover:text-cyan-400"
className="flex min-h-11 items-center rounded-md px-3 transition-colors hover:bg-zinc-900 hover:text-cyan-400"
onClick={() => setIsOpen(false)}
>
{t("features")}
</Link>
<Link
href="/#agents"
className="transition-colors hover:text-cyan-400"
className="flex min-h-11 items-center rounded-md px-3 transition-colors hover:bg-zinc-900 hover:text-cyan-400"
onClick={() => setIsOpen(false)}
>
{t("agents")}
</Link>
<Link
href="/docs"
className="transition-colors hover:text-cyan-400"
className="flex min-h-11 items-center rounded-md px-3 transition-colors hover:bg-zinc-900 hover:text-cyan-400"
onClick={() => setIsOpen(false)}
>
{t("docs")}
</Link>
<Link
href="/manifesto"
className="transition-colors hover:text-cyan-400"
className="flex min-h-11 items-center rounded-md px-3 transition-colors hover:bg-zinc-900 hover:text-cyan-400"
onClick={() => setIsOpen(false)}
>
{t("manifesto")}
@@ -113,7 +114,7 @@ export function NavHeader() {
href="https://github.com/code-yeongyu/oh-my-openagent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 transition-colors hover:text-cyan-400 sm:hidden"
className="flex min-h-11 items-center gap-2 rounded-md px-3 transition-colors hover:bg-zinc-900 hover:text-cyan-400 sm:hidden"
onClick={() => setIsOpen(false)}
>
<GitHubMark className="h-4 w-4" />
+10 -2
View File
@@ -17,6 +17,13 @@ export interface StatsData {
weeklyDownloads: number
}
export interface FormattedStatsData {
readonly stars: string
readonly totalDownloads: string
readonly monthlyDownloads: string
readonly weeklyDownloads: string
}
let cache: StatsCache | null = null
function formatCount(num: number): string {
@@ -85,7 +92,8 @@ async function fetchAllNpmDownloadsForPackage(pkg: string): Promise<number> {
while (year <= now.getFullYear()) {
const start = `${year}-01-01`
const endDate = new Date(year, 11, 31)
const end = endDate > now ? now.toISOString().split("T")[0]! : `${year}-12-31`
const end =
endDate > now ? (now.toISOString().split("T")[0] ?? `${year}-12-31`) : `${year}-12-31`
try {
const res = await fetch(`https://api.npmjs.org/downloads/point/${start}:${end}/${pkg}`, {
@@ -129,7 +137,7 @@ export async function getStats(): Promise<StatsData> {
return data
}
export function formatStats(stats: StatsData) {
export function formatStats(stats: StatsData): FormattedStatsData {
return {
stars: formatCount(stats.stars),
totalDownloads: formatCount(stats.totalDownloads),
+10 -2
View File
@@ -28,7 +28,7 @@ function getInstallationDocsPath(pathname: string): string | null {
return locale ? `/${locale}/docs` : "/docs"
}
export default function middleware(request: NextRequest) {
export default function middleware(request: NextRequest): NextResponse {
const forwardedHost = request.headers.get("x-forwarded-host")
const requestHost = request.headers.get("host")
const hostname = (forwardedHost ?? requestHost ?? request.nextUrl.hostname).split(":")[0]
@@ -59,7 +59,15 @@ export default function middleware(request: NextRequest) {
export const config = {
matcher: [
"/",
"/((?!api|_next|_vercel|.*\\..*).+)",
/*
* Match everything except:
* - api routes
* - Next.js internals (_next, _vercel)
* - Next.js file-based metadata routes (opengraph-image, twitter-image, icon, apple-icon, manifest, robots, sitemap)
* These serve images / JSON / XML directly and must NOT be redirected by i18n.
* - Any path containing a dot (favicon.ico, *.webp, *.png, etc.)
*/
"/((?!api|_next|_vercel|opengraph-image|twitter-image|icon|apple-icon|manifest\\.webmanifest|robots\\.txt|sitemap\\.xml|.*\\..*).+)",
"/installation.md",
"/:locale(en|ko|ja|zh)/installation.md",
"/docs/installation.md",