storyblok / react-next-boilerplate Goto Github PK
View Code? Open in Web Editor NEWNextjs Storyblok boilerplate
Home Page: https://www.storyblok.com/tp/add-a-headless-cms-to-next-js-in-5-minutes
Nextjs Storyblok boilerplate
Home Page: https://www.storyblok.com/tp/add-a-headless-cms-to-next-js-in-5-minutes
`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.
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.
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
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]
}
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! :)
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.
/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!
If boilerplate could use getStaticProps instead of getInitialProps, that would help optimize the website more :)
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
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?
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])
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
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.
The storyblokInit function should be running somewhere on the server so that the API token does not get exposed to the client.
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.
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.