GithubHelp home page GithubHelp logo

storyblok / react-next-boilerplate Goto Github PK

View Code? Open in Web Editor NEW
62.0 11.0 34.0 85 KB

Nextjs Storyblok boilerplate

Home Page: https://www.storyblok.com/tp/add-a-headless-cms-to-next-js-in-5-minutes

JavaScript 71.91% CSS 28.09%
nextjs devrel demo

react-next-boilerplate's Introduction

Next.js Storyblok Boilerplate

This repository is a Next.js Storyblok starter template used in following 5 minute tutorial.

Requirements

To use this project you have to have a Storyblok account. If you don't have one yet you can register at Storyblok, it's free.

How to get started?

Read the Next.js tutorial about connecting Storyblok and Next.js

1. Clone the repo

  $ git clone https://github.com/storyblok/react-next-boilerplate.git

2. Install all dependecies

$  yarn # or npm install

3. Adding the Access token

Create a new empty Space and exchange the preview token with your own in pages/_app.js.

// in pages/_app.js
storyblokInit({
  accessToken: "your-preview-token",
  use: [apiPlugin],
  components,
});

4. Run your project

Set the preview domain in Storyblok to http://localhost:3000/

# to run in developer mode
$ yarn dev # or npm run dev
# to build your project
$ yarn build # or npm run build

Resources

react-next-boilerplate's People

Contributors

ademarcardoso avatar christianzoppi avatar dohomi avatar fgiuliani avatar kubessandra avatar onefriendaday avatar samuells avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-next-boilerplate's Issues

Can you add a getStaticPaths example?

This example is out of date and uses a POST request to make getStories queries. And the example in this repo uses a plain react functional component and neither of getStaticProps or getStaticPaths which are kind of core to NextJs.

Axios not installed, Axios needs options object

Expected Behavior

when cloning this project and the running yarn install and yarn dev as per the readme.md I expect to see a working page when visiting http://localhost:3000

Current Behavior

When visiting http://localhost:3000 the following error shows:

./node_modules/@storyblok/react/dist/storyblok-react.mjs:30:0 Module not found: Can't resolve 'axios'

If I install axios by yarn add axios the error changes to:

AxiosError: options must be an object This error happened while generating the page. Any console logs will be displayed in the terminal window.

Steps to Reproduce

  1. Follow the steps described in the "How to get started?" section of the Readme.md

api/preview.js missing closing curly bracket

/pages/api/preview.js line 22 is missing a closing curly bracket.

res.redirect(/${slug}?${params[1])
should be...
res.redirect(/${slug}?${params[1]})

I can submit a pull request if you'd like, since it's a super straightforward fix.
Thanks!

Update to NextJS 8 and hooks

Hello,
I'm just starting to learn a little bit about React and Next. The new version 8 brings in some benefits for lambda creating and the React hooks could be a nice alternative to set page settings. It would be great if this boilerplate would show some of the best examples, especially how to integrate markup or and richt text editing with links inside
Cheers

Auto-enter preview mode

I wrote a quick middleware to enable auto-entering preview mode. It works by detecting (through rough heuristics) whether the link is a preview link, and then makes some assumptions, and forwards you on to the "enable preview" API url, which sets the cookie, then returns you to the page to be previewed. This eliminates the need for the CMS user to manually enable the preview mode using that drop down.

A limitation is that it doesn't work for root level pages - in my app, that's not a big deal because everything is localized. I'm sure there's a way to get it to work, but i wanted to share to see if there's any merit to this, or interest in helping to flesh it out.

Anyway, here it is:

import type { NextMiddleware } from "next/server";
import { NextResponse } from "next/server";

export const middleware: NextMiddleware = (req, ev) => {
  const id = req.nextUrl.searchParams.get("_storyblok");
  const space = req.nextUrl.searchParams.get("_storyblok_tk[space_id]");
  // If there is an id and a space, it's probably a preview link
  if (req.url.indexOf("?") && id && space) {
    // We are going to reuse the URL that comes in from the request, but
    // that URL will be regional, so we need to do some surgery to keep
    // it from redirecting to a regionalized API url.
    const url = req.nextUrl.clone();
    url.pathname = `/api/preview`;
    url.locale = "en";
    url.search = `?secret=${process.env.STORYBLOK_PREVIEW_TOKEN}&slug=${req.nextUrl.locale}${req.nextUrl.pathname}`;
    return NextResponse.redirect(url);
  }
  return NextResponse.next();
};

Happy to make a PR if there's interest.

Initial steps results in axios 404 error

Steps I took:

git clone https://github.com/storyblok/react-next-boilerplate.git
cd react-next-boilerplate/
yarn install

Inside storyblok UI:

  • Created new space

  • Set preview URL to http://localhost:3000

  • Extracted preview key and put it in lib/storyblok.js

const Storyblok = new StoryblokClient({
  accessToken: "MY_TOKEN",
// ...
yarn dev # ready - started server on 0.0.0.0:3000

image

Error: Request failed with status code 404
    at createError (/home/carl/Documents/GitHub/storyblok-test/newtest/react-next-boilerplate/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/home/carl/Documents/GitHub/storyblok-test/newtest/react-next-boilerplate/node_modules/axios/lib/core/settle.js:17:12)
    at IncomingMessage.handleStreamEnd (/home/carl/Documents/GitHub/storyblok-test/newtest/react-next-boilerplate/node_modules/axios/lib/adapters/http.js:269:11)
    at IncomingMessage.emit (events.js:412:35)
    at endReadableNT (internal/streams/readable.js:1334:12)
    at processTicksAndRejections (internal/process/task_queues.js:82:21) {
  config: {
    url: '/cdn/stories/home',
    method: 'get',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'User-Agent': 'axios/0.21.4'
    },
    proxy: false,
    params: {
      version: 'published',
      token: 'MY_TOKEN',
      cv: undefined
    },
    baseURL: 'https://api.storyblok.com/v2',
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    paramsSerializer: [Function: paramsSerializer],
    timeout: 0,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    validateStatus: [Function: validateStatus],
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false
    },
    data: undefined
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      socket: [Function (anonymous)],
      timeout: [Function (anonymous)],
      prefinish: [Function: requestOnPrefinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: false,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: 0,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    socket: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      secureConnecting: false,
      _SNICallback: null,
      servername: 'api.storyblok.com',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 10,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'api.storyblok.com',
      _readableState: [ReadableState],
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: [TLSWrap],
      _requestCert: true,
      _rejectUnauthorized: true,
      parser: null,
      _httpMessage: [Circular *1],
      [Symbol(res)]: [TLSWrap],
      [Symbol(verified)]: true,
      [Symbol(pendingSession)]: null,
      [Symbol(async_id_symbol)]: 45632,
      [Symbol(kHandle)]: [TLSWrap],
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(connect-options)]: [Object],
      [Symbol(RequestTimeout)]: undefined
    },
    _header: 'GET /v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'User-Agent: axios/0.21.4\r\n' +
      'Host: api.storyblok.com\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: noopPendingOutput],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object],
      requests: {},
      sockets: [Object],
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'GET',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    path: '/v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      socket: [TLSSocket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 404,
      statusMessage: 'Not Found',
      client: [TLSSocket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      responseUrl: 'https://api.storyblok.com/v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published',
      redirects: [],
      [Symbol(kCapture)]: false,
      [Symbol(RequestTimeout)]: undefined
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: 'api.storyblok.com',
    protocol: 'https:',
    _redirectable: Writable {
      _writableState: [WritableState],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 1,
      _redirects: [],
      _requestBodyLength: 0,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'https://api.storyblok.com/v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published',
      _isRedirect: true,
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'user-agent': [Array],
      host: [Array]
    }
  },
  response: {
    status: 404,
    statusText: 'Not Found',
    headers: {
      'content-type': 'application/json; charset=utf-8',
      'content-length': '34',
      connection: 'close',
      date: 'Tue, 09 Nov 2021 06:52:20 GMT',
      server: 'nginx/1.18.0',
      'x-frame-options': 'SAMEORIGIN',
      'x-xss-protection': '1; mode=block',
      'x-content-type-options': 'nosniff',
      'x-download-options': 'noopen',
      'x-permitted-cross-domain-policies': 'none',
      'referrer-policy': 'strict-origin-when-cross-origin',
      'cache-control': 'max-age=0, public, s-maxage=604800',
      'x-request-id': '54798678-65fa-4be0-9c6e-d425b7adbea5',
      'x-runtime': '0.021564',
      vary: 'Origin',
      'x-cache': 'Error from cloudfront',
      via: '1.1 a370d34019720f60dd35cbe89cb3994b.cloudfront.net (CloudFront)',
      'x-amz-cf-pop': 'ARN1-C1',
      'x-amz-cf-id': '_0eixnyXGibgrnOhPJnHwDIhGAXlUdHngAlqEyxbg9L4dnjhsIasjQ=='
    },
    config: {
      url: '/cdn/stories/home',
      method: 'get',
      headers: [Object],
      proxy: false,
      params: [Object],
      baseURL: 'https://api.storyblok.com/v2',
      transformRequest: [Array],
      transformResponse: [Array],
      paramsSerializer: [Function: paramsSerializer],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus],
      transitional: [Object],
      data: undefined
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: false,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: 0,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      socket: [TLSSocket],
      _header: 'GET /v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'User-Agent: axios/0.21.4\r\n' +
        'Host: api.storyblok.com\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: noopPendingOutput],
      agent: [Agent],
      socketPath: undefined,
      method: 'GET',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      path: '/v2/cdn/stories/home?cv=1636440740&token=MY_TOKEN&version=published',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'api.storyblok.com',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    data: [ 'This record could not be found' ]
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

merge index and slug routes into one file

Hello,

I think for micro optimization it would be better to merge index, [slug] and [...slug] into one file:

[[...index]].jsx // or [[...index]].tsx

NextJS handles double square brackets as catch all route and you can merge all content into one file rather than keeping 3 separate files

Now the only adjustment to make is inside getStaticProps

export getStaticProps: GetStaticProps<AppPageProps> = async (props) => {
  const { params, preview, previewData, locale, locales, defaultLocale } = props
  const slug = params?.index?.length
    ? params.index !== 'index'
      ? params.index
      : 'home'
    : 'home'
  (...) // all other content 

Preview Mode doesn't work

I've tried to set up draft previews with Next.js Preview Mode and this boilerplate.

For that I've created an HTTPS tunnel to my local development instance via the preview URL https://21f40123xxxx.eu.ngrok.io/api/preview?secret=MY_SECRET_TOKEN&slug=.

But preview from https://github.com/storyblok/react-next-boilerplate/blob/main/pages/%5B...slug%5D.js#L31 is always false from inside the Storyblok editor, even though the redirect in /api/preview.js happens.

However everything works perfectly when I open https://21f40123xxxx.eu.ngrok.io/api/preview?secret=MY_SECRET_TOKEN&slug=mypage directly in my browser.

Does anybody have the same problem?

only changing state if in enterEditmode

the returned story will always be the
first originalStory use in useState(originalStory) ?

initialState is never overwritten.

export function useStoryblok(originalStory, preview, locale) {
  const [story, setStory] = useState(originalStory)

and the state is only changed if we are in preview mode as initEventListeners runs setStory

useEffect(() => {
  // only load inside preview mode
  if (preview) {
    // first load the bridge, then initialize the event listeners
    addBridge(initEventListeners)
  }
}, [])
  
if (data.story) {
  setStory(data.story)
}

add a useEffect to update story to originalStory?

useEffect(() => {
  setStory(originalStory)
}, [originalStory])

[...slug].js dev mode will not show draft entries

`export async function getStaticPaths() {
    const storyblokApi = getStoryblokApi();
    let { data } = await storyblokApi.get("cdn/links/");

    let paths = [];
    Object.keys(data.links).forEach((linkKey) => {
      if (data.links[linkKey].is_folder || data.links[linkKey].slug === "home") {
        return;
      }

      const slug = data.links[linkKey].slug;
      let splittedSlug = slug.split("/");

      paths.push({ params: { slug: splittedSlug } });
   });

    return {
      paths: paths,
      fallback: false,
    };
}`

No sbParams included in the links call on your template, confused me when trying to view draft stories in dev mode.
I kept getting errors that the story did not exist, only once published it worked.

Accesstoken is exposed to client when running storyblokInit in _app file

Expected Behavior

The storyblokInit function should be running somewhere on the server so that the API token does not get exposed to the client.

Current Behavior

The storyblokInit function is placed in the _app file, which runs on the clientside. This means that even when using environment variables, it is possible to retrieve the access token from the client.

[slug.js] and [...slug].js always load draft version

It appears that [slug].js params.version will always be 'draft' no matter what (same goes for [...slug].js). Unless I'm missing something, this means the draft version would always be loaded, whether in production, preview or dev (inside or outside of the SB editor).

Line 35 version defaults to draft and then is set to draft if context.preview is true. In the useStoryblok hook, there's a conditional that will only load the bridge if in preview mode, but the draft version will still be loaded outside preview mode. Seems fairly inconsequential for the bridge to be loaded outside of preview mode, but it would be pretty bad for the draft version to be loaded outside preview mode and outside the sb editor interface.

I realize this brings up a more complicated issue of the coordination between draft/published and preview mode cookies. However, I feel that if it's glossed over, the confusion only increases.

Thanks! :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.