How to create one page site with multi languages routes? - javascript

I'm using Gatsby and I would like to create a one site using multilanguage, so far I've defined pages/index.js which contains this:
import React from "react"
import Layout from "../components/layout/layout"
import BGTState from "../context/bgt/bgtState"
import { Router } from "#reach/router"
import Home from "../components/pages/home"
import Collection from "../components/pages/collection"
import NotFound from "../components/pages/404"
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Router>
<Home path="/" />
<Collection path="collection/:id" />
<NotFound default />
</Router>
</Layout>
</BGTState>
)
}
export default IndexPage
and I have modified gatsby-node.js as:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
each request is forwarded on index.js, but there is a problem. I'm using the plugin gatsby-plugin-intl that add to the url a dynamic prefix like: http://localhost:3001/en/
If I visit http://localhost:3001/en/, then I get the NotFound component displayed because no Route match the url. Is there a way to prefix the url and reroute everything to the correct component?

Why you are using client-only routes/wrapping everything inside the <Router>?
I don't know what's the goal in your scenario to change the gatsby-node.js with:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
If you are not using client-only routes, you can remove them.
It's a broad question but, just define your languages and translation files. In your gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-intl`,
options: {
// language JSON resource path
path: `${__dirname}/src/intl`,
// supported language
languages: [`en`,`es`],
// language file path
defaultLanguage: `en`,
// option to redirect to `/en` when connecting `/`
redirect: true,
},
},
]
The useIntl hook will capture the internal requests so, you just need to worry about the views, forgetting the routing:
import React from "react"
import { useIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
const IndexPage = () => {
const intl = useIntl()
return (
<Layout>
<SEO title={intl.formatMessage({ id: "title" })}/>
<Link to="/page-2/">
<FormattedMessage id="go_page2" />
</Link>
</Layout>
)
}
export default IndexPage
Your Collection component should be a page, wrapped inside /page folder, or a custom collection with a specific id. If that page is dynamically created, you should manage the customizations in your gatsby-node.js, and, in that case, it should be a template of collections in that scenario.
To link between pages, I would recommend using page-queries to get the needed data to create your components. Your page should look like:
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Link to="/"> // home path
<Link to="collection/1">
</Layout>
</BGTState>
)
}
export default IndexPage
The 404-page will automatically be handled by Gatsby, redirecting all wrong requests (in development will show a list of pages). Your other routing should be managed using the built-in <Link> component (extended from #reach/router from React).
To make dynamic the <Link to="collection/1"> link, you should make a page query, as I said, to get the proper link to build a custom dynamic <Link> from your data.
Once you installed the gatsby-plugin-intl plugin, all your pages will be prefixed automatically, however, to point to them using <Link> or navigate you need to get the current language and prefix it:
export const YourComponent = props => {
const { locale } = useIntl(); // here you are getting the current language
return <Link to={`${locale}/your/path`}>Your Link</Link>;
};
Because useIntl() is a custom hook provided by the plugin, the value of locale will be automatically set as you change the language.

Related

NextJS: Why i18next works only on http://localhost:3000/de (landingpage) but not as http://localhost:3000/de/about

Sorry for being noob but I am really confused on how to work on this. So I followed the instructions on this https://github.com/i18next/next-i18next but confused when it comes to index.js. Whenever I click my toggle switch for /de in my landing page it translates alright in url "http://localhost:3000/de".
But in another page like "About" or in any other page it doesn't translate but the url switch to "http://localhost:3000/de/about". It doesnt go to my 404 error page. But I don't get it why it doesn't translate.
In my index.js if I removed "Service" component which contained all the components of landing page. And replace with other component file like "About" component page it translate alright.
It seems "http://localhost:3000/de" url only works in translation. But in different url path it doesn't. How to do this? Thank you..
Kindly see my code..
My locales path
public/locales/de/common.json
src/pages/_app.js
import nextI18NextConfig from '../../next-i18next.config'
<Component {...pageProps} />
export default appWithTranslation(App, nextI18NextConfig);
src/pages/index.js
import React from 'react';
import Service from 'views/Service';
import i18nextConfig from '../../next-i18next.config';
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
const IndexPage = () => {
return (
<Service/> <— this contains my landing page the only can be translated as “localhost:/3000/de” (src/pages/views/service)
)
};
export async function getServerSideProps({ locale }) {
return { props: {
...(await serverSideTranslations(locale, ['common', 'footer', 'stripe', ‘navbar'], i18nextConfig))
} }
}
export default IndexPage;
in my navbar it is in global component I put my toggle language switcher
src/layouts/Main/components/Navbar/Navbar.js
const onToggleLanguageClick = (locale) => {
const { pathname, asPath, query } = router
router.push({ pathname, query }, asPath, { locale })
}
const changeTo = router.locale === 'de' ? 'en' : 'de'
return (
<button onClick={() => onToggleLanguageClick(changeTo)}>
{t('change-locale', { changeTo })}
</button>
)
this is my next-i18next.config
const path = require('path');
module.exports = {
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en',
localePath: path.resolve('./public/locales')
},
}
my next.config.js
const nextConfig = {
i18n,
…some code
}
module.exports = nextConfig
src/pages/_document.js
import i18nextConfig from '../../next-i18next.config';
export default class MyDocument extends Document {
render() {
const currentLocale = this.props.__NEXT_DATA__.query.locale ?? i18nextConfig.i18n.defaultLocale;
return (
<Html lang={currentLocale}>
.....
First of all, remove the second argument in the appWithTranslation. There is also no need to change the language in the html tag (src/pages/_document.js). i18 does it itself.
public/locales/en ---common.json & /de ---common.json
Wrong. Use the default paths 'public/locales/en/common.json'. Also you can remove the 'localePath' in i18 config file.
reference: https://github.com/i18next/next-i18next
I figured out my problem. Got confused with many of my file paths. Maybe it can help someone.
Add these to your component
export async function getServerSideProps({ locale }) {
....code
}
that doesn't translate your "http://localhost:3000/de/componentname", which in the obvious path: src/pages/componentname.js
... just like my src/pages/index.js above.

Single Page Application in React with routing - same structure, different content

Problem
I'm trying to make an SPA with routing (ideally with React hooks) in React, but all the examples, descriptions i find are about displaying different components based on the URL. What i want is something like Youtube or Google docs, where the page structure/components are (mostly) the same and only the content changes.
Context
(Edit: adding a bit more context.)
This is going to be a document editor/presenter.
Page structure: after login, there is always a toolbar(blue color) on the top, for menus, notifications, etc. The rest of the screen will be mostly like the two examples below:
Example1:
Example2:
The search pane(orange) could be switched on/off by a button on the toolbar or by a user session variable. The document will be presented in the document section(grey) based on either a user session variable, doc ID provided in URL or selecting a document in the search pane.
Planned URLs
(Added in edit.)
Landing page: /login , login page.
Landing page: / , here the toolbar and a preconfigured, user session based default doc would be presented.
Document page: /doc?id=oys2OPkfOwQ , same as landing page but document section would contain the document with ID provided as query param.
Anything else: /something , toolbar and something under it.
Idea
(Added in edit.)
The layout is defined by CSS grid and page structure changes based on a variable. So this is going to be a prop for the App component coming from default value and user session configured variable and could change later.
This is the functionality i imagine for the App component (pseudo code-like thing):
<Router>
<Route path='/login'>
<Login/>
// Components: Toolbar and something under it
</Route>
<Route path='/'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Default document loaded for default, not logged in user
// Default document loaded from stored user session
</Route>
<Route path='/doc'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Same as for '/' except document with ID set as query param is displayed
// This could be called/triggered from search and document component as well
</Route>
<Route path='/somethingelse'>
<SomethingElse/>
</Route>
</Router>
Question
(Edit: rephrased, original question was how to implement a solution where different documents loaded based on URL query parameter.)
What i'm mostly interested in if there is a simpler way to draw the landing layout '/' and specific doc presenter /doc?id=oys2OPkfOwQ layout? In both cases the same components get displayed, only the provided parameter(doc to present) is different.
Solution
(Added in edit.)
By reading the answers and feedback and re-thinking my problem i realized that i have a multiple URLs same content problem.
Using React Router to render components based on UrlParams.
First of all, edit your routes to render DocumentLoader component under the route /doc
// file: app.js
import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import DocumentLoader from "./DocumentLoader";
const App = (props) => {
return <BrowserRouter>
<Routes>
<Route path="/doc" element={<DocumentLoader />}>
</Routes>
</BrowserRouter>
}
Create custom hooks for loading documents
You need two custom hooks, one for loading new document by changing the docId query parameter, and another hook to listen to docId changes to reload new document from your backend.
NOTE: Edit loadDocumentData to load from your backend
// file: hooks.js
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
/**
* TODO:// Refactor this function to call your backend to get
* Document data by docId
*/
const loadDocumentData = (docId) =>
new Promise((resolve, reject) => {
// this setTimeout for demonstration porpuse only
setTimeout(() => {
resolve({ id: docId, name: `Document name for ${docId}` });
}, 3000);
});
export const useDocument = () => {
const [loading, setLoading] = useState(true);
const { docId, loadDocument } = useDocumentParam();
const [document, setDocument] = useState(null);
useEffect(() => {
setLoading(true);
// Load your document data based on docID
loadDocumentData(docId)
.then((doc) => {
setDocument(doc);
setLoading(false);
})
.catch((e) => {
console.error('Failed to load doc', docId);
});
}, [docId, setLoading]);
return { document, loading, loadDocument };
};
export const useDocumentParam = () => {
const [searchParams, setSearchParams] = useSearchParams();
const docId = searchParams.get('d');
const loadDocument = useCallback(
(newDocId) => {
setSearchParams({ d: newDocId });
},
[setSearchParams]
);
return { docId, loadDocument };
};
Create DocumentLoader component
To listen on query param changes, load document from server-side, display loading indicator and render the "DocPresenter" component.
// file: DocumentLoader.js
import * as React from 'react';
import DocPresenter from './DocPresenter';
import { useDocument } from './hooks';
const DocumentLoader = (props) => {
const { loading, document, loadDocument } = useDocument();
if (loading) {
return <div>Display loading indicator while loading the document</div>;
}
return (
<div className="document-container">
<div className="toolbar">NavBar</div>
<div className="searchbox">search component</div>
<div className="editor">
<DocPresenter document={document} setParentstate={loadDocument} />
</div>
</div>
);
};
export default DocumentLoader;
Checkout Live Example on StackBlitz.
Helper Links:
React Router Docs
React Custom Hooks Docs
Here's how I would do it. Notice that the URL will remain the same.
const DynamicComponent = () => {
const components = {
Component1: <Component1 />,
Component2: <Component2 />,
Component3: <Component3 />,
};
const [component, setComponent] = useState(components["Component1"]);
return (
<div>
<div id="nav">
<span onClick={(e) => setComponent(components["Component1"])}>
Set to component 1
</span>
<span onClick={(e) => setComponent(components["Component2"])}>
Set to component 2
</span>
<span onClick={(e) => setComponent(components["Component3"])}>
Set to component 3
</span>
</div>
<div>{component}</div>
</div>
);
};
export default DynamicComponent;

functions called in_app.js won't always load at some point next.js

I want to add a user behavior tracking in my next.js application. I am using Next 10.2.3. In the _app.js I have a function that will call the tracking to start once the page is loaded. I have the impression that every time the page is loaded the function in _app.js will load(reload) as well but unfortunately at some point it does not. Let me know if these are correct or I miss understood some part. This app is hosted in vercel. how the code looks like below.
import App from "next/app";
import Head from "next/head";
import * as tool from "../lib/tools";
const MyApp = ({ Component, pageProps }) => {
tool.callUserBehavior(); //i want to call this function everytime the page is rendered/loaded
return (
<>
<Head>
<link rel="shortcut icon" href={getStrapiMedia(global.favicon.url)} />
</Head>
{/* Display the content */}
<Layout global={global}>
<Component {...pageProps} />
</Layout>
</>
);
};
MyApp.getInitialProps = async (ctx) => {
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(ctx);
// Fetch global site settings from Strapi
const global = await getGlobalData();
// Pass the data to our page via props
return { ...appProps, pageProps: { global, path: ctx.pathname } };
};
export default MyApp;

Next.js - understanding getInitialProps

I have an app that uses next.js along with Apollo/ Graphql and i'm trying to fully understand how the getInitialProps lifecycle hook works.
The lifecycle getInitialProps in my understanding is used to set some initial props that will render server side for when the app first loads which can be used prefetch data from a database in order to help SEO or simply to enhance page load time.
My question is this:
Every time I have a query component that fetches some data in my
components across my app, do I have to use getInitialProps to be
sure that data will be rendered server side?
My understanding is also that getInitialProps will only work in the page index components (as well as in _app.js), this would mean that any component lower down in the component tree would not have access to this lifecycle and would need to get some initial props from way up at the page level and then have them passed down the component tree. (would be great if someone could confirm this assumption)
Here is my code:
_app.js (in /pages folder)
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';
class AppComponent extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Component client={client} {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default AppComponent;
Index.js (in /pages/users folder)
import React, { PureComponent } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const USERS_QUERY = gql`
query USERS_QUERY {
users {
id
firstName
}
}
`;
class Index extends PureComponent {
render() {
return (
<Query query={USERS_QUERY}>
{({data}) => {
return data.map(user => <div>{user.firstName}</div>);
}}
</Query>
);
}
}
export default Index;
The answer is NO
If you use Apollo with Next JS you will not have to use getInitialProps on each page to get some initial data rendered server side. The following configuration for getInitialProps is enough for all the components to render out with their respective queries if they have <Query> components in them
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
My issue and why I wasnt seeing any server side rendering is that Heroku or Now wouldnt perform SSR with a public URL ie my-app.heroku.com. To resolve this I purchased and applied a custom URL in Heroku and it worked. Along with a custom URL I had the following configuration in my Apollo config
const request = (operation) => {
operation.setContext({
fetchOptions: {
credentials: 'include'
},
headers: { cookie: headers.cookie }
});
};
This completely resolved it and now I have SSR without the pain of having to manually set getInitialProps on each page
Hope this helps someone

Code Splitting with react-loadable gives Error : Cannot find module "."

I'm using react-loadable v4.0.4 and webpack v3.5.1.
Here is my code,
import Dashboard from '../../scenes/dashboard/dashboard';
import ReactLoadable from 'react-loadable';
...
const yoPath = 'src/components/scenes/dashboard/dashboard';
const DashboardWrap = ReactLoadable({
loading: Dashboard,
loader: () => {
return new Promise((resolve, reject) =>
require.ensure(
[],
(require) => resolve(require(yoPath)),
(error) => reject(error),
'dashboardChunk'
)
)
}
});
And using react-router-dom v4.1.2, I've set Route as follows,
<Switch>
...
<Route exact path='/dashboard' component={DashboardWrap} />
...
</Switch>
I'm able to build the chunks for the respective component with the name dashboardChunk.
But while loading that component I'm getting the issues as follows.
In the console,
And the chunkfile,
Please let me know if I'm doing anything wrong.
I basically wanted to do code splitting, for that I've just done the following and it works fine.
I've created a common component(wrapper component) as follows,
import React, { Component } from 'react';
class Async extends Component {
componentWillMount = () => {
this.props.load.then((Component) => {
this.Component = Component
this.forceUpdate();
});
}
render = () => (
this.Component ? <this.Component.default /> : null
)
}
export default Async;
Then I've used above component as follows,
export const AniDemo = () => <Async load={import(/* webpackChunkName: "aniDemoChunk" */ "../../scenes/ani-demo/AniDemo.js")} />
export const Dashboard = () => <Async load={import(/* webpackChunkName: "dashboardChunk" */ "../../scenes/dashboard/Dashboard.js")} />
And using the above, I've made the changes in route as follows,
<Route exact path="/ani-demo" component={AniDemo} />
<Route exact path="/dashboard" component={Dashboard} />
With the help of the above changes that I made, I'm able to create chunks properly with the names that I've mentioned in the comments inside import statements ie aniDemoChunk.js and dashboardChunk.js respectively.
And these chunks load only when the respective component is called ie aniDemoChunk.js is loaded on browser only when AniDemo component is called or requested. Similarly for the Dashboard component respectively.
Note: If anyone is getting error re:Unexpected token import. So to support import() syntax just replace import to System.import() or else use babel-plugin-syntax-dynamic-import.
Webpack must be able to determine the imported path during static analysis. If you pass an argument into require, this is not possible.
It is best to put the actual path into require.ensure, i.e.
require.ensure(
['src/components/scenes/dashboard/dashboard']
require =>
resolve(require('src/components/scenes/dashboard/dashboard').default)
error => reject(error),
'dashboardChunk'
)
or use the newer dynamic import syntax. With the newer syntax you could simplify the above into:
Loadable({
loader: () => import(/* webpackChunkName: "dashboardChunk" */ 'src/components/scenes/dashboard/dashboard')
loading: MyLoader
})
Also, the loading argument should be a component to display while your asynchronous load is taking place, e.g. some kind of loading animation.

Categories