How to create a Next.js PWA with TypeScript, Mantine and Custom Fonts (Optional)
Introduction
I have been using this stack for a long time. I once attempted to create a template repository to use for every new project, but this was not feasible due to the rapid growth of libraries. The template would quickly become outdated. Therefore, I decided to document every step for myself. Later, I realized that publishing this documentation could be useful to others. Hence, I am writing this blog post. Eventually, I will make a video about this stack, as it is my primary medium of expression. Let's get started!
GitHub set up
To begin, create a GitHub (or any other git manager) account and create a repository. Then, clone the repository onto your development machine.
It's important to give the repository an appropriate name, as the next project will use the same name in the package.json
file.
Create Next App
Open the folder in the terminal and use
yarn create next-app --ts .
Follow along with the instructions
√ Would you like to use ESLint with this project? ... Yes
√ Would you like to use `src/` directory with this project? ... Yes
√ Would you like to use experimental `app/` directory with this project? ... No
√ What import alias would you like configured? » @
Prettier Setup (Optional)(Recommended)
This step is going to make your life so much easier while coding
Create a .prettierignore
file in the root directory
# Add files here to ignore them from prettier formatting
/dist
/coverage
Create a .prettierrc
file in the root directory
#Add prettier rules here
{
"singleQuote": true
}
Making it PWA
Installing next-pwa
Installing next-pwa
package will first and foremost task because it handles everything else.
In the root folder
yarn add next-pwa
Creating manifest.json
There can be 2 ways that you can follow
Using simicart (recommended)
Go to simicart and fill out the details.
Upload your app logo (that needs to be at least 512x512 px2)
Make sure the Display is
standalone
Click on generate
It will generate a .zip
file for you. After unzipping the file you will get
icon-192x192.png
icon-256x256.png
icon-384x384.png
icon-512x512.png
manifest.webmanifest
Copy all of the files into public
folder then rename the manifest.webmanifest
file to manifest.json
Using my template (what I do)
Create a
manifest.json
file in thepublic
folderA
.png
(which is 1080 x 1080 px2) and.svg
copy of the logo to the folder I use this.png
file for everything, as a favicon as well as in the manifest.
My friend Vasanth and I were discussing PWAs when he mentioned that modern websites are faster than before. With caching, it only takes milliseconds to load a logo. Therefore, we can use PWAs without worrying.
paste this code on
manifest.json
{ "theme_color": "#fff", "background_color": "#fff", "display": "standalone", "scope": "/", "start_url": "/", "name": "App Name | App Tagline", "short_name": "App Name", "description": "App Description", "orientation": "portrait", "display_mode": "fullscreen", "icons": [ { "src": "/icon.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon.png", "sizes": "256x256", "type": "image/png" }, { "src": "/icon.png", "sizes": "384x384", "type": "image/png" }, { "src": "/icon.png", "sizes": "512x512", "type": "image/png" } ], "theme_color_in_manifest": true, "background_color_in_manifest": true, "display_in_manifest": true, "scope_in_manifest": true, "start_url_in_manifest": true, "name_in_manifest": true, "short_name_in_manifest": true, "description_in_manifest": true, "orientation_in_manifest": true, "display_mode_in_manifest": true, "icons_in_manifest": true }
Making Changes in _document.ts
Go to
_document.ts
file underpages
directoryMake changes in the
<Head>
tag I have changed self-closing<Head/>
tag into a normal tag to include manifest dataimport Document, { Html, Head, Main, NextScript } from "next/document"; class MyDocument extends Document { render() { return ( <Html> <Head> <link rel="icon" href="/icon.png" /> <link rel="manifest" href="/manifest.json" /> <link rel="apple-touch-icon" href="/icon.png"></link> <meta name="theme-color" content="#fff" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } } export default MyDocument;
Putting auto-generated files into .gitignore
Though this will be effective after the next step, still I’m putting it before the next step because there are multiple times I have forgotten to do this
Go to you
.gitignore
file and add the code# PWA files **/public/sw.js **/public/workbox-*.js **/public/worker-*.js **/public/sw.js.map **/public/workbox-*.js.map **/public/worker-*.js.map
Later on, we will see where it is coming from
Configuring PWA in Next Config
Go to
next.config.js
file in the root directory and add this code/** @type {import('next').NextConfig} */ const runtimeCaching = require('next-pwa/cache'); const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, runtimeCaching, buildExcludes: [/middleware-manifest.json$/], }); const nextConfig = { reactStrictMode: true, }; module.exports = withPWA(nextConfig);
Run your code, you’ll be able to see that your PWA is running
Now you’ll be able to see the file that we put into
.gitignore
in the previous step is generated in the public folder.But that’s not it. For the development environment, you should disable PWA as it generates too much console.log and you can easily get lost in it while resolving other code issues.
So go to
next.config.js
and add this line in your codedisable: process.env.NODE_ENV === 'development'
/** @type {import('next').NextConfig} */ const runtimeCaching = require('next-pwa/cache'); const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, runtimeCaching, buildExcludes: [/middleware-manifest.json$/], disable: process.env.NODE_ENV === 'development', }); const nextConfig = { reactStrictMode: true, }; module.exports = withPWA(nextConfig);
Now if you re-run your code, you’ll be able to see it in the console
PWA disabled
But still, you can run your app in PWA mode as you’ve installed the “shell” already.
Just click on this icon and choose your installed shell
Our PWA step is done
Adding Mantine
Basic Mantine setup is easy, though I’ll suggest you go to the Mantine docs and follow as it may change in the future as Mantine is a rapidly growing component library with frequent updates and upgrades now and then.
Install Mantine dependencies
yarn add @mantine/core @mantine/hooks @mantine/next @emotion/server @emotion/react
2 extra dependencies that you need to add
yarn add cookies-next @tabler/icons-react
Update
_document.tsx
fileimport { createGetInitialProps } from '@mantine/next'; import Document, { Head, Html, Main, NextScript } from 'next/document'; const getInitialProps = createGetInitialProps(); export default class _Document extends Document { static getInitialProps = getInitialProps; render() { return ( <Html> <Head> <link rel="icon" href="/icon.png" /> <link rel="manifest" href="/manifest.json" /> <link rel="apple-touch-icon" href="/icon.png"></link> <meta name="theme-color" content="#fff" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } }
Update
_app.tsx
import { AppProps } from 'next/app'; import Head from 'next/head'; import { MantineProvider } from '@mantine/core'; export default function App(props: AppProps) { const { Component, pageProps } = props; return ( <> <Head> <meta charSet="utf-8" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> </Head> <MantineProvider withGlobalStyles withNormalizeCSS theme={{ /** Put your mantine theme override here */ colorScheme: 'light', }} > <Component {...pageProps} /> </MantineProvider> </> ); }
Adding Logo, Layout and Theme Toggle (Bonus)
Logo as a re-usable component
It is really helpful to create your logo as a component, as it will be used throughout your app in multiple places.
Create a
Logo.tsx
file in yourcomponents
folderimport Image from 'next/image'; import React from 'react'; type Props = { size?: number; }; function Logo({ size }: Props) { return <Image src="/icon.svg" alt="App Name Logo" width={size} height={size} />; } export default Logo;
Remember earlier in this tutorial I added an
.svg
variant of my logo into a public folder and now I’m using it in this component asnext/image
As I’m using a square (or any 1:1) logo, I can set my
width
andheight
same. But depending upon your logo it can vary. Though you can usewidth
andheight
standalone as long as your.svg
logo hasviewBox
in it. It will maintain the ratio.
Layout
In this demonstration, I will showcase the layout using the AppShell
component of Mantine as it is the most effective one.
To achieve maximum flexibility in layout, I always adhere to a specific folder structure where the content of each section of the AppShell
is placed in a separate folder. Although this may be overly complex for some applications, I find it useful in reducing the amount of code in a single file, thereby improving the DX. To implement this, I create a Layout
directory in the component folder.
Layout
|--AsideBar
| |--index.tsx
|--FooterNav
| |--index.tsx
|--HeaderNav
| |--index.tsx
|--Navigation
| |--index.tsx
| |--ToggleTheme.tsx
|--index.tsx
Depending on the layout of the AppShell
, the ToggleTheme
may be placed in different folders. Here, I am showing the alt
layout. However, it can be created separately to be placed anywhere in the app, like on Facebook or Twitter, where changing the theme requires more effort. However, that is not our goal here. The code is as follows:
ToggleTheme.tsx
import React from 'react';
import { ActionIcon, useMantineColorScheme } from '@mantine/core';
import { IconSun, IconMoonStars } from '@tabler/icons-react';
type Props = {};
function ToggleTheme({}: Props) {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
return (
<ActionIcon
radius="md"
variant="outline"
color={dark ? 'yellow' : 'blue'}
onClick={() => toggleColorScheme()}
title="Toggle color scheme"
>
{dark ? <IconSun size={18} /> : <IconMoonStars size={18} />}
</ActionIcon>
);
}
export default ToggleTheme;
index.tsx
in AsideBar, FooterNav, HeaderNav, Navigation
These four codes are primarily the same; the only changes needed are the name of the export component and the content in the file.
import React from 'react';
type Props = {};
function AsideBar({}: Props) {
return <>This is a side AsideBar</>;
}
export default AsideBar;
index.tsx
in Layout
import {
AppShell,
Aside,
Box,
Burger,
Container,
Divider,
Footer,
Group,
Header,
MediaQuery,
Navbar,
ScrollArea,
Space,
Text,
Title,
} from '@mantine/core';
import React, { useState } from 'react';
import Logo from '../Logo';
import AsideBar from './AsideBar';
import FooterNav from './FooterNav';
import HeaderNav from './HeaderNav';
import Navigation from './Navigation';
import ToggleTheme from './Navigation/ToggleTheme';
type Props = {
children: React.ReactNode;
};
function Layout({ children }: Props) {
const [opened, setOpened] = useState(false);
return (
<AppShell
layout="alt"
navbarOffsetBreakpoint="sm"
asideOffsetBreakpoint="sm"
navbar={
<Navbar
hiddenBreakpoint="sm"
hidden={!opened}
width={{ sm: 200, lg: 320 }}
>
<Navbar.Section>
<Container
p="md"
sx={(theme) => ({
borderBottom: `1px solid ${
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2]
}`,
height: '66px',
})}
>
<Group position="apart">
<Group spacing="xs">
<Logo size={32} />
<Group spacing={0}>
<Title order={3} fz={24} fw={700}>
finstahq
</Title>
</Group>
</Group>
<Group>
<ToggleTheme />
<MediaQuery largerThan="sm" styles={{ display: 'none' }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
mr="xl"
/>
</MediaQuery>
</Group>
</Group>
</Container>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea} px="md">
<Navigation />
</Navbar.Section>
<Navbar.Section>
<Container
p="md"
sx={(theme) => ({
borderTop: `1px solid ${
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2]
}`,
height: '66px',
})}
></Container>
</Navbar.Section>
</Navbar>
}
aside={
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
<Aside hiddenBreakpoint="sm" width={{ sm: 200, lg: 320 }}>
<Aside.Section>
<Container
p="md"
sx={(theme) => ({
borderBottom: `1px solid ${
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2]
}`,
height: '66px',
})}
></Container>
</Aside.Section>
<Aside.Section grow component={ScrollArea} p="md">
<AsideBar />
</Aside.Section>
<Aside.Section>
<Container
p="md"
sx={(theme) => ({
borderTop: `1px solid ${
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2]
}`,
height: '66px',
})}
></Container>
</Aside.Section>
</Aside>
</MediaQuery>
}
header={
<Header height={66}>
<MediaQuery largerThan="sm" styles={{ display: 'none' }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
mr="xl"
/>
</MediaQuery>
<Container p="md">
<HeaderNav />
</Container>
</Header>
}
footer={
<Footer height={66}>
<FooterNav />
</Footer>
}
>
{children}
</AppShell>
);
}
export default Layout;
Updating _app.tsx
import { useState } from 'react';
import { AppProps } from 'next/app';
import { GetServerSidePropsContext } from 'next';
import {
ColorSchemeProvider,
ColorScheme,
MantineProvider,
} from '@mantine/core';
import { getCookie, setCookie } from 'cookies-next';
import '../styles/globals.css';
import Head from 'next/head';
export default function App(props: AppProps & { colorScheme: ColorScheme }) {
const { Component, pageProps } = props;
const [colorScheme, setColorScheme] = useState<ColorScheme>(
props.colorScheme
);
const toggleColorScheme = (value?: ColorScheme) => {
const nextColorScheme =
value || (colorScheme === 'dark' ? 'light' : 'dark');
setColorScheme(nextColorScheme);
// when color scheme is updated save it to cookie
setCookie('mantine-color-scheme', nextColorScheme, {
maxAge: 60 * 60 * 24 * 30,
});
};
return (
<>
<Head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
/** Put your mantine theme override here */
colorScheme,
fontFamily: 'IBM Plex Mono, mono space',
headings: { fontFamily: 'IBM Plex Sans, sans-serif' },
}}
>
<Component {...pageProps} />
</MantineProvider>
</ColorSchemeProvider>
</>
);
}
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
// get color scheme from cookie
colorScheme: getCookie('mantine-color-scheme', ctx) || 'light',
});
I’m using cookies to save the color theme preference of the user here.
As well as using a custom font for the Heading and Body. To use a custom font just add the font link in <Head>
of the _document.tsx
<link
rel="preconnect"
href="<https://api.fonts.coollabs.io>"
// crossorigin
/>
<link
href="<https://api.fonts.coollabs.io/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap>"
rel="stylesheet"
/>
Coollabs fonts are a great, privacy-friendly Google Fonts CDN alternative. Check it out.
Now use your Layout anywhere
import Layout from '@/components/Layout';
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
</Head>
<Layout>
<h1>Hello World</h1>
</Layout>
</>
);
}
Conclusion
It's great to see how easy it is to create a custom layout in Next.js using Mantine and Coollabs fonts. By using cookies to save the user's color theme preference, we can provide a personalized experience. Plus, Coollabs fonts are a privacy-friendly alternative to Google Fonts. Overall, this setup allows for a clean and professional-looking website with a touch of customization.
Indeed! It's important to remember that a good layout is not just about aesthetics but also about usability and accessibility. By using Mantine, we can ensure that the website will be accessible to all users, regardless of their abilities or the device they're using. Additionally, using custom fonts can enhance the readability of the website and make the content more engaging.
Overall, creating a custom layout with Mantine and Coollabs fonts is a great way to add a personal touch to your Next.js website while also ensuring accessibility and usability.
So that’s it. Bye ✌🏼
Subscribe to my newsletter
Read articles from Mehul Kundu ✦ directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mehul Kundu ✦
Mehul Kundu ✦
✦ Product Designer foyer.work Maker of Merlin - merlin.foyer.work ✦ Core team fueler.io ✦ Building bohon.co