Blog

Tagged by 'algolia'

  • If you haven't noticed (and I hope you have), back in June I finally released an update to my website to look more pleasing to the eye. This has been a long time coming after being on the back-burner for a few years.

    Embarrassingly, I’ve always stated in my many year in reviews that I planned on redeveloping this site over the next coming year, but never came to fruition. This is partly down to time and deciding to make content a priority. If I’m honest, it’s mostly down to lacking the skills and patience in carrying out the front-end development work.

    Thankfully, I managed to knuckle down and decided to become acquainted and learnt enough about HTML and CSS to get the site where it currently stands, with the help of Tailwind CSS and an open-source base template to act as a good starting point for a novice front-end developer.

    Tailwind CSS

    Very early on, I knew the only hope I had to give this site a new look was to use a front-end framework like Tailwind CSS, requiring a minimal learning curve to produce quick results. It’s definitely not a front-end framework to be sniffed at as more than 260000 developers have used it for their design system. So it’s a framework that is here to stay - a worthwhile investment to learn.

    Tailwind CSS is predominantly a CSS framework consisting of predefined classes to build websites directly within the markup without having to write a single line of custom CSS.

    As you’re styling directly within the markup, at first glance it can be overwhelming, especially where multiple classes need to be declared on a single HTML block. A vast difference when compared to the cleanliness of builds carried out by the very skilful team from where I work.

    It’s a small trade-off in an otherwise solid framework that gives substantial benefits in productivity. Primarily because Tailwind CSS classes aren’t very specific and gives a high level of customisability without you having to concoct CSS styles.

    Even though there are many utility classes to get acquainted with, once you have an understanding of the core concepts, front-end builds become less of an uphill battle. Through rebuilding my site, I managed to quite quickly get familiarity with creating different layouts based on viewport size and modifying margins and padding.

    I found it to be a very modular and component-driven framework, helping avoid repetition. There are UI kits on the market that give good examples of the power of Tailwind CSS that you can use to help speed up development:

    Using Tailwind CSS took away my fear of front-end development without having to think about Bootstrap, BEM, SASS mix-ins, custom utility classes, purge processing, etc.

    Base Template

    I gave myself a 3-week target (not full-time) to get the new site released and this couldn't have been done without getting a head start from a base theme. I found an open-source template built by Timothy Lin on Tailwind Awesome website that suited my key requirements:

    • Clean
    • Simple
    • Elegant
    • Maintainable
    • Easily customisable

    Another developer by the name of Leo, developed another variation of this already great template where I felt it met my requirements down to a tee.

    Even though the template code-base used was developed in Next.js, this did not matter as I could easily migrate the Tailwind markup into my Gatsby JS project. Getting Tailwind set up initially for Gatsby took a little tinkering to get right and to ensure the generated CSS footprint was kept relatively small.

    As you can see from the new site build, I was able to make further modifications to suit my requirements. This in itself is a testament to the original template build quality and the power of Tailwind CSS.

    Improvements

    As well as changing the look of my site, I thought it would be an opportune time to make a few other small enhancements.

    Google Ads

    Removing Google Ads had been on the forefront of my mind ever since I moved over to Netlify to host my website. Previously, it was a way to contribute to the yearly hosting cost. Now, this is no longer of any relevance (as I'm on the free Netlify free hosting plan), especially when weighing the importance of a meagre monetary return over improving the overall website look and load times of the site.

    In its place, I have a Buy Me A Coffee profile for those who would like to support the content I write.

    Updated Version of Gatsby JS

    It seemed natural to upgrade the version of Gatsby JS from version 2 to 4 during the reworking of my site to keep up-to-date with the latest changes and remove any deprecated code.

    Upgrading from version 2 to 4 took a little longer than I'd hoped as other elements required updating such as Node and NPM packages. This resulted in a lot of breaking changes within my code-base that I had to rectify.

    The process was arduous but worth doing as I found site builds in Netlify reduced significantly.

    Gatsby Build Caching

    I briefly spoke about improved Netlify build times (above) due to efficiencies in code changes relating to upgrading to Gatsby 4. There is one more quiver to my bow to aid further build efficiencies and that is by installing the netlify-plugin-gatsby-cache plugin within Netlify - one-click install.

    I highly recommend everyone who has a Gatsby site install this plugin as it instantly reduces build times. For a website like my own that houses over 300 posts the build minutes do start to add up.

    Features Yet To Be Implemented

    Even though the new version of my site is live, there are features I still plan on implementing.

    Algolia Site Search

    As part of getting a new version of my site released in such a short period, I had to focus on the core areas and everything else was secondary. One of the features that didn’t make the cut was the site search using Algolia.

    I do plan on reinstating the site search feature at some point as I found it helpful for me to search through my older posts and surprisingly (based on the stats) visitors to the site also made use of it.

    Short-Form Content

    I like the idea of posting smaller pieces of content that doesn't have to result in very lengthy written blog posts. Not sure what I will call this new section. There are only two names that come to mind: "Short-form" or "Bytesize". It could consist of the following types of content:

    • Small, concise code snippets.
    • Links to content I found useful online that could be useful in certain technical use-cases.
    • Book recommendations.
    • Quotes.
    • Thoughts on news articles - John Gruber style!

    At one point, I wrote blog posts I categorised as Quick Tips, till this date consists of a mere four blog posts that I never added to. I think the naming of this category wasn't quite right.

    I see this section functioning in a similar fashion to Marco Heine's Today I Learned.

    My Bookmarks

    I like the idea of having single page with a bunch of links to useful sites I keep going back to. It could be sites that you have never come across before, making all the more reason to share these links.

    Closing Thoughts

    I normally find a full-site rebuild quite trying at times. This time was different and there were two reasons for this.

    Firstly, I've already built the site in Gatsby JS and involved minimal code changes, even when taking into consideration the changes needed to update to version 4. Secondly, using Tailwind CSS as a front-end framework was a very rewarding experience especially when page builds come to fruition in such a quick turnaround.

    I hope you find the new design is more aesthetically pleasing and makes reading through blog posts a more enjoyable experience.

  • You probably haven't noticed (and you'd be forgiven if this is the case!) that my site now has the ability to search through posts. This is a strange turn of events for me as I decided to remove search capability from my site many years ago as I didn't feel it added any benefits for the user. This became evident from Google Analytics stats where searches never hit high enough numbers to warrant having it. The numbers don't lie!

    So what caused this turnaround?

    I've noticed that I'm regularly referring back through posts to refresh myself on things I've done in the past and to find solutions to issues I know I've previously written about. Having a search would make trawling through my few hundred posts a lot easier. So this is more of a personal requirement than commercial. But there is an exciting aspect to this as well - experimenting with Algolia. Using Algolia search is something I've been meaning to look into for a long time and integrating with GatbsyJS.

    The thought of having the good ol' magnifying glass back in the navigation makes me nostalgic!

    Note: In this post, I won't be covering the basic Algolia setup or the plugins needed to install as there is already a great wealth of information online. Check out my "Useful Links" section at the end of the post.

    Basic Setup

    Integrating Algolia into GatbsyJS was relatively straight-forward due to the wealth of information that others have already written and also the plugins themselves. The plugins make light work of rendering search results quickly allowing enough customisations to the HTML markup for easy implementation within any site. By default, the plugins contain the following components:

    • InstantSearch
    • SearchBox
    • Hits
    import algoliasearch from 'algoliasearch/lite';
    import PropTypes from 'prop-types';
    import { Link } from 'gatsby';
    import { InstantSearch, Hits, Highlight, SearchBox } from 'react-instantsearch-dom';
    import React from 'react';
    
    // Get API keys from the environment file.
    const appId = process.env.GATSBY_ALGOLIA_APP_ID;
    const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
    const searchClient = algoliasearch(appId, searchKey);
    
    const SearchPage = () => (
      <InstantSearch
        searchClient={searchClient}
        indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
      >
        <SearchBox />
        <Hits hitComponent={Hit} />
      </InstantSearch>
    );
    
    function Hit(props) {
      return (
        <article className="hentry post">
          <h3 className="entry-title">
            <Link to={props.hit.fields.slug}>
              <Highlight attribute="title" hit={props.hit} tagName="mark" />
            </Link>
          </h3>
          <div className="entry-meta">
            <span className="read-time">{props.hit.fields.readingTime.text}</span>
          </div>
          <p className="entry-content">
            <Highlight hit={props.hit} attribute="summary" tagName="mark" />
          </p>
        </article>
      );
    }
    
    Hit.propTypes = {
      hit: PropTypes.object.isRequired,
    };
    
    export default SearchPage;
    

    The InstantSearch is the core component that directly interacts with Algolia's API and takes in two properties, "searchClient" and "indexName" containing the Application ID and Search Key that is acquired from the Algolia account setup. This component contains two child components, SearchBox is the search textbox and Hits that displays results from the search query.

    It is the Hits component where we can customise the HTML with our own markup by using it's "hitComponent" attribute. In my case, I created a function to generate HTML where I access the properties from the search index. What's really cool is here is we have the ability to also highlight our search term where they may occur in the results by using the Highlight component (also provided by the Algolia plugin) and adding a "tagName" attribute.

    Removing The SearchBox Component

    The standard implementation may not suit all scenarios as you may want a search term to be sent to the InstantSearch component differently. For example, it could be from a custom search textbox or (as in my case) read from a query-string parameter. It wasn't until I started delving further into the standard setup I realised you cannot just remove the SearchBox component and pass a value directly, but there is a workaround.

    I have expanded upon the code-snippet, above, to demonstrate how my search page works...

    import algoliasearch from 'algoliasearch/lite';
    import PropTypes from 'prop-types';
    import { Link } from 'gatsby';
    import { InstantSearch, Hits, Highlight, connectSearchBox } from 'react-instantsearch-dom';
    import Layout from "../components/global/layout";
    import React, { Component } from "react";
    
    // Get API keys from the environment file.
    const appId = process.env.GATSBY_ALGOLIA_APP_ID;
    const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
    const searchClient = algoliasearch(appId, searchKey);
    const VirtualSearchBox = connectSearchBox(() => <span />);
    
    class SearchPage extends Component { 
      state = {
        searchState: {
          query: '',
        },
      };
    
      componentDidMount() {   
        // Get "term" query string parameter value.
        let search = window.location.search;
        let params = new URLSearchParams(search);
        let searchTerm = params.get("term");
    
        // Send the query string value to a "searchState" object used by Algolia.
        this.setState(state => ({
          searchState: {
            ...state.searchState,
            query: searchTerm,
          },
        }));
     }
    
      render() {
          // Default "instantSearch" HTML to prompt user to enter a search term.
          var instantSearch = null;
          
          // If there is a search term, utilise Algolia's instant search.
          if (this.state.searchState.query) {
            instantSearch = <div className="entry-content">
                              <h2>You've searched for "{this.state.searchState.query}".</h2>
                              <div className="post-list archives-list">
                              <InstantSearch
                                  searchClient={searchClient}
                                  indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
                                  searchState={this.state.searchState}
                                >
                                  <VirtualSearchBox />
                                  <Hits hitComponent={Hit} />
                                </InstantSearch>  
                              </div>
                            </div>
          }
          else {
            instantSearch = <div className="entry-content">
                              <h2>You haven't entered a search term.</h2>
                              <p>Carry out a search by clicking the <em>magnifying glass</em> in the navigation.</p>
                            </div>
          }
    
          return (
            <Layout>
              <header className="page-header">
                <h1>Search</h1>
                <p>Search the knowledge-base...</p>
              </header>
              <div id="primary" className="content-area">
                <div id="content" className="site-content" role="main">
                    <div className="layout-fixed">
                        <article className="page hentry">
                          {instantSearch}
                        </article>
                    </div>
                </div>
              </div>
          </Layout>
        )
      }
    }
    
    function Hit(props) {
      return (
        <article className="hentry post">
          <h3 className="entry-title">
            <Link to={props.hit.fields.slug}>
              <Highlight attribute="title" hit={props.hit} tagName="mark" />
            </Link>
          </h3>
          <div className="entry-meta">
            <span className="read-time">{props.hit.fields.readingTime.text}</span>
          </div>
          <p className="entry-content">
            <Highlight hit={props.hit} attribute="summary" tagName="mark" />
          </p>
        </article>
      );
    }
    
    Hit.propTypes = {
      hit: PropTypes.object.isRequired,
    };
    
    export default SearchPage
    

    My code is reading from a query-string value and passing that to a "searchState". The searchState object is created by React InstantSearch internally. Every widget inside the library has its own way of updating it. It contains parameters on the type of search that should be performed, such as query, sorting and pagination, to name a few. All we're interested in doing is updating the query parameter of this object with our search term.

    If the query parameter from the "searchState" object is empty, render search results, otherwise, display a message stating a search term is required.

    One thing to notice is the SearchBox has been replaced with a VirtualSearchBox, which uses the connector of the search box to create a virtual widget - in our case an empty span tag. This will link the InstantSearch component with the query. Having some form of search box component is compulsory.

    Conclusion

    I prefer not to use the out-of-the-box search box component as I can potentially save requests to Algolia's API, as searches aren't being made on the fly as a user enters a search term. This is the plugins default behaviour.

    Passing a search term through a query-string may come across as a little backwards, especially when it's rather nice to see search results change before your eyes as you type letter-by-letter. However, this approach misses one key element: Tracking in Google Analytics. Even though I will be primary the person making the most use of my site search, it'll be interesting to see who else uses it and what search keywords are used.

    Useful Links