Building this site #7: How I add new pages

Though this site is constantly changing according to my personal whims, by now I have a well-established process for adding new pages. Following WordPress custom, a page is anything that isn't a (dated) post – although each post is also its own page. Structurally, they are very similar, making use of the same core components.

For example, every page in this site is now wrapped in a PageWrapper component, whose purpose to supply the page with elements common to all pages: meta and title tags, the top navigation bar, and the footer. I've also ended up including here our ThemeProvider, which previously wrapped the entire _app.js page. This is to allow all the Head elements to be rendered statically (since ThemeProvider and its children are rendered dynamically); otherwise the meta tags won't work.

// components/PageWrapper.js

import Head from "next/head"
import dynamic from "next/dynamic"
import { Container } from "react-bootstrap"
import NavBar from "./NavBar"
import Footer from "./Footer"

const ThemeProvider = dynamic(() => {
  return import("./ThemeProvider")
}, { ssr: false })

export default function PageWrapper({ 
  title, 
  absoluteTitle, 
  description, 
  children 
}) {
  return (<>
    <Head>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta property="og:url" content="https://cerdenia.com" />
      <meta property="og:title" content={title} />
      <meta property="og:type" content="personal website" />
      <meta property="og:description" content={description || "By Joshua Cerdenia"} />
      <meta property="og:image" content="https://cerdenia.com/images/profile-moshed.jpeg" />
      <title>{title}{!absoluteTitle ? " – Joshua Cerdenia" : null}</title>
    </Head>

    <ThemeProvider>
      <NavBar />
      <Container className="page-container">
        {children}
      </Container>
      <Footer />
    </ThemeProvider>
  </>)
}

The second component common to most pages, including all posts, is called MainText, which simply consists of a title and other text-based elements, which could be a combination of tags, a date, a description, and the content itself. Here's what it looks like:

// components/MainText.js

import styles from "../styles/MainText.module.css"
import Link from "next/link"
import TagGroup from "./TagGroup"
import moment from "moment"

export default function MainText({ data }) {
  const { title, description, date, tags, content } = data

  return (<>
    {tags && <TagGroup tags={tags} />}
    <h1 className={styles.title}>{title}</h1>
    <p className="text-muted">{description}</p>

    {content && <section 
      className={styles.content} 
      dangerouslySetInnerHTML={{ __html: content }}
    />}

    {date && <p className={`${styles.footer} small text-muted`}>
      Posted on {formatDate(date, false)}.  
      <Link href="/rss.xml">Subscribe by RSS</Link> or, in place of comments, 
      <Link href="mailto:joshua@cerdenia.com">contact me.</Link>
    </p>}
  </>)
}

function formatDate(rawDate, showTime) {
  let format = "dddd, D MMMM YYYY"
  if (showTime && rawDate.includes(":")) format += ", h:mm A"
  return moment(rawDate).format(format)
}

Together, these two components form the backbone of all this site's pages. They are also all that's needed to create a typical page. For example, adding, say, an About page is as simple as creating a new file pages/about.js:

// pages/about.js

import { PageWrapper, MainText } from "../components"
import { getPage } from "../lib/content"

export default function About({ page }) {
  return (
    <PageWrapper title="About">
      <MainText data={page} />
    </PageWrapper>
  )
}

export async function getStaticProps() {
  return {
    props: { 
      page: getPage("about") 
    }
  }
}

The function getStaticProps, naturally, handles fetching the needed data for each page at build time. Just like posts, I prefer writing all my page content in markdown – so I have a content folder full of markdown files, divided between pages and posts that effectively functions as my site's database in place of a real one. I use a helper function called getPage to retrieve and parse data from markdown according to a given page name. It looks something like this:

// lib/content/content.js

const OPTIONS = { encoding: "utf-8" }
const PAGES_DIR = `${process.cwd()}/content/pages`
// Markdown parser:
const md = new MarkdownIt({ html: true })

...

// Get page content parsed from markdown.
export function getPage(name) {
  const rawContent = fs.readFileSync(`${PAGES_DIR}/${name}.md`, OPTIONS)
  const { data, content } = matter(rawContent)
  return { ...data, content: md.render(content) }
}

So it is not dissimilar at all to how I would add new posts, except each page gets its own route by virtue of being included in the pages folder, as opposed to posts, whose routes are generated dynamically. Speaking of, this is how my BlogPostPage component at pages/[slug].js now looks like, following the same pattern as the About page. But in addition to PageWrapper and MainText, it also renders a PostList and BottomBlogNav.

// pages/[slug].js

import { PageWrapper, MainText, PostList, BottomBlogNav } from "../components"
import { getPosts } from "../lib/content"

export default function BlogPostPage({ post, backlinks, prev, next }) {
  return (
    <PageWrapper title={post.title} description={post.description}>
      <MainText data={post} />
      {backlinks.length > 0 && <div className="mt-5 mb-3">
        <PostList heading="Backlinks" posts={backlinks} />
      </div>}
      <BottomBlogNav {...{ next, prev }} />
    </PageWrapper>
  )
}

...

One last thing to consider is whether a new page should also show up as a navigation menu item. I keep all my nav items as an array of objects in data/navItems.js for easy reference. Each object simply contains the item's label (or display name), link, and whether or not it is a primary navigation item. Organizing them this way makes them easy to use in multiple places: in my case, both the NavBar and Footer contain nav items.

// data/navItems.js

export default [
  ...
  {
    label: "About",
    link: "/about",
    isPrimary: true
  },
  ...
]

And since about.js is an existing file under the pages folder, navigating to /about seamlessly opens that page thanks to Next.js's built-in routing system.