storyblok / storyblok-nuxt

Storyblok Nuxt module
https://www.storyblok.com/tp/nuxt-js-multilanguage-website-tutorial
MIT License
278 stars 44 forks source link
headless-cms nuxt nuxtjs vue vuejs
Storyblok Logo

@storyblok/nuxt

Nuxt 3 module for the Storyblok, Headless CMS.


Storyblok JS Client npm

Follow @Storyblok
Follow @Storyblok

Kickstart a new project

Are you eager to dive into coding? Follow these steps to kickstart a new project with Storyblok and Nuxt, and get started in just a few minutes!

Ultimate Tutorial

Are you looking for a hands-on, step-by-step tutorial? The Nuxt Ultimate Tutorial has you covered! It provides comprehensive instructions on building a complete, multilingual website using Storyblok and Nuxt from start to finish.

Installation

Install @storyblok/nuxt:

npx nuxi@latest module add storyblok

Add following code to modules section of nuxt.config.js and replace the accessToken with API token from Storyblok space.

import { defineNuxtConfig } from 'nuxt';

export default defineNuxtConfig({
  modules: [
    ['@storyblok/nuxt', { accessToken: '<your-access-token>' }]
    // ...
  ]
});

You can also use the storyblok config if you prefer:

import { defineNuxtConfig } from 'nuxt';

export default defineNuxtConfig({
  modules: ['@storyblok/nuxt'],
  storyblok: {
    accessToken: '<your-access-token>'
  }
});

Warning This SDK uses the Fetch API under the hood. If your environment doesn't support it, you need to install a polyfill like isomorphic-fetch. More info on storyblok-js-client docs.

Options

When you initialize the module, you can pass all @storyblok/vue options plus a bridge option explained in our JS SDK Storyblok bridge section and a enableSudoMode option to define your own plugin (see below).

Note If you want to use Storyblok inside nuxt-devtools you can use the option devtools, if enabled, make sure to have installed the @nuxt/devtools module and enable it on your nuxt config.

// Defaults
["@storyblok/nuxt", {
  {
    accessToken: "<your-access-token>",
    bridge: true,
    devtools: true,
    apiOptions: {}, // storyblok-js-client options
  }
}]

Define your own plugin

While the recommended approach covers most cases, there are specific instances where you may need to use the enableSudoMode option and disable our plugin, allowing you to incorporate your own.

// nuxt.config.ts
modules: [
  [
    '@storyblok/nuxt',
    {
      accessToken: '<your-access-token>',
      enableSudoMode: true
    }
  ]
];

To include additional functionalities in the SDK's apiOptions, such as custom cache methods, you can implement the following solution inside the plugins folder (autoimported):

// plugins/storyblok.js
import { apiPlugin, StoryblokVue } from '@storyblok/vue';

export default defineNuxtPlugin(({ vueApp }) => {
  vueApp.use(StoryblokVue, {
    accessToken: '<your-access-token>',
    apiOptions: {
      cache: {
        type: 'custom',
        custom: {
          flush() {
            console.log('all right');
          }
        }
      }
    },
    use: [apiPlugin]
  });
});

Region parameter

Possible values:

Full example for a space created in the US:

["@storyblok/nuxt", {
  {
    accessToken: "<your-access-token>",
    apiOptions: {
      region: "us"
    }
  }
}]

Important For spaces created in the United States or China, the region parameter must be specified.

Getting started

1. Creating and linking your components to the Storyblok Visual Editor

To link your Vue components to the equivalent one in your Storyblok space:

<div v-editable="blok"></div>
<StoryblokComponent :blok="blok" />

The blok is the actual blok data coming from Storblok's Content Delivery API.

2. Getting Storyblok Stories and listen to Visual Editor events

Composition API

The simplest way is by using the useAsyncStoryblok one-liner composable (it's autoimported). Where you need to pass as first parameter the slug, while the second and third parameters, apiOptions and bridgeOptions respectively, are optional.

Check the available apiOptions in our API docs and bridgeOptions passed to the Storyblok Bridge.

Note If you want to know more about versioning { version: "draft" /* or "publish" */ } then go to the section Working with preview and/or production environments

<script setup>
  const story = await useAsyncStoryblok(
    "vue",
    { version: "draft", resolve_relations: "Article.author" }, // API Options
    { resolveRelations: ["Article.author"], resolveLinks: "url" } // Bridge Options
  );

  if (story.value.status) {
    throw createError({
      statusCode: story.value.status,
      statusMessage: story.value.response
    });
  }
</script>

<template>
  <StoryblokComponent v-if="story" :blok="story.content" />
</template>

Which is the short-hand equivalent to using useStoryblokApi inside useState and useStoryblokBridge functions separately:

<script setup>
  const story = useState();
  const storyblokApi = useStoryblokApi();

  const { data } = await storyblokApi.get(
    `cdn/stories/vue`,
    {
      version: "draft"
    }
  );
  story.value = data.story;

  onMounted(() => {
    useStoryblokBridge(
      story.value.id,
      (evStory) => (story.value = evStory),
      { resolveRelations: ["Article.author"], resolveLinks: "url" } // Bridge Options
    );
  });
</script>

<template>
  <StoryblokComponent v-if="story" :blok="story.content" />
</template>

The useState is an SSR-friendly ref replacement. Its value will be preserved after server-side rendering (during client-side hydration).

Rendering Rich Text

You can render rich text fields by using the StoryblokRichText component:

<template>
  <StoryblokRichText :doc="blok.articleContent" />
</template>

Or you can have more control by using the useStoryblokRichText composable:

<script setup>
  const { render } = useStoryblokRichText({
    // options like resolvers
  })

  const root = () => render(blok.articleContent);
</script>

<template>
  <root />
</template>

For more incredible options you can pass to the useStoryblokRichText, please consult the Full options documentation.

Overriding the default resolvers

You can override the default resolvers by passing a resolver prop to the StoryblokRichText component, for example, to use vue-router links or add a custom codeblok component: :

<script setup>
  import { NuxtLink } from '#components';
  import type { StoryblokRichTextNode } from '@storyblok/vue';
  import CodeBlok from "./components/CodeBlok.vue";

  const resolvers = {
    // NuxtLink example:
    [MarkTypes.LINK]: (node: StoryblokRichTextNode<VNode>) =>
      h(NuxtLink, {
        to: node.attrs?.href,
        target: node.attrs?.target,
      }, node.text),
    // Custom code block component example:
    [BlockTypes.CODE_BLOCK]: (node: Node) => {
      return h(CodeBlock, {
        class: node?.attrs?.class,
      }, node.children)
    },
  }
</script>

<template>
  <StoryblokRichText :doc="blok.articleContent" :resolvers="resolvers" />
</template>

If you want to use the useStoryblokRichText composable, you can pass the resolvers via the options object:

<script setup>
  import CodeBlok from "./components/CodeBlok.vue";

  const { render } = useStoryblokRichText({
    resolvers: {
      // NuxtLink example:
      [MarkTypes.LINK]: (node: StoryblokRichTextNode<VNode>) =>
        h(NuxtLink, {
          to: node.attrs?.href,
          target: node.attrs?.target,
        }, node.text),
      // Custom code block component example:
      [BlockTypes.CODE_BLOCK]: (node: Node) => 
        h(CodeBlock, {
          class: node?.attrs?.class,
        }, node.children)
    }
  });

  const root = () => render(blok.articleContent);
</script>

Legacy Rendering Rich Text

[!WARNING]
The legacy richTextResolver is soon to be deprecated. We recommend migrating to the new approach described above instead.

You can easily render rich text by using the renderRichText function that comes with @storyblok/nuxt and a Vue computed property:

<template>
  <div v-html="articleContent"></div>
</template>

<script setup>
  const props = defineProps({ blok: Object });
  const articleContent = computed(() => renderRichText(props.blok.articleContent));
</script>

You can also set a custom Schema and component resolver by passing the options as the second parameter of the renderRichText function:

<script setup>
  import cloneDeep from 'clone-deep';

  const mySchema = cloneDeep(RichTextSchema); // you can make a copy of the default RichTextSchema
  // ... and edit the nodes and marks, or add your own.
  // Check the base RichTextSchema source here https://github.com/storyblok/storyblok-js-client/blob/v4/source/schema.js

  const props = defineProps({ blok: Object });

  const articleContent = computed(() =>
    renderRichText(props.blok.articleContent, {
      schema: mySchema,
      resolver: (component, blok) => {
        switch (component) {
          case 'my-custom-component':
            return `<div class="my-component-class">${blok.text}</div>`;
          default:
            return 'Resolver not defined';
        }
      },
    }),
  );
</script>

3. Working with preview and/or production environments

Remember that the bridge only works using version: 'draft' and the Preview Access Token.

For the production site, NOT used as a preview for content editors, version: 'published' and Public Access Token should be used.

Note If you're using production as a preview for marketeers and your public site, you will need a plugin to handle different .env variables, or versions using the Preview Access Token, checking if you are inside Storyblok or not. For example, something like if (window.location.search.includes(_storyblok_tk[token]=<YOUR_TOKEN>).

Check the official docs on how to access different content versions.

The recommended way to handle different content versions with Nuxt is by using environment variables in combination with Nuxt runtime config to expose configuration and secrets within your application

In your nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      storyblokVersion: process.env.STORYBLOK_VERSION || 'published'
    }
  }
});

Then you can access the runtime config in your components:

const config = useRuntimeConfig();

const story = await useAsyncStoryblok(
  'blog',
  {
    version: config.public.storyblokVersion,
    resolve_relations: 'overview.featured_story'
  },
  { resolveRelations: 'overview.featured_story' }
);

// or

const { data: articles } = await storyblokApi.get('cdn/stories', {
  version: config.public.storyblokVersion,
  starts_with: 'blog',
  is_startpage: false
});

API

useAsyncStoryblok(slug, apiOptions, bridgeOptions)

(Recommended Option) Uses useState under the hood to help with SSR compatibility.

Check the available apiOptions (passed to storyblok-js-client) and bridgeOptions (passed to the Storyblok Bridge).

useStoryblok(slug, apiOptions, bridgeOptions)

It could be helpful to use useStoryblok instead of useAsyncStoryblok when we need to make full client-side requests, for example, getting personalized data for a logged user.

Check the available apiOptions (passed to storyblok-js-client) and bridgeOptions (passed to the Storyblok Bridge).

useStoryblokApi()

Returns the instance of the storyblok-js-client.

useStoryblokBridge(storyId, callback, bridgeOptions)

Use this one-line function to cover the most common use case: updating the story when any kind of change happens on Storyblok Visual Editor.

The Storyblok JavaScript SDK Ecosystem

A visual representation of the Storyblok JavaScript SDK Ecosystem

Further Resources

Support

Contributing

Please see our contributing guidelines and our code of conduct. This project use semantic-release for generate new versions by using commit messages and we use the Angular Convention to naming the commits. Check this question about it in semantic-release FAQ.