Nuxt.js: Managing CMS collections for a statically generated website - vue.js

On nuxt generate, we need to fetch all collections from the CMS. Since our website is completely static, it should be sufficient to fetch every collection only 1 time, and then make them available to all routes.
We run into issues making this happen, without exposing all our websites content to every single route.
We could use vuex to store all the content. However, we don't want a giant store object with all our websites content to be available to the client. Only during the build process, the server should have access to it and pass the content needed to every single route, eg. via asyncData()
(remember, we have a statically generated website, therefore asyncData() is never called from the client).
Example: We need the blog collection on /blog, but also on /blog/:slug, and on /press. Currently we fetch them individually:
// /pages/blog/index.vue
export default {
asyncData() {
return fetchArticles();
}
}
// /pages/blog/_slug.vue
export default {
asyncData(context) {
// made available in nuxt.config.js
return context.payload;
}
}
// nuxt.config.js
export default {
generate: {
// generate dynamic routes
routes: async function() {
const collection = await fetchArticles();
const result = collection.map(item => {
return {
route: `/blog/${item.slug}`,
// save in payload, to make available in context
payload: item
};
});
return result;
}
}
}
Problem: We are fetching the same collection twice. We want to fetch it once, and then make it available to all routes.
Expected result: Being able to
fetch every collection only 1 time
storing all the collections on the server side only, passing needed content to routes via asyncData()
on client side, not being able to access that "server-side-store"
Actual result:
having to refetch the same collection for several routes

Just fill your /blog/index.vue the same way as your single blog post, with payload.
You only need to make sure to exclude /blog path from list of static routes. You can try this https://github.com/nuxt/nuxt.js/issues/2719#issuecomment-508583281. Or make nuxt build && nuxt generate --no-build command (this way all static routes will be excluded, you'll need to specify them manually).
PS:
because context.payload is only available to the dynamic routes
I have just tried the method above and it worked.

Related

How to protect multiple routes from unauthorized access in Next.js using next-auth

I am using Next.js and I have a folder learning inside my pages folder. Now, this learning folder has about 10 pages.
All these pages need to redirect to the index page if the user is not logged in. The following code does the job, but is there any other way to protect multiple pages, so that I don't need to add this same code again and again to all the pages ?
export async function getServerSideProps(context) {
//redirect to index page if not logged in
const session = await unstable_getServerSession(context.req, context.res, authOptions);
if (!session) {
return {
redirect: {
destination: '/',
permanent: false
}
}
}
}
I believe you are confused between protecting pages and protecting API ROUTES.
If you simply want to protect pages, you can indeed use middleware
However, if you wish to protect API Routes (e.g prevent a user from deleting data using your API endpoint and postman), I believe you need to use this unstable_getServerSession
Except creating reusable function, it's true that I didn't find anywhere in the doc how to set it for multiple paths in one folder only...
you can use middleware. docs: https://next-auth.js.org/configuration/nextjs#middleware
Create a middleware.ts (or .js) file at the root or in the src directory (same level as your pages).
If you only want to secure certain pages, export a config object with a matcher:
export { default } from "next-auth/middleware"
// otherwise your app would require authentication for all
export const config = { matcher: ["/dashboard"] }
Now you will still be able to visit every page, but only /dashboard
will require authentication.
If a user is not logged in, the default behavior is to redirect them
to the sign-in page.
that example is from the docs. You can also write a custom middleware
import { NextResponse } from "next/server";
export function middleware(req) {
const sessionCookie = req.cookies.get("session");
}
// you could add more if logic for other pages
if (req.nextUrl.pathname.startsWith("/admin ")) {
if (!sessionCookie) {
return NextResponse.redirect("/home");
}
}

Custom PageLayoutComponent

To have specific layout for some pages at our project we create few custom PageLayoutComponent's. Some contfiguration example:
{
// #ts-ignore
path: null,
canActivate: [CmsPageGuard],
component: CartPageLayoutComponent,
data: {
cxRoute: 'cart',
cxContext: {
[ORDER_ENTRIES_CONTEXT]: ActiveCartOrderEntriesContextToken,
},
},
},
All work fine with storefront until you will not try to select specific page at smartedit. As result it not use our custom CartPageLayoutComponent, but will use PageLayoutComponent for rendering.
Probably this is because it's not a normal route navigation. Can somebody from spartacus team suggest how this bug can be fixed?
Probably this is because it's not a normal route navigation
I believe your Route should be recognized normally, there is nothing special in adding a custom Angular Route.
So I guess there is something special about the page or URL of Spartacus Storefront that runs in your SmartEdit.
It's hard to tell the reason of your problem without more debugging.
You said your app works as expected when run differently (locally?), but when used in SmartEdit, then there is a problem. Please identify factors that makes the run SmartEdit different from your (local?) run. And try to isolate them. Guesses from top of my head:
production vs dev mode of the build?
exact URL of the cart page?
any difference in configuration between a local version and deployed one to be used in SmartEdit?
I would also add some debugging code to better know which routes configs are available and which one is used for the current route. For debugging purposes please add the following constructor logic in your AppModule:
export class AppModule {
// on every page change, log:
// - active url
// - the Route object that was matched with the active URL
// - all the Route objects provided to Angular Router
constructor(router: Router) {
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
console.log({
activeUrl: router.url,
activeRouteConfig:
router.routerState.snapshot.root.firstChild.routeConfig,
allRoutesConfigs: router.config,
});
}
});
}
}
The pages opened in SmartEdit have the same route of cx-preview (e.g. to open faq page in smartedit, request is https://localhost:4200/electronics-spa/en/USD/cx-preview?cmsTicketId=xxxxx. backend can get page id from cmsTicketId). If you want to change the page layout, you can consider use PageLayoutHandler. Spartacus has some PageLayoutHandlers, e.g.
{
provide: PAGE_LAYOUT_HANDLER,
useExisting: CartPageLayoutHandler,
multi: true,
},

Fetching global api data in nuxt

I'm starting Nuxt project and I have to fetch basic data from my backend.
for example language dictionary or some data for leftbar and etc.
in Vue I can just call everything I need in App.vue and save it in vuex and then use whenever I want, but with Nuxt every page is different and I'm curious where should i call this global data api calls.
I'm guessing I have to create middleware and call actions if data is not loaded already or there is better solution?
You mention it's global data and you're using universal mode- I think you're looking for nuxtServerInit(). This store action runs once on the server, before created, mounted hooks etc. You can use it to populate your store with data that your components (including pages) rely on.
Take a look at the docs.
actions: {
nuxtServerInit ({ commit }, { req }) {
if (req.session.user) {
commit('user', req.session.user)
}
}
}

Load async data into the vuex store when Nuxt app loads

I am trialling a project in Nuxt. Liking it so far except I am a little confused as to how to load data from an external async service so that it is available in Vuex from the very first route.
I have tried adding middleware on the default layout to dispatch the store action but I do not see the service being called straight away. Only when I navigate deeper into the routes do I see the action dispatched.
I did something similar in a standard Vue project and added the created method to the App.vue.
Is there a similar way in Nuxt?
What you need is called a fetch.
The fetch method, if set, is called every time before loading the component (only for page components). It will be called server-side once (on the first request to the Nuxt app) and client-side when navigating to further routes.
Warning: You don't have access of the component instance through this inside fetch because it is called before initiating the component.
async fetch({ store }) {
await store.dispatch('your-action')
}
If you need parameter:
async fetch({ store, params }) {
await store.dispatch('your-action', params.id)
}
I gave an example of id. The name of the parameter depends on the name of your page.
_id => params.id
_slug => parmas.slug
...

How to defer routes definition in Angular.js?

I have configured some basic routes that are available for all users before they log in:
App.config(function ($routeProvider) {
$routeProvider.
when('/login', { templateUrl: 'views/login.html', controller: PageStartCtrl.Controller }).
otherwise({ redirectTo: '/login' });
});
So the only thing user can do is to log in. After the user logs in, I would like to register additional routes like this:
$http
.post('api/Users/Login', { User: userName, Password: userPassword })
.success(function (response : any) {
App.config(function ($routeProvider) {
$routeProvider
.when('/dashboard',
{ templateUrl: 'part/dashboard.html',
controller: DashboardCtrl.Controller });
});
However, I suppose I should call .config method only once, because the $routeProvider is brand new instance that knows nothing about /login route. Further debugging showed me that the first instance of $resourceProvider is used when resolving view change.
Q: Is there a way how to register routes later?
Solution from Add routes and templates dynamically to $routeProvider might work, but is quite ugly (involved global variable nastyGlobalReferenceToRouteProvider).
Since routes are defined on a provider level, normally new routes can only be defined in the configuration block. The trouble is that in the configuration block all the vital services are still undefined (most notably $http). So, on the surface it looks like w can't define routes dynamically.
Now, it turns out that in practice it is quite easy to add / remove routes at any point of the application life-cycle! Looking at the $route source code we can see that all the routes definition are simply kept in the $route.routes hash which can be modified at any point in time like so (simplified example):
myApp.controller('MyCtrl', function($scope, $route) {
$scope.defineRoute = function() {
$route.routes['/dynamic'] = {templateUrl: 'dynamic.tpl.html'};
};
});
Here is the jsFiddle that demonstrates this in action: http://jsfiddle.net/4zwdf/6/
In reality, if we want to be close to what AngularJS is doing the route definition logic should be a bit more complex as AngularJS is also defining a redirect route to correctly handle routes with / at the end (make it effectively optional).
So, while the above technique will work, we need to note the following:
This technique depends on the internal implementation and might break if the AngularJS team decides to change the way routes are defined / matched.
It is also possible to define the otherwise route using the $route.routes as the default route is stored in the same hash under the null key
I found that the answer by #pkozlowski.opensource works only in angularjs 1.0.1. However, after angular-route.js becomes an independent file in the later version, directly set the $route doesn't work.
After reviewing the code, I find the key of $route.routes is no longer used to match location but $route.route[key].RegExp is used instead. After I copy the origin when and pathRegExp function, route works. See jsfiddle:
http://jsfiddle.net/5FUQa/1/
function addRoute(path, route) {
//slightly modified 'when' function in angular-route.js
}
addRoute('/dynamic', {
templateUrl: 'dynamic.tpl.html'
});