---
title: "Jeg byggede min portfolio om til et samtale-AI-produkt — Blackbook"
description: "Sådan driver ét Sanity-indholdslag både et visuelt portfolio, en schema.org-vidensgraf og en AI-agent, man kan tale med – semantisk søgning uden vektordatabase."
canonical_url: https://www.blackbook.dk/journal/portfolio-som-ai-agent/
md_url: https://www.blackbook.dk/journal/portfolio-som-ai-agent/index.md
language: da
last_updated: 2026-06-14
---

# Jeg byggede min portfolio om til et samtale-AI-produkt — Blackbook

Sådan driver ét Sanity-indholdslag både et visuelt portfolio, en schema.org-vidensgraf og en AI-agent, man kan tale med – semantisk søgning uden vektordatabase.

> Alternate (English): https://www.blackbook.dk/en/journal/portfolio-as-ai-agent/index.md
> Author: Jeppe Sloth Carlsen — Senior Product Designer & Creative Technologist (https://www.blackbook.dk/about/)
> Author ID: https://www.blackbook.dk/#person-jeppe
> Publisher ID: https://www.blackbook.dk/#organization
> Topics: AI-agent, llms.txt, Portfolio som chatbot, Claude API
> Part of: Blackbook — digitalt designbureau, København (https://www.blackbook.dk/)
> Published: 2026-06-14
> LLM reference: https://www.blackbook.dk/llms.txt
> Full reference: https://www.blackbook.dk/llms-full.txt
> Markdown sitemap: https://www.blackbook.dk/sitemap.md
> Preferred citation URL: https://www.blackbook.dk/journal/portfolio-som-ai-agent/

## Journal — Jeg byggede min portfolio om til et samtale-AI-produkt

- Forsiden på blackbook.dk – 'Making AI work'-sektionen med ekspertiseområder

Alle taler om AI. Jeg byggede et produkt. [blackbook.dk](https://www.blackbook.dk/) er en designportfolio, hvor den primære grænseflade er en samtale – en AI-agent, der kender hvert projekt, hver kompetence og hver kunde, og finder dem ud fra betydning, ikke søgeord. Under motorhjelmen er det ét indholdslag, der fodrer tre output: det visuelle site, en schema.org-vidensgraf og AI-agenten. Ingen duplikering, ingen manuel synkronisering.

Sådan fungerer det.

## Kerneideen: indhold som strukturerede data

Hvert projekt, hver kompetence, kunde og rolle findes én gang – som struktureret indhold i Sanity CMS. Et projekt er ikke en side; det er et dokument med typede relationer til kunder, kategorier (kompetencer), ekspertiseområder og erfaringsposter.

```typescript
// Sanity project schema — simplified
defineField({
  name: 'client',
  type: 'reference',
  to: [{ type: 'clientLogo' }],
}),
defineField({
  name: 'areas',
  type: 'array',
  of: [{ type: 'reference', to: [{ type: 'portfolioArea' }] }],
}),
defineField({
  name: 'categories',
  type: 'array',
  of: [{
    type: 'object',
    fields: [
      { name: 'category', type: 'reference', to: [{ type: 'category' }] },
      { name: 'isPrimary', type: 'boolean' },
    ],
  }],
}),
```

Én indholdsmodel. Tre forbrugere: den visuelle grænseflade renderer det som sider, vidensgrafen serialiserer det som JSON-LD, og AI-agenten forespørger det, mens samtalen foregår. Ret et projekt ét sted – det opdateres alle steder.

- Projektside for Valiyou.io renderet fra struktureret Sanity-indhold

## Søgning på betydning, ikke søgeord

Det her er den del, der overraskede mig mest. Når nogen beder agenten "vis mig pharma-arbejde", leder den ikke efter ordet "pharma" i titlerne. Den bruger **Sanitys indbyggede semantiske similarity** – vektor-embeddings beregnet server-side, forespurgt med ét enkelt GROQ-udtryk:

```groq
*[_type == "project" && hidden != true]
  | score(text::semanticSimilarity($q))
  | order(_score desc)[0...12] {
    _score,
    "titleEn": title.en,
    "slug": slug.current,
    "clientName": client->name,
    featured,
    "categories": categories[0..5]{
      "en": category->title.en
    }
  }
```

Brugerens fulde spørgsmål sendes med som `$q`. Sanity returnerer de 12 bedste projekter rangeret efter semantisk lighed, hver med en `_score`. Resultater under en tærskel på 0.05 filtreres fra.

**Ingen Pinecone. Ingen Weaviate. Ingen pgvector.** Sanitys eget embedding-indeks håndterer vektorsøgningen. Hele opslaget er én GROQ-query med en scoringsfunktion.

Når semantisk søgning intet returnerer (sjældent, men muligt), falder systemet tilbage på keyword-matching – en simpel scoringsfunktion, der tæller hvor mange udtrukne nøgleord der optræder i hvert projekts titel, tagline, kategorier og SEO-felter:

```typescript
function scoreProject(p: Project, keywords: string[]): number {
  const hay = [
    p.titleEn, p.titleDa, p.taglineEn, p.taglineDa,
    p.seoEn, p.seoDa, p.clientName, cats,
  ].join(' ').toLowerCase()
  return keywords.filter(k => hay.includes(k)).length
}
```

Semantik først, keywords som fallback. Agenten logger, hvilken metode der blev brugt ved hvert svar.

- Kompetencesiden med AI-agenten, der svarer på 'Who is Jeppe Sloth Carlsen?'

## Én model, tre output

Det samme Sanity-indhold, som AI'en forespørger, genererer også en schema.org-vidensgraf – 32 entitetstyper fordelt over sitet. Hver kompetence linker til Wikidata, og hvert projekt er et `CreativeWork` med typede relationer:

```json
{
  "@type": "CreativeWork",
  "@id": "https://www.blackbook.dk/en/portfolio/danske-bank/#work",
  "name": "White-Label Banking Solution — Danske Bank",
  "creator": { "@id": "https://www.blackbook.dk/#person-jeppe" },
  "sourceOrganization": {
    "@type": "Organization",
    "name": "Danske Bank",
    "sameAs": ["https://en.wikipedia.org/wiki/Danske_Bank",
               "https://www.wikidata.org/wiki/Q1636974"]
  },
  "about": [
    { "@type": "Thing", "name": "UX & Product Design",
      "sameAs": ["https://www.wikidata.org/wiki/Q11248500"] },
    { "@type": "Thing", "name": "FinTech",
      "sameAs": ["https://www.wikidata.org/wiki/Q16319025"] }
  ]
}
```

Organization-entiteten bærer et kurateret `knowsAbout` – 12 emner, der definerer hvad studiet laver, hver især linket til Wikidata. Person-entiteten bærer 94 kompetencer (biografiske). To adskilte sæt: kommerciel positionering vs. individuel ekspertise, forbundet via `founder` / `worksFor`.

Google, ChatGPT og Perplexity kan alle læse grafen. Når nogen spørger en AI-assistent "hvem laver fintech-UX i København", er det de strukturerede data, der gør svaret muligt – ikke sideteksten.

- Portfoliosiden med Talk to My Agent-panelet og foreslåede spørgsmål

## Den svære del: at gøre site og samtale til ét system

Den ærlige udfordring var ikke at bygge en AI-chat. Det var at gøre sitet og samtalen til **ét system**.

AI-agenten kører på Claude Haiku. Dens kontekst samles ved hver request fra tre kilder – alle fra det samme Sanity-datasæt, som sitet renderer fra:

```typescript
// Same Sanity content, three context blocks
const portfolioContext = await fetchPortfolioContext(messages)
const servicesContext  = await getServicesContext(lang)

const dynamicBlock = langInstruction
  + (servicesContext ? '\n\n' + servicesContext : '')
  + (portfolioContext ? '\n\n' + portfolioContext : '')
```

`fetchPortfolioContext` forespørger de samme `project`-dokumenter, som portfolio-siderne renderer. `getServicesContext` læser de samme `category`-beskrivelser, som vises på service-siderne. Der findes ikke noget separat "AI-indhold" – agenten læser sitet.

Det betyder, at når jeg opdaterer en projektbeskrivelse, tilføjer en ny kompetenceside eller ændrer et kundenavn, samler AI-agenten det op i den næste samtale. Ingen eksport, ingen embedding-pipeline, ingen manuel synkronisering. Én kilde til sandhed.

Prisen: agentens viden er afgrænset af indholdsmodellen. Den kan ikke vide ting, der ikke er struktureret i Sanity. Det er en feature, ikke en bug – det holder agenten ærlig og indholdet autoritativt.

## En note om motion

Sitet har et håndbygget motion-lag – ingen template, intet plugin, intet animationsbibliotek.

Sideovergange bruger et **cirkelgrid** – et 15×17 CSS-grid af cirkulære `<div>`-elementer, der skalerer fra centrum og udad, drevet af scroll-progress eller klik. Hver celle har en tærskel baseret på dens afstand til centrum:

```typescript
// Each cell scales based on how far past its threshold the progress is
const cp = Math.min(1, Math.max(0,
  (progress * (1 + SPREAD) - cell.threshold) / SPREAD
))
cell.el.style.transform = cp <= 0
  ? 'scale(0)'
  : `scale(${cp * GRID_SCALE})`
```

På projektsider er overgangen scroll-drevet – brugeren styrer afsløringen ved at scrolle forbi bunden. Samme komponent håndterer klik-baserede sideovergange, billedafsløringer (iris-mønster, cirkler der skrumper og afslører billedet) og den scroll-drevne VennDiagram-animation på about-siden. Én genbrugelig komponent, fire anvendelser.

Projekt-tiles bruger en fysikbaseret evade-effekt – tiles deformeres og fjedrer væk fra cursoren via mesh-segmentering og spring damping. WebGL bruges kun til hero-billedernes displacement-overgange på forsiden. Alt andet er CSS-transforms ved 60 fps.

## Hvad jeg ville gøre anderledes

**Semantisk søgning skulle have været udgangspunktet, ikke en tilføjelse.** Jeg byggede keyword-scoring først og tilføjede semantik bagefter. Set i bakspejlet er Sanitys `text::semanticSimilarity()` så enkel at implementere (én GROQ-funktion), at den skulle have været den eneste retrieval-metode fra dag ét, med keywords som nød-fallback.

**Schema.org-grafen tog længere tid end forventet.** 32 entitetstyper, tosproget, med Wikidata-krydsreferencer – det virker, men validerings- og debug-cyklussen er langsom. Skulle jeg gøre det igen, ville jeg bygge en testsuite, der validerer hver sides JSON-LD mod schema.org ved hvert deploy.

**Motion-laget er det værd, men dyrt.** Håndbyggede overgange betyder håndvedligeholdte overgange. Hver ny sidetype kræver scroll-håndtering, touch-håndtering og iOS-edge-cases. Et framework som Framer Motion ville have sparet tid – men ville ikke have givet mig scroll-drevne cirkelgrids.

- Interaktiv vidensgraf på blackbook.dk bygget på det samme Sanity-datasæt

## Prøv den selv

Åbn [blackbook.dk](https://www.blackbook.dk/), klik på orb'en nederst til højre, og spørg den om hvad som helst. Spørg om fintech, spørg om en bestemt kunde, eller bed den overraske dig.

Eller udforsk vidensgrafen direkte: [blackbook.dk/cv](https://www.blackbook.dk/cv/) – det er de samme data, visualiseret som et force-directed netværk.

Kilden til sandhed er ét Sanity-datasæt. AI'en, sitet og vidensgrafen er tre visninger af det samme indhold. Det er arkitekturen.

Bygget med Next.js 16, Sanity, Claude Haiku og alt for mange `requestAnimationFrame`-callbacks.

## Mere

- [Journal](https://www.blackbook.dk/journal/)

## Om Blackbook

Blackbook forener kreativitet og teknologi og skaber digitale løsninger inden for UX, branding, websites og eCommerce.

Ekspertiseområder:

- [Apps & Products](https://www.blackbook.dk/area/apps-products/index.md)
- [Brand & Campaigns](https://www.blackbook.dk/area/brand-campaigns/index.md)
- [Websites & eCommerce](https://www.blackbook.dk/area/websites-ecommerce/index.md)

Nøgletal:

- 250+ Digitale projekter
- 150+ Kunder
- 15+ Fortune 500
- 5+ C25
- 25+ Års erfaring

## Site

[Forside](https://www.blackbook.dk/index.md)
[Portfolio](https://www.blackbook.dk/portfolio/index.md)
[Om mig](https://www.blackbook.dk/about/index.md)
[Services](https://www.blackbook.dk/services/index.md)
[Experience](https://www.blackbook.dk/experience/index.md)
[Anerkendelser](https://www.blackbook.dk/awards/index.md)
[Kontakt](https://www.blackbook.dk/contact/index.md)
[CV](https://www.blackbook.dk/cv/index.md)
[Journal](https://www.blackbook.dk/journal/index.md)

Blackbook · Jerichausgade 7 · 1777 København · (+45) 31 10 79 13 · [jeppe@blackbook.dk](mailto:jeppe@blackbook.dk)
