Remove next-view-transitions and use native View Transition API

- Remove external next-view-transitions dependency
- Use Next.js 16 native navigation and Safari 18+ native View Transition API
- Add ViewTransitionProvider for minimal wrapping with Safari 18+ detection
- Updated all Link imports from external package to next/link
- Removed link-wrapper.tsx and view-transitions-wrapper.tsx

This resolves Safari compatibility issues while maintaining transitions on modern browsers.
This commit is contained in:
2026-03-14 23:00:21 +08:00
parent efb57b691b
commit 1b495d2d2d
33 changed files with 1124 additions and 830 deletions

View File

@@ -1,4 +1,4 @@
import { Link } from 'next-view-transitions';
import Link from 'next/link';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
@@ -222,7 +222,7 @@ export default async function BlogPostPage({ params }: Props) {
href={`/tags/${encodeURIComponent(
t.toLowerCase().replace(/\s+/g, '-')
)}`}
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100"
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-white"
>
#{t}
</Link>

View File

@@ -1,4 +1,4 @@
import { Link } from 'next-view-transitions';
import Link from 'next/link';
import { getAllPostsSorted } from '@/lib/posts';
import { PostListWithControls } from '@/components/post-list-with-controls';
import { TimelineWrapper } from '@/components/timeline-wrapper';

View File

@@ -7,7 +7,7 @@ import { ThemeProvider } from 'next-themes';
import { Playfair_Display, LXGW_WenKai_TC } from 'next/font/google';
import { JsonLd } from '@/components/json-ld';
import { WebVitals } from '@/components/web-vitals';
import { ViewTransitions } from 'next-view-transitions';
import { ViewTransitionProvider } from '@/components/view-transition-provider';
import NextTopLoader from 'nextjs-toploader';
const playfair = Playfair_Display({
@@ -17,12 +17,12 @@ const playfair = Playfair_Display({
});
const lxgwWenKai = LXGW_WenKai_TC({
weight: ['400', '700'], // 只加载 Regular 和 Bold
weight: ['400', '700'],
subsets: ['latin'],
variable: '--font-serif-cn',
display: 'swap',
preload: true,
adjustFontFallback: false, // 中文字体不需要 fallback 调整,使用系统字体作为 fallback
adjustFontFallback: false,
});
export const metadata: Metadata = {
@@ -88,7 +88,6 @@ export default async function RootLayout({
.slice(0, 5)
.map((p) => ({ title: p.title, url: p.url }));
// WebSite Schema
const websiteSchema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
@@ -111,7 +110,6 @@ export default async function RootLayout({
},
};
// Organization Schema
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
@@ -125,28 +123,27 @@ export default async function RootLayout({
].filter(Boolean),
};
return (
<ViewTransitions>
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={`${playfair.variable} ${lxgwWenKai.variable}`}>
<head>
{/* Preconnect to Google Fonts for faster font loading */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body>
<NextTopLoader
color={theme.accent}
height={3}
showSpinner={false}
speed={200}
shadow={`0 0 10px ${theme.accent}, 0 0 5px ${theme.accent}`}
/>
<JsonLd data={websiteSchema} />
<JsonLd data={organizationSchema} />
<style
// Set CSS variables for accent colors (light + dark variants)
dangerouslySetInnerHTML={{
__html: `
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={`${playfair.variable} ${lxgwWenKai.variable}`}>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body>
<NextTopLoader
color={theme.accent}
height={3}
showSpinner={false}
speed={200}
shadow={`0 0 10px ${theme.accent}, 0 0 5px ${theme.accent}`}
/>
<JsonLd data={websiteSchema} />
<JsonLd data={organizationSchema} />
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--color-accent: ${theme.accent};
--color-accent-soft: ${theme.accentSoft};
@@ -155,13 +152,14 @@ export default async function RootLayout({
}
`
}}
/>
/>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<LayoutShell recentPosts={recentPosts}>{children}</LayoutShell>
<ViewTransitionProvider>
<LayoutShell recentPosts={recentPosts}>{children}</LayoutShell>
</ViewTransitionProvider>
</ThemeProvider>
<WebVitals />
</body>
</html>
</ViewTransitions>
<WebVitals />
</body>
</html>
);
}

View File

@@ -1,4 +1,4 @@
import { Link } from 'next-view-transitions';
import Link from 'next/link';
import { getAllPostsSorted } from '@/lib/posts';
import { siteConfig } from '@/lib/config';
import { PostListItem } from '@/components/post-list-item';
@@ -51,7 +51,7 @@ export default function HomePage() {
<Link
href="/blog"
prefetch={true}
className="text-xs text-blue-600 hover:underline dark:text-blue-400"
className="text-xs text-accent hover:underline"
>
</Link>

View File

@@ -1,4 +1,4 @@
import { Link } from 'next-view-transitions';
import Link from 'next/link';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';

View File

@@ -1,4 +1,4 @@
import { Link } from 'next-view-transitions';
import Link from 'next/link';
import type { Metadata } from 'next';
import { FiTag, FiTrendingUp } from 'react-icons/fi';
import { getAllTagsWithCount } from '@/lib/posts';
@@ -87,7 +87,7 @@ export default function TagIndexPage() {
>
<span className={`mb-3 block h-1.5 w-16 rounded-full bg-gradient-to-r ${color}`} aria-hidden="true" />
<div className="flex items-center justify-between">
<h2 className="type-subtitle font-semibold text-slate-900 group-hover:text-blue-600 dark:text-slate-50 dark:group-hover:text-blue-400">
<h2 className="type-subtitle font-semibold text-slate-900 group-hover:text-accent dark:text-slate-50 dark:group-hover:text-accent">
{tag}
</h2>
<span className="type-small text-slate-600 dark:text-slate-300">

View File

@@ -1,9 +1,8 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
export default function Template({ children }: { children: React.ReactNode }) {
const containerRef = useRef<HTMLDivElement>(null);
const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);
useEffect(() => {
@@ -14,26 +13,6 @@ export default function Template({ children }: { children: React.ReactNode }) {
return () => mq.removeEventListener('change', handler);
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
if (prefersReducedMotion) {
container.style.animation = 'none';
container.style.opacity = '1';
container.style.transform = 'none';
return;
}
// Trigger animation on mount
container.style.animation = 'none';
void container.offsetHeight;
container.style.animation = 'pageEnter 0.45s cubic-bezier(0.32, 0.72, 0, 1) forwards';
}, [children, prefersReducedMotion]);
return (
<div ref={containerRef} className="page-transition">
{children}
</div>
);
// ViewTransitions handles page transitions - no additional wrapper needed
return <>{children}</>;
}