reckoning.dev
Navigate back to the homepage
About Me
Code::Stats

Using Novela Theme with Modifications

Sadanand Singh
April 17th, 2020 · 5 min read
TL;DR
  • Using novela as a starting point to start using gatsby themes, theme-ui and styled components.
  • Several modifications: tags, math rendering via katex, Table of Contents, Image Gallery, Scroll to top, next/previous posts, title of code blocks etc.
  • gatsby cloud for fatster build and deployment.
  • Find all the modifications at my github repo

The world of web design, programming, and specially gatsbyjs has been moving at an astonishing pace. If you have been following me here, you would know, I am all for new, and cleaner looks. I firmly believe in keeping myself up to date with latest trends so that I do not start lagging behind. With this philosophy, I have been following gatsby themes for some time now. Gatsby themes along with mdx, theme-ui and styled components, make up an ultimate JAM stack to build websites and blogs.

Till now, I have been using an adhoc design based on one developed by Tania Rascia. Making any changes to it was little cumbersome, including tracking down every css code across many files. When searching for right gatsby theme for my blog, I came across a great theme: Novela! It has a lot of cool features that I just loved it: light/dark theme, awesome prism support, a very modern look, Medium features like image zoom, highlight to share etc. And the best part, It uses all the tools I have been interested in - mdx, theme-ui, and styled components!

Light Theme
Dark Theme

The novela theme still lacked a lot of features that I needed. Some of the major lacking features were lack of tags, support of rendering math properly. I have been following their github repo quite closely since last few months. Although few pull requests have been added to the repo, the development has been extremely slow. No one ever reviews the pull requests in time. Some PRs have been pending for months. After a few months of wait, I gave upon them, and finally decided to take matters on my own.

This post is about all the steps I have taken to modify this theme to personalize it. I will mainly focus on development of some of the main features. You can find all of my changes at this blog’s gihub repo.

Tags

After cloning the master branch of the Novela repo, I merged the local branch from the PR repo.

I found this PR to be quite complete. Only change I had to make was to the CSS of the TagBox. I ended up adding some additional colors in theme-ui color definitions to support these.

@narative/gatsby-theme-novela/src/sections/tags/Tags.List.tsx
1...
2const TagBox = styled.div`
3 display: flow-root;
4 height: 24px;
5 line-height: 24px;
6 position: relative;
7 margin: 0 4px 4px 0;
8 padding: 0 10px 0 12px;
9 background-color: ${(p) => p.theme.colors.tagBackground};
10 -webkit-border-bottom-right-radius: 3px;
11 border-bottom-right-radius: 3px;
12 -webkit-border-top-right-radius: 3px;
13 border-top-right-radius: 3px;
14 -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
15 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
16 color: ${(p) => p.theme.colors.tagText};
17 font-size: 12px;
18 font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, sans-serif;
19 text-decoration: none;
20 text-overflow: ellipsis;
21 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
22 overflow: hidden;
23 font-weight: bold;
24 white-space: nowrap;
25`;
26...

Rendering Math via katex

Once again, I used a pending PR and merged its local branch to mine.

One major update, I had to make was to ensure that the document variable is utilized only at run time, not at the build time. In all the files where katex-display class is modified via the global document variable, I had to add the conditional check of document not being undefined. (See the highlighted line in the codeblock.)

1//Change all KaTeX colors
2 if (typeof document !== `undefined`) {
3 Array.from(document.getElementsByClassName('katex-display')).forEach(
4 (element) => {
5 element.style.color = isDark ? 'white' : 'black';
6 },
7 );
8 }

Draft Posts

When writing new posts, many a times I like to check-in new posts to git that are still in development. However, I like them to be rendered during the development, but not in the production build. A post can be flagged as “draft” either explicitly via a frontmatter field called draft or by using a date of post in the future.

I enabled this by first enabling the draft field in the frontmatter by adding it to the @narative/gatsby-theme-novela/src/gatsby/data/data.query.js

Then, in @narative/gatsby-theme-novela/src/gatsby/node/onCreateNode.js, I added code to create new field for it (See the highlighted code below).

@narative/gatsby-theme-novela/src/gatsby/node/onCreateNode.js
1...
2const moment = require('moment-timezone');
3require('dotenv');
4...
5
6if (node.internal.type === `Mdx` && source === contentPath) {
7 const getDraftValue = ({ node, options }) => {
8 const { fieldName, timezone } = options;
9 if (!node.frontmatter) {
10 return false;
11 }
12
13 if (node.frontmatter.hasOwnProperty(fieldName)) {
14 return node.frontmatter[fieldName];
15 }
16
17 if (!node.frontmatter.date) {
18 return false;
19 }
20
21 const dateNode = moment.tz(node.frontmatter.date, timezone);
22 const dateNow = moment().tz(timezone);
23 const value = dateNow.isSameOrBefore(dateNode);
24
25 return value;
26 };
27
28 const options = {
29 fieldName: 'draft',
30 timezone: 'UTC',
31 force: process.env.NODE_ENV === 'development', // if developmet, force to be NOT draft
32 };
33
34 const fieldData = {
35 author: node.frontmatter.author,
36 date: node.frontmatter.date,
37 hero: node.frontmatter.hero,
38 secret: node.frontmatter.secret || false,
39 draft: options.force === true ? false : getDraftValue({ node, options }),
40 ...

Finally, in @narative/gatsby-theme-novela/src/gatsby/node/createPages.js file, filter articles to the ones that are not drafts before creating pages.

@narative/gatsby-theme-novela/src/gatsby/node/createPages.js
1...
2const articlesThatArentDraft = articles.filter((article) => !article.draft);
3const articlesThatArentSecret = articlesThatArentDraft.filter((article) => !article.secret);
4...

Novela theme by default has a very simple way to show one or two (depending on the scr related posts below a given post. I wanted to update its logic such that posts with most matching tags and the ones with the least difference in post times from the current post get higher weight. The second rule ensures that posts around same dates are found to be more related than only the more recent ones.

This involved only a little bit of javascript update in @narative/gatsby-theme-novela/src/gatsby/node/createPages.js.

@narative/gatsby-theme-novela/src/gatsby/node/createPages.js
1...
2let OtherArticlesThatArentSecretSorted = articlesThatArentSecret
3 .filter((art) => art !== article)
4 .sort((a, b) => {
5 const allTags = article.tags.map((t) => t.trim().toLowerCase());
6 const allATags = a.tags.map((t) => t.trim().toLowerCase());
7 const allBTags = b.tags.map((t) => t.trim().toLowerCase());
8 let intersectionA = allATags.filter((x) => allTags.includes(x));
9 let intersectionB = allBTags.filter((x) => allTags.includes(x));
10
11 let num_days_A = diff_days(
12 new Date(a.dateForSEO),
13 new Date(article.dateForSEO),
14 );
15 let num_days_B = diff_days(
16 new Date(b.dateForSEO),
17 new Date(article.dateForSEO),
18 );
19
20 return (
21 intersectionB.length - intersectionA.length || num_days_A - num_days_B
22 );
23 }
24);
25
26let next = OtherArticlesThatArentSecretSorted.slice(0, 2);
27if (next.length === 0)
28 next = articlesThatArentSecret.slice(index + 1, index + 3);
29
30if (next.length === 1 && OtherArticlesThatArentSecretSorted.length !== 2)
31 next = [
32 ...next,
33 OtherArticlesThatArentSecretSorted.filter((art) => art !== next[0])[0],
34 ];
35
36// If it's the last item in the list, there will be no articles. So grab the first 2
37if (next.length === 0) next = articlesThatArentSecret.slice(0, 2);
38// If there's 1 item in the list, grab the first article
39if (next.length === 1 && articlesThatArentSecret.length !== 2)
40 next = [...next, articlesThatArentSecret[0]];
41if (articlesThatArentSecret.length === 1) next = [];
42...

Notice that at the end, I have kept the old logic of picking last two posts as related posts as a backup.

Code Title and Optional Line Numbers

Similar to my last theme, I sometimes need to add a title to a code block, for example in some of the above code blocks, I can show the filename at the top. I also wanted to disable line numbers optionally.

Implementing this was pretty simple - update the Code component in the @narative/gatsby-theme-novela/src/components/Code/Code.Prism.jx to enable optional line numbers and title. See the highlighted lines for exact changes.

@narative/gatsby-theme-novela/src/components/Code/Code.Prism.jx
1...
2return (
3 <Highlight {...defaultProps} code={codeString} language={language}>
4 {({ className, tokens, getLineProps, getTokenProps }) => {
5 return (
6 <React.Fragment>
7 <div style={{ overflow: 'auto' }}>
8 <pre className={className} style={{ position: 'relative' }}>
9 {title && <div className="code-title">{title}</div>}
10 <Copy toCopy={codeString} />
11 {tokens.map((line, index) => {
12 const { className } = getLineProps({
13 line,
14 key: index,
15 className: shouldHighlightLine(index)
16 ? 'highlight-line'
17 : '',
18 });
19
20 return (
21 <div key={index} className={className}>
22 {!props['nolines'] && (
23 <span className="number-line">{index + 1}</span>
24 )}
25 {line.map((token, key) => {
26 const { className, children } = getTokenProps({
27 token,
28 key,
29 });
30...

Additionally, I had to update the Prism CSS in the MDx component definition.

@narative/gatsby-theme-novela/src/components/MDX/MDX.tsx
1const PrismCSS = (p) => css`
2 .prism-code {
3 overflow: auto;
4 width: 100%;
5 max-width: 944px;
6 margin: 0 auto;
7 padding: 38px 32px;
8 font-size: 13px;
9 margin: 15px auto 50px;
10 border-radius: 5px;
11 font-family: ${p.theme.fonts.monospace};
12 background: ${p.theme.colors.prism.background};
13
14 .code-title {
15 background: #404040;
16 padding: 0.5rem 1rem 0.75rem;
17 position: absolute;
18 left: 0px;
19 color: #f3f7f9;
20 font-weight: bold;
21 width: 100%;
22 margin-top: -38px;
23
24 ${mediaqueries.desktop`
25 left: 20px;
26 `};
27
28 ${mediaqueries.tablet`
29 margin-top: -40px;
30 left: 0;
31 `};
32
33 ${mediaqueries.phablet`
34 margin-top: -44px;
35 margin-bottom: 0px;
36 left: 0;
37 `};
38 }
39
40 .token-line {
41 border-left: 3px solid transparent;
42
43 ${Object.keys(p.theme.colors.prism)
44 .map((key) => {
45 return `.${toKebabCase(key)}{color:${p.theme.colors.prism[key]};}`;
46 })
47 .reduce((curr, next) => curr + next, ``)};
48
49 & > span {
50 }
51 }
52...

Table of Contents

For longer posts, its helps to add a Table of Contents at the top for easy navigation. MDX provides automatic list of headings along with its depth to help with creating Table of Contents.

In the novela theme, I had to update the MDX component and the article template to achieve this. In the MDX component, I had to pass the headings parameter to the MDXRenderer.

@narative/gatsby-theme-noeval/src/components/MDX/MDX.tsx
1<MDXProvider components={components}>
2 <CustomBlockCSS>
3 <MDXBody>
4 <MDXRenderer
5 headings={headings}
6 isDark={colorMode === 'dark'}
7 {...props}
8 >
9 {content}
10 </MDXRenderer>
11 {children}
12 </MDXBody>
13 </CustomBlockCSS>
14</MDXProvider>

Then in the article template, add additional graphql query to extract headings data from the current post. Here, I use ‘title’ of the post to filter the current post.

@narative/gatsby-theme-noeval/src/templates/article.template.tsx
1/* eslint no-undef: "off" */
2export const pageQuery = graphql`
3 query BlogPostBySlug($title: String!) {
4 posts: mdx(frontmatter: { title: { eq: $title } }) {
5 frontmatter {
6 title
7 }
8 headings {
9 depth
10 value
11 }
12 }
13 site: allSite {
14 edges {
15 node {
16 siteMetadata {
17 name
18 }
19 }
20 }
21 }
22 }
23`;

Additionally, I also had to develop a TOC component that could be called directly from the mdx files. This also required to add this component to the known list of components in MDX.

You can find my detailed implementation at my repo.

I also used a package called react-anchor-link-smooth-scroll for creating smooth scroll to all links with the post. You can find a working example at this post.

Scroll Up Button

For longer posts, it also helps to have scroll to Top button at the bottom right of the page. This is pretty starightword by using a package called react-scroll-up-button. I just had to add a small section in Layout component.

@narative/gatsby-theme-noeval/src/components/Layout/Layout.tsx
1<ScrollUpButtonContainer>
2 <ScrollUpButton
3 StopPosition={0}
4 ShowAtPosition={150}
5 EasingType="easeOutCubic"
6 AnimationDuration={500}
7 ContainerClassName="ScrollUpButton__Container"
8 style={{ width: 32, height: 32, outline: 'none' }}
9 ToggledStyle={{ outline: 'none' }}
10 ></ScrollUpButton>
11</ScrollUpButtonContainer>
12
13const ScrollUpButtonContainer = styled.div`
14 .ScrollUpButton__Container {
15 @media (max-width: 767px) {
16 display: none;
17 }
18 }
19`;

Notice use of some extra CSS to disable scroll up button on small screens.

I like to have links to the next and the previous posts on every regular posts. Creating this was as simple as adding context for these links in createPages.js gatsby node scripts.

@narative/gatsby-theme-novela/src/gatsby/node/createPages.js
1// next article
2 let nextPage;
3 if (index === 0) {
4 nextPage = null;
5 } else {
6 idx = index - 1;
7 temp = articlesThatArentDraft[idx];
8 while (temp.secret) {
9 idx = idx - 1;
10 if (idx === -1) {
11 temp = null;
12 break;
13 }
14 temp = articlesThatArentDraft[idx];
15 }
16 nextPage = temp;
17 }
18
19 // prev article
20 let prevPage;
21 if (index === articlesThatArentDraft.length - 1) {
22 prevPage = null;
23 } else {
24 idx = index + 1;
25 temp = articlesThatArentDraft[idx];
26 while (temp.secret) {
27 idx = idx + 1;
28 if (idx === articlesThatArentDraft.length) {
29 temp = null;
30 break;
31 }
32 temp = articlesThatArentDraft[idx];
33 }
34 prevPage = temp;
35 }

Notice use of while loops to skip over any secret posts. Also, I had to explicitly disable those in the article template:

@narative/gatsby-theme-noeval/src/templates/article.template.tsx
1{!article.secret && (
2 <PaginationWrapper>
3 <PaginationButton>
4 {prevPage && !prevPage.secret && (
5 <Link
6 className="previous"
7 to={`${prevPage.slug}`}
8 aria-label="Prev"
9 >
10 ❮❮ Previous
11 </Link>
12 )}
13 </PaginationButton>
14
15 <PaginationButton>
16 {nextPage && !nextPage.secret && (
17 <Link className="next" to={`${nextPage.slug}`} aria-label="Next">
18 Next ❯❯
19 </Link>
20 )}
21 </PaginationButton>
22 </PaginationWrapper>
23)}

Additionally, the CSS for buttons etc. was provided by the definitions of PaginationWrapper and PaginationButton styled components.

Custom Components like tldr, update etc.

gatsby-remark-custom-blocks is a plugin that adds custom blocks to markdown contents in gatsby. These custom blocks allow one to use markdown within the block, by defining custom classes and blocks. I like using these especially for TLDR and Update blocks. You can an example right at the top of this post.

To enable this, at first I had to enable this plugin in gatsby-config.js file.

@narative/gatsby-theme-novela/gatsby-config.js
1...
2gatsbyRemarkPlugins: [
3 {
4 resolve: 'gatsby-remark-custom-blocks',
5 options: {
6 blocks: {
7 tldr: {
8 classes: 'tldr',
9 title: 'optional',
10 },
11 update: {
12 classes: 'update',
13 title: 'optional',
14 },
15 },
16 },
17 },
18...

Then, I had to define my CSS for tldr and update classes in the MDX component.

These components can now be simply used as following in the mdx content:

1[[tldr | TL;DR]]
2| + Using [novela](https://github.com/narative/gatsby-theme-novela) as a starting point to start
3| using gatsby themes, theme-ui and styled components.
4| + **Several modifications:** tags, math rendering via katex, Table of Contents, Image Gallery, Scroll
5| to top, next/previous posts, title of code blocks etc.
6| + [gatsby cloud](https://www.gatsbyjs.com/cloud/) for fatster build and deployment.
7| + Find all the modifications at [my github repo](https://github.com/sadanand-singh/reckoning.dev)

Custom Pages: 404, Code Stats, About Me etc.

This part was pretty starightword. For About Me and resume, I have used the concept of secret posts. You can mark a post as secret simply by including a frontmatter flag of secret: True. For custom 404 page and codestats, I create two pages files in reckoning/src/pages/ with corresponding names, 404.js and codestats.js.

gatsby cloud

As my blog has grown, the build time has become larger and larger. At the current stage, If I build my site on netlify, it takes more than 10 minutes!

Then I discovered gatsby cloud! This sites’ build time now is between 3-4 minutes! Thats some improvement. And setting up and linking it with github and netlify is super simple, just follow the official guide.

So that’s it folks! A lot of changes to the codebase, but I learnt/discovered a lot of things. I plan to add additional features to this theme, keep following this space if you are interested. And as always, feel free to take a look at my code base, clone it, play with it and even put some pull requests if you find some mistakes or improvements!

A Note From The Author

I write free resources for people learning machine learning, deep learning and programming. I turn down everyone who offers to put ads, affiliate links, and sponsored posts on my website.

Be the first to receive my latest content with the ability to opt-out at anytime.

More articles from reckoning.dev

New Features for this Blog

2019 has been a year of gatsby updates for this site. So, I wanted to end this year with the same spirit. And, I am hoping this is going to…

December 29th, 2019 · 3 min read

Updating GatsbyJS Looks: A New Home with A New Look

First of all, apologies for a sudden update of the domain name - from datasciencevision.com to the new address reckoning.dev ! I let the…

September 2nd, 2019 · 3 min read
© 2014–2020 reckoning.dev
Link to $https://twitter.com/reckoningdevLink to $https://github.com/sadanand-singhLink to $https://www.linkedin.com/in/sadanandsingh/Link to $https://stackoverflow.com/users/13244305/reckoningdevLink to $https://www.facebook.com/sadanand4singh