Initial static React HTML rendering with Webpack - reactjs

Is there a way to pre-generate the HTML structure of a (single route) React application directly in the HTML entry point?
Then the page will be able to display HTML (based on React initial state) before any JS is loaded.
I'm actually using webpack-html-loader but any other loader or plugin is welcome ;)
PS: May static-site-generator-webpack-plugin be of any help?
PS: I'm not using React Router

If you want to use static-site-generator-webpack-plugin you first need to build a bundle with webpack bundle.js that exports a render function that takes following arguments.
locals an object with various page metadata e.g. title that go into component parameters (traditionally thought of as template variables).
callback a nodejs style (err, result) callback that you will call with your rendered html as the value for result
e.g.
// entry.js, compiled to bundle.js by webpack
module.exports = function render(locals, callback) {
callback(null,
'<html>' + locals.greet + ' from ' + locals.path + '</html>');
};
It is in this function that you will instantiate your components (possibly via React Router if you want) and render them with ReactDOMServer.renderToString().
You will then specify the compiled bundle.js as bundle in your instantiation of StaticSiteGeneratorPlugin as well as your concrete routes in paths and in locals an object containing the above mentioned metadata values.
var paths, locals; // compute paths from metadata files or frontmatter
module.exports = {
entry: {
bundle: './entry.js' // build bundle.js from entry.js source
},
...,
plugins: [
new StaticSiteGeneratorPlugin('bundle', paths, locals)
]
}
The keys you specify for locals in webpack.config.js in will be present in the locals parameter of every call to render(locals, callback). They will be merged with path, assets and webpackStats keys provided by the plugin.
If you want to load javascript code into your pages after rendering you could compile an additional page.js entry to your webpack config that calls ReactDOM.render() in the typical manner and then load that bundle in a script tag emitted by in your render(locals, callback) function in your bundle.js (above). Ensure that page.js mounts components to the same location in the DOM as they are when rendered by entry.js (you will probably set an id attribute on the parent element). You will also need to ensure that any location (i.e. route path) dependent variables align in both environments.
Check out the source code of Gatsby which also uses this plugin. You can also have a look at the source code for Phenomic for an alternative approach.

You should try server side rendering, it will let react render the first view of your app in a backend and deliver a static HTML. This boilerplate already comes with server rendering set up and you can learn more about it here

Jekyll is great static site generator which can be extended with custom ruby plugins. You need to enable WebPack to make Jekyll calls. See Plugging Webpack to Jekyll Powered Pages

Here you have a working example https://github.com/aganglada/preact-minimal/blob/master/config/webpack.config.js, if you like you can fork it and take a look at how this work all together.
Hope it helps :)

Related

Relative Routing in Next JS

I am using Next JS.
Currently, my page is in the url
http://localhost:3000/project/613
Now, i want to push the page to
http://localhost:3000/project/613/time/123
Is there any way i can push relatively like router.push('/time/123')
Instead of entering the full URL router.push('project/613/time/123')
While it's not exactly the same as relative routing you can prepend router.asPath to the relative part.
router.push(`${router.asPath}/time/123`)
Meaning you don't need to explicitly set the beginning of the path.
We should define a public variable in .env and/or in .env.development (the prefix must be NEXT_PUBLIC)
NEXT_PUBLIC_BASE_PATH=/project/613
That variable is visible both client and server side. (It's probably ok, this is a part of the public url anyway)
Use that value in next.config.js
module.exports = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
// ...rest of our config
}
If the link was a next/link component, we would be good to go, imported images works fine too, at some places need more work like in router.push:
router.push(`${process.env.NEXT_PUBLIC_BASE_PATH}/time/123`)
Alternatively you could just write
router.push(`${router.basePath}/time/123`)
However the .env approach is more general, you don't need useRouter everywhere like even in a simple image component, which displays some image from public static routes:
<img src={`${process.env.NEXT_PUBLIC_BASE_PATH}/logo.svg`}>
And for more options, if you don't want to do data fetching, you can add a shallow option. Check the docs: https://nextjs.org/docs/routing/shallow-routing
Yes. Just add / in the first:
router.push('/time/123')

Routing localization with NextJS

I'm migrating a website developed with Gatsby to NextJS, but something I could achieve using Gatsby's createPage API is localizing the app's routes, but until now I couldn't achieve this with NextJS APIs.
I'm using Next v10.0.1 for this.
As I see in other threads regarding this type of resource, this is actually kinda confusing of what it actually means, so here goes an example of what is the desired result:
User access route /my-data/1234 (where the NextJS equivalent routing would be: /my-data/[dataId].js)
User must be able to access the same page but translated in the URL /pt/meus-dados/1234 (using, for example, portuguese translation).
Some guesses on how to achieve that keeping Next's static optimizations (Static rendering and Incrementing Static Rendering)?
I actually found an answer which is pretty useful for my use case, I'm using NextJS rewrites for that, maybe not the best solution, but fits my needs well.
I use a single file for each route, so the directory structure should be something like this:
pages
-- my-data
-- [id].js
then I'll have some kind of internationalization, in my case I'm using react-i18next, won't think about the aspects of implementations for the library here, it could also be achieved with any other.
Next step is to set a translation somewhere for the pages routes, for example, add an entry for the i18next messages named routes containing a key-value pair for the routes translations. e.g (intl/pt.json):
{
...
"routes": {
"/my-data/:id": "/meus-dados/:id"
}
...
}
and then use NextJS rewrites, so you will have to import the intl messages (e.g: from intl/pt.json) and map them as rewrites in next.config.js:
# next.config.js
...
async rewrites() {
// languages here is a key-value pair object containing the structure { [language]: { routes: {...} } }
// in my case, imported from `intl/pt.json`
const intlRewrites = Object.entries(languages).reduce((rewrites, [ language, { routes } ]) => {
return [
...rewrites,
...Object.entries(pages).map(([ path, translatedPath ]) => {
const source = translatedPath
const destination = path // here you can write a logic to add a language prefix if needed
return { source, destination }
})
]
}, [])
return intlRewrites
}
From my experience, optimizations like ISG work fine. Should work with SSG too, but I haven't tested it.
I've been tackling this exact problem, and whilst I don't yet have a solution that integrates directly into NextJS, this can be achieved fairly simply before your project is compiled.
If you were to organise your pages directory as follows, it should work as you expect:
// Before
pages
-- my-data
-- [id].js
// After
pages
-- pt
-- meus-dados
-- [id].js
-- en
-- my-data
-- [id].js
However the developer experience here isn't nice. So what I have done to solve this currently is written a simple build step that runs before next build. It takes a regular pages directory and converts it to the above format, allowing next build to run against a version that works as intended for the translated paths. This allows SSG and ISG to work as expected.
Ideally I'd like to hook into the Next ecosystem so this works seamlessly for dev and build, but I haven't yet gotten that far

How to load script in specific page in gatsby

Hello.
I recently got stuck with pretty important thing on my gatsby site.
I have to import script from other site cause it is providing map widget. This is the widget from polish delivery company and it is only available under link https://mapa.ecommerce.poczta-polska.pl/widget/scripts/ppwidget.js.
It is activated by a function window.PPWidgetApp.toggleMap(). Problem is when i try to activate, html and css markup from widget are showing but map coming from js it is not.
Here is how I'm loading the script:
{
resolve: "gatsby-plugin-load-script",
options: {
src:
"https://mapa.ecommerce.poczta-polska.pl/widget/scripts/ppwidget.js",
},
},
When I'm on specific route where I'm using this widget and i refresh the page everything is working properly. So I'm guessing problem is that when this script is loaded in index it gets cached somehow by gatsby and most of the important features are not working. So can I load the script only when I'm on let's say route /delivery ? Or is there another, better way to load this script that may work fine ?
Link to github repo with this problem: https://github.com/Exanderal/gatsby-problem
The easiest, native and built-in way to achieve is using <Helmet> component. Basically, this component embeds everything that is inside in your <head> tag.
The problem using it is that if you need to activate or to wait for its loading to make some actions (like window.PPWidgetApp.toggleMap() in your case), it could be kind of buggy since sometimes it may load properly but sometimes not. I will show you different approaches to check which one fits you better.
<Helmet> approach:
<Helmet>
<script src="https://mapa.ecommerce.poczta-polska.pl/widget/scripts/ppwidget.js"/>
</Helmet>
As I said, this workaround may work for standalone scripts, but if you need to perform actions or wait for its loading it may not work. The next approach should fit you.
Custom script loading approach:
const addExternalScript = (url, callback) => {
const script = document.createElement('script');
script.src = url;
script.async=true;
script.onload = callback;
document.body.appendChild(script);
};
useEffect(()=>{
addExternalScript("https://mapa.ecommerce.poczta-polska.pl/widget/scripts/ppwidget.js",window.PPWidgetApp.toggleMap())
},[])
Basically, you are setting a custom function (addExternalScript) that creates the same script tag as the first approach and embeds the passed URL as a first function parameter. The second parameter is the callback function to trigger once it's loaded in the onload function.
Everything it's triggered in the useEffect function with empty deps ([]). The useEffect is a hook (available in React version ^16) that is triggered once the DOM tree is loaded, in this case, it's a nice way to ensure that the window object is properly loaded and set to avoid some common issues in Gatsby using global objects.

How to use React components on WebWorker

I am creating a PDF generator using the react-pdf package at https://react-pdf.org/. The process of converting the react-pdf components into a pdf blocks the main thread, so I would like to convert them on a separate Worker thread.
I am using create-react-app with worker-loader to recognize WebWorker files. I am having a hard time coming up with a way to import the react components into the Webworker and convert them using the pdf(Component).toBlob() method provided by react-pdf. The pdf(Component).toBlob() takes a react component as input and outputs the blob files, this means that the worker has to somehow be able to load the React component. The WebWorker works as expected when no React components are included.
I naively tried importing the React components into the WebWorker and tried running the conversion method:
pdf(<Component />).toBlob().then(blob=>{
...
})
which causes a module parse error:
Uncaught Error: Module parse failed: Unexpected token (14:8)
File was processed with these loaders:
* ./node_modules/eslint-loader/index.js
You may need an additional loader to handle the result of these loaders.
Any help would be extremely appreciated.
Edit:
I am using the create-react-app default webpack configuration with the addition of worker-loader which detects .worker.js files and registers them as Webworker. I added the work-loader using the react-app-rewired package:
module.exports = function override(config, env) {
config.module.rules.push({
test: /\.worker\.js$/,
use: { loader: 'worker-loader' }
})
config.output['globalObject'] = 'this';
return config;
}
This answer is an extension of the discussion we had in the chat.
The issue at hand here is the .push() from the override function. As stated in the MSDN documentation:
The push() method adds one or more elements to the end of an array and returns the new length of the array.
Effectively the config.module.rules.push() is adding the loader at the end of the rules array, thus processing first the React code and then the worker.js.
As Ali Zeaiter found out the solution was to use .unshift() (cf. doc) instead of .push() in order to prepend the loader to the rules array and thus processing first the worker.js file.

2 levels nested routing in nextjs

I carefully read the docs of next routing system.
It only mentions that I could achieve dynamic routing like this:
http://localhost:3000/level1/dynamicSlug
But I am trying to achive something like this:
http://localhost:3000/level1/level2/dynamicSlug
And I want level2 to be created dynamic too
Thanks so much !
It is possible to do nested scenarios according to your request in this way.
for example:
pages/
level1/
[dynamicSlug]/
- index.js // will match for /level1/1234
level2/
- index.js // will match for /level1/level2
- [dynamicSlug].js // will match for /level1/level2/1234
Or
pages/
level1/
[dynamicSlug]/
- index.js // will match for /level1/1234
level2/
- index.js // will match for /level1/level2
[dynamicSlug]/
- index.js // will match for /level1/level2/1234
You have 2 choices:
Using v9 Dynamic Routing by calling the folder as [dynSlag] and putting your page file inside.
Using custom server and routing, you will need to define a custom server, map your path to a specific next page.
I know this is a bit of an old post, but I'd just like to share my working response with NextJS v11.
I want dynamic routing at two levels. E.g.:
{siteroot}/dynamicPage
{siteroot}/dynamicUrlSection/dynamicPage
My folder structure is:
/pages/[section]/[page].tsx
/pages/[section]/index.tsx
This way, the "dynamicPage" path at the root is handled by index.tsx, and nested routes are handled by [page].tsx
BONUS INFO: I am working with Contentful as a CMS. I use a single content model for all the pages at both levels.
The model has "section" and "page" properties.
The entries that serve the root dynamic pages (i.e. /pages/[section]/index) have a compound value in the "page" property of {section}-index. I then have to be a bit smart in my client code:
if (!page) {
page = `${section}-index`;
}
await fetchData(section, page);
Using the example right from the NextJs documentation, I use this hack, maybe you could use it.
<Link href="/posts/[id]" as={`/posts/${subFolder}${id}`}>
"as" will have a value like /posts/nested_subfolder_file.md
And in the getPostData function, just do this little change:
const nestedPaths = id.split('_')
const fileName = `${nestedPaths.pop()}.md`
const fullPath = path.join(postsDirectory, ...nestedPaths, fileName)

Resources