Compare commits
3 Commits
237e5d403b
...
ba60d49fc6
| Author | SHA1 | Date | |
|---|---|---|---|
| ba60d49fc6 | |||
| 0bb3ee40c6 | |||
| 6badd76733 |
@@ -12,6 +12,7 @@ import { PostCard } from '@/components/post-card';
|
|||||||
import { PostStorylineNav } from '@/components/post-storyline-nav';
|
import { PostStorylineNav } from '@/components/post-storyline-nav';
|
||||||
import { SectionDivider } from '@/components/section-divider';
|
import { SectionDivider } from '@/components/section-divider';
|
||||||
import { FooterCue } from '@/components/footer-cue';
|
import { FooterCue } from '@/components/footer-cue';
|
||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return allPosts.map((post) => ({
|
return allPosts.map((post) => ({
|
||||||
@@ -76,8 +77,88 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
|
|
||||||
const hasToc = /<h[23]/.test(post.body.html);
|
const hasToc = /<h[23]/.test(post.body.html);
|
||||||
|
|
||||||
|
// Generate absolute URL for the post
|
||||||
|
const postUrl = `${siteConfig.url}${post.url}`;
|
||||||
|
|
||||||
|
// Get the OG image URL (same as in metadata)
|
||||||
|
const ogImageUrl = new URL('/api/og', siteConfig.url);
|
||||||
|
ogImageUrl.searchParams.set('title', post.title);
|
||||||
|
if (post.description) {
|
||||||
|
ogImageUrl.searchParams.set('description', post.description);
|
||||||
|
}
|
||||||
|
if (post.tags && post.tags.length > 0) {
|
||||||
|
ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image URL - prefer feature_image, fallback to OG image
|
||||||
|
const imageUrl = post.feature_image
|
||||||
|
? `${siteConfig.url}${post.feature_image.replace('../assets', '/assets')}`
|
||||||
|
: ogImageUrl.toString();
|
||||||
|
|
||||||
|
// BlogPosting Schema
|
||||||
|
const blogPostingSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
headline: post.title,
|
||||||
|
description: post.description || post.custom_excerpt || post.title,
|
||||||
|
image: imageUrl,
|
||||||
|
datePublished: post.published_at,
|
||||||
|
dateModified: post.updated_at || post.published_at,
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: post.authors?.[0] || siteConfig.author,
|
||||||
|
url: siteConfig.url,
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: siteConfig.name,
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
url: `${siteConfig.url}${siteConfig.avatar}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mainEntityOfPage: {
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': postUrl,
|
||||||
|
},
|
||||||
|
...(post.tags && post.tags.length > 0 && {
|
||||||
|
keywords: post.tags.join(', '),
|
||||||
|
articleSection: post.tags[0],
|
||||||
|
}),
|
||||||
|
inLanguage: siteConfig.defaultLocale,
|
||||||
|
url: postUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// BreadcrumbList Schema
|
||||||
|
const breadcrumbSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 1,
|
||||||
|
name: '首頁',
|
||||||
|
item: siteConfig.url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: '所有文章',
|
||||||
|
item: `${siteConfig.url}/blog`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 3,
|
||||||
|
name: post.title,
|
||||||
|
item: postUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<JsonLd data={blogPostingSchema} />
|
||||||
|
<JsonLd data={breadcrumbSchema} />
|
||||||
<ReadingProgress />
|
<ReadingProgress />
|
||||||
<PostLayout hasToc={hasToc} contentKey={slug}>
|
<PostLayout hasToc={hasToc} contentKey={slug}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiAlertTriangle } from 'react-icons/fi';
|
||||||
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
@@ -20,10 +19,7 @@ export default function Error({
|
|||||||
<div className="flex min-h-screen items-center justify-center px-4">
|
<div className="flex min-h-screen items-center justify-center px-4">
|
||||||
<div className="max-w-md text-center">
|
<div className="max-w-md text-center">
|
||||||
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20">
|
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20">
|
||||||
<FontAwesomeIcon
|
<FiAlertTriangle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||||
icon={faTriangleExclamation}
|
|
||||||
className="h-8 w-8 text-red-600 dark:text-red-400"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="mb-2 text-2xl font-semibold text-slate-900 dark:text-slate-100">
|
<h2 className="mb-2 text-2xl font-semibold text-slate-900 dark:text-slate-100">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { siteConfig } from '@/lib/config';
|
|||||||
import { LayoutShell } from '@/components/layout-shell';
|
import { LayoutShell } from '@/components/layout-shell';
|
||||||
import { ThemeProvider } from 'next-themes';
|
import { ThemeProvider } from 'next-themes';
|
||||||
import { Playfair_Display } from 'next/font/google';
|
import { Playfair_Display } from 'next/font/google';
|
||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
|
||||||
const playfair = Playfair_Display({
|
const playfair = Playfair_Display({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -49,9 +50,48 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
const theme = siteConfig.theme;
|
const theme = siteConfig.theme;
|
||||||
|
|
||||||
|
// WebSite Schema
|
||||||
|
const websiteSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: siteConfig.title,
|
||||||
|
description: siteConfig.description,
|
||||||
|
url: siteConfig.url,
|
||||||
|
inLanguage: siteConfig.defaultLocale,
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: siteConfig.author,
|
||||||
|
url: siteConfig.url,
|
||||||
|
},
|
||||||
|
potentialAction: {
|
||||||
|
'@type': 'SearchAction',
|
||||||
|
target: {
|
||||||
|
'@type': 'EntryPoint',
|
||||||
|
urlTemplate: `${siteConfig.url}/blog?search={search_term_string}`,
|
||||||
|
},
|
||||||
|
'query-input': 'required name=search_term_string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Organization Schema
|
||||||
|
const organizationSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: siteConfig.name,
|
||||||
|
url: siteConfig.url,
|
||||||
|
logo: `${siteConfig.url}${siteConfig.avatar}`,
|
||||||
|
sameAs: [
|
||||||
|
siteConfig.social.github,
|
||||||
|
siteConfig.social.twitter && `https://twitter.com/${siteConfig.social.twitter.replace('@', '')}`,
|
||||||
|
siteConfig.social.mastodon,
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={playfair.variable}>
|
<html lang={siteConfig.defaultLocale} suppressHydrationWarning className={playfair.variable}>
|
||||||
<body>
|
<body>
|
||||||
|
<JsonLd data={websiteSchema} />
|
||||||
|
<JsonLd data={organizationSchema} />
|
||||||
<style
|
<style
|
||||||
// Set CSS variables for accent colors (light + dark variants)
|
// Set CSS variables for accent colors (light + dark variants)
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|||||||
26
app/page.tsx
26
app/page.tsx
@@ -4,12 +4,35 @@ import { siteConfig } from '@/lib/config';
|
|||||||
import { PostListItem } from '@/components/post-list-item';
|
import { PostListItem } from '@/components/post-list-item';
|
||||||
import { TimelineWrapper } from '@/components/timeline-wrapper';
|
import { TimelineWrapper } from '@/components/timeline-wrapper';
|
||||||
import { SidebarLayout } from '@/components/sidebar-layout';
|
import { SidebarLayout } from '@/components/sidebar-layout';
|
||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const posts = getAllPostsSorted().slice(0, siteConfig.postsPerPage);
|
const posts = getAllPostsSorted().slice(0, siteConfig.postsPerPage);
|
||||||
|
|
||||||
|
// CollectionPage Schema for homepage
|
||||||
|
const collectionPageSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'CollectionPage',
|
||||||
|
name: `${siteConfig.name} 的最新動態`,
|
||||||
|
description: siteConfig.description,
|
||||||
|
url: siteConfig.url,
|
||||||
|
inLanguage: siteConfig.defaultLocale,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: siteConfig.title,
|
||||||
|
url: siteConfig.url,
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
'@type': 'Blog',
|
||||||
|
name: siteConfig.title,
|
||||||
|
description: siteConfig.description,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6">
|
<>
|
||||||
|
<JsonLd data={collectionPageSchema} />
|
||||||
|
<section className="space-y-6">
|
||||||
<SidebarLayout>
|
<SidebarLayout>
|
||||||
<header className="space-y-1 text-center">
|
<header className="space-y-1 text-center">
|
||||||
<h1 className="type-title font-bold text-slate-900 dark:text-slate-50">
|
<h1 className="type-title font-bold text-slate-900 dark:text-slate-50">
|
||||||
@@ -40,5 +63,6 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</SidebarLayout>
|
</SidebarLayout>
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ReadingProgress } from '@/components/reading-progress';
|
|||||||
import { PostLayout } from '@/components/post-layout';
|
import { PostLayout } from '@/components/post-layout';
|
||||||
import { ScrollReveal } from '@/components/scroll-reveal';
|
import { ScrollReveal } from '@/components/scroll-reveal';
|
||||||
import { SectionDivider } from '@/components/section-divider';
|
import { SectionDivider } from '@/components/section-divider';
|
||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return allPages.map((page) => ({
|
return allPages.map((page) => ({
|
||||||
@@ -39,8 +40,39 @@ export default async function StaticPage({ params }: Props) {
|
|||||||
|
|
||||||
const hasToc = /<h[23]/.test(page.body.html);
|
const hasToc = /<h[23]/.test(page.body.html);
|
||||||
|
|
||||||
|
// Generate absolute URL for the page
|
||||||
|
const pageUrl = `${siteConfig.url}${page.url}`;
|
||||||
|
|
||||||
|
// Get image URL if available
|
||||||
|
const imageUrl = page.feature_image
|
||||||
|
? `${siteConfig.url}${page.feature_image.replace('../assets', '/assets')}`
|
||||||
|
: `${siteConfig.url}${siteConfig.ogImage}`;
|
||||||
|
|
||||||
|
// WebPage Schema
|
||||||
|
const webPageSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: page.title,
|
||||||
|
description: page.description || page.title,
|
||||||
|
url: pageUrl,
|
||||||
|
image: imageUrl,
|
||||||
|
inLanguage: siteConfig.defaultLocale,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: siteConfig.title,
|
||||||
|
url: siteConfig.url,
|
||||||
|
},
|
||||||
|
...(page.published_at && {
|
||||||
|
datePublished: page.published_at,
|
||||||
|
}),
|
||||||
|
...(page.updated_at && {
|
||||||
|
dateModified: page.updated_at,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<JsonLd data={webPageSchema} />
|
||||||
<ReadingProgress />
|
<ReadingProgress />
|
||||||
<PostLayout hasToc={hasToc} contentKey={slug}>
|
<PostLayout hasToc={hasToc} contentKey={slug}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { getTagSlug } from '@/lib/posts';
|
|||||||
import { SidebarLayout } from '@/components/sidebar-layout';
|
import { SidebarLayout } from '@/components/sidebar-layout';
|
||||||
import { SectionDivider } from '@/components/section-divider';
|
import { SectionDivider } from '@/components/section-divider';
|
||||||
import { ScrollReveal } from '@/components/scroll-reveal';
|
import { ScrollReveal } from '@/components/scroll-reveal';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiTag } from 'react-icons/fi';
|
||||||
import { faTag } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
const slugs = new Set<string>();
|
const slugs = new Set<string>();
|
||||||
@@ -57,7 +56,7 @@ export default async function TagPage({ params }: Props) {
|
|||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<div className="motion-card mb-8 rounded-2xl border border-white/40 bg-white/60 p-8 text-center shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-slate-900/60">
|
<div className="motion-card mb-8 rounded-2xl border border-white/40 bg-white/60 p-8 text-center shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-slate-900/60">
|
||||||
<div className="inline-flex items-center gap-2 text-accent">
|
<div className="inline-flex items-center gap-2 text-accent">
|
||||||
<FontAwesomeIcon icon={faTag} className="h-5 w-5" />
|
<FiTag className="h-5 w-5" />
|
||||||
<span className="type-small uppercase tracking-[0.4em] text-slate-500 dark:text-slate-400">
|
<span className="type-small uppercase tracking-[0.4em] text-slate-500 dark:text-slate-400">
|
||||||
TAG ARCHIVE
|
TAG ARCHIVE
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiTag, FiTrendingUp } from 'react-icons/fi';
|
||||||
import { faTags, faFire } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { getAllTagsWithCount } from '@/lib/posts';
|
import { getAllTagsWithCount } from '@/lib/posts';
|
||||||
import { SectionDivider } from '@/components/section-divider';
|
import { SectionDivider } from '@/components/section-divider';
|
||||||
import { ScrollReveal } from '@/components/scroll-reveal';
|
import { ScrollReveal } from '@/components/scroll-reveal';
|
||||||
@@ -30,7 +29,7 @@ export default function TagIndexPage() {
|
|||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<div className="motion-card rounded-2xl border border-white/40 bg-white/60 p-8 text-center shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-slate-900/60">
|
<div className="motion-card rounded-2xl border border-white/40 bg-white/60 p-8 text-center shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-slate-900/60">
|
||||||
<div className="inline-flex items-center gap-2 text-accent">
|
<div className="inline-flex items-center gap-2 text-accent">
|
||||||
<FontAwesomeIcon icon={faTags} className="h-5 w-5" />
|
<FiTag className="h-5 w-5" />
|
||||||
<span className="type-small uppercase tracking-[0.4em] text-slate-500 dark:text-slate-400">
|
<span className="type-small uppercase tracking-[0.4em] text-slate-500 dark:text-slate-400">
|
||||||
標籤索引
|
標籤索引
|
||||||
</span>
|
</span>
|
||||||
@@ -65,7 +64,7 @@ export default function TagIndexPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="mt-1 inline-flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
|
<span className="mt-1 inline-flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
|
||||||
<FontAwesomeIcon icon={faFire} className="h-3 w-3 text-orange-400" />
|
<FiTrendingUp className="h-3 w-3 text-orange-400" />
|
||||||
熱度 #{index + 1}
|
熱度 #{index + 1}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FaGithub, FaTwitter, FaMastodon, FaGit, FaLinkedin } from 'react-icons/fa';
|
||||||
import {
|
import { FiMail, FiFeather } from 'react-icons/fi';
|
||||||
faGithub,
|
|
||||||
faTwitter,
|
|
||||||
faMastodon,
|
|
||||||
faGitAlt,
|
|
||||||
faLinkedin
|
|
||||||
} from '@fortawesome/free-brands-svg-icons';
|
|
||||||
import { faEnvelope, faPenNib } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { MetaItem } from './meta-item';
|
import { MetaItem } from './meta-item';
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
@@ -19,37 +12,37 @@ export function Hero() {
|
|||||||
key: 'github',
|
key: 'github',
|
||||||
href: social.github,
|
href: social.github,
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
icon: faGithub
|
icon: FaGithub
|
||||||
},
|
},
|
||||||
social.twitter && {
|
social.twitter && {
|
||||||
key: 'twitter',
|
key: 'twitter',
|
||||||
href: `https://twitter.com/${social.twitter.replace('@', '')}`,
|
href: `https://twitter.com/${social.twitter.replace('@', '')}`,
|
||||||
label: 'Twitter',
|
label: 'Twitter',
|
||||||
icon: faTwitter
|
icon: FaTwitter
|
||||||
},
|
},
|
||||||
social.mastodon && {
|
social.mastodon && {
|
||||||
key: 'mastodon',
|
key: 'mastodon',
|
||||||
href: social.mastodon,
|
href: social.mastodon,
|
||||||
label: 'Mastodon',
|
label: 'Mastodon',
|
||||||
icon: faMastodon
|
icon: FaMastodon
|
||||||
},
|
},
|
||||||
social.gitea && {
|
social.gitea && {
|
||||||
key: 'gitea',
|
key: 'gitea',
|
||||||
href: social.gitea,
|
href: social.gitea,
|
||||||
label: 'Gitea',
|
label: 'Gitea',
|
||||||
icon: faGitAlt
|
icon: FaGit
|
||||||
},
|
},
|
||||||
social.linkedin && {
|
social.linkedin && {
|
||||||
key: 'linkedin',
|
key: 'linkedin',
|
||||||
href: social.linkedin,
|
href: social.linkedin,
|
||||||
label: 'LinkedIn',
|
label: 'LinkedIn',
|
||||||
icon: faLinkedin
|
icon: FaLinkedin
|
||||||
},
|
},
|
||||||
social.email && {
|
social.email && {
|
||||||
key: 'email',
|
key: 'email',
|
||||||
href: `mailto:${social.email}`,
|
href: `mailto:${social.email}`,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
icon: faEnvelope
|
icon: FiMail
|
||||||
}
|
}
|
||||||
].filter(Boolean) as {
|
].filter(Boolean) as {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -73,7 +66,7 @@ export function Hero() {
|
|||||||
{name}
|
{name}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<MetaItem icon={faPenNib}>
|
<MetaItem icon={FiFeather}>
|
||||||
{tagline}
|
{tagline}
|
||||||
</MetaItem>
|
</MetaItem>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +80,7 @@ export function Hero() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="motion-link flex items-center gap-2 rounded-full bg-white/80 px-3 py-1 text-slate-600 shadow-sm ring-1 ring-slate-200 hover:-translate-y-0.5 hover:bg-accent-soft hover:text-accent hover:shadow-md dark:bg-slate-900/80 dark:text-slate-200 dark:ring-slate-700"
|
className="motion-link flex items-center gap-2 rounded-full bg-white/80 px-3 py-1 text-slate-600 shadow-sm ring-1 ring-slate-200 hover:-translate-y-0.5 hover:bg-accent-soft hover:text-accent hover:shadow-md dark:bg-slate-900/80 dark:text-slate-200 dark:ring-slate-700"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={item.icon} className="h-3.5 w-3.5 text-accent" />
|
<item.icon className="h-3.5 w-3.5 text-accent" />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
12
components/json-ld.tsx
Normal file
12
components/json-ld.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* JSON-LD component for rendering structured data
|
||||||
|
* Safely serializes and injects Schema.org structured data into the page
|
||||||
|
*/
|
||||||
|
export function JsonLd({ data }: { data: Record<string, any> }) {
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SiteHeader } from './site-header';
|
import { SiteHeader } from './site-header';
|
||||||
import { SiteFooter } from './site-footer';
|
import { SiteFooter } from './site-footer';
|
||||||
|
|
||||||
import { BackToTop } from './back-to-top';
|
import { BackToTop } from './back-to-top';
|
||||||
|
|
||||||
export function LayoutShell({ children }: { children: React.ReactNode }) {
|
export function LayoutShell({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FaMastodon } from 'react-icons/fa';
|
||||||
import { faMastodon } from '@fortawesome/free-brands-svg-icons';
|
import { FiArrowRight } from 'react-icons/fi';
|
||||||
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
parseMastodonUrl,
|
parseMastodonUrl,
|
||||||
@@ -76,10 +75,7 @@ export function MastodonFeed() {
|
|||||||
<section className="motion-card group rounded-xl border bg-white px-4 py-3 shadow-sm hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-900/90">
|
<section className="motion-card group rounded-xl border bg-white px-4 py-3 shadow-sm hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-900/90">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="type-small mb-3 flex items-center gap-2 font-semibold uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
|
<div className="type-small mb-3 flex items-center gap-2 font-semibold uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
|
||||||
<FontAwesomeIcon
|
<FaMastodon className="h-4 w-4 text-purple-500 dark:text-purple-400" />
|
||||||
icon={faMastodon}
|
|
||||||
className="h-4 w-4 text-purple-500 dark:text-purple-400"
|
|
||||||
/>
|
|
||||||
微網誌
|
微網誌
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,10 +115,7 @@ export function MastodonFeed() {
|
|||||||
{/* Boost indicator */}
|
{/* Boost indicator */}
|
||||||
{status.reblog && (
|
{status.reblog && (
|
||||||
<div className="type-small flex items-center gap-1 text-slate-400 dark:text-slate-500">
|
<div className="type-small flex items-center gap-1 text-slate-400 dark:text-slate-500">
|
||||||
<FontAwesomeIcon
|
<FiArrowRight className="h-2.5 w-2.5 rotate-90" />
|
||||||
icon={faArrowRight}
|
|
||||||
className="h-2.5 w-2.5 rotate-90"
|
|
||||||
/>
|
|
||||||
<span>轉推了</span>
|
<span>轉推了</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -162,7 +155,7 @@ export function MastodonFeed() {
|
|||||||
className="type-small mt-3 flex items-center justify-end gap-1.5 text-slate-500 transition-colors hover:text-accent-textLight dark:text-slate-400 dark:hover:text-accent-textDark"
|
className="type-small mt-3 flex items-center justify-end gap-1.5 text-slate-500 transition-colors hover:text-accent-textLight dark:text-slate-400 dark:hover:text-accent-textDark"
|
||||||
>
|
>
|
||||||
查看更多
|
查看更多
|
||||||
<FontAwesomeIcon icon={faArrowRight} className="h-3 w-3" />
|
<FiArrowRight className="h-3 w-3" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
|
|
||||||
interface MetaItemProps {
|
interface MetaItemProps {
|
||||||
icon: IconDefinition;
|
icon: IconType;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
tone?: 'default' | 'muted';
|
tone?: 'default' | 'muted';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetaItem({ icon, children, className, tone = 'default' }: MetaItemProps) {
|
export function MetaItem({ icon: Icon, children, className, tone = 'default' }: MetaItemProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -19,7 +18,7 @@ export function MetaItem({ icon, children, className, tone = 'default' }: MetaIt
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} className="h-3.5 w-3.5 text-slate-400 dark:text-slate-500" />
|
<Icon className="h-3.5 w-3.5 text-slate-400 dark:text-slate-500" />
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import {
|
import {
|
||||||
faBars,
|
FiMenu,
|
||||||
faXmark,
|
FiX,
|
||||||
faHouse,
|
FiHome,
|
||||||
faNewspaper,
|
FiFileText,
|
||||||
faFileLines,
|
FiFile,
|
||||||
faUser,
|
FiUser,
|
||||||
faEnvelope,
|
FiMail,
|
||||||
faLocationDot,
|
FiMapPin,
|
||||||
faPenNib,
|
FiFeather,
|
||||||
faTags,
|
FiTag,
|
||||||
faServer,
|
FiServer,
|
||||||
faMicrochip,
|
FiCpu,
|
||||||
faBarsStaggered
|
FiList
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from 'react-icons/fi';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export type IconKey =
|
export type IconKey =
|
||||||
@@ -33,17 +32,17 @@ export type IconKey =
|
|||||||
| 'menu';
|
| 'menu';
|
||||||
|
|
||||||
const ICON_MAP: Record<IconKey, any> = {
|
const ICON_MAP: Record<IconKey, any> = {
|
||||||
home: faHouse,
|
home: FiHome,
|
||||||
blog: faNewspaper,
|
blog: FiFileText,
|
||||||
file: faFileLines,
|
file: FiFile,
|
||||||
user: faUser,
|
user: FiUser,
|
||||||
contact: faEnvelope,
|
contact: FiMail,
|
||||||
location: faLocationDot,
|
location: FiMapPin,
|
||||||
pen: faPenNib,
|
pen: FiFeather,
|
||||||
tags: faTags,
|
tags: FiTag,
|
||||||
server: faServer,
|
server: FiServer,
|
||||||
device: faMicrochip,
|
device: FiCpu,
|
||||||
menu: faBarsStaggered
|
menu: FiList
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface NavLinkItem {
|
export interface NavLinkItem {
|
||||||
@@ -72,7 +71,7 @@ export function NavMenu({ items }: NavMenuProps) {
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={open ? faXmark : faBars} className="h-4 w-4" />
|
{open ? <FiX className="h-4 w-4" /> : <FiMenu className="h-4 w-4" />}
|
||||||
</button>
|
</button>
|
||||||
<nav
|
<nav
|
||||||
className={`${open ? 'flex' : 'hidden'} flex-col gap-2 sm:flex sm:flex-row sm:items-center sm:gap-3`}
|
className={`${open ? 'flex' : 'hidden'} flex-col gap-2 sm:flex sm:flex-row sm:items-center sm:gap-3`}
|
||||||
@@ -84,10 +83,10 @@ export function NavMenu({ items }: NavMenuProps) {
|
|||||||
className="motion-link type-nav group relative inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-slate-600 hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200"
|
className="motion-link type-nav group relative inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-slate-600 hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
{(() => {
|
||||||
icon={ICON_MAP[item.iconKey] ?? faFileLines}
|
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||||
className="h-3.5 w-3.5 text-slate-400 transition group-hover:text-accent"
|
return <Icon className="h-3.5 w-3.5 text-slate-400 transition group-hover:text-accent" />;
|
||||||
/>
|
})()}
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
<span className="absolute inset-x-3 -bottom-0.5 h-px origin-left scale-x-0 bg-accent transition duration-180 ease-snappy group-hover:scale-x-100" aria-hidden="true" />
|
<span className="absolute inset-x-3 -bottom-0.5 h-px origin-left scale-x-0 bg-accent transition duration-180 ease-snappy group-hover:scale-x-100" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from 'next/link';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import type { Post } from 'contentlayer2/generated';
|
import type { Post } from 'contentlayer2/generated';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { faCalendarDays, faTags } from '@fortawesome/free-solid-svg-icons';
|
import { FiCalendar, FiTag } from 'react-icons/fi';
|
||||||
import { MetaItem } from './meta-item';
|
import { MetaItem } from './meta-item';
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
@@ -35,14 +35,14 @@ export function PostCard({ post, showTags = true }: PostCardProps) {
|
|||||||
<div className="space-y-3 px-4 py-4">
|
<div className="space-y-3 px-4 py-4">
|
||||||
<div className="flex flex-wrap items-center gap-3 text-xs">
|
<div className="flex flex-wrap items-center gap-3 text-xs">
|
||||||
{post.published_at && (
|
{post.published_at && (
|
||||||
<MetaItem icon={faCalendarDays}>
|
<MetaItem icon={FiCalendar}>
|
||||||
{new Date(post.published_at).toLocaleDateString(
|
{new Date(post.published_at).toLocaleDateString(
|
||||||
siteConfig.defaultLocale
|
siteConfig.defaultLocale
|
||||||
)}
|
)}
|
||||||
</MetaItem>
|
</MetaItem>
|
||||||
)}
|
)}
|
||||||
{showTags && post.tags && post.tags.length > 0 && (
|
{showTags && post.tags && post.tags.length > 0 && (
|
||||||
<MetaItem icon={faTags} tone="muted">
|
<MetaItem icon={FiTag} tone="muted">
|
||||||
{post.tags.slice(0, 3).join(', ')}
|
{post.tags.slice(0, 3).join(', ')}
|
||||||
</MetaItem>
|
</MetaItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { FiList, FiChevronRight } from 'react-icons/fi';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faListUl, faChevronRight, faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { PostToc } from './post-toc';
|
import { PostToc } from './post-toc';
|
||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
@@ -23,70 +21,50 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
|||||||
)}>
|
)}>
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<motion.div
|
<div className={cn("mx-auto transition-all duration-500 ease-snappy", isTocOpen && hasToc ? "max-w-3xl" : "max-w-4xl")}>
|
||||||
layout
|
|
||||||
className={cn("mx-auto transition-all duration-500 ease-snappy", isTocOpen && hasToc ? "max-w-3xl" : "max-w-4xl")}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Sidebar (TOC) */}
|
{/* Desktop Sidebar (TOC) */}
|
||||||
<aside className="hidden lg:block">
|
<aside className="hidden lg:block">
|
||||||
<div className="sticky top-24 h-[calc(100vh-6rem)] overflow-hidden">
|
<div className="sticky top-24 h-[calc(100vh-6rem)] overflow-hidden">
|
||||||
<AnimatePresence mode="wait">
|
{isTocOpen && hasToc && (
|
||||||
{isTocOpen && hasToc && (
|
<div className="toc-sidebar h-full overflow-y-auto pr-2">
|
||||||
<motion.div
|
<PostToc key={contentKey} />
|
||||||
initial={{ opacity: 0, x: 20 }}
|
</div>
|
||||||
animate={{ opacity: 1, x: 0 }}
|
)}
|
||||||
exit={{ opacity: 0, x: 20 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="h-full overflow-y-auto pr-2"
|
|
||||||
>
|
|
||||||
<PostToc key={contentKey} />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile TOC Overlay */}
|
{/* Mobile TOC Overlay */}
|
||||||
<AnimatePresence>
|
{isTocOpen && hasToc && (
|
||||||
{isTocOpen && hasToc && (
|
<div className="toc-mobile fixed bottom-24 right-4 z-40 w-72 rounded-2xl border border-white/20 bg-white/90 p-6 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/90 lg:hidden">
|
||||||
<motion.div
|
<div className="max-h-[60vh] overflow-y-auto">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<PostToc key={contentKey} onLinkClick={() => setIsTocOpen(false)} />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
</div>
|
||||||
exit={{ opacity: 0, y: 20 }}
|
</div>
|
||||||
transition={{ duration: 0.2 }}
|
)}
|
||||||
className="fixed bottom-24 right-4 z-40 w-72 rounded-2xl border border-white/20 bg-white/90 p-6 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/90 lg:hidden"
|
|
||||||
>
|
|
||||||
<div className="max-h-[60vh] overflow-y-auto">
|
|
||||||
<PostToc key={contentKey} onLinkClick={() => setIsTocOpen(false)} />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Toggle Button (Glassmorphism Pill) */}
|
{/* Toggle Button (Glassmorphism Pill) */}
|
||||||
{hasToc && (
|
{hasToc && (
|
||||||
<motion.button
|
<button
|
||||||
layout
|
|
||||||
onClick={() => setIsTocOpen(!isTocOpen)}
|
onClick={() => setIsTocOpen(!isTocOpen)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed bottom-8 right-8 z-50 flex items-center gap-2 rounded-full border border-white/20 bg-white/80 px-4 py-2.5 shadow-lg backdrop-blur-md transition-all hover:bg-white hover:scale-105 dark:border-white/10 dark:bg-slate-900/80 dark:hover:bg-slate-900",
|
"toc-button fixed bottom-8 right-8 z-50 flex items-center gap-2 rounded-full border border-white/20 bg-white/80 px-4 py-2.5 shadow-lg backdrop-blur-md hover:bg-white dark:border-white/10 dark:bg-slate-900/80 dark:hover:bg-slate-900",
|
||||||
"text-sm font-medium text-slate-600 dark:text-slate-300",
|
"text-sm font-medium text-slate-600 dark:text-slate-300",
|
||||||
"lg:right-20" // Adjust position for desktop
|
"lg:right-20" // Adjust position for desktop
|
||||||
)}
|
)}
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
aria-label="Toggle Table of Contents"
|
aria-label="Toggle Table of Contents"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
{isTocOpen ? (
|
||||||
icon={isTocOpen ? faChevronRight : faListUl}
|
<FiChevronRight className="h-3.5 w-3.5" />
|
||||||
className="h-3.5 w-3.5"
|
) : (
|
||||||
/>
|
<FiList className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
<span>{isTocOpen ? 'Hide' : 'Menu'}</span>
|
<span>{isTocOpen ? 'Hide' : 'Menu'}</span>
|
||||||
</motion.button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from 'next/link';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Post } from 'contentlayer2/generated';
|
import { Post } from 'contentlayer2/generated';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { faCalendarDays, faTags } from '@fortawesome/free-solid-svg-icons';
|
import { FiCalendar, FiTag } from 'react-icons/fi';
|
||||||
import { MetaItem } from './meta-item';
|
import { MetaItem } from './meta-item';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -37,14 +37,14 @@ export function PostListItem({ post }: Props) {
|
|||||||
<div className="flex-1 space-y-1.5">
|
<div className="flex-1 space-y-1.5">
|
||||||
<div className="flex flex-wrap gap-3 text-xs">
|
<div className="flex flex-wrap gap-3 text-xs">
|
||||||
{post.published_at && (
|
{post.published_at && (
|
||||||
<MetaItem icon={faCalendarDays}>
|
<MetaItem icon={FiCalendar}>
|
||||||
{new Date(post.published_at).toLocaleDateString(
|
{new Date(post.published_at).toLocaleDateString(
|
||||||
siteConfig.defaultLocale
|
siteConfig.defaultLocale
|
||||||
)}
|
)}
|
||||||
</MetaItem>
|
</MetaItem>
|
||||||
)}
|
)}
|
||||||
{post.tags && post.tags.length > 0 && (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<MetaItem icon={faTags} tone="muted">
|
<MetaItem icon={FiTag} tone="muted">
|
||||||
{post.tags.slice(0, 3).join(', ')}
|
{post.tags.slice(0, 3).join(', ')}
|
||||||
</MetaItem>
|
</MetaItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,13 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Post, Page } from 'contentlayer2/generated';
|
import { Post, Page } from 'contentlayer2/generated';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiArrowDown, FiArrowUp, FiSearch, FiList } from 'react-icons/fi';
|
||||||
import {
|
|
||||||
faArrowDownWideShort,
|
|
||||||
faArrowUpWideShort,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faListUl
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { PostListItem } from './post-list-item';
|
import { PostListItem } from './post-list-item';
|
||||||
import { TimelineWrapper } from './timeline-wrapper';
|
import { TimelineWrapper } from './timeline-wrapper';
|
||||||
@@ -83,7 +77,7 @@ export function PostListWithControls({ posts, pageSize }: Props) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col gap-4 text-xs text-slate-500 dark:text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 text-xs text-slate-500 dark:text-slate-400 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="inline-flex items-center gap-2 rounded-full bg-slate-100/70 px-2 py-1 text-slate-600 dark:bg-slate-800/70 dark:text-slate-300">
|
<div className="inline-flex items-center gap-2 rounded-full bg-slate-100/70 px-2 py-1 text-slate-600 dark:bg-slate-800/70 dark:text-slate-300">
|
||||||
<FontAwesomeIcon icon={faListUl} className="h-3.5 w-3.5" />
|
<FiList className="h-3.5 w-3.5" />
|
||||||
<span>排序</span>
|
<span>排序</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -93,7 +87,7 @@ export function PostListWithControls({ posts, pageSize }: Props) {
|
|||||||
: 'bg-white text-slate-600 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-700'
|
: 'bg-white text-slate-600 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faArrowDownWideShort} className="h-3 w-3" />
|
<FiArrowDown className="h-3 w-3" />
|
||||||
新到舊
|
新到舊
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -104,7 +98,7 @@ export function PostListWithControls({ posts, pageSize }: Props) {
|
|||||||
: 'bg-white text-slate-600 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-700'
|
: 'bg-white text-slate-600 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faArrowUpWideShort} className="h-3 w-3" />
|
<FiArrowUp className="h-3 w-3" />
|
||||||
舊到新
|
舊到新
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,8 +107,7 @@ export function PostListWithControls({ posts, pageSize }: Props) {
|
|||||||
搜尋文章
|
搜尋文章
|
||||||
</label>
|
</label>
|
||||||
<div className="relative w-full sm:w-64">
|
<div className="relative w-full sm:w-64">
|
||||||
<FontAwesomeIcon
|
<FiSearch
|
||||||
icon={faMagnifyingGlass}
|
|
||||||
className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400"
|
className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Post } from 'contentlayer2/generated';
|
import { Post } from 'contentlayer2/generated';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiArrowLeft, FiArrowRight } from 'react-icons/fi';
|
||||||
import { faArrowLeftLong, faArrowRightLong } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
current: Post;
|
current: Post;
|
||||||
@@ -84,10 +83,11 @@ function Station({ station }: { station: StationConfig }) {
|
|||||||
className={`group flex flex-col gap-1 text-center transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 dark:focus-visible:ring-blue-300 ${alignClass}`}
|
className={`group flex flex-col gap-1 text-center transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 dark:focus-visible:ring-blue-300 ${alignClass}`}
|
||||||
>
|
>
|
||||||
<p className="text-[11px] uppercase tracking-[0.4em] text-slate-400 transition group-hover:text-blue-500 dark:text-slate-500">
|
<p className="text-[11px] uppercase tracking-[0.4em] text-slate-400 transition group-hover:text-blue-500 dark:text-slate-500">
|
||||||
<FontAwesomeIcon
|
{align === 'end' ? (
|
||||||
icon={align === 'end' ? faArrowLeftLong : faArrowRightLong}
|
<FiArrowLeft className="mr-1 inline h-3 w-3" />
|
||||||
className="mr-1 h-3 w-3"
|
) : (
|
||||||
/>
|
<FiArrowRight className="mr-1 inline h-3 w-3" />
|
||||||
|
)}
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-semibold leading-snug tracking-tight text-slate-900 transition group-hover:text-blue-600 dark:text-slate-50 dark:group-hover:text-blue-300">
|
<p className="text-lg font-semibold leading-snug tracking-tight text-slate-900 transition group-hover:text-blue-600 dark:text-slate-50 dark:group-hover:text-blue-300">
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiList } from 'react-icons/fi';
|
||||||
import { faListUl } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
interface TocItem {
|
interface TocItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -101,7 +100,7 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<nav className="not-prose sticky top-20 text-slate-500 dark:text-slate-400">
|
<nav className="not-prose sticky top-20 text-slate-500 dark:text-slate-400">
|
||||||
<div className="mb-2 inline-flex items-center gap-2 font-semibold text-slate-700 dark:text-slate-200">
|
<div className="mb-2 inline-flex items-center gap-2 font-semibold text-slate-700 dark:text-slate-200">
|
||||||
<FontAwesomeIcon icon={faListUl} className="h-4 w-4 text-slate-400" />
|
<FiList className="h-4 w-4 text-slate-400" />
|
||||||
目錄
|
目錄
|
||||||
</div>
|
</div>
|
||||||
<div className="relative pl-4">
|
<div className="relative pl-4">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FaGithub, FaMastodon, FaLinkedin } from 'react-icons/fa';
|
||||||
import { faGithub, faMastodon, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
import { FiTrendingUp, FiArrowRight } from 'react-icons/fi';
|
||||||
import { faFire, faArrowRight } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { getAllTagsWithCount } from '@/lib/posts';
|
import { getAllTagsWithCount } from '@/lib/posts';
|
||||||
import { allPages } from 'contentlayer2/generated';
|
import { allPages } from 'contentlayer2/generated';
|
||||||
@@ -21,19 +20,19 @@ export function RightSidebar() {
|
|||||||
siteConfig.social.github && {
|
siteConfig.social.github && {
|
||||||
key: 'github',
|
key: 'github',
|
||||||
href: siteConfig.social.github,
|
href: siteConfig.social.github,
|
||||||
icon: faGithub,
|
icon: FaGithub,
|
||||||
label: 'GitHub'
|
label: 'GitHub'
|
||||||
},
|
},
|
||||||
siteConfig.social.mastodon && {
|
siteConfig.social.mastodon && {
|
||||||
key: 'mastodon',
|
key: 'mastodon',
|
||||||
href: siteConfig.social.mastodon,
|
href: siteConfig.social.mastodon,
|
||||||
icon: faMastodon,
|
icon: FaMastodon,
|
||||||
label: 'Mastodon'
|
label: 'Mastodon'
|
||||||
},
|
},
|
||||||
siteConfig.social.linkedin && {
|
siteConfig.social.linkedin && {
|
||||||
key: 'linkedin',
|
key: 'linkedin',
|
||||||
href: siteConfig.social.linkedin,
|
href: siteConfig.social.linkedin,
|
||||||
icon: faLinkedin,
|
icon: FaLinkedin,
|
||||||
label: 'LinkedIn'
|
label: 'LinkedIn'
|
||||||
}
|
}
|
||||||
].filter(Boolean) as { key: string; href: string; icon: any; label: string }[];
|
].filter(Boolean) as { key: string; href: string; icon: any; label: string }[];
|
||||||
@@ -77,7 +76,7 @@ export function RightSidebar() {
|
|||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
className="motion-link inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-600 hover:-translate-y-0.5 hover:bg-accent-soft hover:text-accent dark:bg-slate-800 dark:text-slate-200"
|
className="motion-link inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-600 hover:-translate-y-0.5 hover:bg-accent-soft hover:text-accent dark:bg-slate-800 dark:text-slate-200"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={item.icon} className="h-4 w-4" />
|
<item.icon className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +97,7 @@ export function RightSidebar() {
|
|||||||
{tags.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<section className="motion-card rounded-xl border bg-white px-4 py-3 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100">
|
<section className="motion-card rounded-xl border bg-white px-4 py-3 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100">
|
||||||
<h2 className="type-small flex items-center gap-2 font-semibold uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
|
<h2 className="type-small flex items-center gap-2 font-semibold uppercase tracking-[0.35em] text-slate-500 dark:text-slate-400">
|
||||||
<FontAwesomeIcon icon={faFire} className="h-3 w-3 text-orange-400" />
|
<FiTrendingUp className="h-3 w-3 text-orange-400" />
|
||||||
熱門標籤
|
熱門標籤
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-2 flex flex-wrap gap-2 text-base">
|
<div className="mt-2 flex flex-wrap gap-2 text-base">
|
||||||
@@ -120,7 +119,7 @@ export function RightSidebar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex items-center justify-between type-small text-slate-500 dark:text-slate-400">
|
<div className="mt-3 flex items-center justify-between type-small text-slate-500 dark:text-slate-400">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<FontAwesomeIcon icon={faArrowRight} className="h-3 w-3" />
|
<FiArrowRight className="h-3 w-3" />
|
||||||
一覽所有標籤
|
一覽所有標籤
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiSearch, FiX } from 'react-icons/fi';
|
||||||
import { faMagnifyingGlass, faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
interface SearchModalProps {
|
interface SearchModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -136,7 +135,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 dark:border-slate-700">
|
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4 dark:border-slate-700">
|
||||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
|
||||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="h-5 w-5" />
|
<FiSearch className="h-5 w-5" />
|
||||||
<span className="text-sm font-medium">全站搜尋</span>
|
<span className="text-sm font-medium">全站搜尋</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -144,7 +143,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
|||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
aria-label="關閉搜尋"
|
aria-label="關閉搜尋"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} className="h-5 w-5" />
|
<FiX className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +198,7 @@ export function SearchButton({ onClick }: { onClick: () => void }) {
|
|||||||
className="motion-link inline-flex h-9 items-center gap-2 rounded-full bg-slate-100 px-3 py-1.5 text-sm text-slate-600 transition hover:bg-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
className="motion-link inline-flex h-9 items-center gap-2 rounded-full bg-slate-100 px-3 py-1.5 text-sm text-slate-600 transition hover:bg-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||||
aria-label="搜尋 (Cmd+K)"
|
aria-label="搜尋 (Cmd+K)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="h-3.5 w-3.5" />
|
<FiSearch className="h-3.5 w-3.5" />
|
||||||
<span className="hidden sm:inline">搜尋</span>
|
<span className="hidden sm:inline">搜尋</span>
|
||||||
<kbd className="hidden rounded bg-white px-1.5 py-0.5 text-xs font-semibold text-slate-500 shadow-sm dark:bg-slate-900 dark:text-slate-400 sm:inline-block">
|
<kbd className="hidden rounded bg-white px-1.5 py-0.5 text-xs font-semibold text-slate-500 shadow-sm dark:bg-slate-900 dark:text-slate-400 sm:inline-block">
|
||||||
⌘K
|
⌘K
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FaGithub, FaTwitter, FaMastodon, FaGit, FaLinkedin } from 'react-icons/fa';
|
||||||
import {
|
import { FiMail } from 'react-icons/fi';
|
||||||
faGithub,
|
|
||||||
faTwitter,
|
|
||||||
faMastodon,
|
|
||||||
faGitAlt,
|
|
||||||
faLinkedin
|
|
||||||
} from '@fortawesome/free-brands-svg-icons';
|
|
||||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
// Calculate year at build time for PPR compatibility
|
// Calculate year at build time for PPR compatibility
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
@@ -20,37 +13,37 @@ export function SiteFooter() {
|
|||||||
key: 'github',
|
key: 'github',
|
||||||
href: social.github,
|
href: social.github,
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
icon: faGithub
|
icon: FaGithub
|
||||||
},
|
},
|
||||||
social.twitter && {
|
social.twitter && {
|
||||||
key: 'twitter',
|
key: 'twitter',
|
||||||
href: `https://twitter.com/${social.twitter.replace('@', '')}`,
|
href: `https://twitter.com/${social.twitter.replace('@', '')}`,
|
||||||
label: 'Twitter',
|
label: 'Twitter',
|
||||||
icon: faTwitter
|
icon: FaTwitter
|
||||||
},
|
},
|
||||||
social.mastodon && {
|
social.mastodon && {
|
||||||
key: 'mastodon',
|
key: 'mastodon',
|
||||||
href: social.mastodon,
|
href: social.mastodon,
|
||||||
label: 'Mastodon',
|
label: 'Mastodon',
|
||||||
icon: faMastodon
|
icon: FaMastodon
|
||||||
},
|
},
|
||||||
social.gitea && {
|
social.gitea && {
|
||||||
key: 'gitea',
|
key: 'gitea',
|
||||||
href: social.gitea,
|
href: social.gitea,
|
||||||
label: 'Gitea',
|
label: 'Gitea',
|
||||||
icon: faGitAlt
|
icon: FaGit
|
||||||
},
|
},
|
||||||
social.linkedin && {
|
social.linkedin && {
|
||||||
key: 'linkedin',
|
key: 'linkedin',
|
||||||
href: social.linkedin,
|
href: social.linkedin,
|
||||||
label: 'LinkedIn',
|
label: 'LinkedIn',
|
||||||
icon: faLinkedin
|
icon: FaLinkedin
|
||||||
},
|
},
|
||||||
social.email && {
|
social.email && {
|
||||||
key: 'email',
|
key: 'email',
|
||||||
href: `mailto:${social.email}`,
|
href: `mailto:${social.email}`,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
icon: faEnvelope
|
icon: FiMail
|
||||||
}
|
}
|
||||||
].filter(Boolean) as {
|
].filter(Boolean) as {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -75,7 +68,7 @@ export function SiteFooter() {
|
|||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
className="text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100"
|
className="text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={item.icon} className="h-4 w-4" />
|
<item.icon className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FiMoon, FiSun } from 'react-icons/fi';
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@@ -27,12 +26,11 @@ export function ThemeToggle() {
|
|||||||
onClick={() => setTheme(next)}
|
onClick={() => setTheme(next)}
|
||||||
aria-label={theme === 'dark' ? '切換為淺色主題' : '切換為深色主題'}
|
aria-label={theme === 'dark' ? '切換為淺色主題' : '切換為深色主題'}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
{isDark ? (
|
||||||
icon={isDark ? faSun : faMoon}
|
<FiSun className="h-4 w-4 rotate-0 text-amber-400 transition-transform duration-260 ease-snappy" />
|
||||||
className={`h-4 w-4 transition-transform duration-260 ease-snappy ${
|
) : (
|
||||||
isDark ? 'rotate-0 text-amber-400' : 'rotate-180 text-blue-500'
|
<FiMoon className="h-4 w-4 rotate-180 text-blue-500 transition-transform duration-260 ease-snappy" />
|
||||||
}`}
|
)}
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||||
|
|
||||||
|
const withBundleAnalyzer = bundleAnalyzer({
|
||||||
|
enabled: process.env.ANALYZE === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// Image optimization configuration
|
// Image optimization configuration
|
||||||
@@ -35,4 +41,4 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withBundleAnalyzer(nextConfig);
|
||||||
|
|||||||
304
package-lock.json
generated
304
package-lock.json
generated
@@ -10,14 +10,9 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
|
||||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
|
||||||
"@vercel/og": "^0.8.5",
|
"@vercel/og": "^0.8.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"contentlayer2": "^0.5.8",
|
"contentlayer2": "^0.5.8",
|
||||||
"framer-motion": "^12.23.24",
|
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"markdown-wasm": "^1.2.0",
|
"markdown-wasm": "^1.2.0",
|
||||||
"next": "^16.0.3",
|
"next": "^16.0.3",
|
||||||
@@ -25,6 +20,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
@@ -35,6 +31,7 @@
|
|||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@next/bundle-analyzer": "^16.0.3",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
@@ -452,6 +449,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@effect-ts/core": {
|
"node_modules/@effect-ts/core": {
|
||||||
"version": "0.60.5",
|
"version": "0.60.5",
|
||||||
"resolved": "https://registry.npmjs.org/@effect-ts/core/-/core-0.60.5.tgz",
|
"resolved": "https://registry.npmjs.org/@effect-ts/core/-/core-0.60.5.tgz",
|
||||||
@@ -534,7 +541,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/memoize": "^0.9.0"
|
"@emotion/memoize": "^0.9.0"
|
||||||
}
|
}
|
||||||
@@ -1126,65 +1132,6 @@
|
|||||||
"integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==",
|
"integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
|
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==",
|
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/react-fontawesome": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-5OUQH9aDH/xHJwnpD4J7oEdGvFGJgYnGe0UebaPIdMW9UxYC/f5jv2VjVEgnikdJN0HL8yQxp9Nq+7gqGZpIIA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@fortawesome/fontawesome-svg-core": "~6 || ~7",
|
|
||||||
"react": "^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@grpc/grpc-js": {
|
"node_modules/@grpc/grpc-js": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz",
|
||||||
@@ -2009,6 +1956,16 @@
|
|||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@next/bundle-analyzer": {
|
||||||
|
"version": "16.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.0.3.tgz",
|
||||||
|
"integrity": "sha512-6Xo8f8/ZXtASfTPa6TH1aUn+xDg9Pkyl1YHVxu+89cVdLH7MnYjxv3rPOfEJ9BwCZCU2q4Flyw5MwltfD2pGbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"webpack-bundle-analyzer": "4.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.0.3",
|
"version": "16.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz",
|
||||||
@@ -2590,6 +2547,13 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@polka/url": {
|
||||||
|
"version": "1.0.0-next.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
|
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@protobufjs/aspromise": {
|
"node_modules/@protobufjs/aspromise": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
@@ -3465,6 +3429,19 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -4513,6 +4490,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/debounce": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -4660,6 +4644,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/duplexer": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@@ -5756,33 +5747,6 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
|
||||||
"version": "12.23.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
|
|
||||||
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"motion-dom": "^12.23.23",
|
|
||||||
"motion-utils": "^12.23.6",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emotion/is-prop-valid": "*",
|
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@emotion/is-prop-valid": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -6104,6 +6068,22 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gzip-size": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"duplexer": "^0.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||||
@@ -6430,6 +6410,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-escaper": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html-void-elements": {
|
"node_modules/html-void-elements": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||||
@@ -6864,6 +6851,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
@@ -8555,21 +8552,16 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
"node_modules/mrmime": {
|
||||||
"version": "12.23.23",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"motion-utils": "^12.23.6"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-utils": {
|
|
||||||
"version": "12.23.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
|
||||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -8930,6 +8922,16 @@
|
|||||||
"node": ">= 14.17.0"
|
"node": ">= 14.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/opener": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(WTFPL OR MIT)",
|
||||||
|
"bin": {
|
||||||
|
"opener": "bin/opener-bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -9489,6 +9491,15 @@
|
|||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@@ -10318,6 +10329,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sirv": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@polka/url": "^1.0.0-next.24",
|
||||||
|
"mrmime": "^2.0.0",
|
||||||
|
"totalist": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.7.6",
|
"version": "0.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||||
@@ -10941,6 +10967,16 @@
|
|||||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/totalist": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tree-dump": {
|
"node_modules/tree-dump": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
|
||||||
@@ -11480,6 +11516,44 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webpack-bundle-analyzer": {
|
||||||
|
"version": "4.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz",
|
||||||
|
"integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@discoveryjs/json-ext": "0.5.7",
|
||||||
|
"acorn": "^8.0.4",
|
||||||
|
"acorn-walk": "^8.0.0",
|
||||||
|
"commander": "^7.2.0",
|
||||||
|
"debounce": "^1.2.1",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"gzip-size": "^6.0.0",
|
||||||
|
"html-escaper": "^2.0.2",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"opener": "^1.5.2",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"sirv": "^2.0.3",
|
||||||
|
"ws": "^7.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -11690,6 +11764,28 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"dev": "concurrently \"contentlayer2 dev\" \"next dev --turbo\"",
|
"dev": "concurrently \"contentlayer2 dev\" \"next dev --turbo\"",
|
||||||
"sync-assets": "node scripts/sync-assets.mjs",
|
"sync-assets": "node scripts/sync-assets.mjs",
|
||||||
"build": "npm run sync-assets && contentlayer2 build && next build && npx pagefind --site .next && rm -rf public/_pagefind && cp -r .next/pagefind public/_pagefind",
|
"build": "npm run sync-assets && contentlayer2 build && next build && npx pagefind --site .next && rm -rf public/_pagefind && cp -r .next/pagefind public/_pagefind",
|
||||||
|
"build:analyze": "ANALYZE=true npm run build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"contentlayer": "contentlayer build"
|
"contentlayer": "contentlayer build"
|
||||||
@@ -17,14 +18,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
|
||||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
|
||||||
"@vercel/og": "^0.8.5",
|
"@vercel/og": "^0.8.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"contentlayer2": "^0.5.8",
|
"contentlayer2": "^0.5.8",
|
||||||
"framer-motion": "^12.23.24",
|
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"markdown-wasm": "^1.2.0",
|
"markdown-wasm": "^1.2.0",
|
||||||
"next": "^16.0.3",
|
"next": "^16.0.3",
|
||||||
@@ -32,6 +28,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
@@ -42,6 +39,7 @@
|
|||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@next/bundle-analyzer": "^16.0.3",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
|
|||||||
@@ -233,6 +233,69 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TOC transitions - replaces Framer Motion */
|
||||||
|
.toc-sidebar {
|
||||||
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar-exit {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-sidebar-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-mobile {
|
||||||
|
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-mobile-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-mobile-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-mobile-exit {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-mobile-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-button {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-button:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.type-display {
|
.type-display {
|
||||||
font-size: clamp(2.2rem, 1.6rem + 2.4vw, 3.5rem);
|
font-size: clamp(2.2rem, 1.6rem + 2.4vw, 3.5rem);
|
||||||
|
|||||||
Reference in New Issue
Block a user