Write more. Reduce friction with Gatsby

Problem

There are so many things to write about, especially given the fact how many curious developers are out there in the wild exploring, learning, tinkering with code and creating side projects. I’ve noticed one peculiarly strange thing when it comes to content creation. I will have lots of ideas, I will even have the code samples to reinforce the concepts that I want to explain or teach BUT I will in most cases fail to even start writing a blogpost regarding a topic.

Luckily I love debugging and deconstructing things and I tried to solve the problem on the more conceptual level. As a good starting point I’ve decided to list the things that I did not like about content creation - interestingly enough, all of them were more or less connected to the process. What I mean by the process?

As a thorough person I want all my content to be of a high quality, to this end I spend a lot of time creating the blogpost cover images open graph cards to neatly display them on social media . I also provide all my blogposts with rich metadata that is used to split them into categories, eases publishing etc. When I started divdev.io, I’ve done almost all of my blogpost cards in Affinity Designer, which took me a lot of time, then I would manually fill all the meta data for new posts, which took time, then I would produce open graph compatible cards - also time.

monkey ironning shirt

You probably got already the idea I am pointing at. I did not event start to write the blog post and I already need to spend bunch of hours to prepare for it. This causes logical friction which in turn results into unwillingness to actually start. The more you have to do, especially manual tasks, to get started - the less is the likelihood that this thing will at all be started!

Being an engineer luckily opens many doors and automation is one of them. As the second part of the post I will tell you the exact steps with code chunks that helped me to overcome the struggle of starting a new piece of content and enabled me to write more as the result.

popeye automating shirt ironing

Solution

As an example I want to provide three tasks that crated a lot of friction when it came to writing a new blogposts and the solutions to them.

  1. Need of cool blogpost cover
  2. Need of dynamic open graph cards
  3. Creation of metadata of the blog posts

Need for cool blogpost cover

As already mentioned in the previous part of this post I’ve been doing all of my covers in Affinity designer which means every time I needed to change text, icons, background accents etc - it would mean redoing most of the covers I’ve already done in the past.

I’ve come up with an idea that technically I could recreate the blog post cover images just from the meta data of the post itself as it wasn’t super complicated design for pure css. Luckily as Gatsby features duotone images, I didn’t even have much to do in this domain, just a couple of lines of code!

Here goes the most important code for creating the custom duotone blogpost cover which adapts itself based on the post metadata namely title, technologies, part of the series etc.

blogpost.jsx
jsx
1// JSX for the component that composes all of the content which does not go to blog post cover as children
Copy
Copy
2<PostCard
3 path={path}
4 accentColor="#e64a7f"
5 title={title}
6 stack={stack}
7 series={series}
8 featuredImage={featuredImage}
9>
10 <StyledTitle>{title}</StyledTitle>
11 <StyledDescription
12 dangerouslySetInnerHTML={{
13 __html: node.frontmatter.description,
14 }}
15 />
16 <InfoContainer>
17 <StyledDate>
18 <StyledCalendar />
19 {date}
20 </StyledDate>
21 <StyledTimeToRead>
22 <StyledClock />
23 {timeToRead} {timeToRead < 1.1 ? 'minute' : 'minutes'}
24 </StyledTimeToRead>
25 </InfoContainer>
26</PostCard>
postcard.jsx
jsx
1//Actual abstraction of the blogpost cover with styles. I know it's kinda messy in styling, but I never managed to keep my css nice and clean :)
Copy
Copy
2
3import React from 'react'
4import styled from 'styled-components'
5import { ReactSVG } from 'react-svg'
6import Img from 'gatsby-image'
7
8import egghead from '../../assets/eggo.svg'
9
10const StyledBlogPost = styled.div`
11 max-width: 365px;
12 margin: auto;
13 padding: 1.5em 1em;
14 box-shadow: rgba(0, 0, 0, 0.1) 0px 3px 8px 0px;
15 border-radius: 5px;
16 transition: all 0.3s;
17 &:hover {
18 transform: translateY(-2px);
19 transition: all 0.3s;
20 box-shadow: rgba(0, 0, 0, 0.1) 0px 3px 12px 0px;
21 }
22`
23
24const StyledImageContainer = styled.div`
25 position: relative;
26`
27const StyledOverlay = styled.div`
28 position: absolute;
29 top: 0;
30 bottom: 0;
31 height: 260px;
32 display: flex;
33 flex-direction: column;
34 align-items: center;
35 justify-content: center;
36 margin-left: -1em;
37 width: calc(100% + 2em);
38 z-index: 20;
39`
40const StyledOverlayTitle = styled.h3`
41 text-align: center;
42 color: white;
43 width: 250px;
44 font-size: 24px;
45 margin-bottom: 10px;
46`
47
48const StyledSeries = styled.div`
49 font-family: sans-serif;
50 color: white;
51 text-transform: uppercase;
52 opacity: 0.7;
53`
54const StyledOverlayBrand = styled.h4`
55 position: absolute;
56 text-transform: uppercase;
57 bottom: 0;
58 right: 0;
59 color: white;
60 margin-right: 20px;
61 margin-bottom: 20px;
62 span {
63 color: ${props => props.accentColor};
64 }
65`
66
67const StyledStack = styled.div`
68 display: flex;
69 align-items: center;
70 min-height: 25px;
71 &:before {
72 content: '';
73 position: relative;
74 right: 10px;
75 display: block;
76 width: 40px;
77 height: 3px;
78 background: ${props => props.accentColor};
79 }
80 &:after {
81 content: '';
82 position: relative;
83 left: 10px;
84 display: block;
85 width: 40px;
86 height: 3px;
87 background: ${props => props.accentColor};
88 }
89`
90const StyledStackIcon = styled(ReactSVG)`
91 svg {
92 height: 25px;
93 fill: white;
94 }
95`
96
97const StyledImage = styled(Img)`
98 height: 260px;
99 overflow: hidden;
100 object-fit: cover;
101 background-repeat: no-repeat;
102 background-position: center center;
103 background-attachment: fixed;
104 background-size: contain;
105 margin-left: -1em;
106 margin-top: -1.5em;
107 border-radius: 5px 5px 0 0;
108 width: calc(100% + 2em);
109`
110const StyledPublisher = styled.img`
111 width: 60px;
112 height: 60px;
113 margin: 0 0 10px 0;
114`
115
116const PostCard = ({
117 children,
118 title,
119 stack,
120 featuredImage,
121 publisher,
122 accentColor,
123 series,
124 path,
125}) => {
126 return (
127 <StyledBlogPost>
128 <StyledImageContainer>
129 <StyledOverlay data-pup="post" data-path={`${path}`}>
130 {publisher === 'egghead.io' && <StyledPublisher src={egghead} />}
131 <StyledOverlayTitle>{title}</StyledOverlayTitle>
132 <StyledStack accentColor={accentColor}>
133 {stack.map(v => (
134 <StyledStackIcon
135 src={`https://unpkg.com/simple-icons@latest/icons/${v}.svg`}
136 />
137 ))}
138 </StyledStack>
139 {series && (
140 <StyledSeries>
141 Part {series[0]} / {series[1]}
142 </StyledSeries>
143 )}
144 <StyledOverlayBrand accentColor={accentColor}>
145 <span>divdev</span>.io
146 </StyledOverlayBrand>
147 </StyledOverlay>
148 <StyledImage fluid={featuredImage.childImageSharp.fluid} />
149 </StyledImageContainer>
150 {children}
151 </StyledBlogPost>
152 )
153}

Need for dynamic open graph cards

Now I had the cool cards which I don’t need to design in other place then my code and which by the virtue of component structure change dynamically if I decide to do some minor improvements to how they should look like. But then the next problem appeared out of nowhere, as I had all the blogpost covers as .png files I just reused them in open graph meta tags which produce cards like this one:

Having the card generated dynamically does not allow for using them as the images in meta tags. This problem luckily has an easy solution which I am excited to share with you.

As all of the cards are nice and shiny after app loads all of the assets I could technically make the screenshots of the covers in a high resolution and then place them into according blog post folder. It could be a viable option, but as the essence of the task was to make the processes go faster and easier I just automated the screenshot generation.

It did not require a lot of code, I’ve just wrote a small script with puppeteer that I would for now run whenever I need to regenerate all the images. Nice point would be to automate it as the pre commit hook if you want not to care about your images at all.

Here goes the code for the command that would populate my blog post folders with correct images which will be used later in twitter cards for example.

index.js
js
1const
Copy
puppeteer
Copy
=
Copy
Copy
require
Copy
(
Copy
'puppeteer'
Copy
)
Copy
Copy
2
3;(async () => {
4 const browser = await puppeteer.launch()
5 const page = await browser.newPage()
6 await page.goto('http://localhost:8000')
7
8 //Make the overall page resolution higher to make screenshots better quality
9
10 await page.setViewport({ width: 800, height: 800, deviceScaleFactor: 2 })
11 await page.waitFor(1000)
12
13 //Don't forget to provide data-pup and data-path attributes to the element that you want to scrape and screenshot
14
15 const elements = await page.$$('[data-pup="post"]')
16 const paths = await page.$$eval('[data-pup="post"]', posts =>
17 posts.map(post => {
18 return post.getAttribute('data-path')
19 })
20 )
21
22 //How this way of looping event got here (copied from puppeteer docs that's how) :) You are of course free to use something more modern ;)
23
24 for (let i = 0; i < elements.length; i++) {
25 try {
26 console.log('Generating open graph compatible images for blogposts')
27 console.log(paths[i])
28 await elements[i].screenshot({
29 path: `${paths[i].replace('./', './blogContent/')}og.png`,
30 })
31 } catch (e) {
32 console.log(
33 `couldnt take screenshot of element with index: ${i}. cause: `,
34 e
35 )
36 }
37 }
38
39 await browser.close()
40})()

To run it in more convenient way you can add it to the scripts in your package json:

index.js
js
1"scripts"
Copy
:
Copy
{
Copy
Copy
2 "og": "node generateOgImages.js",
3}

Then it is as easy as running yarn run og in your project folder!

Need for blogpost bootstrap

Having automated the most tedious and boring tasks that stopped me from writing more I realised that the last thing I did not like about my process was creating frontmatter for my posts which is nothing more than meta data of the post. I use it to display bunch of info on the frontend and also as you might already noticed to create the blog post covers.

This task is pretty trivial and easy to achieve with small script. I’ve taken as a base the script by Joel Hooks and modified to serve my particular purposes.

Code goes here:

index.js
js
1#
Copy
!
Copy
/
Copy
usr
Copy
/
Copy
bin
Copy
/
Copy
env node
Copy
2const fs = require('fs')
3const slugify = require('slug')
4const pathify = require('path')
5const moment = require('moment')
6const shortid = require('shortid')
7
8const POST_PATH = pathify.resolve('./blogContent')
9console.log(POST_PATH)
10
11const title = process.argv[2]
12const tags = process.argv[3]
13 ? [...process.argv[3].split(',')].map(v => `"${v}"`)
14 : []
15const series = process.argv[4]
16 ? [...process.argv[4].split(',')].map(Number)
17 : []
18
19if (!title) {
20 throw 'No title was supplied as an argument.'
21}
22
23const id = shortid.generate(4)
24const path = `./${slugify(title.toLowerCase())}`
25const date = moment().format('YYYY-MM-DD')
26const dir = `${POST_PATH}/${slugify(title.toLowerCase())}`
27
28const frontmatter = `---
29id: ${id}
30path: ${path}
31date: '${date}'
32title: ${title}
33featuredImage: './featuredImage.jpg'
34ogImage: './og.png'
35description: 'Here goes the description'
36tags: [${tags}]
37series: [${series}]
38stack: ['javascript']
39published: false
40---`.trim()
41
42const ensureDirectoryExistence = filePath => {
43 const dirname = pathify.dirname(filePath)
44 console.log(dirname, filePath)
45 if (fs.existsSync(dirname)) {
46 return true
47 }
48 ensureDirectoryExistence(dirname)
49 fs.mkdirSync(dirname)
50 return true
51}
52
53if (ensureDirectoryExistence(`${dir}/index.mdx`)) {
54 fs.writeFileSync(
55 __dirname + `/blogContent/${path.replace('./', '')}/index.mdx`,
56 frontmatter,
57 function(err) {
58 if (err) {
59 console.error(err)
60 return
61 }
62 }
63 )
64}

As in the previous example just add the command to the scripts in your package json:

package.json
js
1"scripts"
Copy
:
Copy
{
Copy
Copy
2 "og": "node generateOgImages.js",
3}

Then run yarn run newPost "Your title here" react,gatsby,tag3 1,3 in your project folder! This command will generate the frontmatter with “yourTitle” as the title of the blog, put the file into the correct folder with the name your-title-here, create correct references to the images and fill out the tags and series so it looks somewhat like this:

1---
Copy
2id:a3fj
3path: './your-title-here/'
4date: '2019-11-09
5title: Your title here
6featuredImage: './featuredImage.jpg'
7ogImage: './og.png'
8description: 'Here goes the description'
9stack: ['javascript']
10series: [1,3]
11tags: ['react', 'gatsby', 'tag3']
12published:false
13---

Conclusion

I can personally confirm that reducing friction helps in creating more content as you could be more focused on the real creation and not on the processes surrounding it. This solution worked pretty well for myself because those were the real pain points when it came to writing a new post. Might not work for your specific case, but you should at least get an idea of what kind of things can be automated to make your life easier.

approving nod

If you have any other automation ideas or cool stories how you automated your processes definitely share them with me on twitter at twitter.com/divdev_ - I will make sure to retweet them :) As always you are free to leave the comments with questions/corrections or just directly hit me on twitter which is a proven way to get an answer fast!


Comments section

Dimitri Ivashchuk

Written by Dimitri who lives and works in Vienna building useful things. He is passionate about modern web technologies and enjoys learning something new everyday as well as sharing his knowledge with others.

Front-end engineer @CubeTech
Instructor @egghead.io