Storyblok tips - Config sida för global data

2022-12-07

Mossholder image

På WILL & SKILL använder vi oss ofta av ett Headless CMS som heter Storyblok, det är ett smidigt verktyg som gör det enkelt för våra kunder att kunna implementera och förändra material på sidorna vi bygger åt dem.

Vi har tidigare visat hur man snabbt och enkelt sätter upp ett projekt med Storyblok och Next.js här: https://www.willandskill.se/artiklar/enkel-storyblok-next-js-setup

(Storyblok har sedan dess uppdaterat hur man kopplar React/Next något, men repot är uppdaterat och den största ändringen för oss blir att lägga in access token direkt i _app.js istället för lib/storyblok.js)

Den här guiden kommer att använda den ganska enkla setup vi skapade senast, men bygga på med ett litet tips som vi har haft nytta av; en separat Config sida för global data! Det vill säga, saker du vill ska finnas åtkomligt eller synas på alla dina sidor.

Det finns en par olika sätt att sätta upp Storyblok för att kunna komma åt global data, men det här är ett vi använder väldigt ofta för att kunna samla och styra global data som länkar i navigationsmenyn, innehåll i footern, logotyper, etc. Helt enkelt att på ett smidigt sätt kunna organisera och redigera element som är gemensamma för många olika sidor direkt från Storyblok.

Config i Storyblok - Schema och key value

Vi kan börja med att sätta upp Config sidan i Storyblok genom att gå till Content och trycka på +Entry-knappen, skriva in namnet på sidan, välja “blank” och namnet på content-type. Inget av det behöver heta Config, utan kan heta vad man vill, så länge man kommer ihåg vad man namngett den.

Efter det definierar vi schemat på vår nya Config. Detta kan göras antingen inuti själva sidan, eller i components, det viktiga är att det är i schemat som vi anger att vi vill kunna lägga till på sidan. I schemat kommer en av config-sidans fördelar fram, vi kan lägga till olika tabbar för att hålla reda på vad vi lägger in var. General kommer alltid som grund, men vi kan lägga till tabbar för t.ex. Header och Footer.

I tabbarna kan vi sedan lägga till de key values som vi vill kunna komma åt i koden och definiera vilken typ av element de är, det är här vi sedan kommer lägga till text, bilder, etc. Vi kan börja med att definiera ett key value ‘favicon’ i general och sätta den till typ ‘asset/images’. När vi sedan kopplar ihop den med koden kommer vi kunna byta favicon från Storyblok om vi skulle vilja ändra, och inte behöva röra koden i framtiden.

När vi tittar på vår config-sida igen kan vi se att det har dykt upp fält för input i våra tabbar, men nu måste vi fånga upp den inputen i koden så att den kan renderas ut!

Config i koden - globalContext, komponenter och props

Eftersom vi vill ha global data, så börjar vi med att sätta upp en global kontext, skapa en mapp för Context, och i en den en fil GlobalContext.js. I den skapar vi en kontext som kommer att spara data som vi skickar in, för att sedan kunna ge den vidare till resten av appen.

Javascriptimport { createContext, useContext, useState } from "react";

export const GlobalContext = createContext({});

export const useGlobalContext = () => {
  return useContext(GlobalContext);
};

export const GlobalProvider = ({ children }) => {
  const [global, setGlobal] = useState({});

  const handleSetGlobal = (data) => {
    console.log("context data", data);
    const stories = data?.data?.stories[0];
    const content = stories?.content;
    setGlobal(content);
  };

  const value = {
    global,
    handleSetGlobal,
  };

  return (
    <GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>
  );
};

Funktionen handleSetGlobal är en hjälpfunktion för att göra det lite enklare att sätta kontexten, när vi skickar in ‘data’ kommer Storyblok att ge oss väldigt mycket information tillbaka, vi delar upp den och plockar bara ut biten vi faktiskt vill ha med oss vidare.

I vår _app.js fil importerar vi sedan in GlobalProvider och lägger runt Component så att den kan dela med sig av datan som ligger sparad i den.

Javascript[...]

import { GlobalProvider } from "../Context/GlobalContext";

[...]

function MyApp({ Component, pageProps }) {
  return (
    <>
      <GlobalProvider>
        <Component {...pageProps} />
      </GlobalProvider>
    </>
  );
}
export default MyApp;

Nu är det dags att hämta data att lägga i kontexten så att vi kan använda den. I index.js getStaticProps deklarerar vi en variabel ‘global_data’ som kallar på en asynkron GET som Storyblok tillhandahåller och ber den hämta informationen från stories som börjar med ‘config’, sedan returnerar vi den i props. (Glöm inte att hämta in global_data som prop i komponenten Home i index.js heller!)

Javascriptexport async function getStaticProps() {
  let slug = "home";

  let sbParams = {
    version: "draft", // or 'published'
  };

  const storyblokApi = getStoryblokApi();
  let { data } = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);

  let global_data = await storyblokApi.get(`cdn/stories/`, {
    ...sbParams,
    starts_with: "config",
  });

  return {
    props: {
      story: data ? data.story : false,
      key: data ? data.story.id : false,
      global_data: { ...global_data },
    },
    revalidate: 3600,
  };
}

Nu har vi hämtat datan vi behöver från Storybloks API och sagt att vi vill använda den i komponenten, men för att kunna använda datan i vår sidan behöver vi göra ett par steg till.

Först importerar vi useGlobalContext, och sätter upp states handleSetGlobal och global så vi kan använda dem. För att att första som händer ska vara att vi ger global_data till globalContext så använder vi en useEffect.

Javascript[...]

import { useGlobalContext } from "../Context/GlobalContext";

export default function Home({ story, global_data }) {
  story = useStoryblokState(story);

  const { handleSetGlobal, global } = useGlobalContext();

  useEffect(() => {
    if (global_data) {
      handleSetGlobal(global_data);
    }
  }, [global_data]);

  return ( 
	[...]

Nu behöver vi bara kalla på global, så kan vi använda all datan som vi skickar in i Config-sidan i Storyblok!

Så, varför gjorde vi det här?

Vi kan testa om det funkar genom att ändra faviconen till en uppladdad favicon! (Viktigt att komma ihåg när man kallar på bilder i Storyblok är att komma ihåg att använda .filename i slutet.)

Javascript<Head>
	[...]
  {/* <link rel="icon" href="/favicon.ico" /> */}
  <link rel="icon" href={global?.favicon?.filename} />
</Head>

Just nu kanske det inte känns som att vi har vunnit supermycket på att byta ut ”/favicon.ico” mot "global?.favicon?.filename", men det kommer att löna sig i det långa loppet!

För det mesta vill vi inte sitta och skriva saker om och om igen, ha långa, svårnavigerade filer. Det är bättre att försöka att flytta ut komponenter i mindre delar, återanvända vad vi kan och skicka ner props till dem.

Nu när vi har förberett globalContext och kopplat den till vår Config, så kan vi t.ex. skapa en Layout komponent som ligger runt hela StoryblokComponent och som vi skickar ner global som props i.

Layout komponenten kan sedan ansvara för att ta in och sätta information i Head, logotyper och navigationslänkar i Header, adressfält i Footer och allt annat man vill ska vara gemensamt för alla sidor. Och sedan kan vi styra valda element i alla de komponenterna från vår Config-sida i Storyblok!

Smidigt och praktiskt!

Javascriptimport Head from "next/head";
import styles from "../styles/Home.module.css";

import {
  useStoryblokState,
  getStoryblokApi,
  StoryblokComponent,
} from "@storyblok/react";

export default function Home({ story }) {
  story = useStoryblokState(story);

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <header>
        <h1>{story ? story.name : "My Site"}</h1>
      </header>

      <StoryblokComponent blok={story.content} />
    </div>
  );
}

[...]

(Ursprungskoden.)

Javascriptimport React, { useEffect } from "react";

import {
  useStoryblokState,
  getStoryblokApi,
  StoryblokComponent,
} from "@storyblok/react";

import { useGlobalContext } from "../Context/GlobalContext";
import Layout from "../components/Layout";

export default function Home({ story, global_data }) {
  story = useStoryblokState(story);
  const { handleSetGlobal, global } = useGlobalContext();

  useEffect(() => {
    if (global_data) {
      handleSetGlobal(global_data);
    }
  }, [global_data]);

  return (
    <Layout global={global}>
      <StoryblokComponent blok={story.content} />
    </Layout>
  );
}

[...]

(Här skickar vi global props till Layout komponenten istället.)

Psst!

Skarpögda läsare kan ha lagt märke till att tutorialen vi baserat projektet på har både en index.js för Home och en [...slug].js för övriga sidor. För att använda Config-sidan i övriga sidor med så behöver man följa samma steg i [...slug].js som vi gjorde i index.js.

Det kan vara att föredra att istället endast använda dynamic routing med en optional catch-all-route [[...slug].js], och hantera home/’/’ i den. Men för enkelhets skull har vi valt att basera oss på tutorial-repot, men vem vet, det kanske blir en framtida guide för catch-all-routes!

Testa gärna att sätta upp din egen Config sida i Storyblok och dela med dig av andra som kan behöva ett tips om hur man snabbt och smidigt kan byta ut globala element på sin Storyblok sida!