Jake McCrary

HTML markup for better sharing on social media

For a bit more than a year I worked on a project that crawled the web and indexed articles. Two of our sources of data were articles shared on Facebook and Twitter. After seeing hundreds of article previews on these two social networks, I decided to improve how my own articles were previewed.

I thought figuring out the markup I needed to add would be a painless experience. Unfortunately, when you search for this information you end up at various SEO optimization and other similar sites where you get the pleasure of experiencing full screen pop-overs trying to get you to sign up for mailing lists and other annoying features of the modern web. Probably unsurprisingly, the least annoying source for this information turned out to be the social networks themselves.

Below is what you will want to add to the <head> section of your articles' markup. Items in all caps should be values that make sense for your articles. Most fields are pretty self-evident, but check Twitter’s and Facebook’s documentation for more details. The Open Graph documentation has more details as well.

<!-- Twitter Card data -->
<meta name="twitter:card" content="SUMMARY" />
<meta name="twitter:site" content="TWITTER HANDLE OF SITE (@jakemcc for this site)" />
<meta name="twitter:creator" content="YOUR TWITTER HANDLE" />
<meta name="twitter:title" content="ARTICLE TITLE" />
<meta name="twitter:description" content="SHORT DESCRIPTION OF CONTENT" />
<meta name="twitter:image" content="IMAGE THAT SHOWS UP WITH PREVIEW" />

<!-- Open Graph data -->
<meta property="og:site_name" content="SITE TITLE" />
<meta property="og:url" content="CANONICAL URL" />
<meta property="og:title" content="ARTICLE TITLE" />
<meta property="og:description" content="SHORT DESCRIPTION OF CONTENT" />
<meta property="og:image" content="IMAGE THAT SHOWS UP WITH PREVIEW" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="PUBLISHED DATETIME" />

If you have control of your site’s markup and want better previews of your articles on the various social networks then you should add this markup to your web site 1. Hopefully this article has saved you from having a full screen pop-over prompt you to join yet another mailing list.

  1. You can actually remove the twitter:title, twitter:description, and twitter:image lines since Twitter will fallback to the equivalent Open Graph markup if they missing.

Better command history in your shell

My ideal command history would let me search the history of every shell but when I hit the up arrow it would only cycle through my current shell’s history. In February, I was able to achieve this setup in large part because of a utility called hstr.

What is hstr?

hstr is a neat Bash and Zsh utility that lets you easily search, view, and manage your command history. hstr provides a tool named hh that provides a text interface for manipulating your command history. To see what it looks like check out the README and this video tutorial. If you are running OS X and use Homebrew you can install it by running brew install hh.

Making global history searchable but arrows cycle through local history

hstr is a neat tool but my favorite part of my setup is how the global command history is searchable but only a shell’s local history is cycled through with the arrow keys. This is achieved by manipulating where history is written and tweaking some environment variables.

The first step is to change your $PROMPT_COMMAND to append your shell’s history to a global history file. Below is the snippet that does this from my .bashrc file.

# Whenever a command is executed, write it to a global history
PROMPT_COMMAND="history -a ~/.bash_history.global; $PROMPT_COMMAND"

The next step is to bind a keystroke to run hh, which is what hstr provides, with $HISTFILE pointing to ~/.bash_history.global. I wanted to fully replace the default command history searching (and I use Emacs style keyboard shortcuts) so I’ve bound these actions to ctrl-r.

# On C-r set HISTFILE and run hh
bind -x '"\C-r": "HISTFILE=~/.bash_history.global hh"'

With those two additions to my .bashrc I’ve achieved my ideal command history searching. When I hit ctrl-r I’m searching all of my history and yet I only cycle through a shell’s local history with the arrow keys. This small addition1 made my command line productivity higher.

  1. My setup was inspired by this StackExchange post.

Better code reloading in a Clojure web server

A couple weeks ago I released com.jakemccrary/reload. This tiny library provides a ring middleware that uses org.clojure/tools.namespace to reload changed Clojure code on incoming http requests.

This middleware was created because my team was running into problems using ring’s wrap-reload middleware. Unfortunately these problems happened about nine months ago and, since I didn’t write this post back then, I’ve since forgotten these problems. Regardless, this project has been used since the beginning of this year and has helped make my team’s development workflow smoother. If you are running into problems it might help you too.


If you’d like to give it a shot, then add the latest version (at the time of writing [com.jakemccrary/reload "0.1.0"]) to your project.clj.

Require com.jakemccrary.middleware.reload and wrap your handler with wrap-reload.

(ns example
   ;; more deps
   [com.jakemccrary.middleware.reload :as reload]))

;; wherever you are setting up your middleware stack
(reload/wrap-reload routes)

reload/wrap-reload optionally takes a list of directories to monitor as a second parameter. By default it reloads the src directory.

AWS Elastic Beanstalk: Send a SQS message to a specific route in your worker environment

Lumanu uses AWS Elastic Beanstalk. Elastic Beanstalk (from now on abbreviated as EB) helps you provision and tie together Amazon services to fairly easily get web applications and services running with push button (or command line) deploys. We’ve been using EB with a multi-container docker deploy for nearly a year now and it pretty much just works.

EB has a concept of environment tiers and there are two different types; a web tier and a worker tier. Web tier environments provide the configuration and components necessary for serving HTTP requests in a scalable fashion. Worker environments are designed to run operations that you wouldn’t want performed by your front-end serving web application.

A major difference between the two environment tiers is that a worker environment provisions a SQS queue and provides a daemon that reads from this queue and POSTs messages to an instance of your worker service. This daemon prevents your worker service from having to connect to and manage a SQS queue. By default, the daemon POSTs messages to http://localhost/. You can optionally configure it to POST to a different route.

It is possible to have different messages POST to different routes. You can do this by setting the beanstalk.sqsd.path attribute on your SQS message. For example, if you want your worker service to receive a message at /trigger-email you would set the beanstalk.sqsd.path attribute to /trigger-email.

7 tips for a successful remote meeting

As mentioned in my tips for working from home article, I’ve been working remotely for nearly three years. In those three years I’ve been in countless meetings, both productive and unproductive. Meetings, both in-person and remote, are hard. Remote meetings pose some additional challenges but they also offer some unique benefits.

Below are seven tips that will help you have successful remote meetings. I wrote this article focusing on remote meetings but many of these tips will improve your co-located meetings as well.

Have an agenda

If you are organizing a meeting, you should make sure that the meeting has an agenda. The agenda doesn’t have to be a long, complicated document; it can be as simple as a list of topics and goals.

Why should you have an agenda? An agenda helps focus discussion by providing an outline of what the meeting is designed to cover.

Send out your agenda with the meeting invite. This gives invitees time to think about the topics and helps prevent people from showing up clueless. It also provides an opportunity for an invitee to excuse themself or suggest an alternative person if they don’t believe they will contribute to the meeting.

Start and end on time

You should start your meetings on time. You should end your meetings on time or early. If you are not starting and ending on time then you are not being respectful of your attendees time. The lack of punctuality contributes towards people dreading meetings.

If you are running out of time, wind down the meeting. If more discussions need to happen, reflect and see if more time needs to be scheduled with the entire group or if only a subset of the group is required.

Use video chat

Even if you don’t work remotely, you’ve probably had to dial in to a group audio chat. This is almost always a terrible experience. Without body language, it is near impossible to tell when someone is about to start speaking and, as a result, there are awkward pauses while everyone waits for someone else to speak and everyone speaks over each other. It is terrible.

This is why I recommend using video chat. Video chats let you see the other people on the call and this allows you to pick up on physical cues. These cues vastly improve communication in the meeting.

Co-located attendees should use their own device

Sometimes you’ll have a mixed meeting, some attendees are remote and others are together in an office. The co-located attendees should each use their own device to connect to the meeting.

Co-located attendees sharing a single device is non-optimal for many reasons. It is often hard for all the co-located attendees to be captured by the camera in a way that enables the remote attendees to reliably view them. Sharing a single microphone also makes it so some co-located attendees are easy to hear and others are barely audible.

Using a single device also makes it harder for all the co-located attendees to view the remote attendees. Without a clear view of the remote attendees, the co-located attendees often accidentally exclude the remote people by focusing on discussions between the co-located group.

Ignore distractions

Hopefully you have invited just the right people to the meeting and everyone is engaged in the discussion and paying attention. Realistically this doesn’t happen. Computers are incredibly good at so many things and one of those things is distracting the user.

When you are attending a remote meeting, minimize what can distract you. Close your email and hide the chat program. Put your phone out of arms reach. Try to focus intently on the meeting. If you find yourself not paying attention and not contributing, take this as a signal that you shouldn’t be in this meeting. If nothing on the rest of the agenda seems like it requires you, then leave the meeting and be more selective about what you join in the future.

If you notice other attendees not paying attention, gently call them out on it. This can be done by soliciting discussion from them or by being direct.

Have a shared artifact

This is one of the more important tips in this list and it is one of the areas where remote meetings have an advantage over in-person meetings.

When the meeting starts give everyone a link to a shared document that everyone can edit (for example a Google Doc). It can be useful to seed this document with the agenda. This shared document can be used to capture whatever you want. I’ve found it useful to capture options discussed, pros/cons lists, and follow-up actions. Writing in the shared document helps solidify ideas and gives the group a reference both during and after the meeting.

With in-person meetings, this shared artifact is often a whiteboard. Whiteboards are immensely useful but are barely editable by more than one person at once and are harder to reference after a meeting. I know I’m not the only person who dislikes trying to decipher terrible whiteboard handwriting captured by someone’s phone.

Except for when drawing diagrams, I’ve found the Google Docs style shared document used during a remote meeting more effective than using a whiteboard in an in-person meeting. You can always use a shared document in an in-person meeting as well but then you are requiring attendees to have a laptop open and that is an invitation for distracted attendees.

Assign responsibilities

Hopefully you are having a meeting to influence an outcome and not just hear everyone talk. As a result, you should be assigning follow-up responsibilities as the meeting progresses. Make the follow-up actions explicit and assigned to an individual. You can capture these responsibilities in your shared artifact.

Meetings can be difficult. You should do what you can to make them more successful. If you are being invited to a meeting without an agenda, ask for an agenda. If you’re in a meeting and you can tell someone is constantly distracted, try nicely calling them out on it (either privately or in the group). If there isn’t a shared artifact, make one and suggest it to the group. Meetings don’t have to be terrible. We can make them better.

My recommended Clojure testing setup

Occasionally, either on Stack Overflow or in the Clojurians Slack group, someone will ask what tools they should use to test Clojure code. Below is what I would currently recommend. I’ve come to this recommendation through observing teams using a variety of testing tools and through my own use them.

Use clojure.test with humane-test-output and lein-test-refresh.

Use clojure.test

clojure.test is ubiquitous and not a big departure from other languages' testing libraries. It has its warts but your team will be able to understand it quickly and will be able to write maintainable tests.

Use humane-test-output

You should use clojure.test with humane-test-output. Together they provide a testing library that has minimal additional syntax and good test failure reporting.

Use lein-test-refresh

If you’re not using a tool that reloads and reruns your tests on file changes then you are wasting your time. The delay between changing code and seeing test results is drastically reduced by using a tool like lein-test-refresh. Nearly everyone I know who tries adding lein-test-refresh to their testing toolbox continues to use it. Many of these converts were not newcomers to Clojure either, they had years of experience and had already developed workflows that worked for them.

Use lein-test-refresh’s advanced features

lein-test-refresh makes development better even if you don’t change any of its settings. It gets even better if you use some of its advanced features.

Below is a stripped down version of my ~/.lein/profiles.clj. The :test-refresh key points towards my recommended lein-test-refresh settings.

{:user {:dependencies [[pjstadig/humane-test-output "0.8.0"]]
        :injections [(require 'pjstadig.humane-test-output)
        :plugins [[com.jakemccrary/lein-test-refresh "0.16.0"]]
        :test-refresh {:notify-command ["terminal-notifier" "-title" "Tests" "-message"]
                       :quiet true
                       :changes-only true}}}

These settings turn on notifications when my tests finish running (:notify-command setting), make clojure.test’s output less verbose (:quiet true), and only run tests in namespaces affected by the previous code change (:changes-only true). These three settings give me the quickest feedback possible and free me from having the terminal running lein test-refresh visible.

Quick feedback lets you make changes faster. If you’re going to write tests, and you should write tests, having them run quickly is powerful. After years of writing Clojure, this is my current go-to for testing Clojure code and getting extremely fast feedback.

Tips for working from home

I’ve been working remotely since October 2013. I can barely believe that nearly three years have passed and I’ve probably spent about two weeks in a traditional office.

It took me a bit of time to adjust to working remotely. I’m better at it now than I was three years ago. The rest of this post describes some of my learnings.

But first, some background. My remote experience comes from working at Outpace and Lumanu. Both are remote first companies, almost everyone works remotely with a few people occasionally working in a San Francisco office. The work is remote but it is not asynchronous. Both companies value pair programming and real time collaboration and, as a result, employees tend to work a core set of hours. My observations are probably most applicable in a similar environment.

Setup a home workspace

Before working remotely I did not have a great home computing setup. I worked using a 13-inch MacBook Air either sitting on my couch or at my dinner table. This worked fine for the occasional work-from-home day and for evening and weekend programming. It didn’t work fine when I was spending extended hours at a computer every day.

I’ve written about my setup before. Below is a list of the improvements I made to my home working environment.

  1. Outpace provided a beefy MacBook Pro and two 27-inch Apple Cinema displays.
  2. I upgraded my chair to to a Herman Miller Setu.
  3. I bought a mStand Laptop Stand to raise my laptop to a better viewing height.
  4. I upgraded my desk to a sit-stand desk.
  5. I built my own ErgoDox keyboard.
  6. I switched to an adjustable monitor arm so I could adjust my monitor height.

With each change my working experience improved. Each improvement left me feeling better at the end of my day. Many, if not all, of these improvements are things I’d expect to have in a traditional office. Don’t skimp on your setup because it is in your home. Part of your home is your office now.

Introduce a habit that delineates work from after-work

One of the great things about working from home is that you no longer have a commute. You don’t have to dodge cars on your bicycle, squeeze into a train, or sit in traffic while driving. This is a huge benefit.

A downside of not having a commute is that you lose a forced intermission between work and non-work. My commute was either a 30-minute bicycle ride or a 30-minute public transit ride. That time acted as a forced decompression period where I focused on something that wasn’t computing.

It took me months to realize that not having this intermission was stressing me out. The intermission helped me shift between working and non-working mindsets.

I reintroduced a decompression activity between work and non-work and became less stressed and generally happier. I’ve replaced my commute intermission with reading, cooking, or riding my bicycle. I’ve found doing a non-computer activity benefits me the most.

Stop working

When I first started working from home I was very guilty of overworking. It was so easy to just keep working. I would get invested in a problem and all sudden realize it was time to go to bed. Or I’d actually stop working, only to find myself checking our application’s monitoring or pulling up the codebase when I originally sat down to do some personal task.

In an office you have signals; your coworkers leaving, the cleaning crew vacuuming, air conditioning turning off, etc., that provide a hint that you should think about stopping. You don’t have these signals when you are working remotely. There also isn’t that spatial boundary between your office and your home.

You can search online and find many articles about how overwork is detrimental. You and your employer benefit if you are not overworked.

Get out of your house

I live in Chicago. During the winter the weather is very cold. Chicago is also a big city, so all sorts of food delivery options (both cooked and uncooked) exist. The cold weather combined with food delivery makes it easy to stay inside. During the winter, I’ve realized many times that I haven’t left my apartment for days. My girlfriend can tell when I haven’t gotten outside because I’m grumpier.

If I get out of the apartment for a while I almost always come back feeling better. It barely matters what I do when I leave, after an extended period of time inside of my home just getting outside and doing anything helps. A change of scenery is good for you.

Don’t just talk about business with your remote coworkers

When you work in an office you are pretty much forced to have non-work related chats with coworkers. You should do the same with your remote team.

Having non-work related conversations helps you make better connections with your coworkers. These better connections can lead to better communication, both with voice and text, and humanize the person on the other side of the screen.

Its even better if you can do this in a video conference. Then you get to learn the facial expressions and tone of voice of your coworker. This can make it easier to interpret text communication in the way they actually mean it.

Meet in person occasionally

It is great that technology and Internet speeds have progressed enough that working remotely works well. If you want, you can make it feel like your pair is sitting right next to you. This is great.

There are still benefits to meeting in person though. The main one is that it helps you make connections with coworkers. You can eat a meal together or play board games (though, you can do this online as well but it is a different experience). It can also be easier to have certain types of group discussions (video conferences do have limitations).

When you meet in person, I’d recommend doing something different than your normal day-to-day work. Don’t just exchange remote pairing for local pairing. Try to identify that are difficult to do remotely and do them in person.

I don’t have a concrete recommendation for how often your remote company should meet but I think it should be infrequent enough where you don’t feel pressure to do normal work.


Working from home has its challenges but with those challenges come many benefits. It is a different experience than working in an office and that experience isn’t for everyone. The above recommendations are things that have helped me adjust to working remotely. Some of these tips are actionable at an individual level and some require buy in from the company. Hopefully this list can help give guidance towards improving your remote work situation.

Use Google to get a site's favicon

A few months ago I was implementing some changes to Lumanu’s user interface. Lumanu is a tool I’ve been working on that helps its users create, discover, and curate engaging content.

This interface change was to our discovery view. This is the view that surfaces interesting content to our users. The change involved showing the favicon of content’s origin in our interface.

I often browse the Internet with the network tab of the Chrome Developer Tools open. I do this because I find it interesting to see what services other web applications are using. I had the network tab open while browsing a site that displayed many favicons and noticed a lot fetches from google.com. This surprised me, so I took a deeper look at the requests and saw they were hitting a URL that appeared to provide favicons. It turns out you can query Google for favicons.


Let’s pretend we want to get the favicon for jakemccrary.com. You simply construct a URL that looks like https://www.google.com/s2/favicons?domain=jakemccrary.com and all of a sudden you have the favicon. Just replace jakemccrary.com with the domain you care about and you’ll be rewarded with that domain’s favicon.

My favicon from Google

This definitely isn’t a new feature. If you search online you’ll see people talking about it years ago. I had never heard of it before and discovering it saved us an unknown amount of time. It allowed us to iterate on our interface without having to figure out the nuances of favicons. We were able to quickly try out the interface change and then throw it away without costing us too much time.

Speeding up my blog

I was recently reading Jeff Ramnani’s about page and I was somewhat inspired by it. It loads quickly and links to Designing blogs for readers, an interesting essay by Matt Gemmmell. Reading that essay inspired me to think about my own site and what experience I want to deliver to readers.

I can’t imagine what every reader wants but I know what I want to experience when I read an article online. Reading high quality content is my highest priority. Beyond that I enjoy when a page loads fast and the visual design doesn’t get in the way. I think a great example of these two requirements is zen habits (along with Jeff Ramnani’s and Matt Gemmell’s).

My own site sort of achieves those goals. I like to think I’m writing well-written content that helps others. I know it has helped me. With regards to visual design I think there is room for improvement. I don’t think my site’s design is actively distracting from the content though, so I’ve decided to focus on improving the page load time first.

The optimization process

As with any optimization problem it is important figure what you’re going to measure, how you’re going to measure it and your starting point. I decided to focus on my page load time, as measured by Web Page Test. I used Google’s PageSpeed Insights to score and provide helpful tips for improving page speed. Unfortunately I didn’t capture my starting point with PageSpeed Insights but I think I was scoring around a 66/100 for mobile and 79/100 for desktop.

Starting point from Web Page Test

As measured by Web Page Test, the first load of my main page took five seconds and it wasn’t fully loaded for another second. This is ridiculous. My page is almost entirely static content and most of my assets are served from CloudFlare. It should be blazing fast.

Next I looked at what was actually being loaded. Google’s PageSpeed Insights identified that I had three render-blocking script tags. The offending scripts were Modernizr, jQuery, and octopress.js. PageSpeed Insights recommends inlining JavaScript required to render the page or make loading asynchronous. I decided to go a step further and remove the need for the JavaScript.

Removing octopress.js

It turns out octopress.js was the reason Modernizr and jQuery were required. Most of what octopress.js did were things that I don’t need; some sort of flash video fallback, adding line numbers to GitHub Gists, rendering delicious links, and toggling the sidebar visibility. I was able to delete all that code.

Next up was the mobile navigation octopress.js provided. This feature enabled navigation through a <select> element when the reader’s view port was tiny. Restyling my navigation bar to fit better on small screens allowed me to remove this feature. ocotpress.js also did some feature detection for Modernizr. I stopped using image masks and was able to remove that code as well.

The remaining code in octopress.js was a workaround for an iOS scaling bug. This JavaScript was inlined into my html. At this point octopress.js was empty and with it empty the requirements for jQuery and Modernizer disappeared. This let me remove three render-blocking script tags.

Remaining JavaScript

At this point the remaining JavaScript used for my blog was enabling comments with Disqus and showing recent tweets in my sidebar. I still enjoy having comments on my blog so I’m keeping Disqus around. I doubt that readers care what my most recent tweets are so I removed Twitter’s JavaScript. Removing my tweets also cleans up my sidebar and helps keep the focus on my writing.

Nearly no JavaScript, now what?

At this point Google’s PageSpeed Insight was suggesting that I up my cache times, inline my css, and move my web fonts lower on my page. Bumping up my cache times was trivial; I simply tweaked a CloudFlare setting.

I opted to not inline my css. This would require me to modify my site’s generation and I just didn’t feel like diving down that rabbit hole. I also didn’t move the web fonts lower on the page. I find fonts re-rendering jarring and as a result kept them loading1 in my <head>.

The results

I used Web Page Test to measure again and now the page load time is down to 2.5 seconds. Page load times are cut in half from the starting point. My PageSpeed Insights scores are also higher; up to 79/100 for mobile and 92/100 for desktop.

Web Page Test after optimization

Honestly, that number still seems high2 to me and I’m sure I could get it lower. But for now it is good enough3. As a result of doing this I’ve learned more about my blogging setup and managed to speed up my page load. Now it is time to focus on researching for future posts (and at some point restyling).

Update on 2016-05-03

I completely removed web font loading from my site. Getting rid of the fonts reduced my load time, as measured by Web Page Test, by a second. Google’s PageSpeed Insights now scores this site at 90/100 for mobile and 96/100 for desktop.

Web Page Test after font removal

  1. When I first wrote this I didn’t change anything about my web fonts. After thinking about it for a few days I ended up removing them completely. Details are in the update at the end of the post.

  2. I’m asking Web Page Test to load my page using IE10. I get much faster load times using Chrome or Firefox locally which is what most of my readers use. This is good enough for now.

  3. I mean, the starting point was probably good enough but if I admitted that then I wouldn’t have had the excuse to dig into my site’s load time.

The usefulness of Clojure's cond->

Clojure’s cond-> (and cond->>) is a versatile macro. It isn’t a new macro, it has been around since version 1.5, but I finally discovered and started using it sometime last year. It isn’t a workhorse macro, you won’t be using it everyday, but it comes in handy.

What is cond->?

Let’s start by looking at the docstring.

Usage: (cond-> expr & clauses)

Takes an expression and a set of test/form pairs. Threads expr (via ->)
through each form for which the corresponding test
expression is true. Note that, unlike cond branching, cond-> threading does
not short circuit after the first true test expression.

So what does the docstring mean? Let’s break it down with an example.

(cond-> 10
  false inc)
=> 10

In the above example 10 is the expr mentioned in the docstring and everything after it are the clauses. Each clause is a pair made up of a test and a form. In this example there is a single clause with the value false as the test the function inc as the form. Since the test evaluates to a false value the expression is not threaded into the form. As a result the original expression, 10, is returned.

Let’s look at an example with a truthy test.

(cond-> 10
  true (- 2)
=> 8

Once again, 10 is the starting expression. The single clause has a test that evaluates to true so the expression is threaded into the first position of the form (- 2). The result is 8 and this is returned.

Next is an example of a cond-> with multiple clauses. Explanations are inline with the code.

(cond-> 10 ; start with 10
  ;; test evaluates to true, so apply inc to 10. Current value is now 11.
  true inc

  ;; (zero? 1) evaluates to false, do not perform action. Current value stays 11.
  (zero? 1) (+ 2)

  ;; (pos? 4) evaluates to true, thread 11 into first position of form.
  (pos? 4) (- 5))
=> 6 ; The result of (- 11 5) is 6.

If you understand the above example then you have a good grasp of cond->. But when is this functionality useful?

When do I use cond->?

Looking through the codebases I work on, I almost primarily see cond-> being used with the initial expression being a hash-map. It is being used in situations where we want to selectively assoc, update, or dissoc something from a map.

If cond-> did not exist you would accomplish those selective modifications with code similar to below.

(if (some-pred? q)
  (assoc m :a-key :a-value)

You can rewrite the above with cond->.

(cond-> m
  (some-pred? q) (assoc :a-key :a-value))

If you’re not used to seeing cond-> the above transformation might seem like a step backwards. I know it felt that way to me when I first saw cond->. Give yourself time to get familiar with it and you’ll be glad you’re using it.

A meatier example of using cond-> is demonstrated below. Here we’re manipulating data structures designed for use with honeysql to generate SQL statements. We start with a base-query and selectively modify it based on incoming parameters.

(defn query [req-params]
  (let [and-clause (fnil conj [:and])
        base-query {:select [:name :job]
                    :from [:person]}]
    (cond-> base-query
      (:job req-params) (update :where and-clause [:= :job (:job req-params)])
      (:name req-params) (update :where and-clause [:= :name (:name req-params)])
      (:min-age req-params) (update :where and-clause [:> :age (:min-age req-params)]))))

Hopefully this gives you a taste of cond->. I’ve found it to be quite useful. It has a place in every Clojure developer’s toolbox.