Real Estate Listing with Next.js: An Unexpected Journey
Next.js is a React.js based framework that implements the Server-Side Rendering (SSR) technique. At the moment the latest version released is 9.5 with a lot of new features. Zero configurations, TypeScript support, automatic compilation and bundling, code splitting, high performance and, above all, what our product team likes most, the ability to better manage SEO.
For all of these reasons we have chosen it as the frontend technology for our real estate listing.
At the same time, a backend for frontend (Elixir + GraphQL) was also developed, in order to provide the listings data to our web application. But that’s another story…
This is not going to be the usual article on “how to setup an application with Next.js”, the documentation on the official website is enough. I would like to focus on aspects that, although might look trivial now, took me a lot of time in order to master.
1. SWR
Let’s start with the choice of the main library, SWR
that is a React Hooks library for remote data fetching also developed by Vercel
.
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
fetcher
is any function that returns a Promise
.
From the documentation it is not immediately clear that key
is the first value passed to the fetcher
method.
const { data } = useSWR('/api/listings', fetcher)const fetcher = url => fetch(url).then(r => r.json()) // request to /api/listings
As long as the key
remains the same, data is first returned from the cache
and then a new request is made to revalidate it.
2. getStaticProps
Our real estate listing is essentially static. It can be changed, but not so frequently to require a status update for every single user’s request.
All pages, both the detail view ones and listing ones, have been built up using getStaticProps together with getStaticPaths. Next.js will statically pre-render all the paths specified by getStaticPaths at build time.
This function has been really useful for us to manage the internationalization (i.e. /case-in-vendita/milano
for italian and /houses-for-sale/milan
for english), as well as to generate the property details pages /houses-for-sale/milan/listing-id-123
.
/// pages/[listing]/[city]/slug.tsxexport const getStaticPaths: GetStaticPaths = async () => {
const requestPaths = await fetch('/api/paths')
/**
requestPaths = {
listing: 'houses-for-sale',
city: 'milan',
slug: 'listing-id-123'
}
**/
return {
paths: [
{
params: requestPaths
}
],
fallback: true, // true or false
}
}
The params paths
structure must resemble the directory structure under pages
folder
pages/
[listing]/
index.tsx
[city]/
index.tsx
slug.tsx
The fallback
parameter set to true
is used to manage the new real estate listings. In this case, Next.js, instead of sending a 404 HTTP status code
, try to run the getStaticProps function to generate a new page.
Usually, Nextjs will pre-render all pages at build time using the props returned by getStaticProps. However, we need to frequently update the listing pages. Next.js provided a solution for this matter, introducing the data revalidation every x seconds (with x = a fixed value set by devs).
At the first user’s request, getStaticProps rerun in order to render updated page.
export const getStaticProps: GetStaticProps = async (ctx) => {
const { data } = await request<Props>(endpoint, query, filters) return {
props: {
listings: data,
},
revalidate: 60, // seconds
}
}
3. GraphQL
I have always used Apollo Client
for my React applications, but I found a lot of confusion about how to best leverage it in an application that uses SSR.
After some research, also forced by the SWR library which only accepts a Promise
returning function, I opted for graphql-request
library.
Obviously, state and cache management parts are not provided, but that’s why I chose SWR first.
A small example:
import { request } from 'graphql-request'export const getStaticProps: GetStaticProps = async (ctx) => {
const endpoint = process.env.GRAPHQL_ENDPOINT const queryCities = `
query Cities($filter: FilterByCountry!) {
cities(filter: $filter) {
description {
language
text
}
value
}
}
` const filter = {
byCountry: 'ITALY',
} const { cities } = await request<Props>(endpoint, queryCities, { filter }) return {
props: {
cities,
},
}
}
4. Emotion & Global Theme
Emotion is a library designed for writing css styles with JavaScript.
On the configuration side, it is just a matter of inserting a few lines in the .babelrc.js
file to have it compiled using its preset
/// .babelrc.jsmodule.exports = {
presets: [
"next/babel",
[
"@emotion/babel-preset-css-prop",
{
sourceMap: true,
labelFormat: "[dirname]--[filename]--[local]",
},
],
],
plugins: [],
}
Using TypeScript
you also have to globally define the references, I personally used next-env.d.ts
file which is generated by Next.js.
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="@emotion/core" />
/// <reference types="@emotion/styled" />
The easiest part is done!
To use the global theme feature you must first overwrite the native Next.js App component
, which initializes all the pages, creating pages/_app.tsx
file
/// pages/_app.tsximport Head from 'next/head'
import { AppProps } from 'next/app'
import { ThemeProvider } from 'emotion-theming'
import { theme } from '../config/theme.ts'import '../reset.css'
import '../variables.css'const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
return (
<>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover, maximum-scale=1, user-scalable=0"
/>
</Head>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</>
)
}export default MyApp
To use your configurations, you have to extend the default theme provided by Emotion library.
/// config/theme.tsimport styled, { CreateStyled } from '@emotion/styled'type Theme = {
colors: {
blue: string
}
}export const theme: Theme = {
colors: {
blue: '#1f82c0',
},
}export default styled as CreateStyled<Theme>
Using the default export, it’s possibile to create styled-component
with your global theme props already built-in and TypeScript safe.
For example:
/// components/some-styles.tsimport styled from from '../config/theme.ts'export const Container = styled.div(({ theme }) => ({
color: theme.colors.blue,
width: 24,
...
}))
5. Env Variables
Inside the next.config.js
file it is possible to define, in addition to a whole series of Next.js configurations, the accessibility of the environment variables.
/// next.config.jsmodule.exports = {
serverRuntimeConfig: {
// Will only be available on the server side: true,
graphqlEndpoint: process.env.GRAPHQL_ENDPOINT,
},
publicRuntimeConfig: {
// Will be available on both server and client
googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
},
}
This is really useful to recover your configurations on client side at runtime, when the Node.js
process does not exist.
/// components/MyComponent/index.tsximport getConfig from 'next/config'export const MyComponent = () => {
const { publicRuntimeConfig } = getConfig() const { googleMapsApiKey } = publicRuntimeConfig return (
<div>{googleMapsApiKey}</div>
)
}
6. SEO
Static page generation not only brings benefits to performance, but also SEO side. It is possible to generate all specific meta tags
at build time and also more complex data structures such as JSON-LD
. Structured data is a standardized format for providing information about a page and classifying the page content.
This is an example of a Header that we have generated for the single real estate ads:
/// headers/listing-detail.tsximport Head from 'next/head'
import getConfig from 'next/config'export const HeaderListingDetail = ({ listing, language }) => {
const { publicRuntimeConfig } = getConfig() const metaTitle = `create your own page title ${listing.title}`
const metaDescription = `create your own page description ${listing.description}` return (
<Head>
<title>{metaTitle}</title>
<meta name="description" content={metaDescription} />
<meta property="og:locale" content={language} />
<meta property="og:type" content="article" />
<meta property="og:title" content={metaTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:image" content={listing.photos[0].url} />
<meta
property="og:url"
content={`${publicRuntimeConfig.websiteMainUrl}/path/to/page/`}
/>
<meta property="og:site_name" content="Your Website" />
<link
rel="alternate"
hrefLang="en"
href={`${publicRuntimeConfig.websiteMainUrl}/path/to/english/page/`}
/>
<link
rel="alternate"
hrefLang="other language"
href={`${publicRuntimeConfig.websiteMainUrl}/path/to/other/language/page/`}
/>
<link
rel="canonical"
href={`${publicRuntimeConfig.websiteMainUrl}/path/to/page/`}
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:title" content={metaTitle} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: `
{
"@type":[
"OfferForPurchase",
"${listing.residentialType}", // House, Apartment, SingleFamilyResidence
"RealEstateListing"
],
"@context":"http://schema.org",
"price": ${listing.sellingPrice},
"priceCurrency": "EUR",
"availability": "http://schema.org/InStock",
"name": "${listing.propertyName}",
"description": "${listing.propertyDescription}",
"image": "${listing.photos[0].url}",
"floorSize":{
"@type": "QuantitativeValue",
"@context": "http://schema.org",
"value": ${listing.commercialArea},
"unitCode": "MTK"
},
"numberOfRooms": ${listing.rooms},
"numberOfBathroomsTotal": ${listing.bathrooms},
"address":{
"@type": "PostalAddress",
"@context":"http://schema.org",
"streetAddress": "${listing.streetAddress}",
"addressLocality": "${listing.city}",
"addressRegion": "${listing.province}",
"postalCode": "${listing.zipCode}"
},
"geo":{
"@type": "GeoCoordinates",
"@context":"http://schema.org",
"latitude": ${listing.coordinates.latitude},
"longitude": ${listing.coordinates.longitude}
},
"url": "${publicRuntimeConfig.websiteMainUrl}/path/to/page/"
}
`,
}}
/>
</Head>
)
}
I hope I have given you some advice or ideas for your next application Next.js. In conclusion, I love Next.js and I encourage you to try it out!