✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

How tos15 min read

Create a guided tour plugin using reactour

February 22, 2021Updated on May 22, 2026

In this tutorial, we will show you how to create your own plugin on Strapi (version 3.4.6) and, more precisely, how to use reactour to create a guided tour which can be very useful for content managers out there who will have to master Strapi's admin to manage their content.

We implemented this guided tour in the admin of our official demo: Foodadvisor. Give us a try to our demo.

Create a Strapi project

Let's start by creating or simply taking an existing Strapi project. For my part, I will create a new one.

  • Create a new project using the following command:
# Using yarn
yarn create strapi-app my-project --quickstart

# Using npx
npx create-strapi-app my-project --quickstart
  • Wait for the installation to end and then create your administrator.

Now we want to generate a new plugin for the guided tour.

  • Generate a new plugin with the following command:
cd my-project
yarn strapi generate:plugin guided-tour

You should have this in your terminal:

strapi generate:guided-tour plugin
[...] info Generated a new plugin `guided-tour` at`. / plugins`.
  • Add reactour to your package.json at the root of your Strapi project:
yarn add reactour

You can now start your server in watch-admin mode to customize the administration panel, as your application will have the autoReload enabled.

  • Start your server in watch-admin mode by running the following command:
yarn develop --watch-admin

Override some admin files

To make this work, we need to override some admin files. We must create an ./admin/src folder and override the necessary files. Note: The files we are going to create will arrive in the next major of Strapi; I will not delve deeper into these files because it can get too technical but remember that it is to anticipate the arrival of the next major.

  • Create a ./admin/src/containers/PrivateRoute/index.js file with the following code:
/**
 *
 * PrivateRoute
 * Higher Order Component that blocks navigation when the user is not logged in
 * and redirect the user to the login page
 *
 * Wrap your protected routes to secure your container
 */

import React, { memo } from 'react';
import { Redirect, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import { auth, useStrapi } from 'strapi-helper-plugin';

/* eslint-disable react/jsx-curly-newline */

const PrivateRoute = ({ component: Component, path, ...rest }) => {
  const strapi = useStrapi();
  return (
    <Route
      path={path}
      render={(props) =>
        auth.getToken() !== null ? (
          <Component {...rest} {...props} strapi={strapi} />
        ) : (
          <Redirect
            to={{
              pathname: '/auth/login',
            }}
          />
        )
      }
    />
  );
};

PrivateRoute.propTypes = {
  component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
  path: PropTypes.string.isRequired,
};

export default memo(PrivateRoute);
  • Create a ./admin/src/utils/MiddlewareApi.js file with the following code:
import { cloneDeep } from 'lodash';

class MiddlewaresHandler {
  middlewares = [];

  add(middleware) {
    this.middlewares.push(middleware);
  }

  get middlewares() {
    return cloneDeep(this.middlewares);
  }
}

export default () => new MiddlewaresHandler();
  • Create a ./admin/src/utils/Plugin.js file with the following code:
class Plugin {
  pluginId = null;

  decorators = {};

  injectedComponents = {};

  apis = {};

  constructor(pluginConf) {
    this.pluginId = pluginConf.id;
    this.decorators = pluginConf.decorators || {};
    this.injectedComponents = pluginConf.injectedComponents || {};
    this.apis = pluginConf.apis || {};
  }

  decorate(compoName, compo) {
    if (this.decorators && this.decorators[compoName]) {
      this.decorators[compoName] = compo;
    }
  }

  getDecorator(compoName) {
    if (this.decorators) {
      return this.decorators[compoName] || null;
    }

    return null;
  }

  getInjectedComponents(containerName, blockName) {
    try {
      return this.injectedComponents[containerName][blockName] || {};
    } catch (err) {
      console.error('Cannot get injected component', err);

      return err;
    }
  }

  injectComponent(containerName, blockName, compo) {
    try {
      this.injectedComponents[containerName][blockName].push(compo);
    } catch (err) {
      console.error('Cannot inject component', err);
    }
  }
}

export default (pluginConf) => new Plugin(pluginConf);
  • Create a ./admin/src/utils/Strapi.js file with the following code:
import ComponentApi from './ComponentApi';
import FieldApi from './FieldApi';
import MiddlewareApi from './MiddlewareApi';
import PluginHandler from './Plugin';

class Strapi {
  componentApi = ComponentApi();

  fieldApi = FieldApi();

  middlewares = MiddlewareApi();

  plugins = {
    admin: PluginHandler({
      id: 'admin',
      injectedComponents: {
        admin: {
          onboarding: [
            // { name: 'test', Component: () => 'coming soon' }
          ],
        },
      },
    }),
  };

  getPlugin = (pluginId) => {
    return this.plugins[pluginId];
  };

  registerPlugin = (pluginConf) => {
    if (pluginConf.id) {
      this.plugins[pluginConf.id] = PluginHandler(pluginConf);
    }
  };
}

export default () => {
  return new Strapi();
};
  • Create a ./admin/src/app.js file with the following code:
/* eslint-disable */

import '@babel/polyfill';
import 'sanitize.css/sanitize.css';

// Third party css library needed
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.min.css';
import '@fortawesome/fontawesome-free/css/all.css';
import '@fortawesome/fontawesome-free/js/all.min.js';

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
// Strapi provider with the internal APIs
import { StrapiProvider } from 'strapi-helper-plugin';
import { merge } from 'lodash';
import Fonts from './components/Fonts';
import { freezeApp, pluginLoaded, unfreezeApp, updatePlugin } from './containers/App/actions';
import { showNotification } from './containers/NotificationProvider/actions';
import { showNotification as showNewNotification } from './containers/NewNotification/actions';

import basename from './utils/basename';
import injectReducer from './utils/injectReducer';
import injectSaga from './utils/injectSaga';
import Strapi from './utils/Strapi';

// Import root component
import App from './containers/App';
// Import Language provider
import LanguageProvider from './containers/LanguageProvider';

import configureStore from './configureStore';
import { SETTINGS_BASE_URL } from './config';

// Import i18n messages
import { translationMessages, languages } from './i18n';

import history from './utils/history';

import plugins from './plugins';

const strapi = Strapi();

const pluginsReducers = {};
const pluginsToLoad = [];

Object.keys(plugins).forEach((current) => {
  const registerPlugin = (plugin) => {
    strapi.registerPlugin(plugin);

    return plugin;
  };
  const currentPluginFn = plugins[current];

  // By updating this by adding the required methods
  // to load a plugin, you need to update this file
  // strapi-generate-plugins/files/admin/src/index.js needs to be updated
  const plugin = currentPluginFn({
    registerComponent: strapi.componentApi.registerComponent,
    registerField: strapi.fieldApi.registerField,
    registerPlugin,
    settingsBaseURL: SETTINGS_BASE_URL || '/settings',
  });

  const pluginTradsPrefixed = languages.reduce((acc, lang) => {
    const currentLocale = plugin.trads[lang];

    if (currentLocale) {
      const localeprefixedWithPluginId = Object.keys(currentLocale).reduce((acc2, current) => {
        acc2[`${plugin.id}.${current}`] = currentLocale[current];

        return acc2;
      }, {});

      acc[lang] = localeprefixedWithPluginId;
    }

    return acc;
  }, {});

  // Retrieve all reducers
  const pluginReducers = plugin.reducers || {};

  Object.keys(pluginReducers).forEach((reducerName) => {
    pluginsReducers[reducerName] = pluginReducers[reducerName];
  });

  try {
    merge(translationMessages, pluginTradsPrefixed);
    pluginsToLoad.push(plugin);
  } catch (err) {
    console.log({ err });
  }
});

const initialState = {};
const store = configureStore(initialState, pluginsReducers, strapi);
const { dispatch } = store;

// Load plugins, this will be removed in the v4, temporary fix until the plugin API
// https://plugin-api-rfc.vercel.app/plugin-api/admin.html
pluginsToLoad.forEach((plugin) => {
  const bootPlugin = plugin.boot;

  if (bootPlugin) {
    bootPlugin(strapi);
  }

  dispatch(pluginLoaded(plugin));
});

// TODO
const remoteURL = (() => {
  // Relative URL (ex: /dashboard)
  if (REMOTE_URL[0] === '/') {
    return (window.location.origin + REMOTE_URL).replace(/\/$/, '');
  }

  return REMOTE_URL.replace(/\/$/, '');
})();

const displayNotification = (message, status) => {
  console.warn(
    // Validate the text
    'Deprecated: Will be deleted.\nPlease use strapi.notification.toggle(config).\nDocs : https://strapi.io/documentation/developer-docs/latest/plugin-development/frontend-development.html#strapi-notification'
  );
  dispatch(showNotification(message, status));
};
const displayNewNotification = (config) => {
  dispatch(showNewNotification(config));
};
const lockApp = (data) => {
  dispatch(freezeApp(data));
};
const unlockApp = () => {
  dispatch(unfreezeApp());
};

const lockAppWithOverlay = () => {
  const overlayblockerParams = {
    children: <div />,
    noGradient: true,
  };

  lockApp(overlayblockerParams);
};

window.strapi = Object.assign(window.strapi || {}, {
  node: MODE || 'host',
  env: NODE_ENV,
  remoteURL,
  backendURL: BACKEND_URL === '/' ? window.location.origin : BACKEND_URL,
  notification: {
    // New notification api
    toggle: (config) => {
      displayNewNotification(config);
    },
    success: (message) => {
      displayNotification(message, 'success');
    },
    warning: (message) => {
      displayNotification(message, 'warning');
    },
    error: (message) => {
      displayNotification(message, 'error');
    },
    info: (message) => {
      displayNotification(message, 'info');
    },
  },
  refresh: (pluginId) => ({
    translationMessages: (translationMessagesUpdated) => {
      render(merge({}, translationMessages, translationMessagesUpdated));
    },
    leftMenuSections: (leftMenuSectionsUpdated) => {
      store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
    },
  }),
  router: history,
  languages,
  currentLanguage:
    window.localStorage.getItem('strapi-admin-language') ||
    window.navigator.language ||
    window.navigator.userLanguage ||
    'en',
  lockApp,
  lockAppWithOverlay,
  unlockApp,
  injectReducer,
  injectSaga,
  store,
});

const MOUNT_NODE = document.getElementById('app') || document.createElement('div');

const render = (messages) => {
  ReactDOM.render(
    <Provider store={store}>
      <StrapiProvider strapi={strapi}>
        <Fonts />
        <LanguageProvider messages={messages}>
          <BrowserRouter basename={basename}>
            <App store={store} />
          </BrowserRouter>
        </LanguageProvider>
      </StrapiProvider>
    </Provider>,
    MOUNT_NODE
  );
};

if (module.hot) {
  module.hot.accept(['./i18n', './containers/App'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);

    render(translationMessages);
  });
}

if (NODE_ENV !== 'test') {
  render(translationMessages);
}

export { dispatch };

if (window.Cypress) {
  window.__store__ = Object.assign(window.__store__ || {}, { store });
}
  • Create a ./admin/src/configureStore.js file with the following code:
/**
 * Create the store with dynamic reducers
 */

import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
// import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';

const sagaMiddleware = createSagaMiddleware();

export default function configureStore(initialState = {}, reducers, strapi) {
  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [sagaMiddleware];

  strapi.middlewares.middlewares.forEach((middleware) => {
    middlewares.push(middleware());
  });

  const enhancers = [applyMiddleware(...middlewares)];

  // If Redux DevTools Extension is installed use it, otherwise use Redux compose
  /* eslint-disable no-underscore-dangle */
  const composeEnhancers =
    process.env.NODE_ENV !== 'production' &&
    typeof window === 'object' &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
          // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
          // Prevent recomputing reducers for `replaceReducer`
          shouldHotReload: false,
          name: 'Strapi - Dashboard',
        })
      : compose;
  /* eslint-enable */

  const store = createStore(
    createReducer(reducers),
    fromJS(initialState),
    composeEnhancers(...enhancers)
  );

  // Extensions
  store.runSaga = sagaMiddleware.run;
  store.injectedReducers = {}; // Reducer registry
  store.injectedSagas = {}; // Saga registry

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(createReducer(store.injectedReducers));
    });
  }

  return store;
}

Create the tour

Now it's time to work in the guided tour! Following instructions will happen in the ./plugins/guided-tour-admin/src folder.

  • Dive in the guided tour folder by running the following command:
cd ./plugins/guided-tour-admin/src
  • Update the existing index.js file with the following code:
import pluginPkg from '../../package.json';
import pluginId from './pluginId';
import Tour from './components/Tour';
import trads from './translations';

export default (strapi) => {
  const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
  const icon = pluginPkg.strapi.icon;
  const name = pluginPkg.strapi.name;

  const plugin = {
    description: pluginDescription,
    icon,
    id: pluginId,
    initializer: null,
    isReady: true,
    isRequired: false,
    mainComponent: null,
    name,
    preventComponentRendering: false,
    trads,
    boot(app) {
      app.getPlugin('admin').injectComponent('admin', 'onboarding', {
        name: 'guided-tour',
        Component: Tour,
      });
    },
  };

  return strapi.registerPlugin(plugin);
};
  • Remove the containers folder by running the following command and create the new architecture for your plugin:
rm -rf containers
mkdir -p components/Tour/utils
cd components/Tour
touch index.js reducer.js
touch utils/tour.js

Perfect! You have all the necessary files. Now it's time to create the logic of the tour.

We wanted to create a tour where steps are organized by plugins. In fact, if you open the guided tour on the homepage, the tour will start at the very beginning. However, if you open the tour in the Content-Types builder, we want to open the tour at a certain step, actually, the beginning of the Content-Types Builder steps, which makes sense as you don't want to start from the very beginning from there.

To achieve this, we need first to define how we structure our tour. Easy to do, it will simply be a javascript object organizing steps depending on plugins.

  • Update the utils/tour.js with the following:
import React from 'react'

const tour = {
  'admin': {
    steps: [{
      selector: 'a[href="/admin"]',
      content: () => (
        <div>
        <h1>Hi! 👋 </h1><br />
          <h4>Welcome to the official demo of Strapi: <strong>Foodadvisor!</strong></h4><br />
          What about following this little guided tour to learn more about our product? Ready? <strong>Let's go!</strong><br /><br />
          (Use arrow keys to change steps)
        </div>
      )
    },
    {
      selector: 'a[href="/admin/plugins/content-type-builder"]',
      content: () => (
        <div>
        <h1>Content Types Builder</h1><br />
          This is the most important tool, the one that allows you to <strong>create the architecture of your project</strong>.<br /><br />
          <ul>
            <li>
              <strong>Click</strong> on the link to be redirected to the Content Types Builder.
            </li>
          </ul>
        </div>
      )
    }],
  },
  'content-type-builder': {
    steps: [
      {
        selector: 'a[href="/admin"]',
        content: () => (
          <div>
          <h1>Content Types Builder</h1><br />
          Welcome to the CTB! This is where you create your <strong>collection types</strong>, <strong>single types</strong> and <strong>components</strong>.<br /><br />
          Let's see how it's working here!
          </div>
        ),
      },
      {
        selector: 'div.col-md-3',
        content: () => (
          <div>
          <h1>Manage your Content Types</h1><br />
          In this sidebar you can browse your <strong>collection types</strong>, <strong>single types</strong>, <strong>components</strong> and also create new ones!<br /><br />
          Let's see one specific collection type!
          </div>
        ),
      },
    ],
  }
};

export default tour;

As you can see, this is a tour object containing an admin object which contains steps, and you also have another content-type-builder object containing steps.

This way, we completely associate steps with specific plugins. However, admin is not a plugin. Well, this is when you'll be on the homepage, settings page, etc...

Great, now let's build the logic to do our guided-tour works.

  • Update the Tour/index.js file with the following code:
import React, { memo, useEffect, useCallback, useReducer, useMemo } from 'react';
import reducer, { initialState } from './reducer';
import { useRouteMatch } from 'react-router-dom';
import { Button } from '@buffetjs/core';
import ReactTour from 'reactour';
import { get } from 'lodash';

const Tour = () => {
  // Get the current plugin name => pluginId
  const match = useRouteMatch('/plugins/:pluginId');
  const pluginId = get(match, ['params', 'pluginId'], 'admin');

  // Use the usereducer hook to manage our state. See reducer.js file
  const [{ isOpen, tour, actualPlugin, currentStep, totalLength }, dispatch] = useReducer(
    reducer,
    initialState
  );

  // Called when we click on the guided-tour button.
  // Change the isOpen state variable. See TOGGLE_IS_OPEN action type in reducer.js file.
  const handleClick = useCallback(() => {
    dispatch({ type: 'TOGGLE_IS_OPEN', pluginId });
  }, []);

  // Calculate the steps from the tour. See utils/tour.js file.
  const steps = useMemo(() => {
    return Object.values(tour).reduce((acc, current) => {
      return [...acc, ...current.steps];
    }, []);
  }, [tour]);

  // Main logic of the tour.
  useEffect(() => {
    let totalSteps = 0;
    const keys = Object.keys(tour);
    const previousPlugins = keys.slice(0, keys.indexOf(pluginId));
    if (previousPlugins.length > 0) {
      previousPlugins.forEach((plugin, i) => {
        totalSteps += tour[plugin].steps.length;
      });
    }
    if (tour[pluginId] && pluginId !== actualPlugin)
      dispatch({ type: 'SETUP', pluginId, totalSteps });
  }, [tour, pluginId, actualPlugin]);

  const handleNextStep = () => {
    if (tour[pluginId] && currentStep === totalLength - 1 && totalLength > 0) {
      return;
    } else if (tour[pluginId]) {
      dispatch({ type: 'NEXT_STEP', length: tour[pluginId].steps.length });
    }
  };

  return (
    <>
      {tour[pluginId] && (
        <Button
          onClick={handleClick}
          color="primary"
          style={{ right: '70px', bottom: '15px', position: 'fixed', height: '37px' }}
        >
          Guided Tour
        </Button>
      )}
      <ReactTour
        isOpen={tour[pluginId] ? isOpen : false}
        onRequestClose={handleClick}
        steps={steps}
        startAt={currentStep}
        goToStep={currentStep}
        nextStep={handleNextStep}
        prevStep={() => dispatch({ type: 'PREV_STEP' })}
        showNavigation={false}
        rounded={2}
      />
    </>
  );
};

export default memo(Tour);

I agree that's a lot! but don't worry, I will explain everything in this file.

import React, { memo, useEffect, useCallback, useReducer, useMemo } from 'react';
import reducer, { initialState } from './reducer';
import { useRouteMatch } from 'react-router-dom';
import { Button } from '@buffetjs/core';
import ReactTour from 'reactour';
import { get } from 'lodash';

First we import everything we'll need to make it work. Hooks, our reducer (reducer.js) file that we didn't updated yet, useRouteMatch hook attempts to match the current URL in the same way that a <Route> would just to get the current plugin we're in, the Button component from Buffet.js, reactour and finally lodash.

Then in the Tour component we have:

  const match = useRouteMatch('/plugins/:pluginId');
  const pluginId = get(match, ['params', 'pluginId'], 'admin');

It will allow us to get the current plugin name in the pluginId variable.

const [{ isOpen, tour, actualPlugin, currentStep, totalLength }, dispatch] = useReducer(
  reducer,
  initialState
);

Our state defined by the useReducer hook.

const handleClick = useCallback(() => {
  dispatch({ type: 'TOGGLE_IS_OPEN', pluginId });
}, []);

This will be called when we'll click on the guided tour button to change the value of the isOpen state variable.

const steps = useMemo(() => {
  return Object.values(tour).reduce((acc, current) => {
    return [...acc, ...current.steps];
  }, []);
}, [tour]);

steps array that will contains the steps depending on our utils/tour.js file.

useEffect(() => {
  let totalSteps = 0;
  const keys = Object.keys(tour);
  const previousPlugins = keys.slice(0, keys.indexOf(pluginId));
  if (previousPlugins.length > 0) {
    previousPlugins.forEach((plugin, i) => {
      totalSteps += tour[plugin].steps.length;
    });
  }
  if (tour[pluginId] && pluginId !== actualPlugin)
    dispatch({ type: 'SETUP', pluginId, totalSteps });
}, [tour, pluginId, actualPlugin]);

The main logic of the tour. This will define where the tour should start depending on where you start it.

const handleNextStep = () => {
  if (tour[pluginId] && currentStep === totalLength - 1 && totalLength > 0) {
    return;
  } else if (tour[pluginId]) {
    dispatch({ type: 'NEXT_STEP', length: tour[pluginId].steps.length });
  }
};
  • Override of the nexStep function of reactour to create a behaviour like: if you need to make the user change location to another plugin then you can prevent the user to go to the next steps.
return (
  <>
    {tour[pluginId] && (
      <Button
        onClick={handleClick}
        color="primary"
        style={{ right: '70px', bottom: '15px', position: 'fixed', height: '37px' }}
      >
        Guided Tour
      </Button>
    )}
    <ReactTour
      isOpen={tour[pluginId] ? isOpen : false}
      onRequestClose={handleClick}
      steps={steps}
      startAt={currentStep}
      goToStep={currentStep}
      nextStep={handleNextStep}
      prevStep={() => dispatch({ type: 'PREV_STEP' })}
      showNavigation={false}
      rounded={2}
    />
  </>
);

Finally we simply render the guided-tour button only if the tour contains steps for the plugin you are currently in. Then we render the reactour component with the necessary props.

  • Great ! Now let's create our utils/reducer.js file with the following code:
import produce from 'immer';
import { isEmpty, pick } from 'lodash';
import tour from './utils/tour';

const initialState = {
  tour,
  isOpen: true,
  totalLength: 0,
  currentStep: 0,
  actualPlugin: null
};

const reducer = (state, action) =>
  produce(state, draftState => {
    switch (action.type) {
      case 'TOGGLE_IS_OPEN': {
        draftState.isOpen = !state.isOpen;
        draftState.currentStep = state.currentStep;
        break;
      }
      case 'SETUP': {
        draftState.currentStep = action.totalSteps;
        draftState.actualPlugin = action.pluginId;
        draftState.totalLength = action.totalSteps + tour[action.pluginId].steps.length
        break;
      }
      case 'PREV_STEP': {
        draftState.currentStep = state.currentStep > 0 ? state.currentStep - 1 : state.currentStep;
        break;
      }
      case 'NEXT_STEP': {
        draftState.currentStep =
          state.currentStep < state.totalLength - 1 ? state.currentStep + 1 : state.currentStep;
        break;
      }
      default:
        return draftState;
    }
  });

export default reducer;
export { initialState };

We define here our initial state and we create behaviours depending on which action type we received:

case 'TOGGLE_IS_OPEN': {
  draftState.isOpen = !state.isOpen;
  draftState.currentStep = state.currentStep;
  break;
}

We change the value of the isOpen to it's opposite state variable when we click on the tour button. We keep the value of the currentStep so that the user will go back to the step he quit the tour.

case 'SETUP': {
  draftState.currentStep = action.totalSteps;
  draftState.actualPlugin = action.pluginId;
  draftState.totalLength = action.totalSteps + tour[action.pluginId].steps.length
  break;
}

This will setup the currentStep, actualPlugin and totalLength state variables every time we change plugin. These variable will not be the same depending on where you start the tour.

case 'PREV_STEP': {
  draftState.currentStep = state.currentStep > 0 ? state.currentStep - 1 : state.currentStep;
  break;
}
case 'NEXT_STEP': {
  draftState.currentStep =
   state.currentStep < state.totalLength - 1 ? state.currentStep + 1 : state.currentStep;
  break;
}

We simply recreate the behaviour of reactour. This is necessary as we wanted to override the nextStep function of the package.

  • Save you files and it should be ready! You should be able to have a small guided-tour with 4 steps, 2 in the admin and 2 in the Content-Types Builder.

  • Now you can stop your server, build your admin and normally start your server by running the following commands at the root of the project:

yarn build
yarn develop

guided-tour-2.png guided-tour-1.png

Thanks for for following this little tutorial! See you in another one ;)

Please note: Since we initially published this blog post, we released new versions of Strapi and tutorials may be outdated. Sorry for the inconvenience if it's the case. Please help us by reporting it here.

If you want to know more


Get started with Strapi by creating a project using a starter or trying our live demo. Also, consult our forum if you have any questions. We will be there to help you.

See you soon!
Maxime CastresGrowth Engineer

Maxime started to code in 2015 and quickly joined the Growth team of Strapi. He particularly likes to create useful content for the awesome Strapi community. Send him a meme on Twitter to make his day: @MaxCastres

Join our Newsletter

Get all the latest news and events.

Newsletter signup is not configured yet.

How tos·12 min read

How To Build A Static Blog Using Jekyll And Strapi

Static sites deliver fast performance but present a content management challenge: how do you let writers update blog...

·August 3, 2025
Build a Blog with Astro, Strapi, and Tailwind CSS
How tos·22 min read

How to Build a Blog with Astro, Strapi, and Tailwind CSS

This article has been updated to use Astro 4 and Strapi 5 by Juliet Ofoegbu.

·October 20, 2023
Build a CRUD App with Flutter and Strapi
How tos·12 min read

How to Build a Simple CRUD Application Using Flutter & Strapi

This article has been updated to use Strapi 5 and REST API by Ekekenta Odionyefe.

·August 23, 2023