RSS Amplifier

Ajmal Afif — RSS Feed · Jun 16, 2022

How my website works

0
Sign in to vote or save

Ajmal Afif

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!

Read the original on ajmalafif.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.