How my website works

Published on 16th Jun 2022
Thumbnail of ajmalafif.com CMSThumbnail of ajmalafif.com CMS
Thumbnail of ajmalafif.com CMS

First off, this piece is inspired by Brian Lovin in his ”How my website works” article.

Background

I‘ve been redesigning my personal website way too many times than I can recall. I‘ve been doing it since 2012. Some sites were never launched, and some are just under-the-hood changes. These are some of them:

What I do differently than previous iterations

Brian called this ”to reduce friction for iteration and writing”. I can‘t agree hard enough with the statement. That‘s why in this latest iteration, I have Sanity.io as my CMS of choice. I have to say it‘s very impressive; I have tried wiring Gatsby with Ghost, NetlifyCMS, and WordPress. None of them sit right with me and stick. Finally found the one that works for me and I can settle with.

Tech stack

  1. Gatsby v3
  2. Tailwind v2 (via twin.macro)
  3. Sanity
  4. Netlify

Features

This is a list of features that I have built with the site. Some of these might be “hidden” if you don‘t use or are unfamiliar with them, like RSS feed and Progressive Web App.

  • Progressive Web App (PWA)
  • RSS Feed
  • Dark Mode
  • Site Search with Algolia (CMD + K)
  • Sitemap (robot.txt)
  • MDX & Sanity.io for content sources
  • Open Graph image

`gatsby-config.js`

RSS feed

1{
2 resolve: `gatsby-plugin-feed`,
3 options: {
4 query: `
5 {
6 site {
7 siteMetadata {
8 title
9 description
10 siteUrl
11 site_url: siteUrl
12 }
13 }
14 }
15 `,
16 feeds: [
17 {
18 serialize: ({ query: { site, allSanityPost = [], allSanityReview = [], allMdx } }) => {
19 const records = []
20
21 allSanityPost.edges
22 .filter(({ node }) => filterOutDocsPublishedInTheFuture(node))
23 .filter(({ node }) => node.slug)
24 .forEach(({ node }) => {
25 const { title, publishedAt, slug, _rawBody, _rawExcerpt } = node
26 const url = site.siteMetadata.siteUrl + getBlogUrl(slug.current)
27 records.push({
28 title: title,
29 date: publishedAt,
30 url,
31 guid: url,
32 description: PortableText({
33 blocks: _rawExcerpt,
34 serializers: {
35 types: {
36 code: ({ node }) =>
37 h('pre', h('code', { lang: node.language }.node.code)),
38 },
39 },
40 }),
41 custom_elements: [
42 {
43 'content:encoded': PortableText({
44 blocks: _rawBody,
45 serializers: {
46 types: {
47 code: ({ node }) =>
48 h('pre', h('code', { lang: node.language }, node.code)),
49 mainImage: ({ node }) =>
50 h('img', {
51 src: imageUrlFor(node.asset).url(),
52 }),
53 authorReference: ({ node }) => h('p', 'Author: ' + node.author.name),
54 },
55 },
56 }),
57 },
58 ],
59 })
60 })
61
62 allSanityReview.edges
63 .filter(({ node }) => filterOutDocsPublishedInTheFuture(node))
64 .filter(({ node }) => node.slug)
65 .forEach(({ node }) => {
66 const { title, publishedAt, slug, _rawReviews, _rawExcerpt } = node
67 const review = _rawReviews[0].reviewContent
68 const reviewedAt = _rawReviews[0].reviewedAt
69 const url = site.siteMetadata.siteUrl + getReviewUrl(slug.current)
70 records.push({
71 title: title,
72 date: reviewedAt,
73 url,
74 guid: url,
75 description: PortableText({
76 blocks: _rawExcerpt,
77 serializers: {
78 types: {
79 code: ({ node }) =>
80 h('pre', h('code', { lang: node.language }.node.code)),
81 },
82 },
83 }),
84 custom_elements: [
85 {
86 'content:encoded': PortableText({
87 blocks: review,
88 serializers: {
89 types: {
90 code: ({ node }) =>
91 h('pre', h('code', { lang: node.language }, node.code)),
92 },
93 },
94 }),
95 },
96 ],
97 })
98 })
99
100 allMdx.edges.forEach(edge => {
101 records.push(
102 Object.assign({}, edge.node.frontmatter, {
103 description: edge.node.excerpt,
104 date: edge.node.frontmatter.date,
105 url: site.siteMetadata.siteUrl + edge.node.fields.slug,
106 guid: site.siteMetadata.siteUrl + edge.node.fields.slug,
107 custom_elements: [{ 'content:encoded': edge.node.html }],
108 }),
109 )
110 })
111 records.slice().sort((a, b) => new Date(b.date) - new Date(a.date))
112 return records
113 },
114 query: `
115 {
116 allSanityPost(sort: { fields: publishedAt, order: DESC }) {
117 edges {
118 node {
119 _rawExcerpt
120 _rawBody(resolveReferences: {maxDepth: 10})
121 title
122 publishedAt
123 slug {
124 current
125 }
126 }
127 }
128 }
129 allSanityReview(sort: { fields: publishedAt, order: DESC }) {
130 edges {
131 node {
132 _rawExcerpt
133 _rawReviews(resolveReferences: {maxDepth: 10})
134 title
135 publishedAt
136 slug {
137 current
138 }
139 }
140 }
141 }
142 allMdx(
143 limit: 1000,
144 sort: { order: DESC, fields: [frontmatter___date] },
145 filter: { frontmatter: { template: { regex: "/project-post/" } }}
146 ) {
147 edges {
148 node {
149 fields {
150 slug
151 }
152 frontmatter {
153 title
154 date
155 dateModified
156 }
157 html
158 }
159 }
160 }
161 }
162 `,
163 output: '/rss.xml',
164 title: 'Ajmal Afif — RSS Feed',
165 },
166 {
167 serialize: ({ query: { site, allMdx } }) => {
168 return allMdx.edges.map(edge => {
169 return Object.assign({}, edge.node.frontmatter, {
170 description: edge.node.excerpt,
171 date: edge.node.frontmatter.date,
172 url: site.siteMetadata.siteUrl + edge.node.fields.slug,
173 guid: site.siteMetadata.siteUrl + edge.node.fields.slug,
174 custom_elements: [{ 'content:encoded': edge.node.html }],
175 })
176 })
177 },
178 query: `
179 {
180 allMdx(
181 limit: 1000,
182 sort: { order: DESC, fields: [frontmatter___date] },
183 filter: { frontmatter: { template: { regex: "/project-post/" } }}
184 ) {
185 edges {
186 node {
187 fields { slug }
188 frontmatter {
189 title
190 date
191 dateModified
192 }
193 html
194 }
195 }
196 }
197 }
198 `,
199 output: '/projects.xml',
200 title: 'Ajmal Afif‘s projects · RSS Feed',
201 match: '^/projects/',
202 },
203 ],
204 },
205},

PWA

1{
2 resolve: `gatsby-plugin-manifest`,
3 options: {
4 name: `Ajmal Afif`,
5 short_name: `Ajmal Afif`,
6 start_url: `/`,
7 display: `standalone`,
8 icon: `src/images/icon.svg`,
9 icon_options: {
10 purpose: `maskable`,
11 },
12 theme_color_in_head: false,
13 },
14},
15{
16 resolve: `gatsby-plugin-offline`,
17 options: {
18 precachePages: [`/notes/*`, `/projects/*`, `/work`, `/about`],
19 },
20},

Algolia Search

1{
2 resolve: `gatsby-plugin-algolia`,
3 options: {
4 appId: process.env.GATSBY_ALGOLIA_APP_ID,
5 apiKey: process.env.ALGOLIA_ADMIN_KEY,
6 queries: require('./src/utils/algolia-queries'),
7 },
8},

Sitemap & robots.txt

1'gatsby-plugin-advanced-sitemap',
2 {
3 resolve: 'gatsby-plugin-robots-txt',
4 options: {
5 resolveEnv: () => NETLIFY_ENV,
6 env: {
7 production: {
8 policy: [{ userAgent: '*' }],
9 sitemap: 'https://ajmalafif.com/sitemap.xml',
10 },
11 'branch-deploy': {
12 policy: [{ userAgent: '*', disallow: ['/'] }],
13 sitemap: null,
14 host: null,
15 },
16 'deploy-preview': {
17 policy: [{ userAgent: '*', disallow: ['/'] }],
18 sitemap: null,
19 host: null,
20 },
21 },
22 },
23 },

canonical URL

1 {
2 resolve: `gatsby-plugin-canonical-urls`,
3 options: {
4 siteUrl: siteUrl,
5 },
6 },
7

Sanity.io

1{
2 resolve: 'gatsby-source-sanity',
3 options: {
4 ...clientConfig.sanity,
5 token: process.env.SANITY_READ_TOKEN,
6 watchMode: !isProd,
7 overlayDrafts: !isProd,
8 },
9},
10{
11 resolve: 'gatsby-plugin-sanity-image',
12 options: {
13 ...clientConfig.sanity,
14
15 // Automatically use `SanityImage.asset.alt` as alt text
16 altFieldName: 'alt',
17
18 // Recognize custom types that extend SanityImage
19 customImageTypes: ['SanityMainImage'],
20 },
21},

`gatsby-node.js`

Function for `/notes` pages

  • sourced from Sanity CMS
  • previous/next pagination
  • numbered list pagination (that “Goooooogle” like search result pagination)
1// create notes Pages
2async function createBlogPostPages(graphql, actions) {
3 const { createPage } = actions
4 const result = await graphql(`
5 {
6 allSanityPost(
7 filter: { slug: { current: { ne: null } }, publishedAt: { ne: null } }
8 sort: { fields: [publishedAt], order: DESC }
9 limit: 1000
10 ) {
11 edges {
12 node {
13 id
14 publishedAt
15 _updatedAt
16 slug {
17 current
18 }
19 title
20 }
21 }
22 }
23 }
24 `)
25
26 if (result.errors) throw result.errors
27
28 const postEdges = (result.data.allSanityPost || {}).edges || []
29
30 // previous/next pagination
31 postEdges
32 .filter(edge => !isFuture(new Date(edge.node.publishedAt)))
33 .forEach((edge, index) => {
34 const previous = index === postEdges.length - 1 ? null : postEdges[index + 1].node
35 const next = index === 0 ? null : postEdges[index - 1].node
36 const { id, slug = {} } = edge.node
37 const path = `/notes/${slug.current}/`
38 const updated = edge.node._updatedAt
39
40 createPage({
41 path,
42 component: require.resolve('./src/templates/blog-post.js'),
43 context: {
44 id,
45 previous,
46 next,
47 updated,
48 },
49 })
50 })
51
52 // numbered list pagination
53 const postsPerPage = 50
54 const numPages = Math.ceil(postEdges.length / postsPerPage)
55
56 Array.from({ length: numPages }).forEach((_, i) => {
57 createPage({
58 path: i === 0 ? `/notes/` : `/notes/${i + 1}/`,
59 component: require.resolve('./src/templates/blog-list.js'),
60 context: {
61 limit: postsPerPage,
62 skip: i * postsPerPage,
63 numPages,
64 currentPage: i + 1,
65 },
66 })
67 })
68}
69

Function for “article/naked” pages

This function is intended to create “naked” pages like;

1// create articles Pages
2async function createArticlePages(graphql, actions) {
3 const { createPage } = actions
4 const result = await graphql(`
5 {
6 allSanityArticle(
7 filter: { slug: { current: { ne: null } }, publishedAt: { ne: null } }
8 sort: { fields: [publishedAt], order: DESC }
9 limit: 1000
10 ) {
11 edges {
12 node {
13 id
14 publishedAt
15 _updatedAt
16 slug {
17 current
18 }
19 title
20 }
21 }
22 }
23 }
24 `)
25
26 if (result.errors) throw result.errors
27
28 const postEdges = (result.data.allSanityArticle || {}).edges || []
29
30 // previous/next pagination
31 postEdges
32 .filter(edge => !isFuture(new Date(edge.node.publishedAt)))
33 .forEach((edge, index) => {
34 const previous = index === postEdges.length - 1 ? null : postEdges[index + 1].node
35 const next = index === 0 ? null : postEdges[index - 1].node
36 const { id, slug = {} } = edge.node
37 const path = `/${slug.current}/`
38 const updated = edge.node._updatedAt
39
40 createPage({
41 path,
42 component: require.resolve('./src/templates/article-post.js'),
43 context: {
44 id,
45 previous,
46 next,
47 updated,
48 },
49 })
50 })
51}

Function for “category” pages

A category page template accommodates a list of notes with similar topics or categories. An example of a category/topic page:

1// create category Pages
2async function createCategoryPages(graphql, actions) {
3 // Get Gatsby‘s method for creating new pages
4 const { createPage } = actions
5 // Query Gatsby‘s GraphAPI for all the categories that come from Sanity
6 // You can query this API on http://localhost:8000/___graphql
7 const result = await graphql(`
8 {
9 allSanityCategory {
10 nodes {
11 slug {
12 current
13 }
14 id
15 }
16 }
17 }
18 `)
19 // If there are any errors in the query, cancel the build and tell us
20 if (result.errors) throw result.errors
21
22 // Let‘s gracefully handle if allSanityCategory is null
23 const categoryNodes = (result.data.allSanityCategory || {}).nodes || []
24
25 categoryNodes
26 // Loop through the category nodes, but don't return anything
27 .filter(node => !isFuture(new Date(node.publishedAt)))
28 .forEach(node => {
29 // Desctructure the id and slug fields for each category
30 const { id, slug = {} } = node
31 // If there isn't a slug, we want to do nothing
32 if (!slug) return
33
34 // Make the URL with the current slug
35 const path = `/notes/topics/${slug.current}/`
36
37 // Create the page using the URL path and the template file, and pass down the id
38 // that we can use to query for the right category in the template file
39 createPage({
40 path,
41 component: require.resolve('./src/templates/category.js'),
42 context: { id },
43 })
44 })
45}
46

My learnings making my own website

I always have trouble saying I am making my website from scratch. I get it when I saw or heard this, but to me, I didn‘t create React or GatsbyJS or Sanity. So technically, I am just wiring and stitching things together to make things work for my use case.

Ask specific questions with a clear outcome

Bonus if you have tried and approached it, breakdown of what worked and what doesn‘t. Half of the time, you got to the answer before you posted your questions

Make your working repo public

I can‘t stress this enough. It is already tricky to debug and help out a “unique” setup that deviates from a basic, common one. Troubleshooting blindly makes it even harder to get help. Making your working repo public will make it easier for others to help you out. In my case, I even had kind peeps send Pull Requests to help.

Thoughts and plans for the next iteration

Gatsby really makes some of the features easier to implement, but the lengthy build time is really a deal-breaker for me.

Although it will be such a steep learning curve to pick up Next or Remix, I think the challenge (and the learning) I‘ll pick up along the way will be worth the pain and effort.

Implement Storybook.js

I am thinking of overengineering these things, you know; a ”mini” Design System starting from tokens from Figma all the way to a kitchen sink with a Storybook for each component.

Introduce Page Transition interaction & animation

I tend to make my website function and present itself rather static, given that most of it is statically generated anyways. Maybe this time, I will explore and leverage that fresh Page Transition API and make the site slicker and snappier.

What‘s next?

Not to sound cheesy, but literally, right now, my instinct on what the next version of the site should be built with, and you guessed it... NextJS!

But why?

To be honest, with my current use case and need, sticking with Gatsby seems convenient. Given that I don‘t code full-time, my Javascript and React are quite limited and aren‘t that advanced too. But...

For my setup Gatsby v3 (and I have a v4 branch) build time on Netlify is painfully slow. I have less than 100 pages, but it takes on average, 7 to 10 minutes for each build. I had a setup years back in Jekyll (with TravisCI) and HarpJS (with surge.sh) that is probably within the same range.

Right now, my deterrence to migrate is that to have the same features mentioned above (RSS Feed, PWA, Algolia powered search, sitemap, and more) are non-trivial, no thanks to my limited JavaScript and NextJS skills.

However, the new Layout RFC that was just announced really triggers that FOMO and design itch yet again. Just imagining the potential of adding subtle enter/exit animation between pages within a layout might be worth the pain of banging my head on the walls.. yet again.

Given this is already the fifth reincarnation of my personal site, I think it‘s a matter of time before I relapse and feed the itch to redo my website all over again 😭.

Reach out to me on Twitter if you have any questions or feedback!

This note was last updated on 16th Jun 2022,
by
Ajmal Afif
Ajmal Afif
Older note
Learning in public

Copyright © 2009 – 2022 Ajmal Afif, All Rights Reserved.

LinkedIn iconGitHub icon