← Alle guider

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

  1. Hvad er Next.js?
  2. Hornår skal du bruge det?
  3. Kom i gang
  4. Projektstruktur
  5. Sider og routing
  6. Components
  7. Data hentning
  8. Styling
  9. Database
  10. Deployment
  11. Pris
  12. Fordele og ulemper
  13. Ressourcer
  14. 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?

Hvad er Next.js godt til?

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?


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.

  1. Gå til nodejs.org
  2. Download den LTS-version (Long Term Support) — det er den mest stabile
  3. Kør installationsfilen og følg vejledningen
  4. 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

  1. Én komponent per fil — Hold komponenter små og fokuserede
  2. Brug store bogstaverProduktKort ikke produktKort
  3. Gør dem genbrugelige — Undgå at hårdkode data i komponenten
  4. Brug props til data — Lad komponenten modtage data udefra
  5. 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å.

  1. Opret en konto på vercel.com
  2. Push dit projekt til GitHub, GitLab eller Bitbucket
  3. Log ind på Vercel og klik "New Project"
  4. Vælg dit repository
  5. Klik "Deploy"

Din side er live på et URL som mit-projekt.vercel.app i løbet af få minutter.

Fordele ved Vercel:

Netlify

En anden populær platform, der også er gratis for simple projekter:

  1. Push til GitHub
  2. Log ind på netlify.com
  3. Vælg "Add new site" → "Import from Git"
  4. Vælg dit repository
  5. Deploy

Railway

Railway er god, hvis du har brug for backend, databaser og anden infrastruktur:

  1. Opret konto på railway.app
  2. Opret et nyt projekt
  3. Tilføj dit repository
  4. Railway genkender automatisk Next.js

Tilføj et custom domæne

På alle platforme kan du købe og tilføje et domæne:

  1. Køb et domæne hos en registrar (f.eks. one.com eller namecheap.com)
  2. I Vercel/Netlify: Gå til projektindstillinger → Domains
  3. Tilføj dit domæne
  4. Opdater DNS-records hos din registrar

Checklist før deployment


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:

Du kan altså komme i gang med et professionelt setup for under 100 kroner om året.


12. Fordele og ulemper

Fordele

  1. Hurtig performance — Sider indlæses ekstremt hurtigt takket være server-side rendering og caching.
  2. SEO-venlig — Søgemaskiner elsker Next.js-sider, fordi indholdet er tilgængeligt som HTML.
  3. God udvikleroplevelse — Hot reloading, automatisk routing og massevis af værktøjer gør udvikling sjovt.
  4. Stort community — Massevis af tutorials, forums og open-source-projekter.
  5. Nem deployment — Vercel gør det latterligt nemt at sætte sit projekt online.
  6. Skalerbarhed — Fungerer til små blogs og store virksomhedsprojekter.
  7. TypeScript understøttelse — Hvis du vil bruge TypeScript (anbefalet til større projekter).

Ulemper

  1. Indlæringskurve — Hvis du kun kender almindelig HTML/CSS/JS, skal du lære React først.
  2. Overkill til simple sider — En simpel visitkort-side behøver ikke Next.js.
  3. Kompleksitet kan vokse — Store projekter kan blive vanskelige at navigere.
  4. Afhængighed af Node.js — Du skal have Node.js installeret for at udvikle.
  5. 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

Tutorials og kurser

Community

Værktøjer

Danske ressourcer


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:

Trin 9: Deploy det

git init
    git add .
    git commit -m "Første version af portfolio"
    
  1. Opret et repository på GitHub
  2. Push dit projekt
  3. Log ind på Vercel og importer repository
  4. 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:


Afslutning

Tillykke! Du har nu en komplet guide til at bygge hjemmesider og apps med Next.js. Her er de vigtigste ting at huske:

  1. Start simpelt — Byg en enkelt side, før du går videre til komplekse features
  2. Lær React først — Forstå komponenter, state og props
  3. Brug Next.js' styrker — Server-side rendering og filbaseret routing
  4. Vælg den rigtige database — Supabase til begyndere, Prisma til mere kontrol
  5. Deploy tidligt — Det motiverer at se dit projekt live
  6. 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.