Takket være statisk sidegenerering i Gatsby, er 100/100 i Lighthouse oppnåelig.

Del 2: Slik bygget jeg sameiets nye nettsider. Grunnmuren er på plass

Årets dugnad i sameiet ble ikke tilbragt ute på fellesområdet med rake og hekksaks, men innendørs foran PC-skjermen i VS Code.

I del 1 av denne artikkelserien skrev jeg om hvilke teknologivalg jeg gjorde da jeg skulle bygge web-sider for sameiet der jeg bor. Som nevnt falt valget på React/Gatsby og Chakra UI som frontend, Contentful som backend for innhold, og Netlify for hosting av nettsidene. I tillegg måtte jeg ha en autentiseringsløsning for de delene av nettstedet som skulle være tilgjengelig kun for innloggede brukere (de som bor i sameiet).

Opp med grunnmuren

Det å starte opp et Gatsby-prosjekt er så enkelt som å skrive npm init gatsby på kommandolinjen og svare på noen enkle spørsmål (eventuelt gatsby new hvis du har installert Gatsby-CLI på forhånd). Gatsby vil da sette opp et starter-prosjekt for deg, som du så kan modifisere.

Ett av spørsmålene du får er hvilket CMS du ønsker å bruke for å lagre innholdet, og her kan du velge mellom Wordpress, Contentful, Sanity, DatoCMS, Shopify eller Netlify CMS. Du kan bruke nesten hva som helst annet også med Gatsby — men Gatsby kan sette opp en del ting for deg på forhånd hvis du velger ett av de predefinerte valgene. Du får også spørsmål om du ønsker å få forhåndsinstallert noe bestemt styling-system, som Sass, Styled components, Emiton, PostCSS eller Theme UI.

Gatsby-installasjon fra kommandolinjen.
Gatsby kan sette opp mye for deg på forhånd.

Jeg valgte imidlertid å starte noenlunde fra scratch, og installerte jeg de ulike avhengighetene jeg trengte etter hvert. Jeg trengte for eksempel gatsby-source-contentful for å kunne hente innhold fra contentful. Og jeg visste at jeg ville gjøre livet litt enklere for meg selv ved å lage brukergrensesnittet med Chakra UI. Jeg trengte også en del andre pakker, som dotenv for å håndtere miljøvariabler (for eksempel access tokens til Contentful, ting jeg ikke ville ha liggende i kildekoden på Github).

Hvis du setter opp alt via veilederen, får du opp en side som ser omtrent slik ut etter at du har tastet inn gatsby develop på kommandolinjen og gått til http://localhost:8000 :

Gatsby oppretter et startprosjekt som du kan tilpasse.
Gatsby har helt rå dokumentasjon, og til og med startsiden du lager med gatsby new-kommandoen lenker til nyttig info for å komme deg videre.

Det første du gjør, er selvfølgelig å fjerne denne dummy-siden.

I Gatsby er ruting så enkelt som å opprette en React-komponent i mappen /src/pages og eksportere den. Eksporterer du for eksempel en komponent fra filen /src/pages/test.js vil du ende opp med en rute på /test (dvs. at du kan skrive localhost:8000/test for å nå den). Hovedsiden — altså forsiden til nettstedet — er /src/pages/index.js . Hos meg ser den slik ut på det ferdige nettstedet mitt:

// ./src/pages/index.js

import * as React from 'react';
import SEO from '../components/seo';
import CookieConsent from '../components/cookieConsent';
import HeroWide from '../components/sections/hero-wide';
import ArticleGrid from '../components/sections/articleGrid';

const IndexPage = () => {
  return (
    <>
      <SEO />
      <CookieConsent />
      <HeroWide />
      <ArticleGrid />
    </>
  );
};

export default IndexPage;

Normalt ville jeg kanskje hatt en Layout-komponent her, for konsistent layout med header, footer, etc. på tvers av alle sidene. Men siden jeg bruker Chakra UI har jeg lagt Layout-komponenten et annet sted der Layout-komponenten omsluttes av <ChakraProvider> som er nødvendig for at det hele skal fungere — og for at jeg skal kunne style sidene ved hjelp av temaer i Chakra UI. Derfor har jeg opprettet filen ./src/chakra-wrapper.js:

// ./src/chakra-wrapper.js

import * as React from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import Layout from './components/layouts/layout';
import theme from './theme/';

export const wrapPageElement = ({ element }) => {
  return (
    <ChakraProvider resetCSS theme={theme}>
      <Layout>{element}</Layout>
    </ChakraProvider>
  );
};

Og så i ./gatsby-browser.js og ./gatsby-ssr.js:

import * as React from 'react';
import { wrapPageElement as wrap } from './src/chakra-wrapper';
.
.
.
export const wrapPageElement = wrap;

Det gjør at hele siden omsluttes av først ChakraProvider, så Layout-komponenten med header, footer og evt. annen styling utover Chakra UI. I ChakraProvider i den øverste kodesnutten sender jeg også inn temaet jeg har definert for siden.

Jeg endte opp med omtrent mappestrukturen nedenfor, der jeg har lagt alle gjenbrukbare React-komponenter i /src/components, sider under /src/pages, sidemaler under /src/templates og temaer til Chakra UI under /src/theme:

src
├── components
│   ├── article.tsx
│   ├── layouts
│   │   └── layout.tsx
│   ├── private-components
│   └── sections
│       ├── articleGrid.tsx
│       ├── footer.tsx
│       ├── header.tsx
│       └── hero-wide.tsx
├── pages
│   ├── 404.tsx
│   ├── index.tsx
│   ├── informasjon.tsx
│   └── min-side.tsx
├── templates
│   ├── blog-archive-template.tsx
│   ├── blog-template.tsx
│   └── page-template.tsx
├── theme
│   ├── colors.js
│   ├── components
│   │   ├── button.js
│   │   ├── heading.js
│   │   └── text.js
│   ├── index.js
│   ├── renderRichTextOptions.js
│   ├── styles.js
│   └── textStyles.js
└── utils
    └── privateRoute.tsx

Som du ser, valgte jeg å døpe om .js-filene for React-komponentene til .tsx, for å kunne bruke TypeScript i komponentene mine — og redusere risikoen for bugs når jeg sender data rundt omkring som props.

Hente innhold fra Contentful

Som nevnt ville jeg ha innholdet i Contentful, som er et headless CMS-system — det vil si at innholdet er frikoblet fra frontenden. Det gjør det forholdsvis enkelt å senere bytte publiseringsløsning, eventuelt frontend — eller hente innhold fra samme kilde til en annen løsning, for eksempel en mobil-app. Når du bruker Gatsby kan du få innholdet levert via nær sagt hva som helst ved å definere GraphQL-spørringer som du bruker i koden din. Det finnes en masse ferdige plugins som gjør dette veldig enkelt, enten du vil hente data fra markdown-filer i filsystemet, et headless CMS som Contentful eller Sanity, eller fra en nettbutikkløsning som Shopify. Jeg brukte Gatsbys offisielle Contentful-plugin, gatsby-source-contentful.

Så snart du har installert og konfiguert pluginen, kan du besøke localhost:8000/__graphiql for å lage GraphQL-spørringene. I venstre kolonne i GraphiQL-grensesnittet ser du hvilke data som er tilgjengelige (blant annet innhold fra Contentful), mens feltet i midten brukes til å lage spørringene — og kolonnen til høyre viser resultatet av en spørring etter at du har trykket Run-knappen. GraphiQL gjør det veldig enkelt og greit å teste ut ulike spørringer og sjekke at du får tilbake de dataene du forventer, før du kopierer spørringen over i koden din.

GraphiQL gir deg et enkelt og brukervennlig grensesnitt for å lage GraphQL-spørringene dine.
GraphiQL gir deg et enkelt og brukervennlig grensesnitt for å lage GraphQL-spørringene dine.

Før jeg kom så langt, måtte jeg selvfølgelig sette opp alt sammen i Contentful. Det første jeg gjorde var å definere en Content model, som er en beskrivelse av de ulike typene innhold du vil skal være tilgjengelig — og hvilke felter som skal være tilgjengelige for hver innholdstype. For eksempel har jeg en innholdstype som heter Blog post, som inneholder felter som Tittel, Oppsummering, Toppbilde, Brødtekst og Forfatter. For hvert av feltene må du definere hva slags innhold feltet skal kunne inneholde — som for eksempel tekst, tall, boolean-verdier, media (bilder, video, etc). Her er det også mulig å lage referanser mellom ulike typer innhold, for eksempel koblinger mellom et blogginnlegg og en eller flere forfattere (der Forfatter også er en innholdstype).

Jeg definerte egne innholdstyper for blant annet forsidetekst og sider (for eksempel informasjonssider og kontaktsider). I tillegg lagde jeg en innholdstype som het Servicemenu, som brukes til å endre en meny med informasjon til beboerne i sameiet — blant annet lenker for nedlasting av referater, vedtekter og annet. Dette er innhold jeg har lagt på en side som krever innlogging.

Contentful: Start først med å lage en innholdsmodell med alle typer innhold du ønsker, før du definerer hvilke felter hver enkelt innholdstype skal ha.
Contentful: Start først med å lage en innholdsmodell med alle typer innhold du ønsker, før du definerer hvilke felter hver enkelt innholdstype skal ha.
Definisjon av felter i Contentful.
Definisjon av felter i Contentful.

Generering av statiske nettsider

Noe av det som gjør at nettsider laget i Gatsby blir ekstremt kjappe, er at Gatsby genererer statiske nettsider. Det betyr at i det du kjører gatsby build så vil Gatsby hente innhold fra for eksempel Contentful og bygge én og én HTML-side for deg. Dermed er ikke 100/100 uoppnåelig i Lighthouse:

100 av 100 på alt i Lighthouse. You can do it!
100 av 100 på alt i Lighthouse. You can do it!

Som nevnt blir alle komponenter du eksporterer fra /src/pages-mappen til statiske sider automatisk. Men for å opprette egne sider for hver eneste bloggpost og annet innhold programmatisk benyttet jeg ett av Gatsbys innebygde API-er, createPages. For å forklare:

Når du bygger en Gatsby-side, vil kode som ligger i filen gatsby-node.js kjøres én gang før siden er ferdig bygget. APIet createPages (flertall) lar deg kjøre en GraphQL-spørring for å hente innhold (for eksempel blogginnlegg) — i vårt tilfelle fra Contentful. Deretter kan du kjøre en såkalt action kalt createPage (éntall) på hver bloggpost, hvor du som parameter sender inn en React-komponent som du vil bruke som sidemal, sammen med context-data som sidemalen vil motta. Context-data i mitt tilfelle er ID-en til artikkelen i Contentful. Inne i sidemalen kjører du en ny GraphQL-spørring der du kun henter den bloggposten som har riktig ID, og så henter du alt du trenger for å vise innholdet — som tittel, ingress, brødtekst, bilder, osv. Sidemalen er som en hvilken som helst annen React-komponent, så det er ikke noe spesielt hokus-pokus for å få til dette.

Min gatsby-node.js ser slik ut (forkortet — her er det også flere GraphQL-spørringer og actions for å opprette andre typer sider. Se Github-en min for full kildekode) :

// ./gatsby-node.js

const path = require(`path`);

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;
  const blogPostTemplate = path.resolve(`src/templates/blog-template.tsx`);

.
.
.
  return graphql(`
    {
      publicPosts: allContentfulBlogPost(
        filter: { privatePost: { eq: false } }
      ) {
        nodes {
          contentful_id
          slug
        }
      }
    }
  `).then((result) => {
    if (result.errors) {
      throw result.errors;
    }

  const blogNodes = (result.data.publicPosts || {}).nodes || [];
  
  // Create public blog post pages.
  // Skip private pages (in graphQl query)
  blogNodes.forEach((node) => {
    const id = node.contentful_id;
    const slug = node.slug;
    createPage({
      // Path for this page — required
      path: `/blog/${slug}`,
      component: blogPostTemplate,
      context: { id },
    });
  });

.
.
.
}

I filen blog-template.tsx henter jeg én og én bloggpost fra Contentful ved hjelp av GraphQl-queryen nedenfor. Legg merke til variabelen $id i GraphQL-spørringen — denne kommer fra context-parameteret som sendes fra createPage i gatsby-node.js, og sørger for at jeg kun får innhold for den riktige bloggposten:

// ./src/templates/blog-template.tsx

export const query = graphql`
  query BlogPostQuery($id: String!) {
    contentfulBlogPost(contentful_id: { eq: $id }) {
      title
      createdAt(formatString: "DD.MM.YYYY")
      updatedAt(formatString: "DD.MM.YYYY")
      author {
        firstName
        lastName
      }
      excerpt {
        excerpt
      }
      bodyText {
        raw
        references {
          ... on ContentfulAsset {
            contentful_id
            __typename
            title
            description
            gatsbyImageData(layout: CONSTRAINED, aspectRatio: 1.6)
          }
        }
      }

      featuredImage {
        gatsbyImageData(layout: CONSTRAINED, aspectRatio: 1.6)
        file {
          url
        }
        title
        description
      }
    }
  }
`;

Så "destructurer" jeg ut de dataene jeg vil ha, og bruker dette i sidemal-komponenten:

// ./src/templates/blog-template.tsx
.
.
.
const {
    title,
    author,
    createdAt,
    updatedAt,
    bodyText,
    excerpt,
    featuredImage,
  } = contentfulBlogPost;

  return (
    <>
      <SEO
        title={title}
        image={featuredImage?.file?.url || null}
        description={excerpt?.excerpt || null}
      />
      <Article
        title={title}
        bodyText={bodyText}
        createdAt={createdAt}
        updatedAt={updatedAt}
        mainImage={featuredImage}
        author={author}
        buttonLink='/blog'
      />
    </>
  );
}
.
.
.

Ettersom jeg ofte har bruk for å presentere innhold i artikkelformat, med toppbilde, tittel, ingress, forfatter, osv., laget jeg en <Article>-komponent for dette, og bruker props for å sende data til denne.

En utfordring jeg støtte på, var hvordan jeg skulle rendre innhold som var definert som Rich Text i Contentful. Innhold i Rich Text-felter er basert på blokker, og når du gjør en GraphQL-spørring returneres et JSON-array med noder med alt innholdet i. Det finnes en masse ulike måter å rendre dette innholdet på, og Contentful har litt mer info her. Jeg brukte import { renderRichText } from 'gatsby-source-contentful/rich-text' og så kunne jeg i Article-komponenten min bruke {renderRichText(bodyText, renderRichTextOptions)} for å rendre innholdet i bodyText. renderRichTextOptions er en komponent jeg importerer i starten av <Article>-komponenten, og inne i renderRichTextOptions kan jeg fritt definere hvordan for eksempel en <H1>-tittel eller et bilde skal rendres (<Text> og <Heading> i koden nedenfor er Chakra UI-komponenter):

// ./src/theme/renderTichTextOptions.js
.
.
.
const renderRichTextOptions = {
  renderMark: {
    [MARKS.BOLD]: (text) => <strong>{text}</strong>,
    [MARKS.UNDERLINE]: (text) => <u>{text}</u>,
    [MARKS.ITALIC]: (text) => <em>{text}</em>,
  },
  renderNode: {
    [BLOCKS.PARAGRAPH]: (node, children) => (
      <Text
        textAlign='left'
        my={4}
        fontSize={{ base: 'sm', sm: 'md', md: 'lg' }}
      >
        {children}
      </Text>
    ),
    [BLOCKS.HEADING_1]: (node, children) => (
      <Heading as='h1' textAlign='left' size='4xl'>
        {children}
      </Heading>
    ),
.
.
.

Her er det mulig jeg i stedet kunne brukt rich-text-react-renderer, men jeg synes måten jeg løste det på ga meg bra fleksibilitet til å få gjort det jeg ville gjøre.

Styling

Chakra UI har komponenter for det meste du trenger for å lage en lekker nettside. Du har blant annet komponenter som <Badge>, <Alert>, <Text>, <Heading>, <Menu>, <Image>, osv.

Siden Chakra UI er basert på temaer, trenger du ikke å skrive en eneste linje CSS. I stedet utvider du standardtemaet hvis du ønsker å endre på noe.

Noe av det mest geniale med Chakra UI er at du får responsiv design rett ut av boksen, med ferdig definerte breakpoints (som du kan endre hvis du vil). Du trenger ikke å manuelt lage media queries i CSS, men kan gjøre for eksempel slik jeg har gjort i eksempelet nedenfor i JSX-koden din (<Heading> er en Chakra UI-komponent ment for titler og rendrer som default en <H2>-tag, men i eksempelet har jeg valgt å rendre den som <H1>):

<Heading
   as='h1'
   fontSize={['4xl', '6xl', '6xl', '7xl']}
   textAlign={['center', 'left', 'left', 'left']}
   pb={4}
>

Her defineres fontstørrelse og tekstjustering for fire ulike skjermstørrelser. Du trenger faktisk ikke å gjøre noe mer for å få dette til å virke.

Eller på denne måten for å bruke CSS Grid-komponenten i Chakra UI og definere at du vil ha 1 kolonne på små og medium skjermer og 2 kolonner på større skjermer:

<Grid
    templateColumns={{
      sm: 'repeat(1, 1fr)',
      md: 'repeat(1, 1fr)',
      lg: 'repeat(2, 1fr)',
      xl: 'repeat(2, 1fr)',
    }}
    pt={16}
    gap={16}
    mb={16}
    mt={0}
    maxWidth='95vw'
    minHeight='45vh'
  >

Med Chakra UI får du også nettsider som scorer høyt på tilgjengelighet, og du trenger ikke selv å passe på aria-attributter og annet.

Ta gjerne en kikk på https://chakra-ui.com/ for mer info og flere eksempler.

Neste trinn: Autentisering og private ruter

I neste artikkel skal jeg ta for meg hvordan jeg bygget de private delene av sameiets nettsider, altså de sidene som kun skal være tilgjengelig for innloggede brukere. Protected routes og autentisering med Auth0 er stikkord.

Gå til forsiden