Deploying Next.js apps to Netlify
March 7, 2023This post covers the main notes for deploying to Netlify and domain setup. Prerequisites Next.js app bootstrapped Deployment Add a new site…
read more →
This post covers the main notes for deploying to Netlify and domain setup. Prerequisites Next.js app bootstrapped Deployment Add a new site…
read more →
Error tracking and alerting are crucial in the production environment, proactively fixing the errors leads to a better user experience. Sentry is one of the error tracking services, and it provides alerting for unhandled exceptions. You should receive an email when something wrong happens.
Sentry issues show the error stack trace, device, operating system, and browser information. The project dashboard shows an unhandled exception once it's thrown. This post covers the integration of several technologies with Sentry.
Create a Node.js project on Sentry
Install the package
npm i @sentry/node
const Sentry = require('@sentry/node');Sentry.init({dsn: SENTRY_DSN});test();
Create a Next.js project on Sentry (version 13 is not yet supported)
Run the following commands for the setup
npm i @sentry/nextjsnpx @sentry/wizard -i nextjs
Create a Gatsby project on Sentry
Install the package
npm i @sentry/gatsby
module.exports = {plugins: [// ...{resolve: '@sentry/gatsby',options: {dsn: SENTRY_DSN}}]};
Create a React Native project on Sentry
Run the following commands for the setup
npm i @sentry/react-nativenpx @sentry/wizard -i reactNative -p android
This post covers the main notes for deploying to Vercel and domain setup. Prerequisites Next.js app bootstrapped Deployment Add a new…
read more →
Push notifications are a great alternative to email notifications. There is no need for a verification step, UX is improved, and user engagement with the app is increased. This post covers both front-end and back-end setups.
firebase
package installedThe following service worker should be registered for handling background notifications. A custom notificationclick
handler should be implemented before importing firebase libraries. The below implementation opens a new window with the defined URL if it is not already open. Firebase automatically checks for service workers at /firebase-messaging-sw.js
, so it should be publicly available.
// /firebase-messaging-sw.js/* eslint-disable no-unused-vars */self.addEventListener('notificationclick', (event) => {event.notification.close();const DEFAULT_URL = '<URL>';const url =event.notification?.data?.FCM_MSG?.notification?.click_action ||DEFAULT_URL;event.waitUntil(clients.matchAll({ type: 'window' }).then((clientsArray) => {const hadWindowToFocus = clientsArray.some((windowClient) =>windowClient.url === url ? (windowClient.focus(), true) : false);if (!hadWindowToFocus)clients.openWindow(url).then((windowClient) => (windowClient ? windowClient.focus() : null));}));});importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-app-compat.js');importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-messaging-compat.js');const firebaseApp = initializeApp({apiKey: 'xxxxxx',authDomain: 'xxxxxx',projectId: 'xxxxxx',storageBucket: 'xxxxxx',messagingSenderId: 'xxxxxx',appId: 'xxxxxx',measurementId: 'xxxxxx'});const messaging = getMessaging(firebaseApp);
getToken
There are multiple types of errors as response:
messaging/permission-blocked
- user blocks the notificationsmessaging/unsupported-browser
- user's browser doesn't support the APIs required to use the Firebase SDKmessaging/failed-service-worker-registration
- there's an issue with the Firebase messaging service workerThe access token is invalidated when a user manually blocks the notifications in the browser settings.
isSupported
Promise<boolean>
It should be used in useEffect
hooks.
import { isSupported } from 'firebase/messaging';// ...useEffect(() => {isSupported().then((isAvailable) => {if (isAvailable) {// ...}}).catch(console.error);}, []);// ...
initializeApp
import { initializeApp } from 'firebase/app';import { getMessaging, getToken } from 'firebase/messaging';import { firebaseConfig } from 'constants/config';export const initializeFirebase = () => initializeApp(firebaseConfig);export const getTokenForPushNotifications = async () => {const messaging = getMessaging();const token = await getToken(messaging, {vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY});return token;};
The server key for API v1 can be derived from the service account key JSON file. In that case, the JSON file should be encoded and stored in the environment variable to prevent exposing credentials in the repository codebase. The service account key JSON file can be downloaded by clicking Generate new private key on the Project Overview → Project settings → Service accounts tab.
import * as serviceAccountKey from './service-account-key.json';const encodedServiceAccountKey = Buffer.from(JSON.stringify(serviceAccountKey)).toString('base64');process.env.SERVICE_ACCOUNT_KEY = encodedServiceAccountKey;
import 'dotenv/config';import * as googleAuth from 'google-auth-library';(async () => {const serviceAccountKeyEncoded = process.env.SERVICE_ACCOUNT_KEY;const serviceAccountKeyDecoded = JSON.parse(Buffer.from(serviceAccountKeyEncoded, 'base64').toString('ascii'));const jwt = new googleAuth.JWT(serviceAccountKeyDecoded.client_email,null,serviceAccountKeyDecoded.private_key,['https://www.googleapis.com/auth/firebase.messaging'],null);const tokens = await jwt.authorize();const authorizationHeader = `Bearer ${tokens.access_token}`;console.log(authorizationHeader);})();
The icon URL should be covered with HTTPS, so the notification correctly shows it.
// ...try {const response = await axios.post(`https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`,{message: {notification: {title: 'Push notifications with Firebase',body: 'Push notifications with Firebase body'},webpush: {fcmOptions: {link: 'http://localhost:3000'},notification: {icon: 'https://picsum.photos/200'}},token: registrationToken}},{headers: {Authorization: authorizationHeader}});console.log(response.data);} catch (error) {console.error(error?.response?.data?.error);}// ...
A successful response returns an object with name
key, which presents the notification id in the format projects/{project_id}/messages/{message_id}
.
There are multiple types of errors in the response:
The demo with the mentioned examples is available here.
The global state can be helpful when components share some common parts. Also, some parts can stay persistent (in local storage) and be used in the following user's session. React provides a native way to handle state management using context with the hooks.
// ...import { useAppContext } from "context";import { UPDATE_FEATURE_ACTIVATION } from "context/constants";export function CustomComponent() {const { state, dispatch } = useAppContext();// get value from the storeconsole.log(state.isFeatureActivated);// dispatch action to change the statedispatch({ type: UPDATE_FEATURE_ACTIVATION, payload: { isFeatureActivated: true } });// ...}
// context/index.jsximport PropTypes from "prop-types";import React, {createContext,useContext,useEffect,useMemo,useReducer,} from "react";import { getItem, setItem, STATE_KEY } from "utils/local-storage";import { INITIALIZE_STORE } from "./constants";import { appReducer, initialState } from "./reducer";const appContext = createContext(initialState);export function AppWrapper({ children }) {const [state, dispatch] = useReducer(appReducer, initialState);const contextValue = useMemo(() => {return { state, dispatch };}, [state, dispatch]);useEffect(() => {const stateItem = getItem(STATE_KEY);if (!stateItem) return;const parsedState = JSON.parse(stateItem);const updatedState = {...initialState,// persistent stateisFeatureActivated: parsedState.isFeatureActivated,};dispatch({type: INITIALIZE_STORE,payload: updatedState,});}, []);useEffect(() => {if (state !== initialState) {setItem(STATE_KEY, JSON.stringify(state));}}, [state]);return (<appContext.Provider value={contextValue}>{children}</appContext.Provider>);}AppWrapper.propTypes = {children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,};export function useAppContext() {return useContext(appContext);}
// context/reducer.jsimport { INITIALIZE_STORE, UPDATE_FEATURE_ACTIVATION } from "./constants";export const initialState = {isFeatureActivated: false,};export const appReducer = (state, action) => {switch (action.type) {case INITIALIZE_STORE: {return action.payload;}case UPDATE_FEATURE_ACTIVATION: {return {...state,isFeatureActivated: action.payload.isFeatureActivated,};}default:return state;}};
// app/layout.jsximport { AppContextProvider } from "context";export default function RootLayout({ children }) {return (<html lang="en"><head>// ...</head><body><AppContextProvider>{children}</AppContextProvider></body></html>);}
Constants
// context/constants.jsexport const INITIALIZE_STORE = "INITIALIZE_STORE";export const UPDATE_FEATURE_ACTIVATION = "UPDATE_FEATURE_ACTIVATION";
Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.
Consider the following things when the Next.js app is running in production.
Error tracking and alerting are crucial in the production environment, proactively fixing the errors leads to a better user experience. Sentry is one of the error tracking services, and it provides alerting for unhandled exceptions. You should receive an email when something wrong happens.
Sentry issues show the error stack trace, device, operating system, and browser information. The project dashboard shows an unhandled exception once it's thrown. Run the following commands for integration with the Next.js app.
npm i @sentry/nextjsnpx @sentry/wizard -i nextjs
Intl
API is not supported in some browsers. Cover it with a fallback.
localStorage
API is not available when cookies are blocked. Cover it with a fallback.
Here is the link to the boilerplate I use for the development.
Google analytics helps get more insights into app usage.
react-ga4
package is installedAnalytics should be initialized inside pages/_app.js
file.
import ReactGA from "react-ga4";// ...if (isEnvironment("production")) {ReactGA.initialize(ANALYTICS_TRACKING_ID);}
// ...export function trackEvent(category, label, action = "click") {if (isEnvironment("production")) {ReactGA.event({action,category,label,});}}// ...<Button onClick={() => trackEvent("category", "label")}>
Here is the link to the boilerplate I use for the development.
I have recently migrated the React app bootstrapped with create-react-app to Next.js. The main reason was to improve SEO by making the app…
read more →