Next.js: Access request (ctx.req) in "getInitialProps" method in custom Document "_document" - next.js

I have created a blank Next.js app:
npx create-next-app
I have added a custom Document https://nextjs.org/docs/advanced-features/custom-document
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
console.log("ctx: " + JSON.stringify(ctx, null, 2));
return { ...initialProps };
}
render() {
return (
<Html>
<Head />
<body>
Test
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
I would like to access "ctx.req" in getInitialProps but as can be seen by the log message "ctx" does not contain the "req" object. Here is the log message:
ctx: {
"pathname": "/",
"query": {},
"asPath": "/"
}
From https://nextjs.org/docs/advanced-features/automatic-static-optimization I understand that for prerendered pages ctx.req will be undefined:
"If you have a custom Document with getInitialProps be sure you check if ctx.req is defined before assuming the page is server-side rendered. ctx.req will be undefined for pages that are prerendered."
but on the other hand the same page also says:
"Next.js automatically determines that a page is static (can be prerendered) if it has no blocking data requirements. This determination is made by the absence of getServerSideProps and getInitialProps in the page."
From this I understand that if the method getInitialProps exists the page is regarded as non-prerenderable and instead server-side rendered.
What am I missing? How can I access the request in "getInitialProps" method in "_document"?

As soon as I add getServerSideProps to pages/index.js
export async function getServerSideProps() {
const data = "myData";
return { props: { data } };
}
Next.js really does render the page server-side and then ctx.req is available in "getInitialProps" method in "_document".

Related

Embed current build time into humans.txt file with nextjs

Placing a humans.txt file into nextjs' /public folder works fine for a static file.
However I'd like to annotate the file with the date of the latest page build (when next build is called). So I created pages/humans.txt.tsx which renders a string that also contains the build time static date:
export default function Test({ buildTime }) {
return `Hello ${buildTime}`
}
export async function getStaticProps() {
return {
props: {
buildTime: new Date().toISOString()
}
}
}
I tried to customize pages/_document.js but even with everything stripped down (for testing) it still renders the doctype and one div with my text in it.
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
ctx.renderPage = (props) => {
return {
html: "text",
head: null,
}
}
// Run the parent `getInitialProps`, it now includes the custom `renderPage`
const initialProps = await Document.getInitialProps(ctx)
return initialProps
}
render() {
return <Main/>
}
}
Output:
<!DOCTYPE html><div id="__next">text</div>
Returning just string from my documents render instead of <Main/> still renders the doctype and also causes a warning, since render should return an Element.
So I am out of ideas and might resort to using a prebuild script in package.json prebuild: sed ./pages/humans.txt... to replace a marker in the file with the system date and pipe it to public/humans.txt.
Here is an interesting runtime alternative:
Rewriting /humans.txt to /api/humans
You can use the following rule:
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/humans.txt',
destination: '/api/humans',
},
]
},
}
Check the Rewrites docs here
Writing /api/humans
Now you can use any response in your API. However, make sure you are caching it:
// /pages/api/humans.js
export default function handler(req, res) {
res.setHeader('Cache-control', 's-maxage=6000, stale-while-revalidate=30')
res.setHeader('Content-type', 'text/plain')
res.status(200).end('example')
}
Check the API routes docs here

Access data from getServerSideProps in custom document Next.js

I'm using getServerSideProps to fetch details of an event. My understanding is that this data will get fetched on the server, then some sort of prerendering will happen, and then a context object is passed into _document.js. I know _document.js will get rendered on every request, but I would like to add a class name to my html if that server data is present (to prevent flashing when doing this client side).
Here is my getServerSideProps function:
export async function getServerSideProps(context) {
const shortId = context.params.short_id
const event = await GETrequest({ endpoint: API_PUBLIC_EVENT({ shortId }) })
return {
props: {
event,
shortId
} // will be passed to the page component as props
}
}
When I'm within _document.js, I'm able to see that the results of my getServerSideProps are available, as seen when I log the context in getInitialProps like this:
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
console.log(initialProps)
// can see html that has been processed using my data
}
...
I can think of some hacky solutions of getting my className in that initialProps.html, but I'm wondering if there is an easier way.
Is there a way I can structure this so that my data will be more easily available in _document.js, passed from getServerSideProps()?
Here is a hacky way to get your page's getServerSideProps() data into your _document.js, before the app is loaded.
So, as I mentioned you can do some funky stuff with the prerendered html that comes in to use the getServerSideProps() data. This is a hacky solution and very unreliable. In my case however, I'm simply adding a non-consequential className to my document (with a fail-safe on the front-end), so even if this fails everything will be okay.
Use an abundance of caution if you decide to use this.
This is how it works:
I have a getServerSideProps() on a page. In this function, I fetch an event and pass it into my page component. In this component, I add an empty div that I know will be rendered, appending the information I need. This information came from my getServerSideProps().
<div data-meta-event-theme={event?.theme?.preset}></div>
// let's say theme preset = light
Then in my _document.js, within a getInitialProps(ctx) function, I first check to see if this is a server request. If it's not, skip (build will fail otherwise). Then I check to see if the request is coming from the appropriate route. My route is structured like /j/[shortId], and it's the only page using /j/. So before any logic gets triggered, I check to make sure we're on the right page.
const isInviteScreen = ctx.req.url.indexOf('/j/') > -1
If we're on the right page, then we can parse the value from our html, and do what we want with it.
if (isInviteScreen) {
const html = initialProps.html
// whatever you set your data to in the html
// make sure to keep the opening quote "
const key = 'data-meta-event-theme="'
// you don't have to touch this
const begin = html.indexOf(key)
const sub = html.substring(begin + key.length)
const closingQuote = sub.indexOf('"')
const value = sub.substring(0, closingQuote)
console.log(value)
// logs 'light'
}
At this point, I'm able to append that value as a className onto my html, and voila!, no more flashing.
Here is a more full _document.js:
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
// Make sure this operation is on the server and has a request
if (ctx.req) {
// Make sure we're only doing this for the correct route
const isInviteScreen = ctx.req.url.indexOf('/j/') > -1
if (isInviteScreen) {
const html = initialProps.html
// Your HTML key here, make sure to keep quote
const key = 'data-meta-event-theme="'
// Don't have to touch this
const begin = html.indexOf(key)
const sub = html.substring(begin + key.length)
const closingQuote = sub.indexOf('"')
// Value that was sent from the page with getServerSideProps()
const value = sub.substring(0, closingQuote)
return {
...initialProps,
preference: { theme: value }
}
}
}
// Default return just in case
return {
...initialProps,
preference: { theme: 'light }
}
}
render() {
const theme = this.props.preference.theme
return (
<Html data-theme={theme} className={theme} lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
Since this data seems somewhat accessible between the two, I'm hoping there is a more official way to do this?

NextJS case insensitive route for SSG pages

I am using NextJS to translate a CSV of data into static pages. Each page is pages/[slug].jsx. I'm calling toLowerCase() on the slug value inside [slug].jsx getStaticPaths() and getStaticProps() functions. The generated pages are lowercase. e.g. /ab101, /ab102, /cc500 all resolve to the pages/[slug].jsx page.
Unfortunately, people might hand type the url and may use caps or mixed case for the slug value, currently resulting in a 404.
QUESTION: How can I make routing case insensitive with respect to the slug value?
UPDATE
When I return fallback: true from getStaticPaths(), my [slug].jsx file is hit even when there is not an exact path match. I can then check isFallback as illustrated by Anish Antony below.
Additionally, the items param that is passed to my page will be undefined when the page wasn't found. The router's pathname value is "/[slug]" and not the value of "slug". However, there is an asPath value which contains useful data, e.g. /mixedCaseSlug?param=value&foo=bar.
When the page renders, I check if it's a fallback. If it is, show a LOADING... message. Next will display that and call getStaticProps() to generate the "missing" page. You'll then re-render with the page data. In the event that getStaticProps couldn't get page data, I push a path that will lead to the built-in 404 page.
export default function Page({ item }) {
const { isFallback, push } = useRouter()
const hasPageData = item && Object.keys(item).length > 0
useEffect(() => {
if (!isFallback && !hasPageData) {
push('/page-not-found/error')
}
}, [isFallback, hasPageData])
const loadingMsg = <div>Loading...</div>
const notFoundMsg = <div>Page not found</div>
return isFallback ? loadingMsg : hasPageData ? <Item item={item} /> : notFoundMsg
}
I needed to update getStaticProps() to lowercase the slug param, as it may now be mixed case, but we want to find our page data. And I needed to allow for the case when there really is no data for the slug.
export async function getStaticProps({ params }) {
const { slug } = params
const item = data.find(o => o.Practice_Code.trim().toLowerCase() === slug.toLowerCase())
return {
props: {
item: item ? item : {}
}
}
}
This all seems very kludgy, so I'm still wondering if there is a better way.
NextJS routes are case sensitive.You can use fallback property in getStaticPaths to catch the routes which aren't in the same case as the one provided by default in getStaticPaths.
Edit: I have updated the answer based on the discussion with Dave.
We can give fallback:true or fallback:"blocking" , if we give fallback:true we we can show a custom component which will be displayed till the time page is loaded.For fallback:"blocking" new paths not returned by getStaticPaths will wait for the HTML to be generated,
When we give fallback:true or "blocking" static page will be generated when the user first access the site and the generated page will be served for further visits.
Sample code
export async function getStaticPaths() {
const idList = await fetchAllIds();
const paths = [
];
idList.forEach((id) => {paths.push(`/posts/${id}`)})
return { paths, fallback: true };
}
What we have note is our code in getStaticProps should be case insensitive to get the data irrespective of the one provided in url.
export async function getStaticProps({ params }) {
const { slug } = params;
try {
/// This fetch api should be able to fetch the data irrespective of the case of the slug
or we should convert it to the required case before passing it as a parameter to API
const data= await fetchSlugDetails(slug);
//Same logic should be performed if you are getting data filtered based on slug from an existing array.
return data? { props: { data} } : { notFound: true };
} catch (error) {
console.error(error);
return { notFound: true };
}
}
Note: You have to handle the notfound case and fallback case if you are using fallback:true. For fallback you can get the value isFallback from next/router while the page is being static generated

getStaticPath and the need for filetype in the URL

In /pages I have [page].js and index.js.
[page].js generate needed Pages by the Value of "CustomPage". It's content comes from an Data-JSON-File.
It work like expected, as long as I start on the Homepage and use links inside of my Webpage.
For example I have 2 Pages for now: /impressum and /datenschutz.
So clicking the link "Impressum" open myDomain.com/impressum (and it work, BUT notice, there is no .html at the end).
BUT, if I refresh the page, or type myDomain.com/impressum directly in the addressbar of the browser, I got an not found error (from nginx-server, not from next!).
Second try
As I need a fully static page and I've added getStaticPath and getStaticProps in the file for testing purposes, so that "real" html-files will be created:
import { useRouter } from 'next/router';
import Index from './index';
import config from '../content/config.yml';
import CustomPage from '../src/components/CustomPage';
const RoutingPage = () => {
const { customPages } = config;
const router = useRouter();
const { page } = router.query;
const findMatches = (requestedPage) =>
customPages.find((customPage) => customPage.name === requestedPage) ||
false;
const customPageData = findMatches(page);
if (customPageData !== false) {
return <CustomPage pageContext={customPageData} />;
}
return page === 'index' ? (
<Index page={page} />
) : (
<p style={{ marginTop: '250px' }}>whats up {page}</p>
);
};
export async function getStaticPaths() {
return {
paths: [
{ params: { page: 'impressum' } },
{ params: { page: 'datenschutz' } },
],
fallback: false, // See the "fallback" section below
};
}
export async function getStaticProps({ params }) {
return { props: { page: params.page } };
}
export default RoutingPage;
This generates the single pages as real html-files:
But this lead me to the next issue:
I've implemented internal Links in the Webpage like this:
which still lead a user to myDomain.com/impressum, now additionally there is myDomain.com/impressum.html available. From SEO perspective, this are two different paths.
How do I get them unified, so that I have only one path - regardles of whether if I open it from within my Webpage, or enter it directly.
Workaround Idea (??)
Sure, I could everywhere use something like:
<Link href={`/${item.page}.html`}>
But this only work if the Page is exported and copied to the Server. For next dev and next start this won't work, because the .html-File don't exist.... and so I'll lost the "page preview" while working at the page.
So only Idea I have is to set an ENV-Variable for .env.development & .env.production and encapsulate the -Component from NEXT in a HOC.
In that HOC I could check if I'm currently in dev or prod and don't use .html for those links... otherwise add the .html to the link.
What YOU say about this. Do you have any other solution?
I don't know if it's state of the art, but as little workaround I did this:
I place the next/link-Component in a HOC and check if it's run on development or production (process.env.NODE_ENV):
import React from 'react';
import Link from 'next/link';
const LinkHoc = (props) => {
const { as, href, children } = props;
if (process.env.NODE_ENV === 'production') {
return (
<Link
{...props}
as={as ? `${as}.html` : ''}
href={href ? `${href}.html` : ''}
/>
);
}
return <Link {...props}>{children}</Link>;
};
export default LinkHoc;
With this workaround you get mydomain.com/impressum links in DEV and mydomain.com/impressum.html in production.
Only thing what to do at least is to rename the JSON-Files for the generated pages.
They are in /out/_next/data/XYZranadomString/.
They are named like impressum.json and you need to rename it to impressum.html.json to fix the 404 error on clientside for this files.
Would love to see a better Solution, so if you have any suggestions, please let me know!

Aspnet server rendering debugging

I have a react-redux app running on aspnet core, with server side rendering using aspnet prerendering.
Lets say i make a programming error, where in child component I try to access a undefined prop because of a stupid typo.
import {Child} from './child'
export class Parent extends React.Component {
render () {
const someProp = {
something: "something"
};
return <Child someProp={someProp} />;
}
}
export class Child extends React.Component {
render() {
return <div>this.props.someprop.something</div>;
//typo: should be someProp instead of someprop
}
Without server rendering I would have got an error similar to this: cannot access something of undefined at line x:yy
But with serverrendering i get a:
An unhandled exception occurred while processing the request.
Exception: Call to Node module failed with error: Prerendering timed out after 30000ms because the boot function in 'ClientApp/src/boot-server' returned a promise that did not resolve or reject. Make sure that your boot function always resolves or rejects its promise. You can change the timeout value using the 'asp-prerender-timeout' tag helper.
this makes debugging quite hard, when you dont get any feedback on what went wrong.
Any one knows how to setup a reject if something fails ? or is it even possible to debug a server side rendered code ?
here is my boot-server file, tell me if you need some more files.
import * as React from 'react';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
import configureStore from './store/configureStore';
import {getFormById} from './actions/getFormActions';
import {updateUserLocale} from './actions/userLocaleActions';
import FormResponder from './components/mainComponents/formResponder';
export default function renderApp (params) {
return new Promise((resolve, reject) => {
const store = configureStore();
store.dispatch(getFormById(params.data.id, params.data.config, params.data.authenticationToken));
store.dispatch(updateUserLocale(params.data.userLocale));
const app = (
<Provider store={ store }>
<FormResponder />
</Provider>
);
// Perform an initial render that will cause any async tasks (e.g., data access) to begin
renderToString(app);
// Once the tasks are done, we can perform the final render
// We also send the redux store state, so the client can continue execution where the server left off
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config
}
});
}, reject); // Also propagate any errors back into the host application
});
}
I have had similar experience working with Visual Studio 2017. I eventually realized that the diagnostic information for the original error(s) was actually in the Output window.
I have done som research and have come to the conclusion that is not possible for the time beeing to debug the initial server rendered code.
what i have done instead is to implement logic, so that i can disable server rendering.
this is how it looks like:
public async Task<IActionResult> Index(string id, string userLocale = "en", bool server = true)
{
Guid positionId;
if (!Guid.TryParse(id, out positionId))
{
throw new Exception("Invalid position id");
}
var token = await _apiClient.GetToken();
var formData = new ApplicationFormViewModel()
{
Id = positionId,
UserLocale = userLocale,
AuthenticationToken = token.AccessToken,
Server = server
};
return View(formData);
}
view.cshtml:
#{if (#Model.Server) {
<div
class="container"
id="react-app"
asp-prerender-module="ClientApp/src/boot-server"
asp-prerender-data="new {
Id = #Model.Id,
UserLocale = #Model.UserLocale,
AuthenticationToken = #Model.AuthenticationToken,
Config = new {
ApplicationPostUrl = #Url.Action("SaveApplication"),
AttachmentPostUrl = #Url.Action("UploadAttachment"),
FormGetUrl = #Url.Action("GetForm")
}
}"
asp-prerender-webpack-config="webpack.config.js" >
Loading...
</div>
}
else {
<script>
var id= '#Model.Id';
var config= {
applicationPostUrl: '#Url.Action("SaveApplication")',
attachmentPostUrl: '#Url.Action("UploadAttachment")',
formGetUrl: '#Url.Action("GetForm")'
};
var userLocale='#Model.UserLocale';
var authenticationToken='#Model.AuthenticationToken';
var server = false;
</script>
<div class="container" id="react-app">loading</div>
}
}
#section scripts {
<script src="~/dist/main.js" asp-append-version="true"></script>
}
boot-server.jsx:
export default function renderApp (params) {
return new Promise((resolve, reject) => {
const store = configureStore();
store.dispatch(getFormById(params.data.id, params.data.config, params.data.authenticationToken));
store.dispatch(updateUserLocale(params.data.userLocale));
const app = (
<Provider store={ store }>
<FormResponder />
</Provider>
);
// Perform an initial render that will cause any async tasks (e.g., data access) to begin
renderToString(app);
// Once the tasks are done, we can perform the final render
// We also send the redux store state, so the client can continue execution where the server left off
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config,
server: true
}
});
}, reject); // Also propagate any errors back into the host application
});
}
boot-client.jsx:
// Grab the state from a global injected into server-generated HTML
const {id, initialReduxState, authenticationToken, config, server, userLocale } = window;
if (server) {
// Get the application-wide store instance, prepopulating with state from the server where available.
const store = configureStore(initialReduxState);
// This code starts up the React app when it runs in a browser.
ReactDOM.render(
<Provider store={ store }>
<FormResponder authenticationToken={authenticationToken} config={config} />
</Provider>,
document.getElementById('react-app')
);
}
else {
const store = configureStore();
store.dispatch(getFormById(id, config, authenticationToken));
store.dispatch(updateUserLocale(userLocale));
render(
<Provider store ={store}>
<FormResponder authenticationToken={authenticationToken} config={config} />
</Provider>,
document.getElementById('react-app')
); // Take our FormBuilder component and attach it with DOM element "app"
}
so now i can simply turn of server rendering by adding a ?server=false at the end of the url, and start debugging :)
Found a solution that works for me:
I inserted a try/catch on final renderToString.
where in catch i send a dispatch with the error.
updated boot-server.jsx
params.domainTasks.then(() => {
let html;
try {
html = renderToString(app);
}
catch (err) {
store.dispatch(loadFormFailed( {message: err.toString() } ));
}
resolve({
html: html,
globals: {
initialReduxState: store.getState(),
authenticationToken: params.data.authenticationToken,
config: params.data.config,
disableReactServerRendring: false
}
});
}, reject);
// Also propagate any errors back into the host application
});

Resources