Skip to content

Solved: Gitbook build fails with “Template render error”

2017 October 18
by Tedb0t

I’ve been using Gitbook to host some documentation, and finding it buggy and poorly documented at present. For instance, today I wrangled with a failed build that produced this error message:

error: error while generating page "": 

Template render error: (/root/.gitbook/versions/3.2.3/node_modules/gitbook-plugin-search/_layouts/website/page.html)
  TypeError: Path must be a string. Received undefined

You’ll never guess what caused it! Empty links in my Yep. ‘’ is the file that describes the overall content structure. If you have any bulletted lines that are not links, like this:

Section Title
- [Article](Link)
- Article that is not a link

The build will fail with that inscrutable, unhelpful error message. Sigh. Hope this helps someone out there. What makes this all the more sad and frustrating is that this is a platform for hosting documentation that is poorly documented! Seriously, look at their page for update failures—it’s empty. Great.

How (and Why) I Switched from JIRA to Clubhouse

2016 September 8
by Tedb0t

My team of ~10-15 people had been using JIRA Cloud, the ticketing/project management system, for a few years, and I had grown to loath it (why?). Earlier this year I began considering alternatives in earnest, and came across via a Hacker News post.

Why Clubhouse?

Right off the bat, I was in love: it’s beautiful, modern, innovative, tailored to modern agile product development, and above all, FAST.  (Oh, and it’s got a Clojure/Datomic backend too, which I think is neat!)  Despite a proliferation of Kanban Klones, the most well-known being Trello, none of the options I looked at actually worked the way I wanted to work: hierarchical project-epic-story organization, quick filtering, built-in smart Git integrations, etc.. This blog post is a great overview of why to switch to Clubhouse, but needs to be updated, since many of those ‘missing’ features have since been added. Clubhouse is rapidly and continuously releasing effective improvements, and seem to be passionate about creating a great tool with great customer service to boot. Oh, AND it’s free for up to 3 users, AND apparently completely free for qualifying nonprofits.

That being said, it was still a bit of an uphill battle switching: first I had to convince the rest of the management team that it was worthwhile, and then I’d have to figure out how to move our entire JIRA database and get everyone onboard.

I even made a quick Keynote specifically to describe why I wanted to switch to Clubhouse:

I’m happy to say that I finally did it—here’s how.

Importing Tickets

We have over 2000 JIRA tickets, most of which are now completed.  I briefly considered starting with just a nice, clean blank slate, but there’s too much detail in current open tickets we would lose.  Thankfully, I found an importer script, so I forked it and added a number of improvements (and got to learn Go while I was at it!)

Setting Up

First you’ll need a Clubhouse organization and an API Token.  Once you’ve got your org set up, go to[your_organization]/settings/account/api-tokens to create a token.  Copy that and do export CLUBHOUSE_API_TOKEN=asdflkjaseflkjdf in your terminal so the token will be ready to go.

screen-shot-2016-09-08-at-3-06-52-pm Then you’ll need to export your JIRA tickets.  The easiest (or least horrible) way to do this is to do an Issue Search for all the tickets you want to export, then click the “Download” button near the top right, then “Export XML”.  Note: you can right click and “Save As…” right on that link, otherwise it’ll open the XML right there in your browser. Yes, thanks, JIRA.

Now that you’ve got your input file (in the same dir as the importer), you’ll need to create a “user mapping” JSON file (say, userMap.json) that describes how you want to map JIRA users to Clubhouse projects and User IDs.  This looks like:



If you open up your JIRA export XML file, you’ll see elements like:

<assignee username="ted">T3db0t</assignee>

The importer uses only the username attribute, so make sure you use those in your userMap.json file.

Since you may want to assign projects by user, you can put the project IDs in as such. If you only have one Clubhouse project, just put that ID in for every user. To obtain your Clubhouse project IDs, you can do:

curl -X GET \
-H "Content-Type: application/json" \
-L "$CLUBHOUSE_API_TOKEN" | python -m json.tool &gt; projects.json

Then look in that output file for the Project IDs.

Similarly, to obtain Clubhouse User IDs, you can do:

curl -X GET -H "Content-Type: application/json" -L "$CLUBHOUSE_API_TOKEN" | python -m json.tool &gt; users.json

Lastly, you need to setup your workflow mappings, i.e. JIRA “Selected for Development” -> Clubhouse “Selected for Sprint” or however you want it. To obtain your Clubhouse workflow state IDs, do:

curl -X GET \
-H "Content-Type: application/json" \

Right now the workflow mapping is only done in the script (no external config file, sorry!), so you’ll need to go to jiraStructs.go and update this switch statement with your Clubhouse state IDs:

Pull the Trigger

OK, on to the main event: using the importer. With your userMap.json file ready, do:

go run *.go import --in SearchRequest.xml --map userMap.json --token $CLUBHOUSE_API_TOKEN --test

This will run the tool in test mode, which will parse all the input files and show you what it’s going to do, but won’t upload data to Clubhouse. Once you’re satisfied, run the tool without the test flag, open up your Clubhouse workspace and watch those tickets roll in!

Also included are a few utility scripts in Python:, and Use these like so:


The importer will add a “JIRA” label to every ticket it imports. If something gets messed up and you need to redo an import, this makes it easy to select all imported tickets, archive them and use the above command to delete them.


If you’ve got a team of more than a few people, you’ll probably want to schedule a day to do the import and transition, so that they can stop updating JIRA tickets while you verify the import. Then you can do a little team meeting and show off your slick new interface!

Make sure to read the README for full details and create a Github issue if you run into any problems!



Why I Finally Ditched JIRA

2016 September 8
by Tedb0t

When I originally transitioned away from Github Issues (years ago at this point), my company already had JIRA installed and I figured that would be good enough. And it was, for a while—but a number of things drove me crazy, and eventually made me utterly dread using it.  Since I’m trying to organize the architecture of a product line, I’m using the project management tools more frequently than individual developers, and the little aggravations really add up.  But there was a single thing that finally broke me and forced me to switch—read below.

See this post for how I moved to (and how you can too).

JIRA is basically the Salesforce of project management: A bloated, ineffective UI painted thinly over a bloated, ancient database comprising countless miscellaneous fields.

  • Speed: JIRA’s cloud-hosted application is EXTREMELY SLOW.  For some individual developers, this wasn’t much of an issue, because they might only use it once a day to update a few tickets and forget about it.  For me or anyone else trying to organize and understand the project as a whole, it started to feel just downright useless.  Every single operation takes seconds.  Loading our main Kanban board takes anywhere from 4 to 15 seconds (!!).  Clicking on a single ticket to view its details takes up to 6 seconds (!!!).  The best I can hope for is a couple hundred milliseconds for any given operation.
  • Outdated/Senseless UI: But it’s not just the network latency that contributes to an overall slowness: it’s the UX design itself (or total lack thereof).  From the Kanban view, clicking on a ticket opens a sidebar that squishes all the ticket cards so much they’re unreadably useless (unless I have it fullscreened). In order to change any information that the sidebar doesn’t happen to show, I have to click a menu button, then edit, then potentially page through the issue options in order to make the change.  But the worst aspect of this is:
  • Slow, Ineffective Search: Searching for tickets is actually one of the most critical elements in a project management system: during a sprint meeting, say, you often need to see if a ticket was already created for the issue at hand, or find one you know is being referred to.  In JIRA, you type your search, then wait several seconds while a flat, paginated list comes back, containing ALL possible tickets, including completed ones, without any obvious distinction/sorting, across all JIRA projects!  And if you made a typo, or you weren’t exactly sure what the right search term was, you get to do it all over again!
  • Confusing Organization: It’s very difficult to do any kind of meaningful high-level organization in JIRA.  I’m talking about product-level planning, sorting ideas into epics and stories and seeing a clean, filtered view of those.  For instance, Epics are represented as cards in the Kanban view like all other tickets, and so you find yourself asking: wait, do I put this ticket in “In Progress” AND the Epic ticket in “In Progress?”  What do I do with this extra epic card hanging around?  They are not automatically or graphically linked in any way in the Kanban interface, so you end up with this extra epic card that noone knows what to do with.
  • Confusing—or Outright Broken: JIRA appears to have some kind of release management workflow, but I’ve never been able to understand it, and I’ve never been able to use it to do anything but make things worse.  There’s a “Release…” link above the “Done” column, so I once thought, “Oh, ok, when we release a version we just click that and it flushes all the tickets and we have a kind of clean start for the next version.”  But no! It turns out that, if you have multiple Fix Versions (which you shouldn’t be able to do in the first place), and any of them are not part of the release you’re trying to make now, it will fail—but it will add that failed release to the fix versions, making it actually impossible to ever release those tickets (without, I suppose, using the API to manually remove everything?)!
  • Configuration Paralysis: JIRA is basically the Salesforce of project management: A bloated, ineffective UI painted thinly over a bloated, ancient database comprising countless miscellaneous fields.  There are so many possible settings and configuration options, starting in on customizing anything can easily lead to confusing and difficult-to-debug problems.  In fact, the straw that broke this camel’s back for me was that another team in the company changed our “Issue Screen Schema” in a way that literally made it impossible for me to create Epics, because a required field wasn’t included in the schema, and despite 3 different attempts, I could not figure out how to fix it, nor could anyone in IT.  This is an utterly absurd situation to find yourself in with software that you pay good money for.
  • Nonexistent Support/Improvement: Say you set an issue resolution to “Duplicate” or something and then realize the ticket has to be reopened.  You’d want to change the resolution to “Unresolved,” right?  Guess what: people have been requesting this for LITERALLY FIVE YEARS to absolutely no avail.  There’s literally no way to do it without stupid workarounds.
  • …and all the other tiny little things that add up, like scrolling down, clicking in a box, hitting ‘delete’ and typing a number in order to specify story points.  And that’s only if you’re already looking at the issue modal. If you’re not, you need two more clicks to get to that.

I probably don’t need to tell you that even tiny UX frictions add up and results in wasted time, user frustration and secondary effects, like spending less time writing good tickets because it took so long to use the software.  And if you say something like, “Well, you just need to configure JIRA to better suit your needs”—No. I don’t want a one-size-fits-all monstrosity, I want software that’s tailored to agile development.  And that’s how I ended up moving to—read about how I did it.

As a product designer, I know I have different standards and criteria than some others, but I’d love to hear your thoughts—please comment below.

A Python solution to the Zebra Problem using logic programming

2016 July 15
Comments Off on A Python solution to the Zebra Problem using logic programming

In 1962, Life International magazine published a logic puzzle that provided a series of clues about 5 houses that contain people and animals among other things, and asked the reader to figure out which house had the zebra in it:

  1. There are five houses.
  2. The Englishman lives in the red house.
  3. The Spaniard owns the dog.
  4. Coffee is drunk in the green house.
  5. The Ukrainian drinks tea.
  6. The green house is immediately to the right of the ivory house.
  7. The Old Gold smoker owns snails.
  8. Kools are smoked in the yellow house.
  9. Milk is drunk in the middle house.
  10. The Norwegian lives in the first house.
  11. The man who smokes Chesterfields lives in the house next to the man with the fox.
  12. Kools are smoked in the house next to the house where the horse is kept.
  13. The Lucky Strike smoker drinks orange juice.
  14. The Japanese smokes Parliaments.
  15. The Norwegian lives next to the blue house.

This problem can be solved programmatically in a number of ways.  If you already know the technique to solving it (make a table), you can brute force it. However, it’s far more interesting and fun to solve the problem declaratively, using a logic solver.

So what? If it’s a big table, can’t you just dump it into a database and query it with SQL? The trouble is with these kinds of statements:

They drink water in a house next to the house where they smoke Blend.

Such a statement declares a relationship between some house and some other house. The word ‘some’ is actually very important here: it is a logic variable, which acts as a placeholder for a value without specifying a single actual value. It allows the solver to ‘try out’ different possible values and eventually (hopefully) land on a final one.

A logical solution

Here is such a solution using the logpy python library, which is an implementation of the miniKanren logic programming language:

This solution is adapted from / inspired by David Nolen’s solution using Clojure’s core.logic.

(See the full repo of my logpy examples, including an explanatory readme.)

What’s going on here?

If you’ve never done declarative programming, this can be kind of confusing. Think of each clue as a rule or constraint that specifies the way something must be. The solver’s job is to take all those constraints and tell you what (if any) values will satisfy all of them.

Two examples:

(eq, (var(), var(), var(), var(), var()), houses)

Sets up houses as a list of logic variables (each house), each of which will itself contain a list of logic variables (each property of that house), by saying “houses equals a list of 5 logic variables.” Note that this is actually declaring equality, not assignment.

(membero, ('Englishman', var(), var(), var(), 'red'), houses)

Stipulates that one of the houses has both the ‘Englishman’ and ‘red’ properties. The other properties are left ‘blank’ as logic variables, which get ‘filled in’ by the solver.

Once we’ve inputted all the rules, we run the solver by querying for



solutions = run(0, houses, zebraRules)

In other words, “What houses meet the requirements in zebraRules?” The answer may surprise you! 😉

Remote desktop control of a Raspberry Pi

2016 June 8
Comments Off on Remote desktop control of a Raspberry Pi
by Tedb0t

Say you want to configure a nifty dashboard powered by a Raspberry Pi. You can attach a keyboard and mouse to it, but that’s for suckers—we want to control it over the network.  Should be simple, right? We’ll just use X11 or VNC!

Wrong.  It turns out that BOTH X11-forwarding and the Raspbian-included tightvnc start new displays instead of controlling the existing one, and as far as I can tell, cannot be used for that purpose. Super lame.  However, there does turn out to be an easy solution: x11vnc.

  1. First you’ll need to get into your Pi, either locally (kb & mouse) or via SSH.
  2. Once you’re in, install x11vnc:
    sudo apt-get install x11vnc
  3. Setup x11vnc for the first time:
    x11vnc -storepasswd

    It will prompt for a password to use for future connection attempts.

  4. To make x11vnc start at login (since we’re assuming this is a dashboard starting up with an x-window session):
    mkdir /home/pi/.config/autostart
    touch /home/pi/.config/autostart/x11vnc.desktop
    nano /home/pi/.config/autostart/x11vnc.desktop
  5. Paste this in:
    [Desktop Entry]
    Name=X11VNC Server
    Comment=Share this desktop by VNC
    Exec=x11vnc -forever -usepw -httpport 5900 -auth guess
  6. Hit Ctrl-O to save and Ctrl-X to exit
  7. Then reboot:
    sudo reboot
  8. Once the Pi’s back up and you’re logged back in, make sure x11vnc is running:
    ps -ef | grep vnc

    In the results you should see:

    ... x11vnc -forever -usepw -httpport 5900 -auth guess
  9. Great! Now you can connect to and control your Pi from the VNC client of your choice. Remember that OSX actually has one already, called “Screen Sharing.” Just hit command-space to search and start typing that name and it’ll pop right up.  Then put this in as the host:
    <ip of Pi>:5900

Enjoy! Let me know if this works for you.

Better account validation in Meteor: t3db0t:accounts-invite

2016 March 10
Comments Off on Better account validation in Meteor: t3db0t:accounts-invite
by Tedb0t

While working on a Meteor project, I needed a way to allow or deny user account creation only at certain places (such as an ‘accept invitation’ route/template).  This turns out to be difficult, because the core accounts package doesn’t provide a way to do a server-side validation of the attempted login.

So after a whole lot of research and hacking, I’ve published t3db0t:accounts-invite.  See a live demo/explanation.

Using it is easy: register a validation callback, and then call inviteWithLogin in the route/template (or wherever) of your choice.

Comments and Pull Requests are welcome at the Github repo.

Complete mobile/large Meteor starter project with Meteoric

2015 July 13
Comments Off on Complete mobile/large Meteor starter project with Meteoric
by Tedb0t

meteoricMobileI put together a handy starter project for Meteor that combines a Meteoric (Meteor-Ionic) tab-bar mobile layout with an easily customizable ‘large’ screen layout, giving you a full-featured mobile app and desktop app in one Meteor project.

To use, clone the repo, start meteor, and navigate to localhost:3000.  To check it out as an iPhone or Android app, just do meteor add-platform ios (or android), then meteor run ios-device -p <local port>.

Enjoy, and let me know what you think!  Pull requests welcome.

Importing GitHub issues into JIRA OnDemand

2014 October 23
tags: ,
by Tedb0t

JIRA has a GitHub issues importer, but it isn’t available on the Atlassian Marketplace for JIRA OnDemand, the hosted version of JIRA.  I thought I was going to have to fsck around with a CSV import script but thankfully it’s not necessary: buried in the Atlassian OnDemand docs is a section on “Importing Data from GitHub,” which allows you to import one or more GitHub repos as projects, whether new or existing. I just did it, and it worked almost perfectly, retaining issue comments and everything, with the following caveats:

  • The GitHub repo I was interested in importing was under a GitHub organization.  The importer doesn’t recognize this, so at first the repo didn’t show up in the list.  All I had to do was star the repos I wanted to import, back up and it showed up.
  • The importer will create JIRA users for every user referenced in the issue tracker!!  It didn’t tell me this or give me any options, so you’re going to have to live with it.  In my case it wasn’t a big deal, since several of the users were ones I would have to create anyway.  The problem is that if the user had comments or tickets, you won’t be able to delete them from JIRA.  O_O
  • Only one out of ~160 issues didn’t import, and I suspect it was because of a unicode emoticon in a comment. (?)


Two Nifty Mechanical Engineering Resources

2013 August 2
by Tedb0t

Today I stumbled across these nifty mechanical engineering resources:

  1. 507 Mechanical Movements,” a wonderful cross between a 19th-century encyclopedia and interactive web resource for all manner of clever mechanisms.
  2. GearSketch, a demo web app and source code for very rapidly modeling gear-based mechanisms.  I’m really impressed with this, and it’s just the beginning—I could see people continuing to extend the code and taking it really far.

On Secrecy & Authority

2013 June 29
Comments Off on On Secrecy & Authority
by Tedb0t

One week ago, my wife, Lindsey, and I returned home from a fabulous week in Ireland for our honeymoon.  By the time we had landed, Lindsey was suffering from some painful gastritis (she has a very sensitive stomach/GI system) and walking was exacerbating it, so we took advantage of the free wheelchairs being offered right inside the jet bridge.  A very helpful young guy henceforth wheeled Lindsey around the airport. We joked that Lindsey’s red merino wool sweater that we bought in Kenmare adorably exaggerated the “little old lady” look of her in the wheelchair.  We got to the customs booth and presented our passports.  The officer was taking a while with Lindsey’s, so I fiddled around on Instagram on my phone until the officer sternly and sharply instructed me that there is “NO TEXTING” while in the immediate area of the booth (I guess).  Lindsey told me she was still feeling the gastritis pain.  After another couple of minutes I got the impression something was wrong.  They were asking Lindsey if she’d ever notified them of a lost passport. read more…

Strange Loop Talk Online: “Getting Physical”

2013 May 10
Comments Off on Strange Loop Talk Online: “Getting Physical”

I just found out that my talk from the Strange Loop conference last year, “Getting Physical: Networked Hardware with Node.js” is online.  Check it out here!

Sublime Tunnel of Love: How to Edit Remote Files With Sublime Text via an SSH Tunnel

2013 February 4

Eventually you will need to edit a file in-place on a server, for one reason or another (i.e. working on a Javascript front-end that requires templating from a backend); this is partly what Emacs and Vim are for (and they’re both very good at what they do).

There’s nothing wrong with learning either of those tools, but if you really don’t want to, there are options.  If the server is running FTP, you can use something like Transmit to open the file in a local editor and saves will be automatically uploaded to the server.  Unfortunately, FTP is a very old and VERY insecure protocol that should not be used anymore.  What else can we do?

Using Secure Shell (SSH) Tunneling, we can establish an SSH session that routes arbitrary traffic through it to a specified port for any use we want.  Thanks to a nifty set of scripts called rsub, modified originally from TextMate’s rmate, we can run a little utility server on our local machine that interacts with your remote server for you and lets you open up remote files and save them back, all through an encrypted channel.

What Do I Do?

  1. As of writing, these instructions work only for Sublime Text 2.  If I get a chance I’ll look into forking rsub for the newly released ST3 (which runs Python3).
  2. If you don’t already have Sublime Text’s wonderful package manager, install it.
  3. Hit Ctrl+Shift+P, start typing “install” and select “Install Package”
  4. Start typing “rsub” and select it.
  5. Once it’s installed, get on your terminal and do
    nano ~/.ssh/config
  6. Paste the following lines:
        RemoteForward 52698
  7. Save (ctrl+w) and SSH into your server (ssh
  8. ‘Install’ the rsub remote script:
    sudo wget -O /usr/local/bin/rsub
  9. Make that script executable:
    sudo chmod +x /usr/local/bin/rsub
  10. Lastly, run rsub on the remote file you want to edit locally:
    rsub ~/my_project/my_file.html

    and it should magically open in Sublime Text!

Let me know if this works for you! Enjoy!

Updating a Mailchimp mailing list with past EventBrite attendees

2013 January 29
Comments Off on Updating a Mailchimp mailing list with past EventBrite attendees
by Tedb0t

At Kitchen Table Coders we use EventBrite to sell tickets and Mailchimp to email fans about new classes.  Every time we do a new class I wanted to update our main mailing list with the new attendee email addresses, so I wrote this simple Python script to do it automatically.  Usage details are in the readme.  Enjoy!

Batch convert files to PDFs in OSX Mountain Lion

2012 December 12
Comments Off on Batch convert files to PDFs in OSX Mountain Lion
tags: , ,
by Tedb0t

Today I needed to convert a bunch of RTF files (I know, what?) to PDFs.  After stumbling through a bunch of dead ends, I realized this—like most things—could be done incredibly easily in Bash:

for file in *.rtf ; do
filename=$(basename "$file") /usr/sbin/cupsfilter "$file" > "$filename.pdf"

Getting Physical: Workshop Notes

2012 September 22
Comments Off on Getting Physical: Workshop Notes

Here are some notes and links for my Strange Loop workshop, “Getting Physical.”

Slides for TimesOpen “Sockets & Streams”

2012 September 13
Comments Off on Slides for TimesOpen “Sockets & Streams”
by Tedb0t

Last night at the New York Times “TimesOpen” event, I gave a presentation on internet-enabled bubble guns, wireless joysticks for browser games, and how the “Internet of Things” is better understood as an Internet Ecology:

Thanks to everyone for coming and to Brad Stenger and everyone at the Times for inviting me!

Arduino: A new & improved Button library with some handy features

2012 August 2
by Tedb0t

By Ted Hayes, from code originally by Alexander Brevig & Tom Igoe

The Arduino Button library (Github Repo) makes it easy to do some very common but rather tedious tasks. Usually when you interact with a button (such as a momentary switch), you mainly want to detect the state change, not just the current state. You have to do something like:

int lastState = 0;
void loop(){
    int currentState = digitalRead(11);
    if(currentState != lastState){
        // do something
    lastState = currentState;

It’s not hard, just tedious. This new and improved Button library makes this much simpler but adds so much more. Now you can do it this way:

Button button = Button(12);

void onPress(Button& b){
	Serial.print("onPress: ");
	// will print out "onPress: 12"

void setup(){
  // Assign callback function

void loop(){
  // update the buttons' internals


  • Object-oriented design
    Button myButton(11);
  • Automatic pull-up setting
    Button myButton(11, BUTTON_PULLUP_INTERNAL);
  • Simplified state-change detection:
    if(button.isPressed()) ...
  • Callback model
  • Built-in debouncing
    // Sets 50ms debounce duration
    Button button = Button(12, BUTTON_PULLUP_INTERNAL, true, 50);


To install, download the library, extract it to ~/Documents/Arduino/libraries and rename the folder “Button.” (Github generates a different name for the zip link.) Restart Arduino if it was already open.

I hope you find this useful! Please report any bugs using the Github issue tracker.

Arduino: How to Read and Write EEPROM values

2012 August 1
Comments Off on Arduino: How to Read and Write EEPROM values
by Tedb0t

When you reset an Arduino, the state of any variables you might have will be lost. So what if you want to keep a value even after it’s been powered off? Luckily, ATmega chips have an area of memory called EEPROM, or “Electrically Erasable Programmable Read-Only Memory.”  This allows you to store persistent values, and Arduino ships with an EEPROM library that makes it easy to use.

Here’s an example Arduino sketch that demonstrates how to read from and write to this part of memory:

Tutorial: How to use your Raspberry Pi like an Arduino

2012 June 15
by Tedb0t

Finally got to experiment with the Raspberry Pi’s GPIO (General Purpose Input/Output) pins.  I tried three methods: Python, Bash and C, and will describe each.  But first, here’s some setup information. read more…

How to build Pd-extended on the Raspberry Pi

2012 June 14
by Tedb0t

It was quite an ordeal, but I managed to build Pd-extended (Puredata with a bunch of externals) on my Raspberry Pi running Debian Squeeze.  I also uploaded my finished package (.deb) to, see below for details.  Here’s what you need to do: read more…