Komplet guide til Next.js — Byg hjemmesider og apps fra bunden
Målgruppe: Danske freelancere, startup-stiftere og begyndere, der vil bygge moderne hjemmesider og webapps. Forudsætninger: Ingen forudgående erfaring med React eller Next.js. Lidt kendskab til HTML/CSS er en fordel, men ikke et krav.
Indhold
- Hvad er Next.js?
- Hornår skal du bruge det?
- Kom i gang
- Projektstruktur
- Sider og routing
- Components
- Data hentning
- Styling
- Database
- Deployment
- Pris
- Fordele og ulemper
- Ressourcer
- Eksempel-projekt
1. Hvad er Next.js?
Next.js er et gratis, open-source framework bygget ovenpå React. Det er lavet af Vercel og gør det markant nemmere at bygge hurtige, brugervenlige hjemmesider og webapps.
Hvorfor er Next.js populært?
- Hurtigere end traditionelle hjemmesider — Sider indlæses hurtigere, fordi Next.js kan forberede indhold på forhånd (server-side rendering og static generation).
- Bedre SEO — Søgemaskiner som Google kan læse dit indhold, fordi det allerede er HTML, når siden når browseren.
- Nem routing — Opret nye sider ved at oprette filer i en mappe. Ingen kompliceret routing-konfiguration.
- Bygget til produktion — Next.js har optimeringer indbygget, så din side automatisk bliver hurtigere.
- Stor community — Tusindvis af tutorials, plugins og frameworks bygger ovenpå Next.js.
Hvad er Next.js godt til?
- Hjemmesider der skal være hurtige og SEO-venlige
- Webshops med mange produktsider
- Blogs og magasiner
- Webapplikationer med brugerlogin
- Portfolio-sider og landing pages
- SaaS-produkter
Hvad er forskellen på Next.js og almindelig React?
| Funktion | Almindelig React | Next.js |
|---|---|---|
| Routing | Manuel opsætning | Automatisk (filbaseret) |
| Server-side rendering | Komplekst at sætte op | Indbygget |
| Static generation | Ikke indbygget | Indbygget |
| API routes | Ikke muligt | Indbygget |
| SEO | Svært | Nemt |
| Performance | God | Meget god |
Kort sagt: Next.js er React, der er gjort nemmere og hurtigere til produktion.
2. Hornår skal du bruge det?
Next.js er et godt valg til de fleste webprojekter. Her er de mest almindelige brugsscenarier:
Webshops
Next.js er perfekt til webshops, fordi hvert produkt kan have sin egen URL (f.eks. /produkt/blaa-skjorte), og siderne indlæses hurtigt — hvilket er vigtigt for både brugeroplevelse og SEO.
SaaS (Software as a Service)
Hvis du bygger et produkt, hvor brugere logger ind og bruger en applikation, er Next.js et solidt valg. Du kan bygge both frontend og backend i ét projekt.
Blogs og indholdssider
Next.js har indbygget understøttelse for Markdown og kan generere sider statisk, hvilket gør det ideelt til blogs, der skal være hurtige og SEO-venlige.
Portfolio-sider
Vis dit arbejde frem med en hurtig, flot portfolio-side. Next.js gør det nemt at bygge en side, der imponerer potentielle kunder.
Landing pages
Skal du have en enkelt side til et produkt eller en kampagne? Next.js er overkill til dette, men stadig et godt valg, hvis du vil have muligheden for at udvide senere.
Hornår skal du IKKE bruge Next.js?
- Meget simple hjemmesider — En statisk HTML/CSS-side kan være bedre til en simpel visitkort-side.
- Ren mobil-app — Overvej React Native i stedet.
- Projekter uden behov for SEO — Hvis det kun er en intern applikation, kan almindelig React eller endda simplere løsninger være bedre.
3. Kom i gang
Følg disse trin for at oprette dit første Next.js-projekt.
Trin 1: Installer Node.js
Node.js er en runtime, der skal køres på din computer for at udvikle med Next.js.
- Gå til nodejs.org
- Download den LTS-version (Long Term Support) — det er den mest stabile
- Kør installationsfilen og følg vejledningen
- Tjek at det virker ved at åbne terminal (Command Prompt eller PowerShell) og skrive:
node --version
# Skal vise noget som: v20.11.0
npm --version
# Skal vise noget som: 10.2.4
Trin 2: Opret et nyt Next.js-projekt
Åbn terminal og kør følgende kommando:
npx create-next-app@latest mit-projekt
Du bliver stillet et par spørgsmål. Her er anbefalingerne for begyndere:
Would you like to use TypeScript? → No (medmindre du kender TypeScript)
Would you like to use ESLint? → Yes
Would you like to use Tailwind CSS? → Yes
Would you like to use `src/` directory? → No
Would you like to use App Router? → Yes
Would you like to customize the default import alias? → No
Trin 3: Start udviklingsserveren
cd mit-projekt
npm run dev
Åbn din browser og gå til http://localhost:3000. Du skal se en Next.js-forside.
Trin 4: Rediger din første side
Åbn filen app/page.js (eller app/page.tsx hvis du valgte TypeScript) i din foretrukne editor. Prøv at ændre teksten og se siden opdatere automatisk i browseren.
// app/page.js
export default function Home() {
return (
<main>
<h1>Hej verden!</h1>
<p>Dette er min første Next.js-hjemmeside.</p>
</main>
);
}
Nyttige kommandoer
# Start udviklingsserveren
npm run dev
# Byg til produktion
npm run build
# Start produktionsserveren (efter build)
npm start
# Installer nye pakker
npm install pakkenavn
4. Projektstruktur
Når du opretter et nyt Next.js-projekt, ser strukturen sådan ud:
mit-projekt/
├── app/
│ ├── page.js # Forsiden (rute: /)
│ ├── layout.js # Grundlæggende layout for hele siden
│ ├── globals.css # Globale CSS-styles
│ └── favicon.ico # Ikon til fanen i browseren
├── public/
│ └── billeder/ # Statiske billeder
├── node_modules/ # Installeret pakker (ikke rør!)
├── package.json # Projektets konfiguration og afhængigheder
├── next.config.js # Next.js-konfiguration
├── tailwind.config.js # Tailwind CSS-konfiguration
└── postcss.config.js # PostCSS-konfiguration
app/ — App Router
app/-mappen er hjertet i dit projekt. Her definerer du alle dine sider og layouts. Hver mappe i app/ repræsenterer en URL-rute.
components/ — Genbrugelige komponenter
Opret en components/-mappe i rod-mappen til dine genbrugelige komponenter:
mit-projekt/
├── app/
├── components/
│ ├── Header.js
│ ├── Footer.js
│ └── Knap.js
public/ — Statiske filer
public/-mappen er til billeder, ikoner og andre filer, der skal serveres direkte. En fil i public/billede.jpg kan tilgås via URL'en /billede.jpg.
styles/ — CSS-filer
Hvis du bruger CSS Modules (ikke Tailwind), kan du oprette en styles/-mappe. Med Tailwind lægger du dine styles typisk i globals.css.
5. Sider og routing
Next.js bruger filbaseret routing — det betyder, at en fil automatisk bliver en side med en bestemt URL.
Opret en ny side
Opret en ny mappe i app/ med en page.js-fil:
app/
├── page.js # → /
├── om/
│ └── page.js # → /om
├── kontakt/
│ └── page.js # → /kontakt
Eksempel på en "Om"-side:
// app/om/page.js
export default function OmSide() {
return (
<main>
<h1>Om mig</h1>
<p>Jeg er webudvikler baseret i København.</p>
</main>
);
}
Dynamiske ruter (med variable)
Brug brackets [] i mappenavnet for at oprette dynamiske ruter:
app/
├── blogg/
│ ├── page.js # → /blogg (oversigt)
│ └── [slug]/
│ └── page.js # → /blogg/et-eller-andet
├── produkter/
│ └── [id]/
│ └── page.js # → /produkter/123
Adgang til den dynamiske parameter:
// app/blogg/[slug]/page.js
export default function BlogIndlaeg({ params }) {
return (
<main>
<h1>Blogindlæg: {params.slug}</h1>
</main>
);
}
Layouts
Layouts gør det nemt at dele elementer (header, footer, navigation) på tværs af sider. Opret en layout.js i app/:
// app/layout.js
import './globals.css';
import Header from '../components/Header';
import Footer from '../components/Footer';
export const metadata = {
title: 'Mit website',
description: 'Min personlige hjemmeside',
};
export default function RootLayout({ children }) {
return (
<html lang="da">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
Links mellem sider
Brug Next.js' Link-komponent til at navigere mellem sider (uden genindlæsning af siden):
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">Forside</Link>
<Link href="/om">Om</Link>
<Link href="/kontakt">Kontakt</Link>
</nav>
);
}
6. Components
React-komponenter er genbrugelige byggesten til dit brugerflade. Tænk på dem som LEGO-klodser — du bygger dem én gang og genbruger dem mange gange.
Hvad er en komponent?
En komponent er en JavaScript-funktion, der returnerer HTML-lignende kode (JSX):
// components/Knap.js
export default function Knap({ tekst, onClick }) {
return (
<button onClick={onClick} className="min-knap">
{tekst}
</button>
);
}
Brug komponenten i en side
// app/page.js
import Knap from '../components/Knap';
export default function Home() {
const handleClick = () => {
alert('Du klikkede på knappen!');
};
return (
<main>
<h1>Velkommen</h1>
<Knap tekst="Klik her" onClick={handleClick} />
</main>
);
}
Props (egenskaber)
Props er data, du sender til en komponent. De fungerer som funktionens parametre:
// components/ProduktKort.js
export default function ProduktKort({ titel, pris, billede }) {
return (
<div className="produkt-kort">
<img src={billede} alt={titel} />
<h3>{titel}</h3>
<p>{pris} kr.</p>
</div>
);
}
Brug den sådan:
<ProduktKort
titel="Blå skjorte"
pris={499}
billede="/billeder/skjorte.jpg"
/>
Betinget rendering
Vis eller skjul elementer baseret på betingelser:
// components/BrugerStatus.js
export default function BrugerStatus({ erLoggetInd, navn }) {
if (!erLoggetInd) {
return <p>Du er ikke logget ind.</p>;
}
return <p>Velkommen, {navn}!</p>;
}
Loop over en liste
Render en liste af elementer med map():
// components/ProduktListe.js
export default function ProduktListe({ produkter }) {
return (
<ul>
{produkter.map((produkt) => (
<li key={produkt.id}>
{produkt.titel} — {produkt.pris} kr.
</li>
))}
</ul>
);
}
Bedste praksis for komponenter
- Én komponent per fil — Hold komponenter små og fokuserede
- Brug store bogstaver —
ProduktKortikkeproduktKort - Gør dem genbrugelige — Undgå at hårdkode data i komponenten
- Brug props til data — Lad komponenten modtage data udefra
- Hold state minimalt — Brug state kun når det er nødvendigt
7. Data hentning
Next.js gør det nemt at hente data fra forskellige kilder. Her er de mest almindelige metoder.
Fetch API (App Router)
I App Router bruger du fetch direkte i din komponent eller side:
// app/blogg/page.js
async function hentIndlaeg() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
return data;
}
export default async function BloggSide() {
const indlaeg = await hentIndlaeg();
return (
<main>
<h1>Blog</h1>
{indlaeg.map((indlaeg) => (
<article key={indlaeg.id}>
<h2>{indlaeg.title}</h2>
<p>{indlaeg.body}</p>
</article>
))}
</main>
);
}
Server Components vs. Client Components
Server Components (standard) — Kører på serveren. Kan hente data direkte. Bruges til det meste:
// Denne komponent kører på serveren (standard)
export default async function DataSide() {
const data = await fetch('https://api.example.com/data');
const result = await data.json();
return <div>{result.titel}</div>;
}
Client Components — Kører i browseren. Bruges til interaktivitet (klik, formularer, state):
'use client';
import { useState } from 'react';
export default function Soegning() {
const [query, setQuery] = useState('');
const [resultater, setResultater] = useState([]);
const soeg = async () => {
const res = await fetch(`/api/soeg?q=${query}`);
const data = await res.json();
setResultater(data);
};
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={soeg}>Søg</button>
<ul>
{resultater.map((r) => (
<li key={r.id}>{r.titel}</li>
))}
</ul>
</div>
);
}
API Routes (Route Handlers)
Opret server-side endpoints direkte i dit projekt. Opret en fil i app/api/:
app/
├── api/
│ └── brugere/
│ └── route.js
// app/api/brugere/route.js
import { NextResponse } from 'next/server';
export async function GET() {
// Hent data fra et database eller API
const brugere = [
{ id: 1, navn: 'Anna Hansen' },
{ id: 2, navn: 'Lars Nielsen' },
];
return NextResponse.json(brugere);
}
export async function POST(request) {
const body = await request.json();
// Gem data i din database her
return NextResponse.json(
{ message: 'Bruger oprettet', bruger: body },
{ status: 201 }
);
}
Brug den i frontend:
const res = await fetch('/api/brugere');
const brugere = await res.json();
Data fetching strategier
| Metode | Hvornår | Beskrivelse |
|---|---|---|
| Server Component fetch | De fleste tilfælde | Henter data på serveren automatisk |
| Route Handlers | Når du har brug for et API | Opret egne endpoints |
| Client-side fetch | Interaktivitet | Når data skal opdateres uden genindlæsning |
8. Styling
Next.js understøtter flere måder at style dit projekt på. Her er de mest populære.
Tailwind CSS (anbefalet til begyndere)
Tailwind er et "utility-first" CSS-framework, der gør det nemt at bygge flot design uden at skrive meget CSS. Det er allerede konfigureret, hvis du valgte det under installationen.
// app/page.js
export default function Home() {
return (
<main className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg shadow-md max-w-md">
<h1 className="text-3xl font-bold text-gray-800 mb-4">
Hej verden!
</h1>
<p className="text-gray-600">
Dette er en side stylet med Tailwind CSS.
</p>
<button className="mt-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Klik her
</button>
</div>
</main>
);
}
CSS Modules
CSS Modules giver dig lokalt scope til dine styles — du undgår navnekonflikter:
/* app/om/om.module.css */
.knap {
background-color: blue;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.knap:hover {
background-color: darkblue;
}
// app/om/page.js
import styles from './om.module.css';
export default function OmSide() {
return (
<main>
<h1>Om mig</h1>
<button className={styles.knap}>Klik her</button>
</main>
);
}
Global CSS
Globale styles defineres i app/globals.css og gælder hele projektet:
/* app/globals.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
}
Anbefaling
For begyndere anbefaler jeg Tailwind CSS. Det er hurtigt at lære, gør det nemt at bygge responsive designs, og du skal ikke skrive meget custom CSS.
9. Database
For at gemme og hente data har du brug for en database. Her er de to bedste muligheder for begyndere.
Prisma ORM + PostgreSQL
Prisma er et "Object-Relational Mapping" (ORM) værktøj, der gør det nemt at arbejde med databaser i JavaScript.
Trin 1: Installer Prisma
npm install prisma @prisma/client
npx prisma init
Trin 2: Konfigurer din database
Åbn prisma/schema.prisma og definer dine tabeller:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Bruger {
id Int @id @default(autoincrement())
navn String
email String @unique
oprettet DateTime @default(now())
indlaeg Indlaeg[]
}
model Indlaeg {
id Int @id @default(autoincrement())
titel String
indhold String
forfatter Bruger @relation(fields: [forfatterId], references: [id])
forfatterId Int
oprettet DateTime @default(now())
}
Trin 3: Opret en .env-fil
DATABASE_URL="postgresql://bruger:adgangskode@localhost:5432/mitdb?schema=public"
Trin 4: Kør migration
npx prisma migrate dev --name init
npx prisma generate
Trin 5: Brug Prisma i din kode
// lib/prisma.js
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// app/api/brugere/route.js
import { prisma } from '@/lib/prisma';
import { NextResponse } from 'next/server';
export async function GET() {
const brugere = await prisma.bruger.findMany();
return NextResponse.json(brugere);
}
export async function POST(request) {
const { navn, email } = await request.json();
const bruger = await prisma.bruger.create({
data: { navn, email },
});
return NextResponse.json(bruger, { status: 201 });
}
Supabase (gratis og nemt)
Supabase er en open-source alternativ til Firebase med en PostgreSQL-database, authentication og storage — alt sammen gratis i den lille skala.
Trin 1: Opret en konto på supabase.com
Trin 2: Opret et projekt
Trin 3: Installer Supabase-klienten
npm install @supabase/supabase-js
Trin 4: Konfigurer
// lib/supabase.js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Trin 5: Brug den
// Hent data
const { data, error } = await supabase
.from('produkter')
.select('*');
// Indsæt data
const { data, error } = await supabase
.from('produkter')
.insert([
{ titel: 'Blå skjorte', pris: 499 }
]);
Anbefaling
For begyndere anbefaler jeg Supabase — det er gratis, nemt at sætte op, og du får en fuld SQL-database uden at skulle bekymre dig om hosting af databasen.
10. Deployment
Når dit projekt er færdigt, skal det deployes (offentliggøres) på internettet. Her er de bedste muligheder.
Vercel (anbefalet)
Vercel er skabt af holdet bag Next.js og er den nemmeste måde at deploye på.
- Opret en konto på vercel.com
- Push dit projekt til GitHub, GitLab eller Bitbucket
- Log ind på Vercel og klik "New Project"
- Vælg dit repository
- Klik "Deploy"
Din side er live på et URL som mit-projekt.vercel.app i løbet af få minutter.
Fordele ved Vercel:
- Gratis for personlige projekter
- Automatisk deployment ved hver commit
- SSL-certifikat inkluderet
- Global CDN (hurtig indlæsning worldwide)
- Fungerer perfekt med Next.js
Netlify
En anden populær platform, der også er gratis for simple projekter:
- Push til GitHub
- Log ind på netlify.com
- Vælg "Add new site" → "Import from Git"
- Vælg dit repository
- Deploy
Railway
Railway er god, hvis du har brug for backend, databaser og anden infrastruktur:
- Opret konto på railway.app
- Opret et nyt projekt
- Tilføj dit repository
- Railway genkender automatisk Next.js
Tilføj et custom domæne
På alle platforme kan du købe og tilføje et domæne:
- Køb et domæne hos en registrar (f.eks. one.com eller namecheap.com)
- I Vercel/Netlify: Gå til projektindstillinger → Domains
- Tilføj dit domæne
- Opdater DNS-records hos din registrar
Checklist før deployment
- Fjern
console.log()-beskeder - Tjek at alle links virker
- Test på mobil
- Sørg for at miljøvariabler er sat i deploymentsplatformen
- Kør
npm run buildog ret fejl
11. Pris
Her er et overblik over, hvad tingene koster.
Gratis
| Ting | Pris | Note |
|---|---|---|
| Node.js | Gratis | |
| Next.js | Gratis | Open-source |
| Vercel (hosting) | Gratis | Begrænset til hobbyprojekter |
| Supabase | Gratis | 500 MB database, 1 GB storage |
| Tailwind CSS | Gratis | |
| GitHub | Gratis | Private repositories |
Betaling (når du vokser)
| Ting | Ca. pris | Note |
|---|---|---|
| Custom domæne | 50-150 kr./år | Afhænger af extension (.dk, .com, etc.) |
| Vercel Pro | ~140 kr./måned | Hvis du har brug for mere båndbredde |
| Supabase Pro | ~200 kr./måned | Flere databaser, mere storage |
| Database hosting | 50-300 kr./måned | Hvis du ikke bruger Supabase |
Realistisk budget for et lille projekt
For et typisk portfolio- eller lille forretningsprojekt:
- Domæne (.dk): ~100 kr./år
- Hosting (Vercel): Gratis
- Database (Supabase): Gratis
- Total: ~100 kr./år
Du kan altså komme i gang med et professionelt setup for under 100 kroner om året.
12. Fordele og ulemper
Fordele
- Hurtig performance — Sider indlæses ekstremt hurtigt takket være server-side rendering og caching.
- SEO-venlig — Søgemaskiner elsker Next.js-sider, fordi indholdet er tilgængeligt som HTML.
- God udvikleroplevelse — Hot reloading, automatisk routing og massevis af værktøjer gør udvikling sjovt.
- Stort community — Massevis af tutorials, forums og open-source-projekter.
- Nem deployment — Vercel gør det latterligt nemt at sætte sit projekt online.
- Skalerbarhed — Fungerer til små blogs og store virksomhedsprojekter.
- TypeScript understøttelse — Hvis du vil bruge TypeScript (anbefalet til større projekter).
Ulemper
- Indlæringskurve — Hvis du kun kender almindelig HTML/CSS/JS, skal du lære React først.
- Overkill til simple sider — En simpel visitkort-side behøver ikke Next.js.
- Kompleksitet kan vokse — Store projekter kan blive vanskelige at navigere.
- Afhængighed af Node.js — Du skal have Node.js installeret for at udvikle.
- Dyr hosting ved skala — De gratis tiers er fine til begyndere, men store projekter kan blive dyre.
Sammenligning med andre stacks
| Stack | Bedst til | Sværhedsgrad |
|---|---|---|
| Next.js + React | Moderne webapps, blogs, webshops | Mellem |
| WordPress | Blog, firma-hjemmesider | Nem |
| SvelteKit | Simpelt, hurtige apps | Nem-Mellem |
| Nuxt.js (Vue) | Webapps (Vue-økosystem) | Mellem |
| Remix | Komplekse webapps | Høj |
13. Ressourcer
Officiel dokumentation
- Next.js dokumentation — Den mest komplette kilde
- React dokumentation — Forstå fundamentet
- Tailwind CSS dokumentation — Utility-klasser reference
Tutorials og kurser
- Next.js learn — Officiel, interaktiv tutorial (gratis)
- freeCodeCamp — Gratis video tutorials på YouTube
- Fireship — Korte, intense tutorials
Community
- Next.js GitHub — Spørgsmål og fejl
- r/nextjs — Reddit community
- Stack Overflow — Tekniske spørgsmål
Værktøjer
- Vercel — Hosting og deployment
- Supabase — Database og authentication
- Prisma — ORM til databaser
- Lucide React — Flotte, gratis ikoner
- shadcn/ui — Flotte, tilgængelige UI-komponenter
Danske ressourcer
- Udvikler.dk — Dansk udvikler-community
- Hacker News — Tech-nyheder (engelsk)
14. Eksempel-projekt
Lad os bygge et simpelt portfolio-site trin for trin. Du vil lære at oprette sider, navigation, styling og en kontaktformular.
Trin 1: Opret projektet
npx create-next-app@latest portfolio
cd portfolio
npm run dev
Trin 2: Slet standard-indhold
Slet alt indhold i app/page.js og erstat med dit eget.
Trin 3: Opret grund-layoutet
// app/layout.js
import './globals.css';
import Link from 'next/link';
export const metadata = {
title: 'Mitt Portfolio',
description: 'Webudvikler baseret i Danmark',
};
export default function RootLayout({ children }) {
return (
<html lang="da">
<body className="min-h-screen bg-gray-950 text-gray-100">
<header className="border-b border-gray-800">
<nav className="max-w-4xl mx-auto p-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold text-white hover:text-blue-400 transition-colors">
Portfolio
</Link>
<div className="flex gap-6">
<Link href="/" className="hover:text-blue-400 transition-colors">
Forside
</Link>
<Link href="/om" className="hover:text-blue-400 transition-colors">
Om
</Link>
<Link href="/kontakt" className="hover:text-blue-400 transition-colors">
Kontakt
</Link>
</div>
</nav>
</header>
<main className="max-w-4xl mx-auto p-4">
{children}
</main>
<footer className="border-t border-gray-800 mt-16">
<div className="max-w-4xl mx-auto p-4 text-center text-gray-500">
© 2026 Mit Portfolio
</div>
</footer>
</body>
</html>
);
}
Trin 4: Opret forsiden
// app/page.js
import Link from 'next/link';
export default function Home() {
return (
<section className="py-20">
<h1 className="text-5xl font-bold mb-6">
Hej, jeg er <span className="text-blue-400">Anna</span>
</h1>
<p className="text-xl text-gray-400 mb-8 max-w-2xl">
Jeg er webudvikler med fokus på moderne, brugervenlige hjemmesider.
Jeg elsker at bygge ting, der gør en forskel.
</p>
<div className="flex gap-4">
<Link
href="/kontakt"
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg transition-colors"
>
Kontakt mig
</Link>
<Link
href="/om"
className="border border-gray-600 hover:border-gray-400 text-gray-300 font-bold py-3 px-6 rounded-lg transition-colors"
>
Læs mere
</Link>
</div>
{/* Projekter-sektion */}
<section className="mt-20">
<h2 className="text-3xl font-bold mb-8">Udvalgte projekter</h2>
<div className="grid gap-6 sm:grid-cols-2">
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6 hover:border-blue-500 transition-colors">
<h3 className="text-xl font-bold mb-2">E-commerce platform</h3>
<p className="text-gray-400 mb-4">
En fuld webshop bygget med Next.js og Supabase.
</p>
<span className="text-sm text-blue-400">Next.js • Supabase • Tailwind</span>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6 hover:border-blue-500 transition-colors">
<h3 className="text-xl font-bold mb-2">Booking-system</h3>
<p className="text-gray-400 mb-4">
Et booking-system til en frisørsalon med kalender.
</p>
<span className="text-sm text-blue-400">Next.js • Prisma • PostgreSQL</span>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6 hover:border-blue-500 transition-colors">
<h3 className="text-xl font-bold mb-2">Blog platform</h3>
<p className="text-gray-400 mb-4">
En blog med Markdown-understøttelse og kommentarer.
</p>
<span className="text-sm text-blue-400">Next.js • MDX • Tailwind</span>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6 hover:border-blue-500 transition-colors">
<h3 className="text-xl font-bold mb-2">SaaS Dashboard</h3>
<p className="text-gray-400 mb-4">
Et kontrolpanel til et SaaS-produkt med brugerlogin.
</p>
<span className="text-sm text-blue-400">Next.js • Supabase Auth</span>
</div>
</div>
</section>
</section>
);
}
Trin 5: Opret "Om"-siden
// app/om/page.js
export default function OmSide() {
return (
<section className="py-20">
<h1 className="text-4xl font-bold mb-8">Om mig</h1>
<div className="prose prose-invert max-w-2xl">
<p className="text-gray-300 text-lg mb-6">
Jeg er Anna, en passioneret webudvikler baseret i København.
Jeg har erfaring med at bygge moderne hjemmesider og webapplikationer
for virksomheder i alle størrelser.
</p>
<p className="text-gray-300 text-lg mb-6">
Min tilgang til webudvikling er enkel: byg hurtige, brugervenlige
løsninger, der faktisk virker. Jeg tror på, at godt design ikke
behøver at være kompliceret.
</p>
<h2 className="text-2xl font-bold mt-12 mb-4">Teknologier</h2>
<div className="flex flex-wrap gap-3">
{['React', 'Next.js', 'TypeScript', 'Tailwind CSS', 'Node.js', 'Supabase', 'PostgreSQL', 'Git'].map((tech) => (
<span
key={tech}
className="bg-blue-900/50 text-blue-300 px-3 py-1 rounded-full text-sm"
>
{tech}
</span>
))}
</div>
</div>
</section>
);
}
Trin 6: Opret kontaktformularen
// app/kontakt/page.js
'use client';
import { useState } from 'react';
export default function KontaktSide() {
const [navn, setNavn] = useState('');
const [email, setEmail] = useState('');
const [besked, setBesked] = useState('');
const [status, setStatus] = useState('');
const sendBesked = async (e) => {
e.preventDefault();
setStatus('sender...');
// Her ville du normalt sende til en API-route
// For nu simulerer vi en forsinkelse
await new Promise((resolve) => setTimeout(resolve, 1000));
setStatus('Besked sendt! Jeg vender tilbage snart.');
setNavn('');
setEmail('');
setBesked('');
};
return (
<section className="py-20">
<h1 className="text-4xl font-bold mb-8">Kontakt mig</h1>
<p className="text-gray-400 mb-8 max-w-xl">
Har du et projekt i tankerne? Eller vil du bare sige hej?
Udfyld formularen, så vender jeg tilbage hurtigst muligt.
</p>
<form onSubmit={sendBesked} className="max-w-xl space-y-6">
<div>
<label htmlFor="navn" className="block text-sm font-medium mb-2">
Navn
</label>
<input
type="text"
id="navn"
value={navn}
onChange={(e) => setNavn(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-blue-500 focus:outline-none"
placeholder="Dit navn"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-blue-500 focus:outline-none"
placeholder="din@email.dk"
/>
</div>
<div>
<label htmlFor="besked" className="block text-sm font-medium mb-2">
Besked
</label>
<textarea
id="besked"
value={besked}
onChange={(e) => setBesked(e.target.value)}
required
rows={6}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-blue-500 focus:outline-none resize-none"
placeholder="Fortæl om dit projekt..."
/>
</div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg transition-colors"
>
Send besked
</button>
{status && (
<p className="text-green-400 mt-4">{status}</p>
)}
</form>
</section>
);
}
Trin 7: Tilføj kontaktformular-funktionalitet
Opret en API-route til at håndtere formularen:
// app/api/kontakt/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const { navn, email, besked } = await request.json();
// Validering
if (!navn || !email || !besked) {
return NextResponse.json(
{ error: 'Alle felter er påkrævet' },
{ status: 400 }
);
}
// Her ville du typisk:
// 1. Gemme i en database
// 2. Sende en email (med f.eks. Resend eller SendGrid)
// 3. Sende til en webhook (f.eks. Slack)
console.log('Ny besked:', { navn, email, besked });
return NextResponse.json(
{ message: 'Besked modtaget' },
{ status: 200 }
);
}
Trin 8: Kør dit projekt
npm run dev
Åbn http://localhost:3000 i din browser. Du har nu et fungerende portfolio-site med:
- Forside med projekter
- Om-side med teknologier
- Kontaktformular
- Responsive design
- Mørkt tema
Trin 9: Deploy det
git init
git add .
git commit -m "Første version af portfolio"
- Opret et repository på GitHub
- Push dit projekt
- Log ind på Vercel og importer repository
- Klik "Deploy"
Dit portfolio-site er nu live!
Bonus: Tilføj flere funktioner
Når du er klar til at udvide, kan du tilføje:
- En blog med Markdown
- Brugerlogin med Supabase Auth
- Et admin-panel til at redigere indhold
- Dark mode / Light mode toggle
- Animations med Framer Motion
- Formularvalidering med Zod
Afslutning
Tillykke! Du har nu en komplet guide til at bygge hjemmesider og apps med Next.js. Her er de vigtigste ting at huske:
- Start simpelt — Byg en enkelt side, før du går videre til komplekse features
- Lær React først — Forstå komponenter, state og props
- Brug Next.js' styrker — Server-side rendering og filbaseret routing
- Vælg den rigtige database — Supabase til begyndere, Prisma til mere kontrol
- Deploy tidligt — Det motiverer at se dit projekt live
- Fejl er læring — Alle udviklere laver fejl. Det er sådan, vi lærer.
Held og lykke med dit projekt! 🚀
Denne guide blev sidst opdateret i juni 2026.