I want to pass some extra data from the ssr server that's present after the middleware has run, and use that on client side middleware. A bit similar to what nuxt already does with vuex.
Documentation at the render:context hook:
Every time a route is server-rendered and before render:route hook. Called before serializing Nuxt context into window.__NUXT__, useful to add some data that you can fetch on client-side.
Now my custom plugin defines some hooks as stated in the documentation, but not all seem to be called properly:
module.exports = function() {
this.nuxt.hook('render:route', (url, result, context) => {
console.log('This one is called on every server side rendering')
}
this.nuxt.hook('renderer', renderer => {
console.log('This is never called')
}
this.nuxt.hook('render:context', context => {
console.log('This is only called once, when it starts loading the module')
}
}
What am I doing wrong and how can I pass custom ssr data to the client side renderer?
Ok, just found the solution to the core problem of passing custom data from the (ssr) server to the client:
Create a plugin: plugins/my-plugin.js
export default ({ beforeNuxtRender, nuxtState }) => {
if (process.server) {
beforeNuxtRender(({ nuxtState }) => {
nuxtState.myCustomData = true
})
} else {
console.log('My cystom data on the client side:', nuxtState.myCustomData)
}
}
Then register the plugin in your nuxt.config.js:
module.exports = {
plugins: ['~/plugins/my-plugin']
}
Docs here.
Related
I'm trying to integrate Sequelize to my Nuxt 3 project. However, I couldn't figure out how to make it load only once instead of reloading it every time the page was refreshed / navigating to another routes.
I couldn't find any information on the docs. Is it even possible?
~/plugins/sequelize.server.ts
import { Sequelize } from "sequelize"
export default defineNuxtPlugin(async (nuxtApp) => {
const config = useRuntimeConfig()
const sequelize = new Sequelize(config.dbName, config.dbUser, config.dbPass,{
host: config.dbHost,
port: parseInt(config.dbPort),
dialect: 'mysql',
})
try {
await sequelize.authenticate()
// this log was executed every time I navigate to a new route
// or refreshing the browser.
console.log('Connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
return {
provide: {
db: sequelize
}
}
})
OP solved his issue by removing a composable that was initialized on a component's mounted lifecycle hook.
Just a remaining piece of code.
What is the correct pattern to implement Auth0 route guards in Nuxt?
I've adapted the Auth0 sample code to create the following middleware:
import {getInstance} from '~/plugins/auth';
export default function () {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
}
Notice that before the authentication status of Auth0 can be accessed, we must wait for the the instance to finish loading. The Auth0 sample code does this by using $watch.
My middleware code "works" but has the issue of briefly displaying the protected pages before the async $watch triggers. Is there any way to wait and block the route from continuing to render until Auth0 has finished loading and its auth status can be accessed?
I've also tried using almost the exact same code Auth0 provides without my own modifications within the beforeRouteEnter hook of the Nuxt pages. This has the same issue which begs the question as to why the Auth0 example presumably works in VueJS using beforeRouteEnter but not in Nuxt?
Solved it!
A middleware can be asynchronous. To do this return a Promise or use async/await.
https://nuxtjs.org/docs/2.x/directory-structure/middleware/
I simply wrapped my middleware script in a promise. I resolved it if the user is able to pass, otherwise I redirected them to the Auth0 login.
import {getInstance} from '~/plugins/auth';
export default function () {
return new Promise(resolve => {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (!authService.isAuthenticated) {
return authService.loginWithRedirect({
appState: {targetUrl: 'http://localhost:3000'},
});
}
resolve();
};
// If loading has already finished, check our auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before we check isAuthenticated
authService.$watch('loading', loading => {
if (loading === false) {
return fn();
}
});
});
}
It was also important to return the loginWithRedirect to make sure that it didn't go on to resolve the promise outside of the if block.
I have several VueX actions (that run on the server only) and are dispatched from nuxtServerInit. They make HTTP requests to external services, which is slowing down the TTFB.
I would like to implement a cache plugin that can store and retrieve values from Redis. The aim is to avoid making the HTTP requests in actions on every request.
I started out by adding a line to the nuxt.js config file.
{ src: '~/plugins/cache', ssr: true, mode: 'server' },
I then created the following in resources/plugins/cache.js
import redis from 'redis';
export default ({ app }, inject) => {
console.log('Creating redis client');
inject('cache', redis.createClient({
//options removed for brevity
}));
}
I run the app and can see 'Creating redis client' is printed to the console on every page refresh. Is it possible to create a plugin that is instantiated when the server is started and the same instance is used for every request? Or if that is not possible, what is the best way to implement the cache?
As you want to share a data/instance, plugin is not the right place to do that because plugins are created (called) every time new Vue instance is created, which on server means on every request...
So you need something instantiated only once per server...and that's Nuxt module
modules/cacheModule.js
export default function (_moduleOptions) {
// any data you want to share between all requests
const data = {
message: `Hello from cache - ${new Date().toLocalTimeString()}`
};
this.nuxt.hook("vue-renderer:ssr:prepareContext", (ssrContext) => {
ssrContext.$cache = data;
});
}
And use it in server plugin or nuxtServerInit...
store/index.js
export const state = () => ({
cache: {}
});
export const mutations = {
setcache(state, payload) {
state.cache = payload;
}
};
export const actions = {
nuxtServerInit({ commit }, context) {
commit("setcache", context.ssrContext.$cache);
}
};
Demo
Same technique can be used for applying cacheAdapterEnhancer from axios-extensions package on server/client (or both) Axios instance so you can keep your original code (fetching in nuxtServerInit) - more details here
I have a very basic nuxt.js application using JSON in a local db.json file, for some reason the generated static site links leading to network error, but I can access them from the url or page refresh.
nuxt config
generate: {
routes () {
return axios.get('http://localhost:3000/projects')
.then((res) => {
return res.data.map((project) => {
return '/project/' + project.id
})
})
}
},
main root index page
data() {
return {
projects: []
}
},
async asyncData({$axios}){
let projects = await $axios.$get('http://localhost:3000/projects')
return {projects}
}
single project page
data() {
return {
id: this.$route.params.id
}
},
async asyncData({params, $axios}){
let project = await $axios.$get(`http://localhost:3000/projects/${params.id}`)
return {project}
}
P.S. I have edited the post with the code for the main and single project page
Issues with server-side requests of your application are caused by conflicts of ports on which app and json-server are running.
By default, both nuxt.js and json-server run on localhost:3000 and requests inside asyncData of the app sometimes do not reach correct endpoint to fetch projects.
Please, check fixed branch of your project's fork.
To ensure issue is easily debuggable, it is important to separate ports of API mock server and app itself for dev, generate and start commands.
Note updated lines in nuxt.config.js:
const baseURL = process.env.API_BASE_URL || 'http://localhost:3000'
export default {
server: {
port: 3001,
host: '0.0.0.0'
},
modules: [
['#nuxtjs/axios', {
baseURL
}]
],
generate: {
async routes () {
return axios.get(`${baseURL}/projects`)
.then((res) => {
return res.data.map((project) => {
return '/project/' + project.id
})
})
}
}
}
This ensures that API configuration is set from a single source and, ideally, comes from environmental variable API_BASE_URL.
Also, app's default port has been changed to 3001, to avoid conflict with json-server.
asyncData hooks have been updated accordingly to pass only necessary path for a request. Also, try..catch blocks are pretty much required for asyncData and fetch hooks, to handle error correctly and access error specifics.
I'm trying to build a Google Analytics plugin to Nuxt that will fetch tracking IDs from the CMS. I am really close I think.
I have a plugin file loading on client side only. The plugin is loaded from nuxt.config.js via the plugins:[{ src: '~/plugins/google-gtag.js', mode: 'client' }] array.
From there the main problem is that the gtag script needs the UA code in it's URL, so I can't just add that into the regular script object in nuxt.config.js. I need to get those UA codes from the store (which is hydrated form nuxtServerInit.
So I'm using head.script.push in the plugin to add the gtag script with the UA code in the URL. But that doesn't result in the script being added on first page load, but it does for all subsequent page transitions. So clearly I'm running head.script.push too late in the render of the page.
But I don't know how else to fetch tracking IDs, then add script's to the head.
// plugins/google.gtag.client.js with "mode": "client
export default ({ store, app: { head, router, context } }, inject) => {
// Remove any empty tracking codes
const codes = store.state.siteMeta.gaTrackingCodes.filter(Boolean)
// Add script tag to head
head.script.push({
src: `https://www.googletagmanager.com/gtag/js?id=${codes[0]}`,
async: true
})
console.log('added script')
// Include Google gtag code and inject it (so this.$gtag works in pages/components)
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
inject('gtag', gtag)
gtag('js', new Date())
// Add tracking codes from Vuex store
codes.forEach(code => {
gtag('config', code, {
send_page_view: false // necessary to avoid duplicated page track on first page load
})
console.log('installed code', code)
// After each router transition, log page event to Google for each code
router.afterEach(to => {
gtag('event', 'page_view', { page_path: to.fullPath })
console.log('afterEach', code)
})
})
}
I ended up getting this to work and we use it in production here.
Code as of this writing looks like this:
export default ({ store, app: { router, context } }, inject) => {
// Remove any empty tracking codes
let codes = _get(store, "state.siteMeta.gaTrackingCodes", [])
codes = codes.filter(Boolean)
// Abort if no codes
if (!codes.length) {
if (context.isDev) console.log("No Google Anlaytics tracking codes set")
inject("gtag", () => {})
return
}
// Abort if in Dev mode, but inject dummy functions so $gtag events don't throw errors
if (context.isDev) {
console.log("No Google Anlaytics tracking becuase your are in Dev mode")
inject("gtag", () => {})
return
}
// Abort if we already added script to head
let gtagScript = document.getElementById("gtag")
if (gtagScript) {
return
}
// Add script tag to head
let script = document.createElement("script")
script.async = true
script.id = "gtag"
script.src = "//www.googletagmanager.com/gtag/js"
document.head.appendChild(script)
// Include Google gtag code and inject it (so this.$gtag works in pages/components)
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
inject("gtag", gtag)
gtag("js", new Date())
// Add tracking codes from Vuex store
codes.forEach(code => {
gtag("config", code, {
send_page_view: false // Necessary to avoid duplicated page track on first page load
})
// After each router transition, log page event to Google for each code
router.afterEach(to => {
gtag("event", code, { page_path: to.fullPath })
})
})
}
If not in a plug-in, this was a good read on how to load 3rd party scripts: How to Load Third-Party Scripts in Nuxt.js