一键导入
nextjs-core
Master Next.js 15 App Router architecture for building modern portfolio websites with Server Components, routing, and data patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Master Next.js 15 App Router architecture for building modern portfolio websites with Server Components, routing, and data patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Build accessible portfolio websites following WCAG guidelines with proper semantics, keyboard navigation, screen reader support, and inclusive design patterns.
Implement analytics, error monitoring, and performance tracking with Vercel Analytics, Google Analytics, Sentry, and custom event tracking.
Build server-side APIs using Next.js Server Actions and API routes.
Build production-ready contact forms with React Hook Form, Zod validation, Server Actions, and email services like Resend.
Manage portfolio content with MDX, Contentlayer, or local JSON for projects, blog posts, and dynamic content.
Implement robust dark mode and theming systems with next-themes, CSS custom properties, and Tailwind CSS for seamless user experience.
| name | nextjs-core |
| description | Master Next.js 15 App Router architecture for building modern portfolio websites with Server Components, routing, and data patterns. |
| author | Jaivish Chauhan @ GDG SSIT |
| version | 1.0.0 |
| url | https://github.com/JaivishChauhan/vibecoding-starter |
Next.js App Router represents a paradigm shift from "pages" to "layouts and components." We think in React Server Components first, adding client interactivity only where essential. This creates faster, more SEO-friendly portfolio sites.
├── app/
│ ├── (marketing)/ # Route group - no URL impact
│ │ ├── page.tsx # Home/Landing
│ │ ├── about/page.tsx
│ │ └── contact/page.tsx
│ ├── projects/
│ │ ├── page.tsx # Projects list
│ │ └── [slug]/page.tsx # Dynamic project detail
│ ├── blog/
│ │ ├── page.tsx
│ │ └── [slug]/page.tsx
│ ├── layout.tsx # Root layout (nav, footer)
│ ├── globals.css
│ ├── loading.tsx # Global loading UI
│ ├── error.tsx # Global error boundary
│ └── not-found.tsx # Custom 404
├── components/
│ ├── ui/ # Reusable primitives
│ ├── sections/ # Page sections (Hero, About, etc.)
│ └── layout/ # Header, Footer, Navigation
├── lib/
│ ├── utils.ts # Utility functions (cn, formatDate)
│ └── constants.ts # Site config, social links
├── content/ # MDX files or JSON data
└── public/
├── images/
└── resume.pdf
Layouts persist across navigations; templates remount.
// app/layout.tsx - Root Layout (REQUIRED)
import { Inter } from "next/font/google";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.variable}>
<body className="min-h-screen bg-background font-sans antialiased">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}
Organize routes without affecting URL structure.
app/
├── (marketing)/ # URL: /about, /contact
│ ├── about/page.tsx
│ └── contact/page.tsx
├── (portfolio)/ # URL: /projects, /blog
│ ├── projects/
│ └── blog/
└── layout.tsx
// app/projects/[slug]/page.tsx
import { getProjectBySlug, getAllProjects } from "@/lib/projects";
import { notFound } from "next/navigation";
interface Props {
params: Promise<{ slug: string }>;
}
// Generate static paths at build time
export async function generateStaticParams() {
const projects = await getAllProjects();
return projects.map((project) => ({
slug: project.slug,
}));
}
// Generate metadata per page
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) return { title: "Project Not Found" };
return {
title: project.title,
description: project.description,
openGraph: {
images: [project.image],
},
};
}
export default async function ProjectPage({ params }: Props) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) notFound();
return (
<article className="container py-12">
<h1 className="text-4xl font-bold">{project.title}</h1>
{/* Project content */}
</article>
);
}
Show multiple pages simultaneously (e.g., modal over content).
app/
├── @modal/
│ └── (.)projects/[slug]/page.tsx # Intercepted route
├── projects/
│ └── [slug]/page.tsx
└── layout.tsx
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
Fetch directly in async components - no useEffect needed.
// app/projects/page.tsx
import { ProjectCard } from "@/components/project-card";
async function getProjects() {
// Direct fetch with automatic deduplication
const res = await fetch("https://api.example.com/projects", {
next: { revalidate: 3600 }, // ISR: revalidate every hour
});
if (!res.ok) throw new Error("Failed to fetch projects");
return res.json();
}
export default async function ProjectsPage() {
const projects = await getProjects();
return (
<section className="container py-12">
<h1 className="mb-8 text-3xl font-bold">My Projects</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</section>
);
}
For portfolio content that rarely changes.
// lib/projects.ts
import { cache } from "react";
import projectsData from "@/content/projects.json";
// React cache() deduplicates calls in the same request
export const getAllProjects = cache(async () => {
return projectsData.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
});
export const getProjectBySlug = cache(async (slug: string) => {
return projectsData.find((p) => p.slug === slug) ?? null;
});
Next.js automatically deduplicates fetch requests.
// Both components call the same URL - only ONE request is made
async function ProjectHeader() {
const project = await fetch("/api/project/1").then((r) => r.json());
return <h1>{project.title}</h1>;
}
async function ProjectDetails() {
const project = await fetch("/api/project/1").then((r) => r.json());
return <p>{project.description}</p>;
}
// app/projects/loading.tsx
import { Skeleton } from "@/components/ui/skeleton";
export default function ProjectsLoading() {
return (
<section className="container py-12">
<Skeleton className="mb-8 h-10 w-48" />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-64 rounded-lg" />
))}
</div>
</section>
);
}
// app/projects/error.tsx
"use client";
import { useEffect } from "react";
import { Button } from "@/components/ui/button";
export default function ProjectsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="container flex min-h-[50vh] flex-col items-center justify-center gap-4">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<Button onClick={reset}>Try again</Button>
</div>
);
}
// app/not-found.tsx
import Link from "next/link";
import { Button } from "@/components/ui/button";
export default function NotFound() {
return (
<div className="container flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center">
<h1 className="text-6xl font-bold">404</h1>
<p className="text-xl text-muted-foreground">
This page doesn't exist or has been moved.
</p>
<Button asChild>
<Link href="/">Go Home</Link>
</Button>
</div>
);
}
import Image from "next/image";
// Remote images (configure in next.config.js)
<Image
src="https://images.unsplash.com/photo-xxx"
alt="Project screenshot"
width={1200}
height={630}
className="rounded-lg object-cover"
priority // Load immediately (above-the-fold)
/>;
// Local images (automatic size detection)
import heroImage from "@/public/images/hero.jpg";
<Image
src={heroImage}
alt="Hero background"
placeholder="blur" // Blur-up effect
fill // Fill parent container
className="object-cover"
sizes="100vw"
/>;
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "cdn.sanity.io",
},
],
},
};
module.exports = nextConfig;
// app/layout.tsx
import { Inter, Playfair_Display } from "next/font/google";
import localFont from "next/font/local";
// Google Fonts (automatic optimization)
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair",
display: "swap",
});
// Local Font
const calSans = localFont({
src: "../public/fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
display: "swap",
});
export default function RootLayout({ children }) {
return (
<html
lang="en"
className={`${inter.variable} ${playfair.variable} ${calSans.variable}`}
>
<body className="font-sans">{children}</body>
</html>
);
}
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--font-sans: var(--font-inter);
--font-heading: var(--font-cal);
--font-serif: var(--font-playfair);
}
}
import Link from 'next/link';
// Automatic prefetch on viewport enter (production)
<Link href="/projects">View Projects</Link>
// Disable prefetch for less important links
<Link href="/terms" prefetch={false}>Terms</Link>
// Programmatic navigation
'use client';
import { useRouter } from 'next/navigation';
function ContactButton() {
const router = useRouter();
return (
<button onClick={() => router.push('/contact')}>
Contact Me
</button>
);
}
# .env.local (git ignored)
NEXT_PUBLIC_SITE_URL=https://yourportfolio.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
# Server-only (no NEXT_PUBLIC_ prefix)
RESEND_API_KEY=re_xxxxx
NOTION_API_KEY=secret_xxxxx
// Access in code
// Server Component / Server Action
const apiKey = process.env.RESEND_API_KEY;
// Client Component (only NEXT_PUBLIC_*)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
// tsconfig.json
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Only add 'use client' when you NEED hooks or event handlers.
// ❌ Bad: Entire page is client
'use client';
export default function Page() { ... }
// ✅ Good: Only interactive part is client
// page.tsx (Server Component)
export default function Page() {
return (
<div>
<StaticContent />
<InteractiveWidget /> {/* This has 'use client' */}
</div>
);
}
Server Components can be async; Client Components cannot.
// ✅ Server Component
async function UserProfile() {
const user = await getUser();
return <div>{user.name}</div>;
}
// ❌ Client Component - won't work
'use client';
async function UserProfile() { ... } // Error!
Route params are now Promises in Next.js 15.
// Next.js 15
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// use id
}