Oxygen 6: hvorfor vi bygger komponenter før vi bygger sider
De fleste nettsider blir vanskelige å endre etter et halvt år.
En mal, ett datasett og en port som stopper det som ikke holder mål. Slik bygger vi nettsteder der hundre sider vedlikeholdes som én.
Vi driver et dynamisk nettsted selv. Ingen kunde har bestilt det, ingen har betalt for det, og det er nettopp derfor vi kan skrive om innsiden av det — en reiseguide bygget rundt et kart, der hundrevis av steder deler én mal og ett datasett.
Vi kaller det kartprosjektet her, og oppgir ingen tall for hvor stort det er. Ikke fordi det er hemmelig — en oppmerksom leser finner det nok — men fordi det er under kontinuerlig utvikling. Et sidetall derfra er utdatert kort tid etter at det er skrevet, og en artikkel som lener seg på et slikt tall blir en reklame med kort holdbarhet i stedet for en forklaring. Vil du se det, spør oss, så viser vi det fram med det som står der den dagen.
Det som ikke endrer seg, er hvordan det er bygget. Ingen av sidene er laget for hånd som en side. De er rader som møter en mal. Legger vi til et sted, finnes siden, og den ligger allerede i kartet, i filtrene og i de interne lenkene til nabosidene.
Denne artikkelen er en gjennomgang av mekanikken: hvorfor filtrene er en datamodellbeslutning og ikke en designbeslutning, hvorfor de fleste automatiseringene ikke går på klokke, hvordan et scenario ser ut når du åpner det, og hva porten foran publisering faktisk stopper.
Kort vei til svaret: har dere en katalog, et register eller et kart som skal bli til mange sider, be om en gjennomgang av datamodellen — det er den som avgjør prisen senere. Vil du se hvordan vi løste det, les videre.
Alt i denne artikkelen er hentet fra vårt eget oppsett, lest 15. august 2026. Scenarioene er lest ut av Make sitt API, ikke talt opp i grensesnittet og ikke husket.
Det vi kan stå inne for:
Om tallene: artikkelen er full av dem, men ikke ett eneste for størrelsen på kartprosjektet — av grunnen som står i ingressen.
Det vi ikke påstår: at dette er den eneste måten å bygge et dynamisk nettsted på, eller at oppsettet er ferdig. Store deler av datasettet er fortsatt tynt, og noen av scenarioene er byggeklosser vi laget for ett formål og siden gjenbrukte til et annet.
Vi oppgir heller ingen tall for hvor mye tid dette sparer. Vi har ikke bygget nettstedet manuelt først for å måle forskjellen, og et anslag uten en måling bak er bare en påstand med desimaler.
Innholdet endrer seg, og scenarioene endrer seg. Artikkelen gjennomgås derfor på nytt når innholdet eller scenarioene endrer seg. Sist gjennomgått 15. august 2026.
Skrevet av Svein Tore Olsen, daglig leder i Ebizapple, som holder til i Askim.
Et statisk nettsted har én fil per side. Skal du ha tre hundre sider, lager du tre hundre sider, og skal du endre hvordan de ser ut, endrer du tre hundre ganger.
Et dynamisk nettsted snur det: du lager én mal og ett datasett. Malen beskriver hvordan en oppføring skal se ut. Datasettet inneholder radene. Sidene finnes fordi radene finnes.
Geografien i vårt eget tilfelle har to nivåer: kyststrekninger, og comarcaer — den spanske inndelingen under provinsnivå. Det er derfor en oppføring kan filtreres på begge nivåer uten at de er i konflikt. Poenget er ikke geografien; poenget er at to uavhengige nivåer må være to felter i datasettet, ikke ett. Slår du dem sammen til ett, oppdager du det den dagen noen vil filtrere på det smale nivået alene.
Fordelingen mellom kategoriene blir alltid skjev. Der noen har jobbet mye, ligger det gjerne ti ganger så mange oppføringer som i de tynneste — ikke fordi noe er galt, men fordi et datasett vokser ujevnt. Det er en av grunnene til at malen må tåle en kategori med en håndfull oppføringer like godt som en med femti. En mal som ser bra ut bare når listen er lang, er ikke en mal. Den er et designutkast.
Det samme gjelder feltene. En oppføring som mangler et bilde, skal ikke gi et hull i layouten. I praksis betyr det at malen må bygges rundt den fattigste raden, ikke rundt den rikeste. Vi bygger nettsteder etter det samme prinsippet i Oxygen — se hvordan vi setter opp komponenter før sider når innholdet skal komme fra et system.
Er ikke dette bare en database med et pent skall? Nei, og det er verdt å ta innvendingen på alvor, for den er halvveis riktig. Databasen er der. Men et dynamisk nettsted er ikke bare en visning av rader — det er malen, filtrene, de interne lenkene og vedlikeholdet som holder radene sanne over tid. Uten det siste har du en database med et pent skall, og det er nettopp den tilstanden folk pleier å ende i.
Det som gjør et slikt nettsted til noe annet enn en lang liste, er kartet og filtrene. Hos oss kan brukeren snevre inn på stedstype og på område samtidig — strand, vik, kafé, turløype, historisk sted, museum, naturpark, utsiktspunkt, landsby og noen til, krysset med distriktsinndelingen.
Det ser ut som en designbeslutning. Det er en datamodellbeslutning, og det er den dyreste å gjøre om senere.
Grunnen er enkel: et filter kan bare filtrere på noe som ligger i datasettet som et eget felt. Har du skrevet «en fin liten strandby med museum» i et fritekstfelt, kan du ikke filtrere på det.
To spørsmål avgjør derfor hvordan modellen skal se ut, og de bør stilles før noen tegner en eneste skjerm:
Dette er den delen av jobben som ser billigst ut og er dyrest å angre på. Det er også grunnen til at vi vurderer datakilden før vi priser noe: nivågraderingen fra N1 til N5 handler om nøyaktig dette — hvor mye struktur som allerede finnes, og hvor mye som må lages.
Skal dere bygge noe med filtre? Send oss feltene dere har i dag, så sier vi hva som mangler før dere låser modellen.
De fleste scenarioene bak kartprosjektet er on-demand. Det er den viktigste opplysningen i denne seksjonen, og den er verdt mer enn hvor mange de er.
Kjøremåten leses av feltet scheduling.type på hvert scenario, og den er uavhengig av om scenarioet står på. En del av de on-demand er inaktive til enhver tid — de er byggeklosser ingen kaller på for tiden, ikke jobber som har stoppet. Resten fordeler seg på noen som startes av en webhook utenfra, og noen som går på klokke.
Et on-demand-scenario er ikke en jobb som står og går. Det er en byggekloss som andre scenarioer kaller på som et verktøy. Det tar imot parametre, gjør én ting, og returnerer et svar. Det er nøyaktig samme idé som en funksjon i et program: du skriver den én gang, og kaller den fra ti steder.
Arkitekturen har altså få inngangspunkter og mange deler. De tidsstyrte og de webhook-drevne er inngangene. De on-demand er delene, og de kjører bare når noen kaller på dem.
Blir det ikke uoversiktlig med mange scenarioer? Man har ikke like mange jobber. Alternativet til mange små byggeklosser er noen få store scenarioer som gjør alt — og de er verre. Et stort scenario kan ikke gjenbrukes, det er vanskelig å teste isolert, og når det stopper, stopper alt på én gang. Deler du det opp, kan du kjøre én kloss alene og se om den fortsatt gjør jobben sin.
Prisen du betaler for oppdelingen er navngiving og oversikt. Et scenario som heter «test 3» er tapt i det øyeblikket du har hundre av dem — og vi har selv slike i listen. Det er en av grunnene til at automatisering er noe man vedlikeholder, ikke noe man leverer og går fra.
Nå til det som er vanskeligst å forklare med ord, og enklest å forstå når du ser det: hvordan lese et scenario i Make.
Dette er den reelle forskjellen fra et skript. Et skript er tekst, og tekst kan bare leses av noen som kan lese den. Et scenario er en tegning — og en tegning kan leses av kunden, av en kollega, og av deg selv om seks måneder.
Hver modul er en sirkel. De står på rad og leses fra venstre mot høyre, i den rekkefølgen de kjører. Første sirkel er starten, siste sirkel er slutten.
Under hver sirkel står to ting: hva modulen heter, og hvilken handling den utfører — for eksempel notion:makeApiCall eller wordpress:ActionCreatePost. Den nederste linjen er den viktige. Den sier hva modulen faktisk gjør, ikke bare hvilken app den snakker med.
Øverst til venstre på hver sirkel står et lite nummer i en hvit ring. Det er modulens id, og det er ikke pynt.
Nummeret er adressen du bruker når du henter en verdi fra en tidligere modul. Ser du uttrykket {{2.body.results}} et sted i scenarioet, betyr det: «feltet body.results fra modul 2». Ingenting annet.
Det er derfor du kan lese et scenario uten å lese kode. Finner du et uttrykk som peker på modul 2, ser du på sirkel nummer 2 og vet hvor verdien kommer fra.
Linjen mellom to sirkler er en rute. Data flyter langs den.
Ligger det et filter på ruten, tegner Make en liten trakt midt på linjen, med filterets navn ved siden av. Det er hele varselet: det som går inn i traktens venstre side, kommer ikke nødvendigvis ut på høyre.
Et filter er altså ikke en modul. Det er en betingelse som henger på forbindelsen mellom to moduler, og den er synlig som en trakt selv om du ikke åpner noe.
To modultyper hører alltid sammen, og de er verdt å kjenne igjen på formen i teksten under sirkelen:
Ser du en iterator uten en aggregator etterpå, fortsetter scenarioet å kjøre én gang per element helt til slutten. Det er noen ganger riktig, og noen ganger er det grunnen til at du får 100 innlegg i stedet for ett. Fellen er beskrevet nærmere i fem feller i Make.
Fargen velger du ikke. Den følger appen: Notion er svart, WordPress har sin, en innebygd Make-modul er hvit. Det er en detalj, men den er nyttig — du ser hvilke systemer et scenario rører ved, før du har lest ett ord.
Summen av dette er poenget: du kan se hva et scenario gjør uten å lese kode. Det er derfor kunden selv kan åpne kjøringsloggen og peke på hvilken sirkel det stoppet i, i stedet for å sende oss en skjermdump av en feilmelding uten sammenheng.
Første eksempel er «KLYNGE-BYGGER». Det er fem moduler på rad, og det gjør én ting: bygger interne lenker mellom sider som hører til samme region.
| # | Modul | Hva den gjør |
|---|---|---|
| 1 | scenario-service:StartSubscenario |
Tar imot slug og provins |
| 2 | notion:makeApiCall |
Henter sidene fra Notion-databasen |
| 3 | builtin:BasicFeeder |
Itererer over treffene, én kjøring hver |
| 4 | util:TextAggregator |
Bygger listen med lenker |
| 5 | scenario-service:ReturnData |
Returnerer cluster_html og region |
Modul 1 gjør scenarioet til et verktøy. Det tar imot to parametre: slug — sidens egen, som skal ekskluderes — og provins, altså costa-regionen det skal bygges klynge for. Fordi parametrene kommer utenfra, kan det samme scenarioet brukes for hvilken som helst side i hvilken som helst region.
Modul 2 er et POST-kall mot Notion-databasen, med filteret Status = GROUNDED, sortert på Entity, med page_size: 100. Filteret er verdt å stoppe ved: bare sider som er grunnet, blir lenket til. En side som ikke har kommet gjennom kvalitetskontrollen, finnes ikke for klyngebyggeren.
Modul 3 og 4 er iterator-og-aggregator-paret fra forrige seksjon. Feederen itererer over {{2.body.results}} — der ser du modulnummeret i praksis — og gir én kjøring per side. Aggregatoren bygger så én <li><a href='…'> per treff, og hopper over de radene der provinsen ikke stemmer eller sluggen er sidens egen. Uten den siste sjekken ville hver side lenket til seg selv.
Modul 5 returnerer cluster_html og region til den som spurte.
Resultatet er en boks nederst på siden, med en overskrift som selv er en mal: den inneholder et krøllparentes-felt som fylles med regionnavnet i det øyeblikket boksen bygges. Overskriften er altså ikke skrevet én gang per region — den er skrevet én gang, punktum.
Poenget med å gjøre dette i et scenario framfor for hånd: lenkene er alltid ferske. Kommer det en ny destinasjon i en region, dukker den opp i klyngen til naboene neste gang boksen bygges. Ingen sitter og oppdaterer lenkelister.
Her er ironien, og vi tar den med fordi den sier noe ærlig om hvordan slike valg faktisk tas.
Klyngelenkingen på ebizapple.no — pilarsiden lenker ned til hver artikkel i klyngen, og hver artikkel lenker opp igjen til pilaren — er nøyaktig det samme mønsteret som klyngebyggeren lager: en side skal aldri stå alene.
Forskjellen er hvordan den er laget. På kartprosjektet gjør et scenario det, løpende. På denne siden gjør et byggeskript det, når nettstedet bygges.
Er det ene bedre enn det andre? Ikke prinsipielt. Valget følger av hvor ofte innholdet endrer seg og hvem som skal endre det:
Den siste raden er den vi oftest gir bort gratis. Automatisering av noe som skjer tre ganger i året koster mer enn den sparer.
Andre eksempel er «GATE-then-PUBLISH». Tre moduler, og likevel det scenarioet som har spart oss for mest opprydding.
Modulene er:
scenario-service:StartSubscenario — tar imot title, slug, content, meta_description, aof_task_uid, canonical, status, evidence og p0_thresholdwordpress:ActionCreatePost — oppretter innleggetscenario-service:ReturnData — returnerer resultatetDet interessante ligger ikke i modulene. Det ligger på ruten mellom modul 1 og modul 2, der det henger et filter som heter «P0 hard gate (schema-tro)». På tegningen er det trakten.
Filteret har ti betingelser, og alle må være sanne for at bundelen slipper videre til publisering:
| # | Felt | Betingelse |
|---|---|---|
| 1 | title |
finnes |
| 2 | title |
minst 3 tegn |
| 3 | title |
høyst 200 tegn |
| 4 | aof_task_uid |
finnes |
| 5 | slug |
matcher ^[a-z0-9]+(?:-[a-z0-9]+)*$ |
| 6 | canonical |
begynner med https:// |
| 7 | meta_description |
finnes |
| 8 | meta_description |
høyst 320 tegn |
| 9 | content |
minst 300 tegn |
| 10 | evidence |
er minst p0_threshold |
De ni første er formkrav. De sier at en side ikke er en side før den har en tittel, en gyldig slug, en kanonisk adresse og nok innhold til å være verdt å indeksere. Betingelse 5 er den som fanger mest i praksis: en slug med en stor bokstav, en æ, eller to bindestreker etter hverandre stanses her, i stedet for å bli en stygg adresse ingen kan endre etterpå.
Den tiende er en annen type krav. evidence mot p0_threshold er belegg mot terskel: hvor godt underbygget innholdet er, målt mot hvor godt det må være. Er belegget under terskelen, publiseres det ikke, uansett hvor pen teksten er.
Kommer bundelen ikke gjennom, skjer det ingenting. Ingen kladd, ingen halvferdig side, ingen «publisert, men fiks den senere».
Og det er den andre halvdelen av poenget: porten sier heller ikke at det gikk bra. En automatisering som svarer «ferdig» på noe den ikke gjorde, er farligere enn en som stopper. Det er hele resonnementet fra når AI blir blokkert og ingen merker det, og porten her er den praktiske konsekvensen av det.
Kan vi ikke bare skrive sidene selv? Jo, og for tjue sider er det sannsynligvis riktig. Poenget med porten er ikke å erstatte en skribent. Det er at når først noe skal skje maskinelt — enten det er en importert katalog eller en generert beskrivelse — så må det finnes et sted der kravene er skrevet ned som betingelser, ikke som en avtale noen husker. Ti betingelser i et filter er en slik nedskriving.
Rekkefølgen er den samme uansett om det er destinasjoner, produkter eller ansatte. Den er også den samme som vi selv brøt et par ganger før vi lærte det.
Datamodellen først, malen etterpå. Hvilke felter finnes, hvilke er obligatoriske, og hvilke skal det filtreres på? Filtrene er felter. Det du ikke har skilt ut som et felt, kan ikke bli et filter senere uten at alle radene må vurderes på nytt.
Bygg malen mot den fattigste raden. Den raden med manglende bilde og to setningers beskrivelse er den som avgjør om malen holder.
Skaff dataene før du lover en dato. Å hente inn noen hundre rader er en helt annen jobb avhengig av hvor de kommer fra. Vi bruker nivågradering før vi priser datainnhenting, og for katalogjobber spesielt er historien om produktkatalogen vi prøvde å hente en ærligere beskrivelse av hva som møter deg enn noen kravspesifikasjon.
Del automatiseringen i byggeklosser. Ett scenario som gjør én ting og returnerer et svar, kalt fra et scenario som styrer rekkefølgen. Det er derfor de fleste av våre er on-demand.
Sett porten på plass før den første publiseringen, ikke etterpå. Det er lett å legge til en port når ingenting er publisert ennå. Det er en oppryddingsjobb å legge den til når flere hundre sider allerede ligger ute.
Bestem hvem som eier innholdet etterpå. Et dynamisk nettsted flytter arbeidet fra å lage sider til å vedlikeholde data. Det er en bedre jobb, men det er fortsatt en jobb, og den forsvinner ikke fordi noe er automatisert. Se hva som inngår når vi bygger nettsteder på WordPress, og hva et slikt oppsett koster i prisoversikten vår.
Flere titalls, men tallet er mindre interessant enn fordelingen. Rundt tre firedeler er on-demand — altså byggeklosser som andre scenarioer kaller på, ikke jobber som står og går. Inngangene er få: en håndfull som starter på en webhook, og en håndfull som går på klokke. Det er oppdelingen som gjør at én del kan byttes uten at resten stopper.
Vanlige sider lages én om gangen. Et dynamisk nettsted har én mal og ett datasett, og hver rad blir en side. Endrer du malen, endrer du alle sidene samtidig — og legger du til en rad, finnes siden med en gang, med kart, filtre og interne lenker på plass.
Som en rad sirkler koblet med linjer, lest fra venstre mot høyre. Hver sirkel er en modul med et nummer øverst til venstre, og nummeret er adressen du bruker i uttrykk som {{2.body.results}}. Ligger det et filter mellom to moduler, tegnes det som en trakt på linjen.
Ingenting. Bundelen slipper ikke forbi filteret, WordPress-modulen kjører ikke, og det opprettes verken innlegg eller kladd. Porten rapporterer heller ikke at det gikk bra — det er en villet egenskap, ikke en mangel.
Nei. Det samme kan gjøres med skript. Forskjellen er lesbarheten: et scenario kan åpnes av noen som ikke programmerer, og de kan se hvilken modul kjøringen stoppet i. Det er verdt mye i et oppsett som skal leve videre hos noen andre enn den som bygde det.
Ved at innholdet ikke bor i sidene, men i datasettet. De tidsstyrte og webhook-drevne scenarioene oppdaterer datasettet, og sidene følger etter fordi de er bygget av det. Interne lenker mellom sider bygges på samme måte, av klyngebyggeren, i stedet for å vedlikeholdes for hånd.
Sitter dere med et register, en katalog eller et kart som burde vært et nettsted? Vi ser på datamodellen først og sier hva den tåler — ta kontakt for en gjennomgang.
De fleste nettsider blir vanskelige å endre etter et halvt år.
Mange byråer garanterer «PageSpeed 90+». Vi gjør det ikke. Tallet avhenger av fire ting ingen leverandør styrer etter lansering. Her er hva vi lover i stedet.
Denne artikkelen hører til nettsider og wordpress. Skal noen gjøre jobben for deg, er det dette vi tilbyr: WordPress-nettsider bygget på komponenter.