GitHub Repository Health Dashboard
Build and deploy a Next.js dashboard for live public GitHub repository signals.
Introduction
30 Second Summary
Hiring conversations move faster when you can show a working product instead of only describing your skills. A live demo gives every engineering decision something concrete to point to.
In this project, you will build a responsive repository health dashboard with Next.js. The finished app searches live GitHub data from a public Vercel URL.
What You'll Build
Picture entering a public repository into your live app to reveal its current health signals in a polished dashboard you can demonstrate during an interview.
By the end of this project, you'll have:
- A live repository search that opens a dedicated dashboard for any public owner plus repository name.
- A server-rendered signal view with current repository metrics from the GitHub REST API plus a direct link to the source on GitHub.
- A resilient production demo that replaces a missing-repository crash with a polished recovery screen.
- Secret Mission: Add an operational API-budget badge showing the remaining request allowance plus its reset time.
Are there any prerequisites?
This medium project assumes professional programming experience plus a Mac with Visual Studio Code and a GitHub account.
The walkthrough handles local tool verification plus Vercel account setup before each dependency is needed.
Before We Start
Before any hands-on work, choose the public GitHub repository you want to demonstrate. Identify the engineering judgment you want your server-rendered dashboard to prove.
Set Up the Pinned Next.js Project
Every later feature depends on a reproducible Next.js runtime. Framework changes can alter generated files or application behavior while you build.
A pinned baseline keeps those moving parts stable. You will verify Node.js before installing the exact dependency tree with npm.
You will also confirm Git meets the version used for deployment later. Then you will open the project in Visual Studio Code with a small runnable page.
In this step, get ready to:
- Verify the local development toolchain.
- Create the pinned project files.
- Install the dependencies and serve the first page.
Verify the Mac toolchain
Next.js requires Node.js 20.9 or later. This project uses Node.js 24.22.1 LTS as its reproducible target.
- Press Cmd+Space to open Spotlight Search.
- Type Terminal into the search field.
- Press Enter to open Terminal.
- Check your Node.js version by running this command:
node --version
What does this check?
The command prints the active Node.js version. That version determines whether Next.js can start on your Mac.
✔️ I see a supported version
Your output is supported if it shows v24.22.1 or another release at or above 20.9.
- Continue to the npm check below.
ⓧ I see an older version
The installed release is below the minimum supported by this project. Install the pinned LTS release before continuing.
- Visit the official Node.js download page.
- Install Node.js 24.22.1 LTS for macOS.
- Quit Terminal after the installer finishes.
- Reopen Terminal through Spotlight Search.
- Repeat the Node.js version check above.
ⓧ Command not found
Terminal cannot find a Node.js installation. The official installer adds the runtime and its bundled npm CLI.
- Visit the official Node.js download page.
- Install Node.js 24.22.1 LTS for macOS.
- Quit Terminal after the installer finishes.
- Reopen Terminal through Spotlight Search.
- Repeat the Node.js version check above.
- Confirm the bundled npm CLI is available by running this command:
npm -v
What does this check?
The command prints npm's installed version. A version number confirms that the package installer bundled with Node.js is available.
npm still unavailable?
Quit Terminal after installing Node.js. A newly opened Terminal session reloads the command paths created by the installer.
If the command still fails, reinstall the pinned Node.js LTS release from the official download page.
Help me troubleshoot my npm installation.
Git records the project history that you will push to GitHub later. Version 2.28 introduced the initialization option used by this project.
- Check your Git version by running this command:
git version
What does this check?
The command prints the Git version currently available in Terminal. The project needs Git 2.28 or later for its planned initialization command.
✔️ I see version 2.28 or higher
Git is ready for the deployment workflow later in the project.
- Continue to project creation below.
ⓧ I see an older version
Your installed Git release needs an update before the deployment step.
- Visit the official Git installation page for macOS.
- Follow one documented macOS installation option.
- Quit Terminal after the installation finishes.
- Reopen Terminal through Spotlight Search.
- Repeat the Git version check above.
ⓧ Command not found
Git is unavailable in the current Terminal session. Install it through one of the official macOS options.
- Visit the official Git installation page for macOS.
- Follow one documented macOS installation option.
- Quit Terminal after the installation finishes.
- Reopen Terminal through Spotlight Search.
- Repeat the Git version check above.
Your Mac now has the runtime, package installer, and version-control baseline this project needs. That removes environment drift from the feature work ahead.
Create the pinned project files
The project uses the App Router with strict TypeScript settings. You will create the configuration first so every installed package resolves to the intended version.
- Create the github-repo-health-dashboard folder on your Desktop by running these commands:
cd ~/Desktop
mkdir github-repo-health-dashboard
ls
What do these commands do?
- The first command moves Terminal to your Desktop.
- The second command creates the project folder.
- The final command lists the Desktop contents so you can confirm the folder exists.
You should see github-repo-health-dashboard in the Terminal output.
Folder already exists?
A message saying the folder already exists means your Desktop contains a folder with that name. Use the existing folder only if it is empty.
Help me inspect the existing project folder safely.
- Press Cmd+Space to open Spotlight Search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Click File in the top menu bar.
- Select Open Folder....
- Select Desktop in the folder dialog.
- Select the github-repo-health-dashboard folder.
- Select Open.
Seeing a workspace trust prompt?
Visual Studio Code may ask whether you trust the folder's authors. This folder contains code you are creating yourself.
- Select Yes, I trust the authors to enable the project tools.
The package.json file pins every runtime package and development type package. Exact pins make the install reproducible across machines.
- Select New File... in the Explorer toolbar.
- Enter package.json as the file name.
- Paste this package configuration into package.json.
{
"name": "github-repo-health-dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.4.2",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@types/node": "26.6.5",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"typescript": "7.0.2"
}
}
What does this file control?
- The scripts object defines the development, production build, and production start commands.
- The dependencies object pins Next.js and React packages used at runtime.
- The devDependencies object pins TypeScript plus the editor type definitions.
- Save package.json by pressing Cmd+S.
The Explorer now lists package.json inside github-repo-health-dashboard.
Seeing JSON errors?
Check that every property name remains inside double quotes. Confirm that the final property in each object has no trailing comma.
Help me fix my package.json syntax.
The TypeScript configuration enables strict checking. It also includes the generated types that Next.js maintains during development.
- Select New File... in the Explorer toolbar.
- Enter tsconfig.json as the file name.
- Paste this configuration into tsconfig.json.
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"]
}
What does this configuration do?
- The strict setting enables TypeScript's strict checks.
- The moduleResolution setting matches the bundler-based module system used by Next.js.
- The include entries cover source files plus generated Next.js types.
- Save tsconfig.json by pressing Cmd+S.
The Explorer now lists both project configuration files.
TypeScript configuration underlined?
Some type paths remain unresolved until the dependency installation and first development run complete. Check the JSON punctuation if Visual Studio Code reports a syntax problem.
Help me diagnose my tsconfig.json warning.
The ignore file keeps generated dependencies, build output, deployment metadata, logs, and machine-specific files out of Git history.
- Select New File... in the Explorer toolbar.
- Enter .gitignore as the file name.
- Paste these ignore patterns into .gitignore.
node_modules
.next
out
.vercel
*.log
.DS_Store
next-env.d.ts
What stays out of Git?
The ignored paths can be regenerated or belong to one machine. Keeping them out of commits makes the repository smaller and safer to share.
- Save .gitignore by pressing Cmd+S.
The Explorer now shows .gitignore beside the two JSON files.
Can't see the ignore file?
Confirm the file name begins with a single period. The full name must be .gitignore.
Help me find my .gitignore file in Visual Studio Code.
An App Router project needs a root layout. The page beneath it becomes the first React Server Component served at the home URL.
- Select New Folder in the Explorer toolbar.
- Enter src as the folder name.
- Select the new src folder.
- Select New Folder in the Explorer toolbar.
- Enter app as the folder name.
The Explorer now shows the nested path src/app.
- Select the app folder in the Explorer.
- Select New File... in the Explorer toolbar.
- Enter layout.tsx as the file name.
- Paste this root layout into src/app/layout.tsx.
import "./globals.css";
export const metadata = {
title: "Repository Health Dashboard",
description: "Inspect public GitHub repository signals.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
What does this layout do?
- The stylesheet import applies the shared visual foundation to every route.
- The metadata supplies the browser title and page description.
- The RootLayout function wraps every page in the required HTML document structure.
- Save src/app/layout.tsx by pressing Cmd+S.
The Explorer now lists layout.tsx under src/app.
Seeing unresolved React types?
The React type package is declared in package.json. Its definitions become available after the dependency installation.
Help me inspect my RootLayout error.
- Select the app folder in the Explorer.
- Select New File... in the Explorer toolbar.
- Enter page.tsx as the file name.
- Paste this initial home page into src/app/page.tsx.
export default function Home() {
return (
<main className="shell">
<h1>Repository Health Dashboard</h1>
</main>
);
}
What does this page do?
The Home Server Component renders the first visible heading. The shell class connects it to the shared stylesheet.
- Save src/app/page.tsx by pressing Cmd+S.
The Explorer now lists page.tsx beside layout.tsx.
Page file in the wrong folder?
Confirm the Explorer path is src/app/page.tsx. A page created beside package.json does not become the App Router home page.
Help me check my App Router file structure.
The shared stylesheet contains the full design foundation used throughout the project. It includes current global styles plus selectors that later dashboard screens reuse.
- Select the app folder in the Explorer.
- Select New File... in the Explorer toolbar.
- Enter globals.css as the file name.
- Open the full-code tab below.
- Copy the complete src/app/globals.css block into the new file.
✔️ Awesome, I've got everything!
Great. Save every authored file before installing the dependency tree.
ⓧ I'd like to double check the full code
Compare each authored file with the complete versions below. Generated files appear after the installation and development server run.
{
"name": "github-repo-health-dashboard",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.4.2",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@types/node": "26.6.5",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"typescript": "7.0.2"
}
}
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"]
}
node_modules
.next
out
.vercel
*.log
.DS_Store
next-env.d.ts
import "./globals.css";
export const metadata = {
title: "Repository Health Dashboard",
description: "Inspect public GitHub repository signals.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
export default function Home() {
return (
<main className="shell">
<h1>Repository Health Dashboard</h1>
</main>
);
}
:root {
color-scheme: dark;
--background: #07111f;
--panel: rgba(17, 31, 51, 0.86);
--panel-strong: #12213a;
--border: rgba(148, 163, 184, 0.24);
--text: #f8fafc;
--muted: #9fb0c7;
--accent: #67e8f9;
--accent-strong: #22d3ee;
}
* {
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--background);
}
body {
min-height: 100vh;
margin: 0;
color: var(--text);
background:
radial-gradient(circle at 15% 10%, rgba(34, 211, 238, 0.16), transparent 30rem),
radial-gradient(circle at 85% 20%, rgba(99, 102, 241, 0.14), transparent 28rem),
var(--background);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
a {
color: inherit;
}
button,
input {
font: inherit;
}
button,
a,
input {
outline-offset: 4px;
}
.shell {
width: min(1040px, calc(100% - 32px));
margin: 0 auto;
padding: 72px 0;
}
.hero {
max-width: 760px;
margin-bottom: 32px;
}
.eyebrow {
margin: 0 0 10px;
color: var(--accent);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1,
h2,
p {
overflow-wrap: anywhere;
}
h1 {
margin: 0;
font-size: clamp(2.3rem, 7vw, 5rem);
line-height: 0.98;
letter-spacing: -0.055em;
}
h2 {
margin: 0;
font-size: clamp(1.35rem, 3vw, 2rem);
letter-spacing: -0.025em;
}
.lede,
.description {
color: var(--muted);
font-size: 1.05rem;
line-height: 1.75;
}
.lede {
max-width: 640px;
margin: 24px 0 0;
}
.panel,
.preview {
border: 1px solid var(--border);
border-radius: 24px;
background: var(--panel);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(18px);
}
.panel {
padding: clamp(24px, 5vw, 44px);
}
.search-form {
display: grid;
gap: 18px;
margin-top: 28px;
}
.field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.field {
display: grid;
gap: 8px;
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.field input {
width: 100%;
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px 16px;
color: var(--text);
background: rgba(4, 12, 25, 0.72);
}
.field input:focus {
border-color: var(--accent);
}
button,
.github-link {
display: inline-flex;
width: fit-content;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
padding: 12px 18px;
color: #06202a;
background: var(--accent);
font-weight: 800;
text-decoration: none;
cursor: pointer;
}
button:hover,
.github-link:hover {
background: var(--accent-strong);
}
.hint {
margin: 0;
color: var(--muted);
font-size: 0.88rem;
}
.preview {
margin-top: 24px;
padding: 28px;
}
.preview-grid,
.stats-grid,
.skeleton-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-top: 24px;
}
.preview-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.preview-grid article,
.stat-card {
border: 1px solid var(--border);
border-radius: 18px;
padding: 18px;
background: rgba(4, 12, 25, 0.48);
}
.preview-grid span,
.stat-label {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.82rem;
}
.preview-grid strong,
.stat-value {
font-size: 1.05rem;
}
.dashboard {
display: grid;
gap: 18px;
}
.back-link {
width: fit-content;
color: var(--muted);
text-decoration: none;
}
.back-link:hover {
color: var(--text);
}
.repo-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
}
.repo-heading h1,
.error-panel h1 {
font-size: clamp(2rem, 6vw, 4rem);
}
.description {
max-width: 760px;
margin: 24px 0;
}
.stat-value {
display: block;
font-size: clamp(1.2rem, 3vw, 1.75rem);
}
.meta-row {
display: flex;
justify-content: space-between;
gap: 16px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 0.9rem;
}
.error-panel {
max-width: 720px;
margin: 8vh auto 0;
}
.skeleton-panel {
overflow: hidden;
}
.skeleton {
border-radius: 12px;
background: linear-gradient(
90deg,
rgba(148, 163, 184, 0.08),
rgba(148, 163, 184, 0.2),
rgba(148, 163, 184, 0.08)
);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
.skeleton-title {
width: min(420px, 80%);
height: 58px;
}
.skeleton-copy {
width: min(680px, 100%);
height: 24px;
margin-top: 28px;
}
.skeleton-card {
min-height: 104px;
}
.api-budget {
margin: 20px 0 0;
color: var(--accent);
font-size: 0.9rem;
font-weight: 700;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
@media (max-width: 720px) {
.shell {
padding: 44px 0;
}
.field-grid,
.preview-grid,
.stats-grid,
.skeleton-grid {
grid-template-columns: 1fr;
}
.repo-heading,
.meta-row {
align-items: flex-start;
flex-direction: column;
}
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
}
}
The package-lock.json file is generated by npm from this pinned dependency tree. Next.js generates and manages next-env.d.ts after the development server starts.
What does the stylesheet prepare?
The theme variables establish the dashboard's dark palette. Global shell and typography rules style the initial page immediately.
Form, card, route-state, responsive, and reduced-motion selectors support the dashboard screens built in later steps.
- Save src/app/globals.css by pressing Cmd+S.
- Confirm the file starts with the :root selector.
- Confirm the file ends with the reduced-motion media query.
Stylesheet looks incomplete?
Compare the first and final selectors with the full-file reference. Missing braces usually indicate that part of the stylesheet was skipped during copying.
Help me compare my globals.css file.
Launch the pinned application
The authored files describe the application. npm now resolves those declarations into an installed dependency tree and records it in a lock file.
- Click Terminal in the Visual Studio Code menu bar.
- Select New Terminal.
- Install the pinned dependencies by running this command:
npm install
What does this command create?
npm reads the exact versions in package.json. It installs them into node_modules and records the resolved dependency tree in package-lock.json.
- Confirm the generated files are present by running this command:
ls
What should the file list confirm?
The output should include node_modules and package-lock.json. Those entries confirm that npm completed the installation.
Dependency installation failed?
Confirm the integrated terminal is inside the github-repo-health-dashboard folder. Its Explorer should show the same package.json you created.
Review the first error in the Terminal output. Package errors later in the output often follow from that first failure.
Help me troubleshoot npm install.
Before you start the development server, what page title do you expect the browser to show?
- Start the Next.js development server by running this command:
npm run dev
What does the development command do?
The dev script starts the Next.js development server. Next.js also generates and manages next-env.d.ts for the project's TypeScript environment.
The process stays attached to this terminal while the local site is running. Pressing Ctrl+C stops it when you are finished.
- Press Cmd+Space to open Spotlight Search.
- Type Safari into the search field.
- Press Enter to open the browser.
- Enter http://localhost:3000 in the address bar.
- Press Enter to load the local application.
You should see Repository Health Dashboard as a large heading on the dark page. That proves the pinned Next.js application is serving your TypeScript page through the root layout.
- Return to the Explorer in Visual Studio Code.
- Confirm next-env.d.ts now appears at the top level of the project.
Local page not loading?
Keep the integrated terminal running while you load the URL. The development server must remain active for the browser to connect.
Check the Terminal for a compilation error naming one of your authored files. Compare that file with the full-code tab above.
Help me debug my local Next.js page.
Your pinned application is live with its generated dependency and type files in place. Next up, you will turn this first heading into a responsive static repository dashboard.
Build the Static Dashboard
Your local Next.js runtime already serves the project. A single heading cannot show whether repository signals are easy to scan.
A static React mock keeps network behavior out of the test loop. You can settle the visual hierarchy before the API arrives.
In this step, get ready to:
- Render a static repository panel for vercel/next.js.
- Display the repository description plus four health signals.
- Confirm the dashboard adapts to a narrow browser viewport.
Build the repository panel
A useful dashboard establishes the repository identity before presenting its numbers. This first version adds the name plus the descriptive context around it.
- In the VS Code Explorer sidebar, select src/app/page.tsx.
- Press Cmd+A (macOS) or Ctrl+A (Windows) to select the initial component.
- Replace the selected component by pasting the code below:
export default function Home() {
return (
<main className="shell dashboard">
<section className="panel">
{/* Keep the repository identity prominent in the static preview. */}
<div className="repo-heading">
<div>
<p className="eyebrow">vercel</p>
<h1>next.js</h1>
</div>
</div>
{/* Use fixed copy while the layout is disconnected from the API. */}
<p className="description">
A framework for building full-stack web applications with React.
</p>
<div className="meta-row">
<span>vercel/next.js</span>
<span>Updated Oct 22, 2026</span>
</div>
</section>
</main>
);
}
What does this code do?
- The Home function renders the static dashboard page.
- The shell class controls the page width plus its outer spacing.
- The dashboard class creates space between dashboard sections.
- The fixed repository content makes the visual check repeatable before live data arrives.
- Save src/app/page.tsx by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Switch back to the browser tab from earlier.
- Refresh http://localhost:3000.
You should see a dark panel headed by next.js. The repository description appears above the updated date.
Good progress. The page now reads like a repository dashboard instead of a setup check.
Still seeing only the original heading?
Confirm that you edited src/app/page.tsx inside the github-repo-health-dashboard folder. Check that the file is saved.
Look at the terminal from earlier for a compile error. A missing closing tag or brace can stop the page from updating.
Help me debug my static repository panel.
Add the repository signals
Repository signals become easier to compare when each value has the same visual weight. A four-card grid gives every metric a consistent place.
- In src/app/page.tsx, find the description paragraph that ends with </p>.
- Place the cursor on the blank line below that closing tag.
- Insert the four static signal cards by pasting this block:
{/* Group the fixed signals into a responsive comparison grid. */}
<div className="stats-grid">
<article className="stat-card">
<span className="stat-label">Stars</span>
<strong className="stat-value">130,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Forks</span>
<strong className="stat-value">28,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Open items</span>
<strong className="stat-value">2,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Language</span>
<strong className="stat-value">TypeScript</strong>
</article>
</div>
How do the cards work?
- The stats-grid container gives all four signals the same layout.
- Each stat-card groups one label with one value.
- The existing stylesheet supplies the borders plus the dark card background.
- The fixed values provide a stable mock for the live data flow you add next.
- Save src/app/page.tsx.
- Switch back to the browser tab from earlier.
- Refresh the page.
You should see four cards between the description and the updated date. The card labels are Stars, Forks, Open items, and Language.
That is the main visual system working. Each repository signal now has a clear place in the panel.
Cards missing from the panel?
Confirm that the stats-grid block sits inside the panel section. It should appear between the description and the metadata row.
Compare the opening tags with their matching closing tags. An incomplete article element can stop the page from compiling.
Help me fix my repository signal cards.
✔️ Awesome, I've got everything!
Your static repository panel now includes all four signal cards. Make sure src/app/page.tsx is saved.
ⓧ I'd like to double check the full code
Compare your complete src/app/page.tsx with this version:
export default function Home() {
return (
<main className="shell dashboard">
<section className="panel">
{/* Keep the repository identity prominent in the static preview. */}
<div className="repo-heading">
<div>
<p className="eyebrow">vercel</p>
<h1>next.js</h1>
</div>
</div>
{/* Use fixed copy while the layout is disconnected from the API. */}
<p className="description">
A framework for building full-stack web applications with React.
</p>
{/* Group the fixed signals into a responsive comparison grid. */}
<div className="stats-grid">
<article className="stat-card">
<span className="stat-label">Stars</span>
<strong className="stat-value">130,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Forks</span>
<strong className="stat-value">28,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Open items</span>
<strong className="stat-value">2,000+</strong>
</article>
<article className="stat-card">
<span className="stat-label">Language</span>
<strong className="stat-value">TypeScript</strong>
</article>
</div>
<div className="meta-row">
<span>vercel/next.js</span>
<span>Updated Oct 22, 2026</span>
</div>
</section>
</main>
);
}
Test the responsive layout
The existing CSS switches the dashboard grids to one column below 720px. This keeps each signal readable on a narrow screen.
Before you resize the page, predict whether the four cards stay in one row or stack vertically.
- Switch back to the browser tab from earlier.
- Refresh http://localhost:3000.
- Drag the browser window edge until the content area is narrower than 720px.
You should see the four signal cards stack into one column. The repository heading plus its descriptive copy should remain readable.
- Widen the browser window again.
You should see the four cards return to one row when enough space is available. This confirms the static dashboard responds to both viewport sizes.
Cards staying in four columns?
Confirm that src/app/globals.css remains imported by src/app/layout.tsx. Check that the stylesheet contains the responsive rule for .stats-grid.
Make the browser content area narrower if the window frame still leaves more space than expected.
Help me debug the responsive card layout.
You have the visual proof now. Your dashboard stays readable at both wide and narrow viewport sizes.
The static interface is ready. Next, you will replace the mock signals with live data from the GitHub REST API.
Replace the Mock Data with GitHub Data
Your polished dashboard now proves the visual hierarchy. Its repository signals are still fixed in source code, which prevents the project from demonstrating a real API integration.
This step connects the dashboard to the GitHub REST API through a typed server-side request. Next.js keeps each successful response in its server-side cache for 300 seconds.
In this step, get ready to:
- Define the typed repository data returned by GitHub.
- Fetch live repository data through a cached server-side request.
- Render the response through the existing dashboard.
Create the typed GitHub client
A typed data-access file gives the rest of the app one reliable description of a repository. TypeScript can then flag any field name that does not match that description.
- In the Visual Studio Code file sidebar, create a folder named lib inside src.
- Create github.ts inside src/lib.
You should now see src/lib/github.ts in the file tree.
- Define the repository response shape by pasting this code into src/lib/github.ts:
export type Repository = {
name: string;
full_name: string;
description: string | null;
html_url: string;
language: string | null;
stargazers_count: number;
forks_count: number;
open_issues_count: number;
updated_at: string;
owner: {
login: string;
};
};
What does this type describe?
- The Repository type lists the GitHub response fields used by the dashboard.
- The description field accepts null because a public repository may have no description.
- The nested owner.login field preserves the structure of the API response.
- Save src/lib/github.ts.
- Confirm the new type has no red error underlines in the editor.
Seeing type errors?
Check each property name against the code above. Make sure the closing braces for owner and Repository are both present.
Ask for help with the exact underline you see: Help me fix the TypeScript errors in my Repository type.
The response shape protects the UI after data arrives. The next function handles the network boundary that produces that data.
- Add the repository request below the Repository type by pasting this code:
export async function getRepository(
owner: string,
name: string,
): Promise<Repository> {
const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
{
headers: {
Accept: "application/vnd.github+json",
},
next: {
revalidate: 300,
},
},
);
if (!response.ok) {
throw new Error(`GitHub request failed with status ${response.status}`);
}
return (await response.json()) as Repository;
}
What does this request do?
- The owner and name values are encoded before they enter the repository endpoint.
- The Accept header requests GitHub's recommended JSON media type.
- The revalidate value lets Next.js reuse a successful response for 300 seconds.
- The response.ok check throws for every non-success response in this first version.
- The final cast tells TypeScript to treat the parsed JSON as a Repository.
- Save src/lib/github.ts.
- Confirm getRepository has no red error underlines in the editor.
Request function showing errors?
Check that getRepository sits below the closing brace of the Repository type. Confirm every parenthesis in the fetch call has a matching closing parenthesis.
Share the exact editor error here: Help me debug my getRepository function.
Connect the dashboard to live data
A Server Component can wait for repository data before it sends the finished page to the browser. The API request stays on the server side of the application.
- Return to src/app/page.tsx from the previous step.
- Update the opening of src/app/page.tsx to match this reference:
import { getRepository } from "../lib/github";
export default async function Home() {
const repository = await getRepository("vercel", "next.js");
const updatedLabel = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).format(new Date(repository.updated_at));
How does the page load data?
- The import makes getRepository available to the page.
- The async function can wait for the server-side request to finish.
- The hard-coded arguments keep this step focused on vercel/next.js.
- The date formatter turns updated_at into a readable date.
- Save src/app/page.tsx.
- Refresh the local dashboard in your browser.
You should see the dashboard render without an error. This confirms the server completed its first repository request.
Dashboard replaced by an error?
Check the development terminal for the failed request status. Confirm your Mac has an active internet connection.
Check that the import path is exactly ../lib/github from src/app/page.tsx.
Get help with the failure here: Help me debug the failed GitHub repository request.
The repository response now exists inside Home. Each hard-coded display value can now be replaced with a field from that response.
- Replace the current static metadata row with this live version:
<div className="meta-row">
<span>{repository.full_name}</span>
<span>Updated {updatedLabel}</span>
</div>
What changes in the metadata row?
The row now reads the full repository name from repository.full_name. It also displays the formatted value created from repository.updated_at.
- Save src/app/page.tsx.
- Refresh the browser to confirm the metadata row shows vercel/next.js with an updated date.
The bottom row now proves that the response has reached the page. The signal cards need the same treatment.
- Replace the current hard-coded stats array with this live version:
const stats = [
{
label: "Stars",
value: repository.stargazers_count.toLocaleString("en-US"),
},
{
label: "Forks",
value: repository.forks_count.toLocaleString("en-US"),
},
{
label: "Open items",
value: repository.open_issues_count.toLocaleString("en-US"),
},
{
label: "Language",
value: repository.language ?? "Not reported",
},
];
How do the cards use the response?
- The star count comes from stargazers_count.
- The fork count comes from forks_count.
- The open-item count comes from open_issues_count.
- The language card uses Not reported when GitHub returns null.
- The number formatter adds separators for large counts.
- Save src/app/page.tsx.
- Refresh the browser to load the live stars, forks, open items, and language.
The four cards now reflect the server response. The repository identity and description are the final hard-coded pieces.
Cards not rendering?
Confirm the existing JSX still maps over stats. Check that each API field matches the spelling in the Repository type.
Ask for help with the current page code: Help me fix the live repository cards.
- Replace the current static repository heading and description with this live version:
<div className="repo-heading">
<div>
<p className="eyebrow">{repository.owner.login}</p>
<h1>{repository.name}</h1>
</div>
<a
className="github-link"
href={repository.html_url}
target="_blank"
rel="noreferrer"
>
Open on GitHub
</a>
</div>
<p className="description">
{repository.description ?? "This repository has no description."}
</p>
How does the repository identity stay current?
- The eyebrow and heading read owner.login and name from the response.
- The repository link uses the returned html_url.
- The description falls back to a complete sentence when GitHub returns null.
- Save src/app/page.tsx.
- Refresh the browser to render the live repository identity.
- Select Open on GitHub to confirm the link opens the public repository.
You should see the public vercel/next.js repository in a new browser tab. The dashboard link now comes from the API response.
Live fields showing errors?
Check that every expression starts with repository. Confirm the description fallback keeps its braces and double quotes.
Use the current error to get targeted help: Help me fix the repository identity section.
✔️ Awesome, I've got everything!
Great. Make sure src/lib/github.ts and src/app/page.tsx are saved.
ⓧ I'd like to double check the full code
Here is the complete src/lib/github.ts for this step:
export type Repository = {
name: string;
full_name: string;
description: string | null;
html_url: string;
language: string | null;
stargazers_count: number;
forks_count: number;
open_issues_count: number;
updated_at: string;
owner: {
login: string;
};
};
export async function getRepository(
owner: string,
name: string,
): Promise<Repository> {
const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
{
headers: {
Accept: "application/vnd.github+json",
},
next: {
revalidate: 300,
},
},
);
if (!response.ok) {
throw new Error(`GitHub request failed with status ${response.status}`);
}
return (await response.json()) as Repository;
}
What should match?
Compare the response fields, encoded endpoint, request header, cache configuration, error check, and typed return value with your file.
Here is the complete src/app/page.tsx for this step:
import { getRepository } from "../lib/github";
export default async function Home() {
const repository = await getRepository("vercel", "next.js");
const updatedLabel = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).format(new Date(repository.updated_at));
const stats = [
{
label: "Stars",
value: repository.stargazers_count.toLocaleString("en-US"),
},
{
label: "Forks",
value: repository.forks_count.toLocaleString("en-US"),
},
{
label: "Open items",
value: repository.open_issues_count.toLocaleString("en-US"),
},
{
label: "Language",
value: repository.language ?? "Not reported",
},
];
return (
<main className="shell dashboard">
<section className="panel">
<div className="repo-heading">
<div>
<p className="eyebrow">{repository.owner.login}</p>
<h1>{repository.name}</h1>
</div>
<a
className="github-link"
href={repository.html_url}
target="_blank"
rel="noreferrer"
>
Open on GitHub
</a>
</div>
<p className="description">
{repository.description ?? "This repository has no description."}
</p>
<div className="stats-grid">
{stats.map((stat) => (
<article className="stat-card" key={stat.label}>
<span className="stat-label">{stat.label}</span>
<strong className="stat-value">{stat.value}</strong>
</article>
))}
</div>
<div className="meta-row">
<span>{repository.full_name}</span>
<span>Updated {updatedLabel}</span>
</div>
</section>
</main>
);
}
What should match?
Compare the import, asynchronous Home function, live repository fields, number formatting, date formatting, and existing dashboard structure with your file.
Verify the live repository signals
Before you compare the dashboard with GitHub, which signal do you expect to change most often?
- Return to the browser tab serving http://localhost:3000.
- Refresh the page to request the current repository data.
- Select Open on GitHub to open the public repository.
- Compare at least one displayed count with the corresponding count on GitHub.
You should see the same repository signal in both places. That match proves the dashboard now renders data from a live server-side request.
Numbers not matching exactly?
Refresh the local page after 300 seconds if GitHub changed during your comparison. Next.js can reuse the previous successful response during that cache window.
If the dashboard fails completely, inspect the development terminal for the response status. Use it here: Help me diagnose my live GitHub data check.
Your dashboard now turns a typed GitHub response into a server-rendered interface. Next, you will replace the fixed repository with a search flow for any public repository.
Search Any Public Repository
Your dashboard already replaces its mock values with live GitHub data for one repository. You can now turn that fixed demonstration into a reusable tool.
A fixed repository cannot prove that the dashboard works for other projects. In this step, a Next.js Server Function will send form values into dynamic route segments.
The first dynamic version also exposes what happens when a repository does not exist.
In this step, get ready to:
- Connect a repository search form to a Server Function.
- Render live repository data through a dynamic route.
- Compare the outcomes for valid and missing repositories.
Connect the search form to a Server Function
The search form needs server-side logic that turns its two values into a repository URL. The Server Function validates those values before redirecting the browser.
- In the VS Code Explorer sidebar, right-click the src/app folder.
- Select New File.
- Enter actions.ts and press Enter.
- Add the Server Function to src/app/actions.ts by pasting this code:
"use server";
import { redirect } from "next/navigation";
export async function openRepository(formData: FormData) {
const owner = formData.get("owner");
const repository = formData.get("repository");
if (
typeof owner !== "string" ||
typeof repository !== "string" ||
owner.trim() === "" ||
repository.trim() === ""
) {
redirect("/");
}
const ownerSegment = encodeURIComponent(owner.trim());
const repositorySegment = encodeURIComponent(repository.trim());
redirect(`/repo/${ownerSegment}/${repositorySegment}`);
}
What does this Server Function do?
- The "use server" directive keeps the form handler on the server.
- The first formData.get() call reads the repository owner.
- The second formData.get() call reads the repository name.
- The blank-value check redirects incomplete submissions to /.
- The encodeURIComponent() calls make each trimmed value safe for its route segment.
- The final redirect() call opens the dedicated repository URL.
- Save src/app/actions.ts.
The home page now becomes the launch point for every repository lookup. Its form sends both fields directly to openRepository().
- Select src/app/page.tsx in the VS Code Explorer sidebar.
- Press Cmd+A (macOS) or Ctrl+A (Windows) to select the current code.
- Delete the selected code.
- Add the home page import plus its hero section by pasting this first segment:
import { openRepository } from "./actions";
export default function Home() {
return (
<main className="shell">
<section className="hero">
<p className="eyebrow">Engineering portfolio project</p>
<h1>Repository Health Dashboard</h1>
<p className="lede">
Inspect the public signals behind any GitHub repository through a
server-rendered Next.js dashboard.
</p>
</section>
<section className="panel" aria-labelledby="search-title">
<div>
<p className="eyebrow">Live repository lookup</p>
<h2 id="search-title">Choose a public repository</h2>
</div>
How does the home page begin?
- The import gives the form access to openRepository().
- The hero explains what the dashboard inspects.
- The panel heading introduces the repository search.
- Paste the form segment immediately below the closing </div> from the first segment:
<form action={openRepository} className="search-form">
<div className="field-grid">
<label className="field" htmlFor="owner">
<span>Owner</span>
<input
id="owner"
name="owner"
defaultValue="vercel"
autoComplete="off"
required
/>
</label>
<label className="field" htmlFor="repository">
<span>Repository</span>
<input
id="repository"
name="repository"
defaultValue="next.js"
autoComplete="off"
required
/>
</label>
</div>
<button type="submit">Inspect repository</button>
<p className="hint">Public repositories work without an API token.</p>
</form>
</section>
How does the search form work?
- The form passes each submission to openRepository().
- The owner field starts with vercel.
- The repository field starts with next.js.
- The required attributes prevent empty browser submissions.
- Complete src/app/page.tsx by pasting this final segment after the closing </section> from the form segment:
<section className="preview" aria-labelledby="preview-title">
<p className="eyebrow">What this demonstrates</p>
<h2 id="preview-title">Production-minded frontend engineering</h2>
<div className="preview-grid">
<article>
<span>Rendering</span>
<strong>Server Components</strong>
</article>
<article>
<span>Resilience</span>
<strong>Expected failure states</strong>
</article>
<article>
<span>Delivery</span>
<strong>Git-based deployment</strong>
</article>
</div>
</section>
</main>
);
}
What does the preview show?
- The first card highlights server rendering.
- The second card highlights expected failure states.
- The third card highlights Git-based deployment.
- Save src/app/page.tsx.
- Refresh http://localhost:3000 in your browser.
You should see the two-field repository search form above three preview cards. The form should already show vercel as the owner plus next.js as the repository.
Search form not appearing?
Confirm that src/app/page.tsx contains all three segments in order. Check that the file imports openRepository from ./actions.
Review the VS Code terminal for a syntax error that points to a missing closing tag.
Help me debug my repository search form.
Create the dynamic repository route
Each repository needs its own URL so the page can read the owner plus repository name from the path. The route parameters arrive as a promise that the Server Component awaits before fetching data.
This folder path is a little fiddly because both bracketed names must match the parameter fields exactly.
- In the VS Code Explorer sidebar, right-click the src/app folder.
- Select New File.
- Enter repo/[owner]/[name]/page.tsx and press Enter.
- Add the route parameters plus date formatting by pasting this first segment into the empty file:
import { getRepository } from "../../../../../lib/github";
export default async function RepositoryPage({
params,
}: {
params: Promise<{ owner: string; name: string }>;
}) {
const { owner, name } = await params;
const repository = await getRepository(owner, name);
const updatedLabel = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).format(new Date(repository.updated_at));
How does the route load data?
- The params type matches the two bracketed route folders.
- Awaiting params exposes the owner plus repository name.
- The getRepository() call fetches data for those values.
- The date formatter turns updated_at into a readable label.
- Add the four dashboard signals below the updatedLabel declaration by pasting this segment:
const stats = [
{
label: "Stars",
value: repository.stargazers_count.toLocaleString("en-US"),
},
{
label: "Forks",
value: repository.forks_count.toLocaleString("en-US"),
},
{
label: "Open items",
value: repository.open_issues_count.toLocaleString("en-US"),
},
{
label: "Language",
value: repository.language ?? "Not reported",
},
];
Why build a stats array?
The stats array gives every card the same label-and-value shape. The page can render all four cards with one mapping expression.
The language fallback keeps the card useful when GitHub reports no primary language.
- Start the route interface below the stats array by pasting this segment:
return (
<main className="shell dashboard">
<a className="back-link" href="/">
Search another repository
</a>
<section className="panel">
<div className="repo-heading">
<div>
<p className="eyebrow">{repository.owner.login}</p>
<h1>{repository.name}</h1>
</div>
<a
className="github-link"
href={repository.html_url}
target="_blank"
rel="noreferrer"
>
Open on GitHub
</a>
</div>
<p className="description">
{repository.description ?? "This repository has no description."}
</p>
What does the route header render?
- The back link returns the visitor to the search form.
- The heading reads the repository owner plus name from the API response.
- The external link opens the inspected repository on GitHub.
- The description fallback covers repositories without descriptive copy.
- Complete the route below the description paragraph by pasting this final segment:
<div className="stats-grid">
{stats.map((stat) => (
<article className="stat-card" key={stat.label}>
<span className="stat-label">{stat.label}</span>
<strong className="stat-value">{stat.value}</strong>
</article>
))}
</div>
<div className="meta-row">
<span>{repository.full_name}</span>
<span>Updated {updatedLabel}</span>
</div>
</section>
</main>
);
}
How does the route finish?
- The stats.map() expression renders one card for each repository signal.
- The metadata row shows the full repository name.
- The final label shows the formatted update date.
- Save src/app/repo/[owner]/[name]/page.tsx.
✔️ Awesome, I've got everything!
Great. Double check that src/app/actions.ts, src/app/page.tsx, and src/app/repo/[owner]/[name]/page.tsx are saved.
ⓧ I'd like to double check the full code
Here is the complete src/app/actions.ts file:
"use server";
import { redirect } from "next/navigation";
export async function openRepository(formData: FormData) {
const owner = formData.get("owner");
const repository = formData.get("repository");
if (
typeof owner !== "string" ||
typeof repository !== "string" ||
owner.trim() === "" ||
repository.trim() === ""
) {
redirect("/");
}
const ownerSegment = encodeURIComponent(owner.trim());
const repositorySegment = encodeURIComponent(repository.trim());
redirect(`/repo/${ownerSegment}/${repositorySegment}`);
}
Here is the complete src/app/page.tsx file:
import { openRepository } from "./actions";
export default function Home() {
return (
<main className="shell">
<section className="hero">
<p className="eyebrow">Engineering portfolio project</p>
<h1>Repository Health Dashboard</h1>
<p className="lede">
Inspect the public signals behind any GitHub repository through a
server-rendered Next.js dashboard.
</p>
</section>
<section className="panel" aria-labelledby="search-title">
<div>
<p className="eyebrow">Live repository lookup</p>
<h2 id="search-title">Choose a public repository</h2>
</div>
<form action={openRepository} className="search-form">
<div className="field-grid">
<label className="field" htmlFor="owner">
<span>Owner</span>
<input
id="owner"
name="owner"
defaultValue="vercel"
autoComplete="off"
required
/>
</label>
<label className="field" htmlFor="repository">
<span>Repository</span>
<input
id="repository"
name="repository"
defaultValue="next.js"
autoComplete="off"
required
/>
</label>
</div>
<button type="submit">Inspect repository</button>
<p className="hint">Public repositories work without an API token.</p>
</form>
</section>
<section className="preview" aria-labelledby="preview-title">
<p className="eyebrow">What this demonstrates</p>
<h2 id="preview-title">Production-minded frontend engineering</h2>
<div className="preview-grid">
<article>
<span>Rendering</span>
<strong>Server Components</strong>
</article>
<article>
<span>Resilience</span>
<strong>Expected failure states</strong>
</article>
<article>
<span>Delivery</span>
<strong>Git-based deployment</strong>
</article>
</div>
</section>
</main>
);
}
Here is the complete src/app/repo/[owner]/[name]/page.tsx file:
import { getRepository } from "../../../../../lib/github";
export default async function RepositoryPage({
params,
}: {
params: Promise<{ owner: string; name: string }>;
}) {
const { owner, name } = await params;
const repository = await getRepository(owner, name);
const updatedLabel = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
}).format(new Date(repository.updated_at));
const stats = [
{
label: "Stars",
value: repository.stargazers_count.toLocaleString("en-US"),
},
{
label: "Forks",
value: repository.forks_count.toLocaleString("en-US"),
},
{
label: "Open items",
value: repository.open_issues_count.toLocaleString("en-US"),
},
{
label: "Language",
value: repository.language ?? "Not reported",
},
];
return (
<main className="shell dashboard">
<a className="back-link" href="/">
Search another repository
</a>
<section className="panel">
<div className="repo-heading">
<div>
<p className="eyebrow">{repository.owner.login}</p>
<h1>{repository.name}</h1>
</div>
<a
className="github-link"
href={repository.html_url}
target="_blank"
rel="noreferrer"
>
Open on GitHub
</a>
</div>
<p className="description">
{repository.description ?? "This repository has no description."}
</p>
<div className="stats-grid">
{stats.map((stat) => (
<article className="stat-card" key={stat.label}>
<span className="stat-label">{stat.label}</span>
<strong className="stat-value">{stat.value}</strong>
</article>
))}
</div>
<div className="meta-row">
<span>{repository.full_name}</span>
<span>Updated {updatedLabel}</span>
</div>
</section>
</main>
);
}
Before you submit the default search, what repository details do you expect the dedicated route to display?
- Return to http://localhost:3000 in your browser.
- Enter vercel in the Owner field.
- Enter next.js in the Repository field.
- Click Inspect repository.
You should reach the dedicated /repo/vercel/next.js route. You should see the repository description plus current stars, forks, open items, language, and update date.
That is the reusable path working. The form now turns any valid public repository name into a server-rendered dashboard.
Valid repository route not loading?
Confirm that the folder path is exactly src/app/repo/[owner]/[name]. Check that its page.tsx file awaits params before calling getRepository().
Review the running development terminal for a file-path error or TypeScript error.
Help me debug my dynamic repository route.
Test the missing-repository path
The valid route proves that the form passes both path values into the data request. A missing public repository now tests how the same flow handles a normal input mistake.
Before you submit this search, do you expect a repository dashboard or an error screen?
- Click Search another repository.
- Enter nextwork-project in the Owner field.
- Enter does-not-exist in the Repository field.
- Click Inspect repository.
You should reach the Next.js error screen because getRepository() throws for every non-success response. This is the intended shortfall in the current route.
This failure is expected
The search and dynamic route are working. The missing repository reaches the API path successfully before the generic error interrupts the page.
You have now exposed the exact recovery gap that the next step addresses.
Your dashboard can now inspect any valid public repository through its own URL. Next, you will turn the expected missing-repository failure into a polished recovery experience.
Handle Failure and Deploy
Your Next.js dashboard can now inspect any public GitHub repository. A missing repository still turns an expected user mistake into an application crash.
This step adds a recovery boundary for that failure. You will verify the production build before publishing the app through Git and Vercel.
In this step, get ready to:
- Route missing repositories to a custom recovery screen.
- Confirm the production build passes.
- Publish the dashboard through GitHub and Vercel.
Handle missing repositories and verify the build
The App Router can treat a missing repository as a controlled route outcome. An HTTP response with status 404 can show recovery UI while other failed responses remain application errors.
- Select src/lib/github.ts from the VS Code file tree.
- Add this import at the top of src/lib/github.ts:
import { notFound } from "next/navigation";
What does this import provide?
The notFound function stops the current route segment when the requested repository is missing. Next.js then renders the matching missing-state interface.
- Find the existing if (!response.ok) check inside getRepository.
- Insert this status check directly above it:
if (response.status === 404) {
notFound();
}
How does the status check help?
- The new check sends a documented 404 response through notFound().
- The existing response.ok check still throws for other failed responses.
- The successful path still returns a typed Repository object.
- Save src/lib/github.ts.
Before you retry the invalid search, do you expect an application crash or a missing-route response? Make your prediction now.
- Return to the dashboard home page in your browser.
- Enter nextwork-project in the Owner field.
- Enter does-not-exist in the Repository field.
- Click Inspect repository.
You should reach the framework's missing-route response instead of the unhandled error screen. The expected failure now stays inside the dynamic repository route.
A repository request can take long enough to leave the new route without visual feedback. A colocated loading.tsx file supplies an immediate skeleton while the Server Component waits.
- Create loading.tsx inside src/app/repo/[owner]/[name] using the file creation control in the VS Code file tree.
- Paste this loading interface into loading.tsx:
export default function Loading() {
return (
<main className="shell dashboard" aria-busy="true" aria-live="polite">
<span className="back-link">Loading repository</span>
<section className="panel skeleton-panel">
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-copy" />
<div className="skeleton-grid">
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
</div>
</section>
</main>
);
}
What does the loading UI provide?
- The route displays a skeleton panel while the repository request resolves.
- The four skeleton cards preserve the dashboard layout during the transition.
- The accessibility attributes communicate that the route is busy.
- Save loading.tsx.
- Return to the dashboard search form.
- Enter vercel in the Owner field.
- Enter next.js in the Repository field.
- Click Inspect repository.
You should briefly see the skeleton interface during an uncached route transition. The repository dashboard replaces it when the request completes.
Loading state too quick to notice?
A cached repository response can resolve before the skeleton becomes noticeable. Try a different valid public repository to trigger a fresh request.
If the route stays on the skeleton, ask for help debugging the route transition.
The route now needs a recovery screen that matches the dashboard. Its link gives the visitor a direct path back to the search form.
- Create not-found.tsx inside src/app/repo/[owner]/[name] using the file creation control in the VS Code file tree.
- Paste this recovery interface into not-found.tsx:
export default function NotFound() {
return (
<main className="shell">
<section className="panel error-panel">
<p className="eyebrow">404</p>
<h1>Repository not found</h1>
<p className="description">
Check the owner and repository name, and make sure the repository is
public.
</p>
<a className="github-link" href="/">
Try another repository
</a>
</section>
</main>
);
}
What does the recovery screen do?
- The screen names the missing-repository outcome without exposing an internal error.
- The guidance directs the visitor to check the owner name or repository name.
- The guidance also reminds the visitor that the repository must be public.
- The recovery link returns the visitor to the existing search form.
- Save not-found.tsx.
Before you retry the invalid search, what recovery message do you expect to see? Make your prediction now.
- Return to the dashboard search form.
- Enter nextwork-project in the Owner field.
- Enter does-not-exist in the Repository field.
- Click Inspect repository.
You should see the custom Repository not found screen with a Try another repository link. The user mistake now has a clear recovery path.
Still seeing an application error?
Confirm that not-found.tsx sits inside src/app/repo/[owner]/[name]. Check that the response.status === 404 block appears before the existing response.ok check.
If the route still crashes, ask for help tracing the missing-state boundary.
✔️ Awesome, I've got everything!
Your route now distinguishes a missing repository from other upstream failures. Make sure all three files are saved before building.
ⓧ I'd like to double check the full code
Compare your complete src/lib/github.ts file with this version.
import { notFound } from "next/navigation";
export type Repository = {
name: string;
full_name: string;
description: string | null;
html_url: string;
language: string | null;
stargazers_count: number;
forks_count: number;
open_issues_count: number;
updated_at: string;
owner: {
login: string;
};
};
export async function getRepository(
owner: string,
name: string,
): Promise<Repository> {
const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
{
headers: {
Accept: "application/vnd.github+json",
},
next: {
revalidate: 300,
},
},
);
if (response.status === 404) {
notFound();
}
if (!response.ok) {
throw new Error(`GitHub request failed with status ${response.status}`);
}
return (await response.json()) as Repository;
}
What should this file contain?
The final data-access file keeps the typed repository response. It handles only a missing repository through notFound().
Compare your complete loading.tsx file with this version.
export default function Loading() {
return (
<main className="shell dashboard" aria-busy="true" aria-live="polite">
<span className="back-link">Loading repository</span>
<section className="panel skeleton-panel">
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-copy" />
<div className="skeleton-grid">
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
</div>
</section>
</main>
);
}
What should this file contain?
The loading file contains the complete skeleton interface for the dynamic repository route.
Compare your complete not-found.tsx file with this version.
export default function NotFound() {
return (
<main className="shell">
<section className="panel error-panel">
<p className="eyebrow">404</p>
<h1>Repository not found</h1>
<p className="description">
Check the owner and repository name, and make sure the repository is
public.
</p>
<a className="github-link" href="/">
Try another repository
</a>
</section>
</main>
);
}
What should this file contain?
The missing-state file contains the complete recovery interface. Its link returns visitors to the dashboard search form.
- Switch back to the VS Code terminal running the development server.
- Stop the development server by pressing Ctrl+C.
Before you build, do you expect the production checks to accept all three route files? Make your prediction now.
- Create the production build by running:
npm run build
What does this command prove?
The build checks the application against the pinned Next.js configuration. It also runs the project-local TypeScript checks.
A successful exit confirms that the new route files compile with the existing dashboard.
You should see the production build complete without a compilation or type-checking failure. Your local application is ready for deployment.
Build not completing?
Check that notFound uses the import from next/navigation. Confirm that both route-state files are inside src/app/repo/[owner]/[name].
Use the first reported file and line to locate the mismatch. Ask for help diagnosing the production build.
Publish the project to GitHub
A successful local build proves that the application compiles on your Mac. A public repository preserves the source and provides the deployment source for Vercel.
- Check the installed Git version by running:
git --version
What does this check show?
The command prints the installed Git version. Git 2.28 or later supports initializing the repository with main as its first branch.
✔️ I see version 2.28 or higher
Git is ready. You can initialize the project directly on the main branch.
ⓧ I see an older version
The installed Git version needs an upgrade before you use the direct branch-initialization command.
- Visit the official Git installation page for macOS.
- Complete one of the supported macOS installation methods.
- Return to the VS Code terminal.
- Check the upgraded version by running:
git --version
What should this confirm?
The output should show Git 2.28 or later before you continue.
Still seeing the older version?
Close the current terminal session after the installation completes. Return to the VS Code terminal before checking again.
If the older version remains active, ask for help locating the upgraded Git installation.
ⓧ Command not found
Git needs to be installed before the project can become a repository.
- Visit the official Git installation page for macOS.
- Complete one of the supported macOS installation methods.
- Return to the VS Code terminal.
- Verify the installation by running:
git --version
What should this confirm?
The terminal should print the installed Git version. Confirm that it is 2.28 or later before continuing.
Git still unavailable?
Return to the installation page to confirm that the selected macOS method completed successfully.
If the command remains unavailable, ask for help checking the Git installation.
- Initialize github-repo-health-dashboard on the main branch by running:
git init -b main
What does this command do?
The command creates the local repository metadata inside github-repo-health-dashboard. It names the initial branch main.
You should see confirmation that an empty Git repository was initialized. The project files remain unchanged.
✔️ Repository initialized on main
Your local repository is ready for its first commit.
ⓧ Branch option unavailable
An older Git installation may reject the branch option. The documented fallback initializes the repository before pointing HEAD at main.
- Initialize the repository with the fallback by running:
git init && git symbolic-ref HEAD refs/heads/main
How does the fallback work?
The first command creates the repository metadata. The second command sets main as the branch referenced by HEAD.
Fallback not working?
Confirm that the VS Code terminal is still inside github-repo-health-dashboard.
If initialization still fails, ask for help with the terminal output.
- Stage the project files before the first commit by running:
git add .
What does staging include?
Git stages the project files that are not excluded by .gitignore.
- Record the production-ready snapshot by running:
git commit -m "Build repository health dashboard"
What does this commit record?
The command records the staged snapshot with the message Build repository health dashboard.
You should see a commit summary listing the files saved on main. Your project now has a local source history.
Commit asking for your identity?
A first Git commit can require an author name or email address. Follow the terminal guidance to configure your identity before rerunning the commit.
If the commit still cannot continue, ask for help completing the identity setup.
GitHub may ask you to sign in before creating the remote repository. This authentication stays in the browser and keeps credentials out of your project files.
- Return to your GitHub account in the browser.
- Start creating a repository in your personal account.
- Enter github-repo-health-dashboard as the repository name.
- Set the repository visibility to public.
Why create an empty repository?
Your local project already contains the first commit. Leaving GitHub's starter files unselected prevents an unrelated remote commit from conflicting with that history.
- Leave README initialization disabled.
- Leave gitignore initialization disabled.
- Leave license initialization disabled.
- Create the repository.
GitHub should open the empty repository's Quick Setup page. This page provides the remote URL for your local repository.
- Store the repository URL from the Quick Setup page here: REMOTE-URL.
The first push may start a GitHub authentication flow. Complete the browser sign-in if the terminal requests it.
- Connect the local repository to GitHub by running:
git remote add origin [[REMOTE_URL="REMOTE-URL"]]
What does the remote command do?
The command stores the GitHub repository URL under the remote name origin.
- Publish the main branch by running:
git push -u origin main
What does the push publish?
The command uploads the local main branch. The upstream setting links later pushes to the same remote branch.
- Refresh the github-repo-health-dashboard repository page on GitHub.
You should see the project source files on the main branch. The commit history should include Build repository health dashboard.
Push not reaching GitHub?
Confirm that the remote URL came from the empty repository in your personal account. Complete any browser authentication request before retrying the push.
If GitHub rejectss the push, ask for help with the exact terminal output.
Deploy the production app with Vercel
Vercel builds the app from the connected GitHub repository. The production deployment turns your local dashboard into a public URL that you can demonstrate.
Creating the account does not start a paid billing cycle. The Vercel Hobby plan is free for personal non-commercial use.
- Visit the Vercel website in your browser.
- Create a Vercel Hobby account using GitHub as the sign-in provider.
- Authorize the requested GitHub connection.
- Select New Project from the Vercel dashboard.
- Choose github-repo-health-dashboard from the connected repositories.
- Confirm that Vercel detected the Next.js project settings.
Vercel installs the pinned dependencies before creating the production build. Keep the deployment page open while those checks complete.
- Select Deploy.
What does Git-based deployment provide?
Vercel builds the source from the connected GitHub repository. Pushes to the configured production branch can create fresh production deployments.
You should reach a successful deployment page with a live production URL. That URL serves the repository dashboard outside your local machine.
- Store the live production URL from the completed deployment here: your Vercel production URL.
Before you test production, do you expect valid and invalid searches to match your local results? Make your prediction now.
- Open your Vercel production URL in a new browser tab.
- Enter vercel in the Owner field.
- Enter next.js in the Repository field.
- Click Inspect repository.
You should see the live vercel/next.js dashboard with its description and four repository signals.
- Return to the production search form.
- Enter nextwork-project in the Owner field.
- Enter does-not-exist in the Repository field.
- Click Inspect repository.
You should see the custom Repository not found screen instead of an application crash. Your live dashboard now handles the designed failure with a clear recovery path.
Production behavior different from local?
Confirm that GitHub shows the final src/lib/github.ts file on main. Confirm that both route-state files are present in the same branch.
Check that the Vercel project is connected to github-repo-health-dashboard. If the deployment still differs, ask for help checking the build and connected branch.
That completes the delivery loop. Your resilient repository dashboard now runs locally and from a public production URL.
Secret mission
Expose the API Budget
Extend the dashboard so every valid repository page shows the remaining GitHub REST API request allowance. The new operational signal will also show when that allowance resets.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your resources available, pause the deployment for later, or delete everything. This project costs $0 to keep under the personal, non-commercial terms of Vercel Hobby.
Cost warning
Vercel Hobby has no billing cycle. If your included allowance runs out, the affected resource can pause until the allowance resets.
Unauthenticated requests to the GitHub REST API share an allowance based on the originating IP address. Keeping this project does not create paid API usage.
Resources you used:
- Local github-repo-health-dashboard folder containing your production-ready Next.js app.
- Public personal GitHub repository named github-repo-health-dashboard.
- Vercel Hobby project named github-repo-health-dashboard with its production deployment.
Keep everything running
No action is needed while you keep building or using the dashboard as a portfolio demonstration.
- Keep the local github-repo-health-dashboard folder for future development.
- Leave the public github-repo-health-dashboard repository available with its commit history.
- Keep the Vercel project active so the production URL continues serving your dashboard.
Pause - I'll come back to this later
Pause the hosted project when you want to preserve the deployment configuration without keeping the production dashboard available.
- Select the github-repo-health-dashboard project in the Vercel dashboard.
- Open Settings.
- Select General.
- Find the Pause Project section.
- Select Pause Project.
- Open the production URL to confirm the dashboard is no longer being served.
Your Vercel project is paused. Your local files and GitHub commit history remain intact.
- Return to the same section when you want to restore the deployment.
- Select Resume Project to make the production dashboard available again.
Delete - I don't want to use this again
Deletion permanently removes each selected resource. Your public demo survives only in the resources you choose to keep.
- Select the github-repo-health-dashboard project in the Vercel dashboard.
- Open Settings.
- Select General.
- Scroll to the Delete Project section at the bottom of the page.
- Select Delete.
- Enter github-repo-health-dashboard as the project name.
- Select Continue.
The Vercel project and its production deployment are now removed.
- Open the public github-repo-health-dashboard repository on GitHub.
- Open the repository settings page.
- Find the repository deletion controls.
- Choose the option to delete the repository.
- Enter github-repo-health-dashboard when GitHub asks you to confirm the repository name.
- Confirm the repository deletion.
- Refresh the former repository page to verify that the public repository is gone.
You should now see GitHub's missing-repository page instead of the dashboard source code.
- Delete the local github-repo-health-dashboard folder from its parent directory by running these commands:
cd ..
rm -rf github-repo-health-dashboard
What does this command do?
- The first line moves Terminal out of the project folder so macOS can remove it.
- The second line permanently deletes the local project folder and everything inside it.
- Use Finder search to look for a folder named github-repo-health-dashboard.
You should not see the project folder in the search results. That confirms the local copy is gone.
Folder Still Present?
- Confirm that Terminal was inside the github-repo-health-dashboard folder before the first command moved to its parent.
- Check that the folder name uses the same lowercase spelling and hyphens.
- Help me safely remove the local dashboard folder.
Nice Work!
Nice Work!
Excellent work! Your production-ready repository health dashboard is live on Vercel.
You've learned how to:
- Build a responsive repository dashboard with React plus TypeScript. Make it reusable through a Server Function. Give each repository its own URL with dynamic route segments.
- Fetch live repository signals from the GitHub REST API inside a Next.js Server Component. Keep repeated requests controlled with a 300-second cache revalidation window.
- Turn the designed 404 crash into resilient failure states. Publish the source to GitHub. Deliver production updates through Vercel.
- Secret Mission: Expose the GitHub REST API request budget through the typed data boundary. Display the remaining requests plus reset time on the live dashboard.
Ready to quiz yourself?