Route - Redirection (Next JS) - reactjs

I am moving my website (PHP) to Next JS but I have a question regarding routes.
Currently I have this kind of routing (unique ID and slug from an API):
www.website.com/section/id_slug-of-post
And I manage some implementations, like:
www.website.com/section/id => 301 to www.website.com/section/id_slug-of-post
www.website.com/section/id_slug-with-wrong-words => 301 to www.website.com/section/id_slug-of-post
www.website.com/section/wrong-id => 301 to www.website.com/section
Can I do the same thing with Next JS or shall I use Express JS?

A feature like this is currently being discussed in this RFC on the Next.js GitHub. However, there is also a way to do this using the res.writeHead method in getServerSideProps
export const getServerSideProps = (ctx) => {
ctx.res.writeHead(301, { Location: '/new/url' });
}
You could also redirect on the client side using the Router instance provided by next/router.

Related

SvelteKit - /routes/a/+page.server.ts fetch('/b/') url confusion, version #sveltejs/kit#1.0.0-next.512

When I try to fetch('/b/') within the load function of /routes/a/+page.server.ts it refuses to accept local URL references.
Instead of being able to do
/b/
I have to use url:
http://localhost:3000/b/
Because the fetch() call refuses to accept the url (error: "Failed to parse URL"). I'm trying to consume my own api to reuse code. I thought SvelteKit fetch was supposed to support these local routes for api calls?
The example in documentation: https://kit.svelte.dev/docs/routing
Shows +page.svelte calling url '/api/add/' from fetch(), but not from within +page.server.ts - is there some reason they would not allow the same convention?
Thank you.
SvelteKit developers got back to me and indicated that there are two choices of fetch(url) function.
// /src/routes/test/[param0]/[param1]/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
// ERROR: this will fail with URL parsing
let fetchResult = fetch('/api/target/');
}
SvelteKit aware fetch passed as load function parameter:
export const load: PageServerLoad = async ({ params, fetch }) => {
// NOTE: fetch was passed in as a parameter to this function
let fetchResult = fetch('/api/target/');
}
Confusing!
When I have an internal API route I want to hit within my sveltekit application, I structure it as so:
├src
|├routes
||├api
|||├example
||||├+server.js
Now, elsewhere in the app, you can hit the route like so using the fetch api:
let res = await fetch("/api/example")
refer to this section of the SvelteKit docs for a better understanding:
https://kit.svelte.dev/docs/routing

Prevent flash of wrong page in NextJS app after MSAL-React redirect to/from Azure AD B2C

Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
⛔ Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('👍 Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.

Dynamic meta descriptions for certain pages in React SPA

I built a blog with React that fetches the data for each blog entry dynamically from an api.
The page's content obviously looks different depending on the route, e.g. mysite.com/blog/1, mysite.com/blog/2, ...
What I want to achieve is to dynamically change the meta descriptions depending on the data that is fetched from the api based on the url. In particular the og:title, og:description and og:image. Is something like that even possible?
I read about SSR/Next.JS or Gatsby but I am not sure if this is working if the data is received from api calls.
I understand that SSR would render the content on the server, hence it allows Google to crawl the pages but wouldn't that exclude api calls?
I also understand that Gatsby builds static sites but for me that wouldn't work because the api calls are dynamic and cannot be built into a static site.
I would highly appreciate some hints to point me in the right direction.
I understand that SSR would render the content on the server, hence it
allows Google to crawl the pages but wouldn't that exclude api calls?
I also understand that Gatsby builds static sites but for me that
wouldn't work because the api calls are dynamic and cannot be built
into a static site.
I think you've misunderstood how Gatsby works.
To summarize, Gatsby generates static pages from dynamic data (API calls) so, when you run the gatsby develop or gatsby build command, Gatsby fetches the data from the sources (in this case from your API) and generates dynamic pages (mysite.com/blog/1, mysite.com/blog/2, etc).
This data is static, meaning that each change of those sources, will force you to re-fetch the data to display the changes (in production means a new deployment) but, as soon as your pages are built statically, you can build your SEO data on the fly.
This is an old Gatsby's workflow that they used to have on their old website but I find it quite self-explanatory:
In the build-time, your site is being statically generated so, you can fully customize your SEO requirements with a custom SEO component or whatever your want. Most of the starters come with a SEO component ready to be used:
function Seo({ description, lang, meta, title }) {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
const defaultTitle = site.siteMetadata?.title
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata?.author || ``,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
Since the component is taking description, lang, meta and title as a props respectively, you can add it in your blog template to take dynamic data that will be built statically, meaning that, when your site will be deployed, that data will be already built, so Google crawlers will get it instantly.
const YourBlogPostTemplate = ({ data }) =>{
return <article>
<SEO title={data.allAPIData.frontmatter.title} />
<h1>I'm the title: {data.allAPIData.frontmatter.title}</h1>
</article>
}
Note: I won't fully customize it to avoid extending the answer, but get the idea.
The SEO component will be taking the title and the rest of the fields in the build-time.
Your API calls are fetched before the page is built so, at the time your page is being built your data is already static.
There is a pretty good article here that describes how to work around this issue the easiest way.
Basically, I needed to put a simple NodeJS server on top of my current React application. The server is now serving the files from the build directory. For some routes, I am simply replacing the __PAGE_META__ placeholder with the meta tags needed for Google, Facebook, Twitter, etc.
Here is a simple picture to describe the flow:
And a simple code snippet of the NodeJS part:
const path = require("path")
const express = require("express")
const app = express()
const fs = require("fs")
//
const pathToIndex = path.join(__dirname, "build/index.html")
app.get("/", (req, res) => {
const raw = fs.readFileSync(pathToIndex)
const pageTitle = "Homepage - Welcome to my page"
const updated = raw.replace("__PAGE_META__", `<title>${pageTitle}</title>`)
res.send(updated)
})
//
app.use(express.static(path.join(__dirname, "build")))
app.get("*", (req, res) =>
res.sendFile(path.join(__dirname, "build/index.html"))
)
const port = process.env.PORT || 5000
app.listen(port, () => {
console.log(`Server started on port ${port}`)
})
For the dynamic site, an api can be called and the page meta can be replaced with the information appropriately.

NextJS - only run getInitialProps if route is different

I'm setting up a NextJS app using getInitialProps to pull misc data that I would like to be server-side-rendered on first page load. All is working fine, but I noticed that if I click the same link twice, getInitialProps runs again, even though everything is the same.
For example I have a left nav with links to various categories etc (this app is going to be a front-end for an ecommerce site). If I click to a category, the category component (page) loads just fine. Then if I click the exact same link, the getInitialProps of the category component runs again, causing the page to blank out for a second while the same data (the item list) is fetched and re-rendered.
So is there a way to prevent getInitialProps from running if the user clicks the same link twice?
Note that I'm using getInitialProps for two reasons:
getStaticProps is out because I don't plan to build the entire site at build time
getServerSideProps is usually out because I don't like that it ends up doing two http requests: first a request goes to the NextJS server, then the server sends a request to my API (which happens to live on a different server). I'd rather skip the middle man
Some code:
Category.getInitialProps = async (context) => {
let config = await import("../../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getCTGY&ctgyCode=${context.query.code}`);
let queryString = {...context.query};
if ( response ) {
return {
category: response.data,
queryString: queryString,
pathname: context.asPath
};
} else {
return {
category: null
}
}
};
You should Look into shallow routing which enables you to change the URL of the page without re-running the data fetching. This includes getInitialProps.
Shallow routing also works on <Link /> components.
<Link href="/" shallow>
<a>Home</a>
</Link>
You should be aware of the caveats though, they're documented here.

Nextjs How to map multiple routes to one page?

I have a page called blog in my nextjs version 9.
Next create a route /blog for my page. Now I want to have these routes to be mapped to the same blog page and consequently the blog component:
1. /blog/cat1/gold
2. /blog/cat2/silver
3. /blog/cat3/bronze
Actually, I want to use them as query parameters in the route but I don't want to follow this format:
1. /blog?cat=cat1&color=gold
2. /blog?cat=cat2&color=silver
3. /blog?cat=cat3&color=bronze
I used next/router asPath but it is completely client-side and by reloading it returns 404!
In nextjs 9 dynamic routing has provided which may help me but the thing is I want to have only one component and different routes.
Do you have any ideas?
You didn't specify your web server, so I will give an example using expressjs.
const app = next({ dev })
app.prepare().then(() => {
const server = express();
server.get("/blog/:cat/:color", (req, res) => app.render(req, res, `/blog`));
})
If you need to access the value in the page, you can use getInitialProps to get the value by accessing req.params.cat and req.params.color.
Another way is by passing the value directly when calling app.render function:
app.render(req, res, '/posts', { cat: req.params.cat, color: req.params.color })
Here is the example.
You can do this with Next's dynamic routers. Just create a folder for blog-posts and add a dynamic page under it like this:
/pages/blog/[...slug].tsx
Now on your [...slug].tsx will catch all paths, no matter in what depth. And you can catch the path params from the router's query -field. Try this with any path under /blog/**/* -path and it will list path elements:
const Blog = () => {
const {
query: { slug }
} = useRouter();
return (
<ul>
{query.slug.map(value => <li key={value}>{value}</li>)}
</ul>
);
}

Resources