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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user