I'm using Gatsby and I want build a single page site, so without create pages. For achieve this I edited gatsby-node.js with the following code:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
in that case, each request is re-routed to the index.js page, which is the only one.
Then, in the index.js page I have:
const IndexPage = () => {
const intl = useIntl()
const locale = intl.locale
return (
<BGTState>
<BlogState>
<Layout>
<Router>
<Home path={`${locale}/`} />
<Section path={`${locale}/:sectionSlug`} />
<Collection path={`${locale}/:sectionSlug/:collectionSlug`} />
<Season
path={`${locale}/:categorySlug/:collectionSlug/:seasonSlug`}
/>
<Product
path={`${locale}/:categorySlug/:collectionSlug/:seasonSlug/:prodSlug`}
/>
<Blog path={`${locale}/blog`} />
<Article path={`${locale}/blog/:articleSlug`} />
<NotFound default />
</Router>
</Layout>
</BlogState>
</BGTState>
)
}
as you can see, I have different routers that load a specific component based on the url.
I have prefixed each path with the current locale to match the correct path.
This mechanism is working fine for the home page only, but for the other links doesn't work. Infact, if I visit something like:
http://localhost:3001/en/category-home/prod-foo
which must load the Collection component, the site simply redirect to:
http://localhost:3001/en
and display the Home component again.
What I did wrong?
UPDATE
Page Structure:
As you can see I have just the index.js which handle all requests as I configured in the gatby-node.js.
If I remove the localization plugin, at least using this configuration:
{
resolve: `gatsby-plugin-intl`,
options: {
// Directory with the strings JSON
path: `${__dirname}/src/languages`,
// Supported languages
languages: ["it", "en", "ci", "fr"],
// Default site language
defaultLanguage: `it`,
// Redirects to `it` in the route `/`
//redirect: true,
// Redirect SEO component
redirectComponent: require.resolve(
`${__dirname}/src/components/redirect.js`
),
},
},
and I don't prefix the url with intl.locale, everything is working fine. But adding redirect: true in the plugin configuration, and prefixing the link with the locale, the site redirect me to the home component.
If you are creating a SPA (Single Page Application, notice the single) you won't have any created pages but index. You are trying yo access to a /category page that's not created because of:
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
That's why your routes don't work (or in other words, only the home page works).
Adapt the previous condition to your needs to allow creating more pages based on your requirements.
I'm using Gatsby and I want build a single page site, so without
create pages. For achieve this I edited gatsby-node.js with the
following code:
It's a non-sense trying to build a SPA application with Gatsby (without creating pages) but then complaining because there's not collection page created.
Make sure that you understand what you are doing, it seems clearly that you need to create dynamically pages for each collection, season, and product so your approach to create SPA won't work for your use-case.
It's possible to keep just index.js without overcomplicating thing? I
just want to understand why my code isn't working 'cause I've passed
the correct url... Removing the localization Gatsby works, so I
suspect there is a localization problem
The only way that http://localhost:3001/category-home/prod-foo (removing the localization) could be resolved is by creating a folder structure such /pages/category-home/prod-foo.js (since Gatsby extrapolates the folder structure as URLs), so, if you want to use localization using your approach, add a structure such en/pages/category-home/prod-foo.js and es/pages/category-home/prod-foo.js (or the whatever locale), and so on. In my opinion, this is overcomplexitying stuff since, for every category, you'll need to create 2 (even more depending on the locales) files.
Gatsby allows you to create dynamic pages and interpolate the locale automatically using built-in plugins on the process, creating each file for the specifically defined locales.
Related
I'm trying to implement client routes in Gatsby together with gatsby-plugin-react-i18next for two languages. I followed officel Gataby documentation and there is no client only path implementation explained.
Below is the code i implemeted.
gatsby-node.js
function langPrefix(page) {
return page.context.language === page.context.i18n.defaultLanguage &&
!page.context.i18n.generateDefaultLanguagePage
? ''
: `/${page.context.language}`
}
exports.onCreatePage = ({ page, actions }) => {
const { createPage } = actions
// Removing the ^ skips an optional /:lang prefix
if (page.path.match(/\/app/)) {
// adding lang if it's not the default page.
page.matchPath = `${langPrefix(page)}/app/*`
createPage(page)
}
}
Appjs
src/app/app.js
function App() {
return (
<>
<Router basepath="/:lang/app">
<PrivateRoute path="/accounthome" component={AccountHome} location=""/>
</Router>
<Router basepath="/app">
<PrivateRoute path="/accounthome" component={AccountHome} location=""/>
</Router>
</>)
}
export default App
Gatsby config
{
resolve: `gatsby-plugin-react-i18next`,
options: {
localeJsonSourceName: `locale`, // name given to `gatsby-source-filesystem` plugin.
languages: ["en", "fr"],
defaultLanguage: `en-us`,
fallbackLanguage: `en-us`,
// if you are using Helmet, you must include siteUrl, and make sure you add http:https
siteUrl: `https://my.costco.com/`,
ns: langTranslationConfig,
// you can pass any i18next options
i18nextOptions: {
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
nsSeparator: false
},
pages: [
{
matchPath: '/:lang/app/accounthome',
getLanguageFromPath: true,
excludeLanguages: ['en-ca']
},
{
matchPath: '/preview',
languages: ['en']
}
]
}
}
Router path : http://localhost:8000/en-us/app/accounthome
When am accessing this rote in browser This code show Gatsby.js development 404 page not found. Any pointer what am missing and am not sure how to access the translation contents in client only route page example account home page. Do i need to write the graph query in account home page or i dont need to ?
I think your regex is leaking your code. I guess it should look like:
if (page.path.match(/^\/app/)) {
page.matchPath = `${langPrefix(page)}/app/*`
createPage(page)
}
Either way, check the created page list by accessing the 404 page in development mode. By default, there should be a list of all created pages. Check the paths there to see if you can spot any mistakes or trailing slash issues.
The problem here is that your page is not being generated properly, that's why it throws a 404, so checking the created pages may help you to spot the mistake or at least, a thread to pull.
After a few research I've seen that you are basing your approach in: Gatsby can't find client routes when using gatsby-plugin-react-i18next
In their case, the langPrefix function is only prefixing the language to the page slug if it's the default one:
function langPrefix(page) {
return page.context.language === page.context.i18n.defaultLanguage &&
!page.context.i18n.generateDefaultLanguagePage
? ''
: `/${page.context.language}`
}
In your case, I'm not sure the plugin supports en-us (it's en-US according to https://github.com/microapps/gatsby-plugin-react-i18next/issues/100) and I think that's the reason why there's a leak in your page creation. Try using en instead of en-us or directly looking for en-US paths.
I want to create dynamic pages in gatsby but I don't know what the full url name would be.
createPage({
path: '/account/orders/:id',
matchPath: '/account/orders/:id',
component: path.resolve('./src/templates/order.tsx'),
});
I thought the code written would be okay to visit page with any value of 'id' but while building the development bundle it gives an error
account\orders\:id contains invalid WIN32 path characters
The ':id' value can be anything so I dont want to use looping method to create the page. What could be done?
Taking into account that:
You don't want to loop through pages
You will never know the value of the id
Your only chance is to use client-only routes through the file system route API.
In your case, assuming that your "unknown" page will be under /account/orders you should create a folder structure such: src/pages/account/orders/[...].js. The [...].js notation stands for the file system route for an undefined value.
In the [...].js file you can just create something like:
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/Layout"
import Profile from "../components/Profile"
import Default from "../components/Default"
import CustomOrderComponent from "../components/CustomOrderComponent"
const Orders = () => {
return (
<Layout>
<Router basepath="/account/orders">
<Profile path="/profile" />
<CustomOrderComponent path='/:id' />
<Default path="/" />
</Router>
</Layout>
)
}
export default Orders
From the docs:
Briefly, when a page loads, Reach Router looks at the path prop of
each component nested under <Router />, and chooses one to render
that best matches window.location (you can learn more about how
routing works from the #reach/router documentation). In the
case of the /orders/profile path, the Profile component will be
rendered, as its prefix matches the base path of /orders, and the
remaining part is identical to the child’s path.
In the above scenario, CustomOrderComponent will render your variable id (:id).
The new Relay hooks API has put a focus on the React pattern of "render-as-you-fetch" and so far I am really liking this. Relay's useQueryLoader and usePreloadedQuery hooks make implementing this most of the time pretty straight forward.
I am however, struggling to find a good pattern on how to implement this pattern when it comes to routing. There are two typical situations that I find makes this difficult to implement.
Situation A:
User loads a home page (example.com/)
User go deep down one part of the app tree (example.com/settings/user/security/authentication)
They then click on a link to take them to a totally unrelated part of their app (example.com/blog/post-1)
Situation B:
User uses the URL bar to go to a section of the app instead of using a link (example.com/blog/post-1)
With these examples there are two outcomes, either the user goes to a route (example.com/blog/post-1) either via a nest child component or directly via the URL. So the way we are fetching data for this route must support both of these approaches.
I assume we would want to trigger the fetch as early as possible for this route, so when the user clicks on the link or as soon as we detect this route on page load.
There are three ideas I can think of to implement this:
Use a fetch-then-render pattern instead (such as Relay's useLazyLoadQuery hook)
Store a function (say in Context) and have all links for this route call this function in their onClick method, and also have a useEffect for this route that calls the function if there is no data loaded, or the reference for the query is stale
Use render-as-you-fetch functions but implement them to support fetch-then-render also
Approach 1:
This defeats the purpose of render-as-you-fetch pattern however is an easy way out and more likely to be a "cleaner" way to implement fetching data for a route.
Approach 2:
In practice I have found this really hard to implement. Often the link to go to the route is disconnected from part of the component tree where the component renders the route is. And using a Context means that I have to manage different loadData functions for specific routes (which can be tricky when variables etc are involved).
Approach 3:
This is what I have been doing currently. In practice, it often results in being able to pass the load data function to a near by component, however if the route is accessed by a disconnected component, by the URL, or a page reload etc then the components falls back to calling the load data function in a useEffect hook.
Does anyone have any other ideas or examples on how they implemented this?
An update on this topic, React Router v6 recently introduced support for route loaders, allowing preload Relay queries based on routing.
Example:
import { StrictMode, Suspense } from "react";
import ReactDOM from "react-dom/client";
import {
createBrowserRouter,
Link,
RouterProvider,
useLoaderData,
} from "react-router-dom";
import graphql from "babel-plugin-relay/macro";
import {
loadQuery,
PreloadedQuery,
RelayEnvironmentProvider,
usePreloadedQuery,
} from "react-relay";
import { environment } from "./environment";
import { srcGetCurrentUserQuery } from "./__generated__/srcGetCurrentUserQuery.graphql";
const getCurrentUser = graphql`
query srcGetCurrentUserQuery {
viewer {
id
fullname
}
}
`;
const Test = () => {
const data = usePreloadedQuery(getCurrentUser, preloadedQuery);
const preloadedQuery = useLoaderData() as PreloadedQuery<srcGetCurrentUserQuery>;
return (
<Suspense fallback={<>Loading...</>}>
<Viewer preloadedQuery={preloadedQuery} />
</Suspense>
);
};
const router = createBrowserRouter([
{
element: (
<>
{"index"} <br /> <Link to={"/test"}>Go test</Link>
</>
),
path: "/",
},
{
element: <Test />,
path: "test",
loader: async () => {
return Promise.resolve(
loadQuery<srcGetCurrentUserQuery>(environment, getCurrentUser, {})
);
},
},
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<RelayEnvironmentProvider environment={environment}>
<RouterProvider router={router} />
</RelayEnvironmentProvider>
</StrictMode>
);
More information about React Router loaders here: https://reactrouter.com/en/main/route/loader
I've also been struggling with understanding this. I found these resources particularly helpful:
Ryan Solid explaining how to implement fetch-as-you-render
The ReactConf 2019 Relay demo
The Relay Issue Tracker example
What I understand they aim for you to achieve is:
Start loading your query before and outside of the render path
Start loading your component at the same time as the query (code splitting)
Pass the preloaded query reference into the component
The way it's solved in the Relay demo is through something they call an "Entrypoint". These are heavily integrated into their router (you can see this in the Issue Tracker example). They comprise the following components:
A route definition (e.g. /items)
A lazy component definition (e.g. () => import('./Items'))
A function that starts the query loading (e.g. () => preloadQuery(...))
When the router matches a new path, it starts the process of loading the lazy component, as well as the query. Then it passes both of these into a context object to get rendered by their RouterRenderer.
As for how to implement this, it seems like the most important rules are:
Don't request data inside components, request it at the routing or event level
Make sure data and lazy components are requested at the same time
A simple solution appears to be to create a component that is responsible for collecting the data, and then rendering the respective component. Something like:
const LazyItemDetails = React.lazy(() => import('./ItemDetails'))
export function ItemEntrypoint() {
const match = useMatch()
const relayEnvironment = useEnvironment()
const queryRef = loadQuery<ItemDetailsQuery>(relayEnvironment, ItemDetailsQuery, { itemId: match.itemId })
return <LazyItemDetails queryRef={queryRef} />
}
However there are potential issues that the Issue Tracker example adds solutions to:
The lazy component may have previously been requested so should be cached
The data fetching sits on the render path
Instead the Issue Tracker solution uses a router which does the component caching, and the data fetching at the same time as the route is matched (by listening to history change events). You could use this router in your own code, if you're comfortable with maintaining your own router.
In terms of off the shelf solutions, there doesn't appear to be a router that implements the patterns required to do fetch-as-you-render.
TL;DR Use the Relay Issue Tracker example router.
Bonus: I've written a blog post about my process of understanding this pattern
I have a Gatsby site and due to some specific requirements, I need to redirect anyone who attempts to hit a specific URL path, for which there is no page, to an external site. This URL path is not a page within the site, but it's something that a user may be inclined to type due to documentation that is out of my control.
Here's an example: Let's say the site is located at https://www.example.com. A user may visit https://www.example.com/puppies, which does not exist. My file structure does not contain a src/pages/puppies.js file. However, when that URL is entered, I need to redirect the user to another site altogether, such as https://www.stackoverflow.com.
I haven't used Gatsby to that extent to know it has a configuration for this, so someone else may correct me. The way I would handle this is through the hosting provider where your app is.
For example, if you are using Netlify:
Create a _redirects file with the following content:
/* /index.html 200
Or
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
This will cause all https://yourwebsite.com/IDontHaveThisRoute to fallback to /index.html where your .js is loaded.
I provided the Netlify example only to give you the basic idea of how it can be done through the hosting provider of your choice. I would look into configurations I can put into redirects where my domain is deployed.
Thanks to Paul Scanlon he mentioned using onRouteUpdate in Gatsby and it works like a charm
import { navigate } from 'gatsby';
export const onRouteUpdate = ({ location }) => {
if (location.pathname === '/dashboard') {
navigate('/dashboard/reports');
}
};
This question helped point me in the right direction. I was able to get it to work using Gatsby's componentDidMount() to force a redirect as shown below, using a new file called puppies.js to "catch" the path typed by the user:
// puppies.js
import React, { Component } from 'react'
class Puppies extends Component {
componentDidMount() {
window.location.replace("https://www.stackoverflow.com");
}
render() {
return <div />
}
}
export default Puppies
I wonder what type of navigation works well with login authentication? Right now i use conditional rendering for certain pages or components to display and through
if (this.state.loggedIn) {
return <UI loggedIn={this.state.loggedIn} showUser=
{this.state.showUser} logout={this.logout.bind(this)} />;
};
i can render something after the validation. What would it look like if i wanted to render a couple of more different pages? Should i put a state on each page that will change on onclicks and cause the app to render desired page?
Thank you lads
This is an issue which nearly every modern application must tackle. Because of this, many libraries have already solved these issues for you. Take this code for example which uses react-router:
In my example I am showing you what the routes would look like in a routes.js file and then a separate file for what the acl would look like. The acl is a function which is passed into the onEnter of each route you want to protect. You can call it anything you like.
routes.js
import React from 'react';
import { hashHistory, Router, Route, IndexRoute } from 'react-router';
import { acl } from './util/acl-util';
import AppContainer from './containers/app-container';
import DashboardPage from './pages/dashboard-page';
export default class Routes extends React.Component {
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={AppContainer}>
{/* NON-AUTH ROUTES */}
<Route
path="login"
components={{
main: LoginPage,
aside: null,
header: this.getHeader,
}}
/>
{/* AUTH-REQUIRED ROUTES */}
<Route
onEnter={acl}
path="dashboard"
components={{ main: DashboardPage }}
/>
</Router>
);
}
}
acl-util.js
import { hasAuth } from './auth-util';
export function acl(nextState, replace) {
const { pathname, search } = nextState.location;
if (!hasAuth(theState)) {
window.alert(
'Please Log in!'
);
replace(`/login?loginRedirect=${encodeURIComponent(pathname + search)}`);
}
}
I threw this example together from cutting out part of my code that won't apply directly to this concept - and therefore this code won't run as is. You'll need to define your pages, and set up a store etc.
You'd need to define a hasAuth function which can look into your state and determine whether a user is authenticated. For my hasAuth function I am looking for a jwt token and parsing the date, if the date is still in the future, I know they are still authed and any subsequent rest api calls will work.
I know you weren't asking for a certain library, but I recommend this because the app I took this code from has dozens of routes and the acl function also implements a role matrix which looks at what a user can and cannot do based on their jwt token. Our application is pretty massive and this approach keeps it organized.
Without having something like react-router, you're right, you'd need to manually manage which page is showing and manually check for auth state in each component or make a parent component to do it. In my example the "parent component to manage it" is react-router and my onEnter method called acl. In traditional applications acl stands for access control list - you can expand the code in whichever way you like.
edit:
as someone mentioned in a comment about . Your frontend application is only as secure as the backend service it is grabbing data from or posting data to. In my example, the react code attempts to mirror the auth state in the jwt token. But, at the end of the day, your real security will only be provided by your back end services. Just because the frontend thinks a user can be logged in, shouldn't mean the backend should assume they are - you need backend authentication since all frontend application state can be modified by technical users.