How I ported my Latin diction­ary to Next.js

velut is my website serving Latin vocabulary. I develop it in my free time, as an elaborate pet project.

The user interface is defined in React and the database is MongoDB.

For the first couple of years, it used the MERN technology stack, with MongoDB for the database, Express for the server, Create React App for the front-end, and Node for the back-end language tying everything together. It also used React Router for client-side routing, Axios for easier data-fetching, and Mongoose for easier querying of the MongoDB database.

Nowadays, velut continues to access my MongoDB database, but it uses Next.js, which is a framework that combines React and Express in Node with its own routing mechanism; crucially, it also allows for server-side rendering.

In this article I explain how I converted velut from its initial architecture to Next.js.

As of writing this, velut uses Next.js 12.3.1; newer versions may be different to what’s explained below.

Initial­isation

Because I had never ported a site to Next.js before, I had a couple of false starts. Vercel (the company behind Next.js) has several example sites, including one with a Mongoose connection. Eventually I realised that it would be easier to clone the Vercel template and copy the important bits of my velut code into it, rather than try to change the structure of the existing velut codebase to a Next.js architecture unfamiliar to me.

Folder structure

The child folders of the root directory are:

Pages & components

In Create React App, there’s not much distinction between a page and a component; everything is a React component that may or may not be rendered. Front-end routes (ie, URLs the user will see in their browser address-bar) are determined by the code you write using React Router, a dependency that is not bundled with Create React App. (Alternative routing libraries exist.)

Next.js, however, has a specific pages folder, and its contents matches the routing structure. A file called index.jsx inside a many folder inside pages will be rendered at www.velut.co.uk/many/, for instance.

Wildcards work too. A file called [word].jsx inside an english folder inside pages will be rendered at www.velut.co.uk/english/whatever and the string "whatever" will be available inside [word].jsx as the value of the URL parameter word.

Because (nearly) anything within pages will be a URL route, React components that are used within a page, or on multiple pages, must be in a separate directory. CSS, likewise, must be in a separate directory.

The way I’ve done it has a components folder and a css folder as siblings to pages. The components folder contains the JSX for components that are used on multiple pages (such as the footer), as well as CSS particular to those components. The css folder contains the CSS for pages in the pages directory, as well as a globals.css file that provides styles for the entire site.

This is not the most intuitive folder structure, but it works well enough.

CSS scoping

CSS in Next.js is more awkward than I was expecting.

In Create React App, any import 'blah.css' statement makes the CSS apply across the app, rather than being scoped to a component. This is very convenient.

The downside is that styles that you had intended for one component can leak outside the component, if the selectors match. What’s worse, because of lazy loading, a page could look different on different visits, depending on whether you had previously visited a page that contained a conflicting CSS rule! There were a couple of instances of this in the MERN version of velut.

Fortunately, this is a very easy problem to fix: just make sure none your selectors match anything outside the intended component. A simple approach is to give all your components IDs or class-names, and prefix your selectors with those. For example, #my-widget h2 { background: teal; } #my-widget p { font-style: italic; } et cetera.

Of course, if you know you’re not using a class-name outside of a component, you can just use it. I had the code below in the MERN version of velut: I know that the class-names of .title, .title-author, and .title-full are exclusive to the <Header> component. Likewise I also had a CSS selector for h1 because the only <h1> tag in the site is inside <Header>.

import './Header.css'

let Header = (props) => {
	return (
		<header>
			<h1>
<span className="title-author"> Duncan Ritchie’s </span>
				<br />
				<abbr className="title"> velut </abbr>
			</h1>
			<p className="title-full"></p>
		</header>
	)
}

export default Header

Next.js tries to shield us from CSS leakages by forcing to adopt one of several strategies for scoping our styles. I might not have picked the least awkward strategy, but the way I’m doing it now is CSS Modules. This transformed the above code into the following code.

import styles from './Header.module.css'

let Header = (props) => {
	return (
		<header className={styles.header}>
			<h1>
<span className={styles.titleAuthor}> Duncan Ritchie’s </span>
				<br />
				<abbr className={styles.title}> velut </abbr>
			</h1>
			<p className={styles.titleFull}></p>
		</header>
	)
}

export default Header

As you can see, the CSS is imported as an object that has a property corresponding to each class-name (or ID) used in a selector. The value of the property is a string such as "Header_title__MSVhb", which is what ultimately gets used as the class-name.

In my source code, the CSS selectors became as follows. Note the camel-case because writing styles.titleAuthor is easier than styles['title-author'].

.header h1 { … }
.header .titleAuthor { … }
.header .title { … }
.header .title::after { … }
.header p.titleFull { … }

The CSS the end-user receives is

.Header_header__e7muk h1 { … }
.Header_header__e7muk .Header_titleAuthor__Z2GeS { … }
.Header_header__e7muk .Header_title__MSVhb { … }
.Header_header__e7muk .Header_title__MSVhb:after { … }
.Header_header__e7muk p.Header_titleFull__dS314 { … }

The idea of the output CSS classes being different to the source feels a little weird to me, but not too bad. It gets a bit more convoluted when a component imports from more than one CSS file, and uses a class from each in the same HTML element!

import searchStyles from '../search/Search.module.css'
import advancedStyles from './AdvancedSearch.module.css'


<form className={advancedStyles.advancedSearch + ' ' + searchStyles.search}>

</form>

I should probably refactor this. It would be an opportunity to try a different CSS strategy, such as Styled Components.

Server-side rendering

Here’s what happens when a user visits a client-side–rendered website. They request a page, then receive a practically empty HTML file, along with a load of JavaScript. The browser executes the JavaScript, and that fills in the page with whatever the user is supposed to see.

With server-side rendering, this is inverted. It is the server that generates the full HTML for the page, so the first thing that the user sees for the page is the complete page. Next.js then performs a step called “hydration”, which attaches React event-handlers to the DOM nodes (the elements on the page), which re-renders elements and makes the site properly interactive with JavaScript.

Exceptions can be thrown when the rendered output on the server-side doesn’t match the initial render on the client-side — Next.js cannot hydrate the page. My “Subwords” page has a string of letters as an example of an input. Because this is generated at random on each page-visit, it needs to be generated in getServerSideProps (which runs on the back-end only), not anywhere like a render method (which runs on both back-end and front-end), otherwise the front-end will produce a value that does not match what the back-end produced.

Form submission with HTML forms

When velut was client-side–rendered, I used React to handle the search forms that let the user look up words. For the Next.js version, I really wanted to support browsers that don’t support JavaScript. (The number of users actually visiting velut without JavaScript is probably zero or minuscule, but the principle is important to me.)

So I re-wrote the forms to use HTML form submission. I still let JavaScript take over if it’s available.

Here’s a simplified snippet. Without JavaScript, the Search form makes a get request to a route that redirects the user to the correct page. With JavaScript, the handleSubmit function performs that redirection.

<form
	action="/redirectonsearch"
	method="get"
	onSubmit={this.handleSubmit}
	role="search"
>
	<input
		name="word"
		value={this.state.word || ''}
		onChange={this.handleInput}
		title={this.props.searchbarTitle || 'Type something here'}
		enterKeyHint="search"
	/>

	<button type="submit">Search!</button>
</form>

You might have noticed the Search form doesn’t have a <label> element. I think the context makes it obvious what the purpose of the <input> element is, even for people using screen-readers. But it wouldn’t be hard for me to add a label and make it visually hidden. Similarly, there’s a <select> menu (which I omitted from the code-snippet above) that probably should be labelled too. So I’ll do that.

SSR & CSR on /many

velut has a page for looking several Latin words up at once. If the page is rendered on the server, you have to wait for all your words to come back before you see any results. With client-side rendering, the view updates as the results come in, with a neat little progress-bar (incidentally my first use of the <progress> HTML element).

Here’s how I implemented the SSR/CSR distinction. The page now has two separate (but practically identical) components, called ManyCSR and ManySSR. The one you see depends on whether the URL contains ssr=true in the query-string. This is set when you submit your search, by means of a hidden <input> control. This hidden control is wrapped in <noscript> so it only applies when client-side JavaScript is unavailable (meaning SSR is needed).

<noscript>
	<input hidden name="ssr" value="true" onChange="void()" />
</noscript>

Head component

I set a default <head> for the entire site, by making a component using Next.js’s Head component and importing it in _app.js. Any property in the <head> can be overridden on any page by declaring another Head component in the JSX.

function DefaultHead() {
	return (
		<Head>
			<meta charSet="utf-8" />
			<meta name="viewport" content="initial-scale=1.0, width=device-width" />
			<link
				rel="shortcut icon"
				href="https://www.duncanritchie.co.uk/favicon.ico"
			/>
			<meta
				name="viewport"
				content="width=device-width,initial-scale=1,shrink-to-fit=no"
			/>
			<meta name="theme-color" content="#000000" />
			<title>velut — a Latin rhyming dictionary</title>
			<meta
				name="Description"
				content="velut — a Latin dictionary with lists of rhymes, anagrams, homographs, consonyms, subwords, inflected forms, cognates, and links to other online resources."
			/>
		</Head>
	)
}

For example, the About page (abridged):

function About(props) {
	return (
		<>
			<Head>
				<title>About velut — a Latin rhyming dictionary</title>
				<meta name="Description" content="Explanation of the purpose and functionality of velut, the Latin vocabulary website" />
			</Head>

			<!-- Page content here -->
			<h1>About</h1>
			<p>I’m a software developer who loves the Latin language…</p>
		</>
	)
}

_app.js & _document.js

Next.js allows a couple of files in the pages folder that are special because they are not pages themselves, but Next.js uses them to template the actual pages. In _app.js is where I have my default <head> component (see previous section) and a footer for all pages.

function App({ Component, pageProps }) {
	return (
		<>
			<DefaultHead />
			<Component {...pageProps} />
			<Footer />
		</>
	)
}

If you don’t have _document.js, Next.js won’t mind, but your <html> elements would be missing the lang attribute. Since the text of the velut website is mostly English (apart from the Latin words of course), I define _document.js simply like this;

class MyDocument extends Document {
	render() {
		return (
			<Html lang="en">
				<Head />
				<body>
					<Main />
					<NextScript />
				</body>
			</Html>
		)
	}
}

You can imagine specific pages being passed into App as Component, then App getting passed into MyDocument as Main. That’s what seems to happen.

Error pages

I have files in the pages folder named 404.js and 500.js, which define custom pages to show for 404 (Not found) and 500 (Internal server error). So if you navigate to www.velut.co.uk/some/path/of/gobbledegook, you get something reasonable.

If you search for a word that’s not in the dictionary, you also get a 404 status, but this is given by the Word page, not 404.js.

I also have an _error.js file, which applies if the server fails with any other error.

function ErrorPage({ type = '/' }) {
	return (
		<>
			<Head>
				<title>Error on velut — a Latin rhyming dictionary</title>
			</Head>
			<div>
				<Header textBeforeTitle="Error" />
				<Search type={type} searchbarTitle="Type a Latin word" />
				<p>
					<span>Please try searching for something else!</span>
				</p>
			</div>
		</>
	)
}

Being able to send correct status-codes like 404 and 500 is a major reason I like server-side rendering.

Deploy­ment on Fly

The company behind Next.js is Vercel, which is also the name of their hosting platform. The Vercel platform is well-suited to host Next.js sites, and is popular for that purpose.

When I deployed velut to Vercel, it all worked fine, apart from one feature of velut: Anagram Phrases. This feature generates permutations of Latin words with all the letters you specify. It’s resource-intensive, so I moved the computation work onto a separate Node.js thread. (It should really be multi-threaded, but so far I’ve only got it on one thread.)

Vercel didn’t like that, and returned 500 statuses whenever you searched for any anagram phrases. So I added the 500 error-page mentioned in the previous section, and looked for alternatives to Vercel.

Fly was what I landed on. It’s a bit weird in that you use the command-line to interact with it. But it works, even for Anagram Phrases. (“If you can build it into a Dockerfile, we can run it,” says the Fly website.)

The Toml file in the root folder is where Fly keeps its settings.

I set a GitHub workflow up to deploy whenever I push changes to GitHub.

name: Fly Deploy
on: [push]
env:
	FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
	deploy:
		name: Deploy app
		runs-on: ubuntu-latest
		steps:
			- uses: actions/checkout@v2
			- uses: superfly/flyctl-actions/setup-flyctl@master
			- run: flyctl deploy --remote-only

TypeScript

The source-code for velut website is entirely in JavaScript (and CSS). At one point I tried to switch to TypeScript, which I know Next.js supports. For some reason, it didn’t work. Maybe I was doing it wrong. I might try again sometime in the future — type-safety is so nice!

Then again, the threading on Anagram Phrases might not play nicely with TypeScript. It’s something for future investigation.

Comparison with other frame­works

I don’t know much about alternatives to Next.js, by which I mean modern web frameworks using JavaScript/TypeScript with powerful features such as server-side rendering. But several have popped up in the past couple of years. I chose Next.js for its maturity, but in 2023 options include Remix, Fresh, Qwik, Enhance, and possibly even Astro.

Further reading

Code for velut is on GitHub.

For another person’s experience porting a React website from Create React App to Next.js, I recommend this article by Kitty Giraudel.

Here’s a helpful article by Monica Powell on SSR and hydration.

The readme for this GitHub repo compares CSR to SSR, and notes that SSR makes performance worse (because the entire page gets rendered on navigation).

This article by Josh Collinsworth is skeptical of the value of React beyond sheer popularity. I still like JSX (the templating language React uses), even if writing className and htmlFor instead of the attributes class and for is clumsy.

Update: A couple of days after I published this article, Dan Abramov wrote an essay about Create React App’s future, as a lengthy comment on a proposal to make CRA less prominent in React documentation. He says CRA has probably had its day and SSR frameworks are now a better choice for many websites. Coming from a core member of the React team, no less, this was a heartening read for me.

Next.js has a lot of official documentation, including a guide to moving from Create React App.

And for the lolz, here’s a song from Dylan Beattie about rewriting code.

Conclusion

velut is a complex website and Next.js is a powerful framework. I like its file-based routing system. I really like its support for both server-side and client-side rendering. Converting velut to Next.js was tricky, and the way I handled the CSS seems particularly verbose. There are other frameworks with similar capabilities. But I’m glad I made the switch from Create React App to Next.js.

Appendices

Timeline of velut development

Show timeline
2014 Sep
I began a degree in Classics (ie, Latin and Ancient Greek), though I later dropped the Greek.
2016 Feb
I started collecting Latin vocabulary in an Excel file.
2018 Jun 28
Graduation from university with a masters in Latin.
2019 Feb
I learnt React at a web development bootcamp.
2019 Apr 18
I initialised a Create React App project for the initial commit in the velut repository.
2019 May 30
www.velut.co.uk went live, even though it didn’t yet have a working database connection.
2019 Jul 02
Entering a Latin word in the searchbar would return the word from the MongoDB database.
2019 Jul 09
Job interview with a software company, in which I demoed the website. They offered me the job and I’m still working there!
2019 Jul 17
I added a Subwords page for finding all the Latin words contained in a string of letters. I thought it would be fun. I’ve since improved the code.
2019 Aug 07
Functionality for generating multiword anagrams, up to twelve words. I thought it would be fun. I have since improved the code immensely. (Here’s that commit for Anagram Phrases. There’s a lot of nesting because I hadn’t figured out recursion.)
2020 Nov 14/15
Most of the Advanced Search functionality.
2021 May
Page for looking several Latin words up at once.
2021 Sep 19
I made my first Next.js commit in velut.
2022 May 21
Nearly three hundred commits since starting Next.js, I switched www.velut.co.uk over to the new site. (I like to commit little and often!)

Trello

I use Trello for managing my to-do lists. Below is the list of 109 cards I completed for porting velut to Next.js, in case you want a sense of what all I did in eight months.

Show list