<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="http://www.martin-brennan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.martin-brennan.com/" rel="alternate" type="text/html" /><updated>2025-10-19T21:22:31-04:00</updated><id>http://www.martin-brennan.com/feed.xml</id><title type="html">Martin Brennan</title><subtitle>I’m Martin Brennan, and I’m a full stack staff software engineer based in Australia, currently working for Discourse. I primarily enjoy working with Ruby, Rails, and JavaScript, with a focus on product engineering. I’m always seeking ways to expand my knowledge and learn about new tools, languages and frameworks.
</subtitle><author><name>mjrbrennan</name></author><entry><title type="html">My AI appetites</title><link href="http://www.martin-brennan.com/my-ai-appetites/" rel="alternate" type="text/html" title="My AI appetites" /><published>2025-10-12T00:00:00-04:00</published><updated>2025-10-12T00:00:00-04:00</updated><id>http://www.martin-brennan.com/my-ai-appetites</id><content type="html" xml:base="http://www.martin-brennan.com/my-ai-appetites/"><![CDATA[<p>I’d say my overall attitude to using AI for software engineering is still in the “cautious” phase (it gives me the ick when people say <em>bullish</em> and <em>bearish</em>), but more and more I am integrating it into my day-to-day work and responsibilities at Discourse.</p>

<p>In this article, I will go into some things that I am finding AI tooling useful for, and some areas where I wish it would improve, or that I am still finding my feet on. I will also cover how the prevalence of AI affects me as a tech lead and how it impacts our workflows on a product development team. So we are on the same page, I spent most of my time writing Ruby, Rails, JavaScript, Ember, and SQL code.</p>

<!--more-->

<ul id="markdown-toc">
  <li><a href="#ai-editor-integration" id="markdown-toc-ai-editor-integration">AI editor integration</a></li>
  <li><a href="#web-based-ai-tooling" id="markdown-toc-web-based-ai-tooling">Web-based AI tooling</a></li>
  <li><a href="#agentic-ai" id="markdown-toc-agentic-ai">Agentic AI</a></li>
  <li><a href="#code-review" id="markdown-toc-code-review">Code review</a></li>
  <li><a href="#daily-impact" id="markdown-toc-daily-impact">Daily impact</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<h3 id="ai-editor-integration">AI editor integration</h3>

<p>I use neovim as my main editor (I plan to write an article on my 2025 development setup soon), and I still haven’t really found an AI plugin that I like using regularly here. For a while I used <a href="https://github.com/github/copilot.vim">copilot.vim</a>, but it never felt <em>good</em> to use, there always felt like a bit of friction. Ideally I want something that can quickly operate on visual selections for minor refactors, or across multiple buffers using <code class="language-plaintext highlighter-rouge">fzf</code> for selecting file paths. I want to smoothly accept or reject AI suggested edits and refactors. I want to connect it to Claude, not being limited by model choice. I want to be able to ask it questions and search docs from within the code quickly, which is what I currently use ChatGPT for. Overall I am looking for better <em>integration</em> rather than some cobbled together plugin.</p>

<p>I have been experimenting a little with <a href="https://zed.dev/">zed</a>. It feels like a super smooth and performant editor, with an excellent UI and AI integrations. I think it will only get better over time – hell at the time of writing this article it’s already got more features than it did a month ago. I’ve partially set it up with the same configuration as my nvim setup, but I haven’t had enough time to truly migrate everything. It’s hard to juggle this and my day-to-day responsibilities – maybe a project for the quieter Christmas period, this is when I migrated from vim to neovim earlier this year (or was it last year? 😵‍💫). The AI features here feel like what I want for neovim. I need more time to evaluate this, but I can see myself moving over to zed from neovim if I can replicate at least the important parts of my workflow and not get too tripped up by missing features or janky UX.</p>

<p>Of course I use GitHub Copilot for autocompletion. That is really helpful and a lot of the time if you have the right files open in buffers in neovim it’s extraordinarily good at guessing what you want, especially when writing repetitive code like specs, or refactoring an existing file. I even use it in git commit message buffers occasionally, to varying degrees of success. Maybe ~60% of the time it’s helpful there.  I like that you can direct it with comments.</p>

<p>However there are times where I feel like it gets in the way of my thought process – I find it difficult to ignore the suggestions and they can be distracting. Thankfully <code class="language-plaintext highlighter-rouge">:Copilot disable</code> is always on hand to get it to go away. So TL;DR…for novel and creative work it can be distracting and not all that useful, but for ROTE work and refactoring it’s great.</p>

<h3 id="web-based-ai-tooling">Web-based AI tooling</h3>

<p>It feels as though ChatGPT is by far my most used AI tool, and I still reach for the web-based chat interface frequently. For me ChatGPT offers the least friction, I can jump to the chat page and send it any request and get an answer back quickly (well…sometimes not so quickly when it’s “thinking”). It feels like I can ask it absolutely anything, and I don’t have the weird anxiety of seeing tokens ticking up inexorably (I know the tokens are still being used in the background…but I don’t want to see this).</p>

<p>I use it for a lot of different things. It excels at explaining concepts and code in simple terms, especially because you can ask it questions and clarifications until it explains things in the way that helps you understand. I use it often to explain how things work in ruby, rails, SQL, or Ember, and most of the time it’s fairly accurate (though its Ember knowledge can be quite outdated, I trust that the least). It’s excellent for writing little onceoff scripts that only I am ever going to use, especially in languages like bash where I know a little but not enough to write a full script, or I don’t want to spend a bunch of time doing something that AI can do in seconds.</p>

<p>Another thing I use it often for is for converting documents or data into different formats, for example generating a markdown table from a JSON list. It’s good at generating example data, and for generating names of things, or suggesting variations on a word when you are stuck on what to call something, which is a problem in both writing and programming.</p>

<p>A few examples of different things I’ve asked it to help with recently:</p>

<ul>
  <li>Updating a bash script I have for running Discourse specs to take into account symlinks to plugins</li>
  <li>Writing a recursive Ember component (i.e. one that calls itself inside itself)</li>
  <li>How to auto-reload <a href="https://wiki.hypr.land/Hypr-Ecosystem/hyprpaper/">hyprpapr</a> config</li>
  <li>Helping me convert my neovim LSP config to the new <code class="language-plaintext highlighter-rouge">vim.lsp.config</code> format</li>
  <li>Converting <a href="https://github.com/L3MON4D3/LuaSnip">LuaSnip</a> snippets into the JSON format needed for zed</li>
</ul>

<p>I used it <a href="/cleaning-up-this-blog-with-ai">heavily to clean up this blog</a> and I don’t see my usage slowing down much anytime soon, at least until I have a nice neovim integration for the coding-related questions and jobs that I give it.</p>

<p>At Discourse we have several web-based AI tools built into the product. I think the killer feature here is our <a href="https://meta.discourse.org/t/discourse-ai-spam-detection/343541">AI-based spam detection tooling</a>, which I don’t really need to use in my day-to-day, but I know it helps our moderators a lot. Our search and related topic enhancements are the things I use the most – it is quite difficult to make a normal <code class="language-plaintext highlighter-rouge">tsvector</code>-based search that works reliably for all queries, and AI is a great enhancement here, and since it is excellent and finding patterns and relations between things, the recommendations it gives for similar topics are quite accurate.</p>

<p>We also have <a href="https://ask.discourse.com/">Ask Discourse</a> using AI helpers which can also be used on any Discourse site, and I find this useful from time to time to ask questions about certain parts of the Discourse software that I am unfamiliar or for help with configuration, though like any AI tool things like this are only as good as the backing documentation.</p>

<p>Overall the web-based AI tooling I use the least is summarisation. I feel like I have to be too on-guard for incorrect information or inconsistencies in the summaries, and so I do not trust them. AI is still too prone to hallucination and making up links to things that don’t make sense or don’t exist. I am a fast reader anyway, and most of the time I would much prefer to read the source material on my own and understand the original text.</p>

<p>I also do not use AI for writing. Writing is thinking, and writing is a creative process for me. I value my own voice, and I do not want AI’s milquetoast voice to override my own. This is not something I need in my life.</p>

<h3 id="agentic-ai">Agentic AI</h3>

<p>Claude code is by far my most heavily used tool here. I haven’t tried OpenAI Codex yet, but I suppose I should at some point. At Discourse, we use Claude backed by Amazon Bedrock, so individual engineers don’t really need to worry about token use or costs. Generally I use Claude for bugfixes and explanations of existing code, sometimes refactoring but sometimes that feels “wasteful” or overkill for very small refactors. This is where I would like a better neovim system to perform a refactor on a selection of lines.</p>

<p>Claude is extremely useful for “grunt” work. For example, we have <a href="https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780?tl=en">admin UI guidelines and classes</a> and so on for tables at discourse. I made a Glimmer component with a basic HTML table while experimenting, then I could tell Claude to look at existing examples from other Glimmer components in our admin UI and give it a link to the Meta docs to reference so Claude can read them. Claude can then go and inspect these resources and bully the table into shape, it worked extremely well for this.</p>

<p>MCP in Claude can be very useful. We have a <a href="https://github.com/discourse/discourse-mcp">Discourse MCP</a> which can be used to create various resources or to search, read topics, or list users and so on. This can be extremely helpful in local engineering development where you need to create a lot of records fast with natural language. There is a <a href="https://github.com/microsoft/playwright-mcp">Playwright MCP</a> from Microsoft that can be used to control the browser in cases where you need Claude to debug web-based JS errors, logs, or network requests, though I did notice that this seems to use a <em>ton</em> of tokens. Another MCP that is useful as an engineer is the <a href="https://github.com/crystaldba/postgres-mcp">PostgreSQL MCP</a> which allows Claude to inspect the state of a database, though I haven’t used this one much just yet.</p>

<p>What I haven’t had a lot of experience with yet, and the tooling I am most skeptical of, is the “unsupervised” agentic AI where you give it a prompt and free reign to use whatever tooling it wants in a loop, then come back <em>a few hours later</em> to a fully realised changeset. I am only getting a start in this area, so I don’t have too much to comment on, but the results I have gotten so far from <a href="https://jules.google/">Jules</a> have been underwhelming to say the least. Also, I struggle to find things that I would <em>want</em> AI to go off and do itself outside of small bugfixes and chore maintenance work like version bumps. If it’s something more in-depth or creative, I would rather do it myself, since I enjoy that part of software engineering.</p>

<p>I know there are also tools like <a href="https://openai.com/codex/">OpenAI Codex</a> and the <a href="https://docs.github.com/en/enterprise-cloud@latest/copilot/concepts/agents/coding-agent/about-coding-agent">GitHub CoPilot agent</a> that fit into this niche of AI tooling, so I want to find some opportunities to try these out and see what kind of result I can get.</p>

<h3 id="code-review">Code review</h3>

<p>I do a lot of code reviews in my role as tech lead for my own product team, but also general reviews for other engineers and designers at Discourse. By far I think this is the place where AI has had the most impact, in that AI-generated PRs push a lot of work onto the reviewer, rather than front-loading it onto the individual doing the work. I find this similar to editing someone else’s writing, in that it feels like you are not getting the full picture, and you need to try and reconstruct what the other person was thinking in your own mind to review their work.</p>

<p>This is not really possible with AI generated work though…because although AI tools say “Thinking…” now, they are not really. That dimension is missing. AI tools live in a temporary existence without a history or learned lessons that a human has built up over the years. There is no-one behind the end product to relate to, no-one to trust. AI doesn’t care about whatever poor sod is going to be responsible for reading or maintaining its output. AI tools produce a lot of output, but rarely remove or self-edit their output, so the end result is that these tools make it very easy to make a huge volume of generated code or writing (also known as <em>slop</em>) without much thought.</p>

<p>Even when it’s generating code that I requested, I still find reviewing AI-generated code much more time consuming and effortful to review than something generated by myself or another person. It feels like you always need to remind it of the same things, and even when you do it tries to be lazy or forgets what you told it partway through. A commenter on Hacker News put this quite nicely:</p>

<blockquote>
  <p>When it comes to agentically generated code every review feels like interacting with a brand new coworker and need to be vigilant about sneaky stuff</p>
</blockquote>

<p>The caveat here is that I don’t think there is anything inherently <em>wrong</em> with using AI to assist you in writing your PRs, as I’ve covered earlier they are valuable tools that can be integrated in various ways into daily engineering work. What <em>is</em> wrong is generating a PR completely with AI and lobbing it over the fence, making it someone else’s problem, especially without communicating that the work was done solely by AI.</p>

<p>It is still critically important that engineers using AI tooling and expecting colleagues to review it do the following:</p>

<ul>
  <li>Understand the output of the tooling and stand behind it. No hiding behind “Oh claude did this, I don’t get it but it’s fine, let’s merge”.</li>
  <li>Ensure everything is thoroughly tested with both automated and manual testing.</li>
  <li>Make sure telltale signs of AI use are removed, like excessive comments.</li>
  <li>Ensure the code matches the coding style of the rest of the codebase, and any linting rules and UI guidelines that may be defined.</li>
  <li>Recognize that the reviewer needs more context for this work, so add extra PR comments and a great PR description (one generated by AI doesn’t count) that covers the <em>why</em> and not only the <em>what</em> to help with review.</li>
  <li>Anticipate more comments than usual from the reviewer, and be ready to respond to them with an open mind.</li>
</ul>

<p>Even before AI, PR etiquette was sometimes lacking between engineers. We all need to remember that the sole goal of software engineering is not the code output – we all work with other humans that must understand and support our code, and see the history and context behind our decisions, even when we are not around. Code review is supposed to be a back and forth where we learn from one another and make the end result better, not just a chore in the way of steamrolling new code into production.</p>

<p>Maybe over time AI generated code will become easier to review (maybe even with AI-assisted review tools, which seem to be lacking right now), but that remains to be seen. There is still no substitute for a considered PR description and an attentive contributor.</p>

<h3 id="daily-impact">Daily impact</h3>

<p>Generally my daily work is broken down into a few categories:</p>

<ul>
  <li>Reviewing code</li>
  <li>Investigating bugs and fixing them</li>
  <li>Writing creative or novel code to solve a business problem</li>
  <li>Reading and writing product specs and TODOs</li>
  <li>Communicating with my colleagues</li>
</ul>

<p>Out of all these AI has had the most impact on reviewing code (which I cover above) and investigating and fixing bugs. So far the things that AI has helped with the most are clarifications, grunt work, helping with tools or languages I don’t work with often, and so on. I don’t want to have to remember the options of <code class="language-plaintext highlighter-rouge">sed</code> or all the different syntax for writing once-off bash scripts. AI can be great to help you identify a tricky bug, or to do things like write out specs that do not exist or will aid in the identification of a bug. AI is great at recognizing patterns and inconsistencies in those patterns, or missing or invalid data, which are common sources of bugs.</p>

<p>As I’ve already covered I won’t use it for writing, as writing is thinking, in the same way that programming is thinking. It is very hard to know if you’ve ended up at the right place if you never were a participant on the journey that took you there. So much of any deep or creative work is going down dead ends and eliminating things that don’t work, rather than only doing the thing that does work right from the get-go. Especially in an async work environment like Discourse where communication is paramount, you need to think about what you are trying to get across to your colleagues. If you can’t be bothered to write something yourself, or solve a problem with code yourself, why should anybody else be bothered to read it?</p>

<p>In the case of other creative or novel work, it doesn’t really feel satisfying in the slightest to use AI to completely do something for me. The only times where it feels <em>okay</em> to use completely are in the rare cases where it was something I really didn’t want to do or felt very painful to do, or something I didn’t feel was particularly worthwhile to spend my time on. For things I do want to spend my time on that I find fun or interesting, I don’t know why I would ever give that over to AI.</p>

<p>Another use of AI that impacts our whole team is the use of AI to generate UI prototypes and rough demos and implementations of features. AI is able to generate something reasonable-looking that at first glance may look impressive or appear to solve the problem on the surface, but under scrutiny can fall apart. This AI output isn’t well-considered in the same way that it would be if an actual designer, product manager, or engineer worked on the concept with their own skill and experience, and it removes some capacity for independent learning and growth.</p>

<p>As long as we are clear and treat these kind of demos and prototypes as just that – a proof of concept that is entirely disposable – this will not become too much of a problem. But it is <em>mighty</em> tempting to see something 75% of the way there and think that the last 25% is easy to finish, or in any way the same as a human doing the first 75%.</p>

<h3 id="conclusion">Conclusion</h3>

<p>AI is a tool that has the potential to be terribly misused by the wielder, but that does not make it a bad tool per se. I don’t believe the greater hype bubble around AI or that we are going to get something like AGI anytime soon, but over the past year or two it has become an indispensable tool in my day-to-day work as a staff engineer and tech lead. We all need to <em>tread lightly</em> when using AI, and not forget the human factor in our work.</p>

<p>Some recent articles on the subject that I’ve found interesting:</p>

<ul>
  <li><a href="https://hojberg.xyz/the-programmer-identity-crisis/">https://hojberg.xyz/the-programmer-identity-crisis/</a></li>
  <li><a href="https://simonwillison.net/2025/Oct/5/parallel-coding-agents/">https://simonwillison.net/2025/Oct/5/parallel-coding-agents/</a></li>
  <li><a href="https://simonwillison.net/2025/Oct/7/vibe-engineering/">https://simonwillison.net/2025/Oct/7/vibe-engineering/</a></li>
  <li><a href="https://hbr.org/2025/09/ai-generated-workslop-is-destroying-productivity">https://hbr.org/2025/09/ai-generated-workslop-is-destroying-productivity</a></li>
  <li><a href="https://www.itsnicethat.com/features/the-productivity-paradox-light-and-shade-digital-220925">https://www.itsnicethat.com/features/the-productivity-paradox-light-and-shade-digital-220925</a></li>
</ul>

<p>Title reference: The art critic Jerry Saltz wrote this article in 2020 about his life, career, and habits and I find it strangely compelling <a href="https://www.vulture.com/2020/05/jerry-saltz-my-appetites.html">https://www.vulture.com/2020/05/jerry-saltz-my-appetites.html</a></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[I’d say my overall attitude to using AI for software engineering is still in the “cautious” phase (it gives me the ick when people say bullish and bearish), but more and more I am integrating it into my day-to-day work and responsibilities at Discourse. In this article, I will go into some things that I am finding AI tooling useful for, and some areas where I wish it would improve, or that I am still finding my feet on. I will also cover how the prevalence of AI affects me as a tech lead and how it impacts our workflows on a product development team. So we are on the same page, I spent most of my time writing Ruby, Rails, JavaScript, Ember, and SQL code.]]></summary></entry><entry><title type="html">Cleaning up this blog with AI</title><link href="http://www.martin-brennan.com/cleaning-up-this-blog-with-ai/" rel="alternate" type="text/html" title="Cleaning up this blog with AI" /><published>2025-09-14T00:00:00-04:00</published><updated>2025-09-14T00:00:00-04:00</updated><id>http://www.martin-brennan.com/cleaning-up-this-blog-with-ai</id><content type="html" xml:base="http://www.martin-brennan.com/cleaning-up-this-blog-with-ai/"><![CDATA[<p>It has been several years since I wrote a post on this blog. Part of this is not really feeling like something I have worth articulating here (though that is changing a little lately), but a bigger part is that it felt like a huge amount of work to get this blog into a state where I didn’t feel like it was full of outdated articles and junk metadata. Something a little more current.</p>

<p>Well, we now have a tool that all of us can use to help ease this kind of menial work: AI. So, using ChatGPT, I cleaned house and wrote this article to cover how I did it.</p>

<!--more-->

<p>First, after reviewing the articles I had, and considering the long gap I’ve had since last writing, I wanted to figure out what to do with articles I didn’t consider relevant or articles that no longer matched my writing style, or synced with my current opinions and thoughts. So I sent this prompt to ChatGPT:</p>

<blockquote>
  <p>what should i do for a technical blog where i have a bunch of posts from pre-2019 that aren’t very relevant anymore technically, or don’t match the tone of my writing anymore, or that i find not useful for a variety of other reasons. should i delete the posts? will google punish me if i do? should i archive them in some way and start fresh? not sure…</p>
</blockquote>

<p>It waffled on a bit, but at the core of it the recommendation was this:</p>

<ul>
  <li>Keep &amp; refresh posts with value</li>
  <li>Archive or add disclaimers for posts with historical interest but outdated info</li>
  <li>Delete and redirect or respond with 410 Gone for those with no value, no traffic, no links</li>
</ul>

<p>I had already been doing this a little with a “deprecation” warning on extremely old and irrelevant posts like <a href="/php-mysql-and-ftp-on-an-amazon-ec2-instance/">this one I wrote in 2012</a>, but this time I wanted to do this for a lot more of my articles. I then asked ChatGPT for a recommendation on the process behind this to see if we were on the same page, and we were. I don’t need to paste in what it said here, it was quite long but pretty much repeated the above.</p>

<p>Next, I exported the last 3 years of traffic for this blog from Google Analytics, ordered by view count, including the title and other information, then asked this, attaching the Google Sheet for context for ChatGPT, then gave it this prompt:</p>

<blockquote>
  <p>build me a spreadsheet template. also look through the list of my posts and identify ones where it’s likely i should delete/archive it and not show it on the main list of blog posts etc. Here are the view stats for the past 3 years attached as a google sheet. also dont just take into account views but based on title/content on whether it should be outdated like you found with angular earlier</p>
</blockquote>

<p>At first it just did a light once-over and gave me a few recommendations on selected posts. However, what I really wanted here was for it to give me a full CSV with a recommendation on what to do for every post on my blog. It delivered, but unfortunately…here it got a bit confused. The recommendations often didn’t match the URLs, and the recommendations weren’t really in depth.</p>

<p>Here ChatGPT started to go off the rails a bit…I kept asking it to do different things with the information, giving it less source information in the spreadsheet I provided, and asked it to go scrape every article so it could give me a proper informed recommendation. There was a lot of “Thinking…” and back and forth. Eventually, after some context resets, I got it to output a spreadsheet like this with some basic recommendation and reasoning:</p>

<p><img src="/images/chatgpt_recommendation_spreadsheet.png" alt="ChatGPT recommendation spreadsheet" /></p>

<p>It’s not perfect, but it gave me a solid enough base to go on. It’s really important to know when to call it quits with an AI chat, where it starts to lose its marbles and extra instructions and context aren’t helping. Often starting a new chat and seeding it with initial data from the old one is all you need to get things moving again.</p>

<h3 id="working-through-the-spreadsheet">Working through the spreadsheet</h3>

<p>The next step I took was to go through this list one by one, look at the recommendation and reasoning, and alter the source post in markdown. Sometimes I didn’t agree with the recommendation, or thought it overly harsh. Call me sentimental but I didn’t want to delete half my posts. So, most of the time, I went and added a deprecation with a liquid template <code class="language-plaintext highlighter-rouge">include</code> like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>{% include deprecated.html message="There are probably better ways to do this now, especially if you are using https://playwright.dev/ . This article also references many deprecated browser APIs. Leaving this as a historical curiosity." cssclass="deprecated" %}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For many of the articles, I knew there was likely a better way to do something, especially in cases for articles 5-10+ years old, but I didn’t want to spend too much time researching it. So, I fed certain articles here into ChatGPT (it’s already surely ingested them, so no harm really) and asked it for advice on anything that should be tweaked, or in some cases I asked it to recommend a deprecation warning to place at the top of the article.</p>

<p>For example for <a href="http://martin-brennan.com/rails-global-rescue-from-error-application-controller/">Global rescue_from error in Rails application_controller</a>, it gave me these recommendations:</p>

<p><img src="/images/ai_cleanup_recommendations.png" alt="AI cleanup recommendations" /></p>

<p>And so I went through and made some alterations, but I didn’t add a deprecation. In other cases, I did ask it for a deprecation, for longer articles giving it the URL directly. For example for <a href="https://martin-brennan.com/custom-time-formats-in-rails/">Custom time formats in rails</a>, I asked it for a deprecation if necessary. Of course it spat out an overly verbose one:</p>

<p><img src="/images/ai_deprecation_recommendation.png" alt="AI deprecation recommendation" /></p>

<p>So I just condensed it down to this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>{% include deprecated.html message="In 2025 this still works in Rails, but it is likely better to rely on other methods like defining time formats in the Rails I18n system yaml files and using &lt;code&gt;I18n.l&lt;/code&gt; rather than &lt;code&gt;Time.to_fs(:format)&lt;/code&gt;" cssclass="deprecated" %}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Of course, with its recommendations I did go and double check with a brief bit of research to make sure it wasn’t pulling my leg, or giving outdated advice itself, and it was generally accurate. This system helped a <em>lot</em> for programming languages I haven’t touched or kept up with, like C#, for things like <a href="/sharing-dynamic-objects-between-assemblies-csharp/">Sharing dynamic objects between assemblies in C#</a></p>

<h3 id="fixing-encoding-issues">Fixing encoding issues</h3>

<p>Another issue I found was with encoding in many of the markdown posts. Maybe this was a relic of the <a href="/from-wordpress-to-jekyll/">move from wordpress</a>, and it didn’t really affect rendering, but it made the markdown source very ugly, especially within source code blocks. So I asked ChatGPT to give me a script to fix this across all the markdown files. It is perfect for boring manual labour like this:</p>

<blockquote>
  <p>i have an article with lots of encoded characters like <code class="language-plaintext highlighter-rouge">We&amp;#8217;ve</code> in vim markdown, can you write me some script or something to fix all of them? another example is also replacing <code class="language-plaintext highlighter-rouge">^M</code> chars and <code class="language-plaintext highlighter-rouge">12&amp;#8211;15</code></p>
</blockquote>

<p>Here is the script it spat out. Is it nice python? I don’t know and I don’t care, but when I ran it it worked just fine, so problem solved:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="n">html</span><span class="p">,</span> <span class="n">pathlib</span>

<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">pathlib</span><span class="p">.</span><span class="nc">Path</span><span class="p">(</span><span class="sh">"</span><span class="s">.</span><span class="sh">"</span><span class="p">).</span><span class="nf">rglob</span><span class="p">(</span><span class="sh">"</span><span class="s">*.md</span><span class="sh">"</span><span class="p">):</span>
    <span class="n">raw</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="nf">read_bytes</span><span class="p">()</span>
    <span class="c1"># normalize line endings: CRLF/CR -&gt; LF (removes ^M)
</span>    <span class="n">raw</span> <span class="o">=</span> <span class="n">raw</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sa">b</span><span class="sh">"</span><span class="se">\r\n</span><span class="sh">"</span><span class="p">,</span> <span class="sa">b</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">).</span><span class="nf">replace</span><span class="p">(</span><span class="sa">b</span><span class="sh">"</span><span class="se">\r</span><span class="sh">"</span><span class="p">,</span> <span class="sa">b</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">text</span> <span class="o">=</span> <span class="n">raw</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="sh">"</span><span class="s">replace</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">text</span> <span class="o">=</span> <span class="n">html</span><span class="p">.</span><span class="nf">unescape</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>  <span class="c1"># decode &amp;amp; &amp;#8217; etc.
</span>    <span class="n">p</span><span class="p">.</span><span class="nf">write_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">fixed </span><span class="si">{</span><span class="n">p</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I could have probably told it to do it in ruby so I could read it a bit better, but really I was using AI to generate me a hammer so I could smash an annoying nail with it, so it didn’t really matter.</p>

<h3 id="deleting-old-junk-metadata">Deleting old junk metadata</h3>

<p>There was also a bunch of ancient metadata in the markdown source. Like for example these mashsb attributes, which are from a <a href="https://mashshare.net/">Wordpress social media sharing tool</a> that is no longer in use, and which is totally irrelevant for Jekyll anyway:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="na">mashsb_timestamp</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="m">1464957941</span>
<span class="na">mashsb_shares</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="m">0</span>
<span class="na">mashsb_jsonshares</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s1">'</span><span class="s">{"total":0}'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Again, I asked ChatGPT. At first I asked it for some neovim script, but I kept having weird issues with Lualine with the code it provided, so I asked it to just do a unix script of some kind instead, and it ended up using <code class="language-plaintext highlighter-rouge">sed</code>, which worked great:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">''</span> <span class="nt">-e</span> <span class="s1">'/^mashsb_timestamp:/,+1d'</span> <span class="se">\</span>
          <span class="nt">-e</span> <span class="s1">'/^mashsb_shares:/,+1d'</span> <span class="se">\</span>
          <span class="nt">-e</span> <span class="s1">'/^mashsb_jsonshares:/,+1d'</span> <span class="k">*</span>.md
</pre></td></tr></tbody></table></code></pre></div></div>

<p>There were several different chunks of metadata like this – I didn’t need to use AI more, I just modified the original script:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">''</span> <span class="nt">-e</span> <span class="s1">'/^guid:/d'</span> <span class="k">*</span>.md
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Or this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">''</span> <span class="nt">-e</span> <span class="s1">'/^iconcategory:/,+1d'</span> <span class="k">*</span>.md
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I could have eventually gotten there without AI, but who really wants to memorize a bunch of <code class="language-plaintext highlighter-rouge">sed</code> syntax?</p>

<h3 id="excluding-posts-from-the-jekyll-feed-based-on-metadata">Excluding posts from the Jekyll feed based on metadata</h3>

<p>The next thing I wanted to do, which is related to the following section of this article, is hide certain articles from a) the main feed of this blog and b) in certain cases, where I will be redirecting with a <code class="language-plaintext highlighter-rouge">410 Gone</code>, from the <a href="/archive">archive</a> page. I knew there was a way to do this in Jekyll, but couldn’t be bothered to figure it out myself (omitted the code block I sent to ChatGPT):</p>

<blockquote>
  <p>i want to change for post in paginator.posts to only get a subset of posts, i.e. not ones with a certain front matter attribute, how can i do this?</p>
</blockquote>

<p>I forgot that all yaml front matter metadata for Jekyll is attached to <code class="language-plaintext highlighter-rouge">post</code>, so I could just do something like this:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="cp">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="cp">%}</span>
  <span class="cp">{%</span><span class="w"> </span><span class="nt">unless</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">exclude_from_feed</span><span class="w"> </span><span class="cp">%}</span>
    &lt;!-- post content here --&gt;
  <span class="cp">{%</span><span class="w"> </span><span class="nt">endunless</span><span class="w"> </span><span class="cp">%}</span>
<span class="cp">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="cp">%}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On both the main post feed and on the archive page. Then, I went and added <code class="language-plaintext highlighter-rouge">exclude_from_feed: true</code> and <code class="language-plaintext highlighter-rouge">exclude_from_archive: true</code> to the relevant posts.</p>

<h3 id="figuring-out-how-to-respond-with-410-gone-for-irrelevant-posts">Figuring out how to respond with 410 Gone for irrelevant posts</h3>

<p>In the first step, ChatGPT recommended responding with 410 Gone for “deleted” posts. This code is <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/410">defined by MDN</a> like so:</p>

<blockquote>
  <p>The HTTP 410 Gone client error response status code indicates that the target resource is no longer available at the origin server and that this condition is likely to be permanent. A 410 response is cacheable by default.</p>
</blockquote>

<blockquote>
  <p>Clients should not repeat requests for resources that return a 410 response, and website owners should remove or replace links that return this code.</p>
</blockquote>

<p>Whereas a 404 can indicate that the condition of the missing article is only temporary, and 301 redirects to a new location, which in my case was not necessary. I wanted these posts to no longer exist when Google’s little crawler bots go poking around for it.</p>

<p>Naturally I didn’t have a clue how to do this with Jekyll, and I couldn’t even remember the server that was running for my blog’s requests on the DigitalOcean droplet. First I asked ChatGPT:</p>

<blockquote>
  <p>is there a way to serve 410 gone for certain jekyll blog posts</p>
</blockquote>

<p>It gave me a ton of different ways to do it based on hosting provider. Then, I sheepishly asked:</p>

<blockquote>
  <p>im pretty sure i use nginx on a digitalocean droplet, how can i check if nginx is being used / is running</p>
</blockquote>

<p>And it gave me the most obvious answer in the world which I definitely should have thought of straight away. Sometimes, when you are set on using a tool…you use that tool instead of the more obvious method in front of you.</p>

<blockquote>
  <table>
    <tbody>
      <tr>
        <td>ps aux</td>
        <td>grep nginx</td>
      </tr>
    </tbody>
  </table>
</blockquote>

<p>So yes I was indeed running nginx. Knowing that I was running nginx, ChatGPT gave me several great options on how I could use it to serve 410 Gone on certain URLs:</p>

<p><img src="/images/ai_410_option_1.png" alt="AI 410 gone option 1" /> <img src="/images/ai_410_option_2.png" alt="AI 410 gone option 2" /></p>

<p>I wanted it to be super easy, and I wanted it to be easy to do via git. So, I chose the second option, where I could commit a <code class="language-plaintext highlighter-rouge">gone.conf</code> file to the repo, and then read it from nginx:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>server {
  # ...
  include /var/www/your-site/current/nginx/gone.conf;
  # ...
}
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The only downside is that I will have to log in and restart nginx whenever I add an entry here. However, I won’t be doing it often after this initial batch, so it’s no big deal. This worked perfectly! I even had AI generate a script I could run to extract the permalinks from all the posts I marked as hide from archive and morph them into the nginx config format I needed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>location = /a-baseline-for-frontend-developers-by-rebecca-murphey/ { return 410; }
location = /callback-hell/ { return 410; }
location = /creating-share-buttons-with-just-urls/ { return 410; }
location = /goodbye-requirejs-hello-browserify/ { return 410; }
... and so on
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="removing-ads">Removing ads</h3>

<p>This isn’t strictly related to AI, but as I was working on this blog I noticed how awful the experience felt with ads on. I had way too many, they got in the way, they were irrelevant, and what’s worse is that they didn’t serve a purpose – I haven’t made any money from this blog in years, and even when I did it was $100 every few years. Not really worth it for a worse experience.</p>

<p>So, I ripped them out, and it feels a lot nicer. Maybe at some point in future I may introduce them again, but not until there is actually a level of traffic that makes it worth it.</p>

<h3 id="whats-next">What’s next?</h3>

<p>I plan on doing another refresh of this blog’s styling and layout. I want to use AI to completely rework the CSS of this site, along with the HTML templates. I have no idea what a lot of the CSS is doing or where it’s located, and navigating it is trying to traverse the warp without a Gellar field. Maybe I will use something like tailwind, but I’m not quite decided on that. I have never addressed the CSS mess I have here, since it seems such a daunting task, but with AI maybe it will become bearable.</p>

<p>I have already started using ChatGPT to find me examples of other technical blogs from staff engineers that I can draw inspiration from, but with varying levels of success thusfar. Not exactly sure where I am going yet, but I want things to have a more up-to-date feel, and I want code blocks and syntax highlighting to feel a lot better. Maybe present images better with captions and lightboxing. We will see where I end up…</p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[It has been several years since I wrote a post on this blog. Part of this is not really feeling like something I have worth articulating here (though that is changing a little lately), but a bigger part is that it felt like a huge amount of work to get this blog into a state where I didn’t feel like it was full of outdated articles and junk metadata. Something a little more current. Well, we now have a tool that all of us can use to help ease this kind of menial work: AI. So, using ChatGPT, I cleaned house and wrote this article to cover how I did it.]]></summary></entry><entry><title type="html">Batch convert HEIC files to JPEG on Windows</title><link href="http://www.martin-brennan.com/batch-convert-heic-files-to-jpeg-on-windows/" rel="alternate" type="text/html" title="Batch convert HEIC files to JPEG on Windows" /><published>2019-12-23T00:00:00-05:00</published><updated>2019-12-23T00:00:00-05:00</updated><id>http://www.martin-brennan.com/batch-convert-heic-files-to-jpeg-on-windows</id><content type="html" xml:base="http://www.martin-brennan.com/batch-convert-heic-files-to-jpeg-on-windows/"><![CDATA[<div class="alert danger">
  I doubt this is still relevant in 2025.
</div>

<p>I got a new iPhone recently, and getting the photos off of it onto my PC in a way in which Lightroom could import them was a painful experience to say the least. There are a few settings on the iPhone now which cause the photos to be stored in a new HEIC format from Apple, instead of plain old JPEG. When connecting my iPhone to my Windows PC I had all sorts of issues with the PC even recognizing the phone as a storage device. To do so I had to turn off the automatic conversion of photos from HEIC while copying to the PC, and also disable the automatic compression to HEIC entirely, and turn off some iCloud settings.</p>

<div class="alert danger">
  I initially recommended IfranView here. DO NOT USE IT; it strips all metadata from the photo files when converting to JPEG, making them useless in Lightroom.
</div>

<p>EVENTUALLY I ended up with all the HEIC photos from my phone on my PC and I needed a way to convert them to JPEG en masse so I could get them into my old version of Lightroom. I used a program called <a href="http://converseen.fasterland.net/">Converseen</a> for the batch conversion. I was a little wary of it at first because it looked like one of those shady websites which trick you into downloading crap and often show up high on Google results. However the software worked great and didn’t install anything untoward. I just had to add the HEIC images and I could select an output format and destination, and the whole thing converted fine.</p>

<p>And that was it! The conversion took a little while but I was left with good old JPEG files.</p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[I doubt this is still relevant in 2025. I got a new iPhone recently, and getting the photos off of it onto my PC in a way in which Lightroom could import them was a painful experience to say the least. There are a few settings on the iPhone now which cause the photos to be stored in a new HEIC format from Apple, instead of plain old JPEG. When connecting my iPhone to my Windows PC I had all sorts of issues with the PC even recognizing the phone as a storage device. To do so I had to turn off the automatic conversion of photos from HEIC while copying to the PC, and also disable the automatic compression to HEIC entirely, and turn off some iCloud settings. I initially recommended IfranView here. DO NOT USE IT; it strips all metadata from the photo files when converting to JPEG, making them useless in Lightroom. EVENTUALLY I ended up with all the HEIC photos from my phone on my PC and I needed a way to convert them to JPEG en masse so I could get them into my old version of Lightroom. I used a program called Converseen for the batch conversion. I was a little wary of it at first because it looked like one of those shady websites which trick you into downloading crap and often show up high on Google results. However the software worked great and didn’t install anything untoward. I just had to add the HEIC images and I could select an output format and destination, and the whole thing converted fine. And that was it! The conversion took a little while but I was left with good old JPEG files.]]></summary></entry><entry><title type="html">HiDPI fix for Spotify on Ubuntu</title><link href="http://www.martin-brennan.com/hidpi-fix-for-spotify-on-ubuntu/" rel="alternate" type="text/html" title="HiDPI fix for Spotify on Ubuntu" /><published>2019-12-21T00:00:00-05:00</published><updated>2019-12-21T00:00:00-05:00</updated><id>http://www.martin-brennan.com/hidpi-fix-for-spotify-on-ubuntu</id><content type="html" xml:base="http://www.martin-brennan.com/hidpi-fix-for-spotify-on-ubuntu/"><![CDATA[<div class="alert deprecated">
  In 2025 this is likely irrelevant, leaving it up for historical curiosity only.
</div>

<p>There are many HiDPI/4K scaling issues on Ubuntu and Linux in general, and one of the most annoying is that Spotify becomes a music player for ants. To fix the tiny UI, you can first find where Spotify is installed on Ubuntu by searching for it in Applications, right clicking on the application and clicking properties. A couple of examples of what the file location could be:</p>

<ul>
  <li>/var/lib/snapd/desktop/applications/spotify_spotify.desktop</li>
  <li>/home/martin/.local/share/applications/spotify.desktop</li>
</ul>

<p>If you edit the file in vim you will see an Exec command that looks something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>Exec=spotify %U
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You can add <code class="language-plaintext highlighter-rouge">--force-device-scale-factor=1.5</code> to the command and this will fix the HiDPI scaling issue for you. You will just have to relaunch Spotify for this to take effect. See <a href="https://community.spotify.com/t5/Desktop-Linux/Linux-client-barely-usable-on-HiDPI-displays/td-p/1067272">https://community.spotify.com/t5/Desktop-Linux/Linux-client-barely-usable-on-HiDPI-displays/td-p/1067272</a> for more information.</p>

<p>After a while I updated Spotify and I got a newer version installed using snap. If you are using the newer snap install you do the same, only in the /var/lib/snapd/desktop/applications/ directory which will be shown in the spotify.desktop file. For example the snap Exec command may look something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>Exec=env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/spotify_spotify.desktop /snap/bin/spotify --force-device-scale-factor=1.5 %U
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For more information on the snap fix check out this post <a href="https://community.spotify.com/t5/Desktop-Linux/Spotify-Hi-DPI-Fix-for-Snap-install/td-p/4576328">https://community.spotify.com/t5/Desktop-Linux/Spotify-Hi-DPI-Fix-for-Snap-install/td-p/4576328</a>.</p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[In 2025 this is likely irrelevant, leaving it up for historical curiosity only. There are many HiDPI/4K scaling issues on Ubuntu and Linux in general, and one of the most annoying is that Spotify becomes a music player for ants. To fix the tiny UI, you can first find where Spotify is installed on Ubuntu by searching for it in Applications, right clicking on the application and clicking properties. A couple of examples of what the file location could be: /var/lib/snapd/desktop/applications/spotify_spotify.desktop /home/martin/.local/share/applications/spotify.desktop If you edit the file in vim you will see an Exec command that looks something like this: 1 Exec=spotify %U You can add --force-device-scale-factor=1.5 to the command and this will fix the HiDPI scaling issue for you. You will just have to relaunch Spotify for this to take effect. See https://community.spotify.com/t5/Desktop-Linux/Linux-client-barely-usable-on-HiDPI-displays/td-p/1067272 for more information. After a while I updated Spotify and I got a newer version installed using snap. If you are using the newer snap install you do the same, only in the /var/lib/snapd/desktop/applications/ directory which will be shown in the spotify.desktop file. For example the snap Exec command may look something like this: 1 Exec=env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/spotify_spotify.desktop /snap/bin/spotify --force-device-scale-factor=1.5 %U For more information on the snap fix check out this post https://community.spotify.com/t5/Desktop-Linux/Spotify-Hi-DPI-Fix-for-Snap-install/td-p/4576328.]]></summary></entry><entry><title type="html">Taming power-hungry linux laptops</title><link href="http://www.martin-brennan.com/buying-a-software-development-laptop-in-late-2019/" rel="alternate" type="text/html" title="Taming power-hungry linux laptops" /><published>2019-12-17T00:00:00-05:00</published><updated>2019-12-17T00:00:00-05:00</updated><id>http://www.martin-brennan.com/taming-power-hungry-linux-laptops</id><content type="html" xml:base="http://www.martin-brennan.com/buying-a-software-development-laptop-in-late-2019/"><![CDATA[<div class="alert deprecated">
  In 2025 I doubt this is helpful, leaving it up as a historical curiosity.
</div>

<p>My Discourse laptop is an XPS15 7590 with the following specs:</p>

<ul>
  <li>32 GB, 2 x 16 GB, DDR4, 2666 MHz</li>
  <li>1 TB M.2 PCIe NVMe Solid-State Drive</li>
  <li>NVIDIA® GeForce® GTX 1650 4GB GDDR5</li>
  <li>15.6” 4K UHD touch display</li>
</ul>

<p>Out of the box, after installing Ubuntu and KDE plasma, this thing on battery power acted like Daniel Plainview in There Will Be Blood.</p>

<p><img src="/images/idrinkyourmilkshake.gif" alt="i drink your milkshake" /></p>

<p>I got around 2 hours of battery life, 3 hours if I changed the resolution from 4k to 2k and dimmed the screen. The laptop was consuming between 40W and 50W of power. This isn’t great obviously, so I am posting here to list out some of the things I did to improve the battery life. The main secret is disabling the beastly graphics card which is not really needed for software development work in general.</p>

<ul>
  <li>Install <code class="language-plaintext highlighter-rouge">powertop</code> to get a summary of what is using all the power on your computer, as well as a general overview of the wattage being consumed. The output looks like this:</li>
</ul>

<p><img src="/images/powertopstart.png" alt="powertop start" /></p>

<ul>
  <li>
    <p>Run <code class="language-plaintext highlighter-rouge">sudo powertop --auto-tune</code> to automatically tune some of the settings for power, this will save you a small amount of watts.</p>
  </li>
  <li>
    <p>Install <code class="language-plaintext highlighter-rouge">nvidia-prime</code> which is used to disable the dedicated GPU. Run <code class="language-plaintext highlighter-rouge">sudo prime-select intel</code> to switch to the onboard graphics profile and reboot. This saved me about 10-15W off the bat.</p>
  </li>
  <li>
    <p>Install <code class="language-plaintext highlighter-rouge">tlp</code> which is a battery management tool for Linux. Then, all I did was start the service and apply the default power saving settings then rebooted.</p>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>sudo add-apt-repository ppa:linrunner/tlp
sudo apt-get update
sudo apt-get install tlp tlp-rdw 
sudo systemctl status tlp
sudo tlp start
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>Finally install Bumblebee which is more NVidia management stuff <code class="language-plaintext highlighter-rouge">sudo apt-get install bumblebee bumblebee-nvidia primus linux-headers-generic</code> and reboot. Then run <code class="language-plaintext highlighter-rouge">sudo tee /proc/acpi/bbswitch &lt;&lt;&lt;OFF</code> to completely turn off the NVidia card and reboot again. You may have to keep running this bbswitch command, I haven’t quite figured this part out yet. If it seems like my watt usage is really high I just run it again.</li>
</ul>

<p>After this was all done here was my final reading from powertop (the battery was already drained ~10-15% here):</p>

<p><img src="/images/powertopfinal.png" alt="powertop final" /></p>

<p>The wattage used went from 40W-50W to 10W-15W! And the battery life is now up to a more respectable ~6-7 hours. I was pretty stunned by this, thanks to Sam and the other Australian Discourse team at our Xmas lunch for convincing me that this kind of abuse of power is NOT OKAY and putting me onto disabling NVidia. Now all I have is…</p>

<p><img src="/images/unlimitedpower.gif" alt="unlimited power" /></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[In 2025 I doubt this is helpful, leaving it up as a historical curiosity. My Discourse laptop is an XPS15 7590 with the following specs: 32 GB, 2 x 16 GB, DDR4, 2666 MHz 1 TB M.2 PCIe NVMe Solid-State Drive NVIDIA® GeForce® GTX 1650 4GB GDDR5 15.6” 4K UHD touch display Out of the box, after installing Ubuntu and KDE plasma, this thing on battery power acted like Daniel Plainview in There Will Be Blood. I got around 2 hours of battery life, 3 hours if I changed the resolution from 4k to 2k and dimmed the screen. The laptop was consuming between 40W and 50W of power. This isn’t great obviously, so I am posting here to list out some of the things I did to improve the battery life. The main secret is disabling the beastly graphics card which is not really needed for software development work in general. Install powertop to get a summary of what is using all the power on your computer, as well as a general overview of the wattage being consumed. The output looks like this: Run sudo powertop --auto-tune to automatically tune some of the settings for power, this will save you a small amount of watts. Install nvidia-prime which is used to disable the dedicated GPU. Run sudo prime-select intel to switch to the onboard graphics profile and reboot. This saved me about 10-15W off the bat. Install tlp which is a battery management tool for Linux. Then, all I did was start the service and apply the default power saving settings then rebooted. 1 2 3 4 5 sudo add-apt-repository ppa:linrunner/tlp sudo apt-get update sudo apt-get install tlp tlp-rdw sudo systemctl status tlp sudo tlp start Finally install Bumblebee which is more NVidia management stuff sudo apt-get install bumblebee bumblebee-nvidia primus linux-headers-generic and reboot. Then run sudo tee /proc/acpi/bbswitch &lt;&lt;&lt;OFF to completely turn off the NVidia card and reboot again. You may have to keep running this bbswitch command, I haven’t quite figured this part out yet. If it seems like my watt usage is really high I just run it again. After this was all done here was my final reading from powertop (the battery was already drained ~10-15% here): The wattage used went from 40W-50W to 10W-15W! And the battery life is now up to a more respectable ~6-7 hours. I was pretty stunned by this, thanks to Sam and the other Australian Discourse team at our Xmas lunch for convincing me that this kind of abuse of power is NOT OKAY and putting me onto disabling NVidia. Now all I have is…]]></summary></entry><entry><title type="html">Six weeks at Discourse</title><link href="http://www.martin-brennan.com/six-weeks-at-discourse/" rel="alternate" type="text/html" title="Six weeks at Discourse" /><published>2019-12-10T00:00:00-05:00</published><updated>2019-12-10T00:00:00-05:00</updated><id>http://www.martin-brennan.com/six-weeks-at-discourse</id><content type="html" xml:base="http://www.martin-brennan.com/six-weeks-at-discourse/"><![CDATA[<p>I meant to make a post about this when I started, but I have now been working at <a href="https://discourse.org/">Discourse</a> as a Software Engineer for six weeks!</p>

<p><img src="/images/discourse.jpg" alt="discourse logo" /></p>

<p>The best things about working at Discourse are:</p>

<ul>
  <li>The team (39 people and growing, largest I have ever worked on) is fully remote and distributed across every continent! (well, except Antarctica)</li>
  <li>Asynchronous work culture has been the core of the company since its inception. Discourse bleeds asynchronously!</li>
  <li>The team is full of <a href="https://www.discourse.org/team">ridiculously talented people</a> who are generous with their knowledge. Everyone is a stellar written communicator.</li>
  <li>The codebase is fully open-source and is varied. On any day I can be working on the huge core product, a plugin, a theme component, or contributing to an open-source project Discourse uses.</li>
  <li>There is a <em>lot</em> of reading and a <em>lot</em> of writing, which is kind of a <a href="https://writing.martin-brennan.com">thing of mine</a>.</li>
  <li>Everyone is treated like grown-ups, and trusted to do their work on their own schedule.</li>
  <li>How much more time do you have? I can keep listing things!</li>
</ul>

<p>I am over the moon with this job and still pinch myself each day when I think about where I work and who I work with. 🌟</p>

<p><img src="/images/heylookatus.gif" alt="look at us" /></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[I meant to make a post about this when I started, but I have now been working at Discourse as a Software Engineer for six weeks! The best things about working at Discourse are: The team (39 people and growing, largest I have ever worked on) is fully remote and distributed across every continent! (well, except Antarctica) Asynchronous work culture has been the core of the company since its inception. Discourse bleeds asynchronously! The team is full of ridiculously talented people who are generous with their knowledge. Everyone is a stellar written communicator. The codebase is fully open-source and is varied. On any day I can be working on the huge core product, a plugin, a theme component, or contributing to an open-source project Discourse uses. There is a lot of reading and a lot of writing, which is kind of a thing of mine. Everyone is treated like grown-ups, and trusted to do their work on their own schedule. How much more time do you have? I can keep listing things! I am over the moon with this job and still pinch myself each day when I think about where I work and who I work with. 🌟]]></summary></entry><entry><title type="html">CSV header converters in Ruby</title><link href="http://www.martin-brennan.com/csv-header-converters-in-ruby/" rel="alternate" type="text/html" title="CSV header converters in Ruby" /><published>2019-10-06T00:00:00-04:00</published><updated>2019-10-06T00:00:00-04:00</updated><id>http://www.martin-brennan.com/csv-header-converters-in-ruby</id><content type="html" xml:base="http://www.martin-brennan.com/csv-header-converters-in-ruby/"><![CDATA[<p>The <a href="https://ruby-doc.org/3.4.1/gems/csv/CSV.html">CSV library in the Ruby stdlib</a> is a really great and easy to use one, and I’ve often used it for data migrations and imports. When importing data I often find it useful to validate the headers of the imported CSV, to ensure that valid columns are provided. Some users may provide columns in different cases to what you expect or with different punctuation (including spaces etc.). To normalize the headers when parsing a CSV, you can use an option passed to <a href="https://ruby-doc.org/3.4.1/gems/csv/CSV.html#method-c-new"><code class="language-plaintext highlighter-rouge">new</code></a> (other methods such a <code class="language-plaintext highlighter-rouge">parse</code>, <code class="language-plaintext highlighter-rouge">read</code>, and <code class="language-plaintext highlighter-rouge">foreach</code> accept the same options) called <code class="language-plaintext highlighter-rouge">header_converters</code>. Here is a simple example of how you can convert the headers of the parsed CSV to lowercase:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c1">###</span>
<span class="c1"># Source CSV looks like:</span>
<span class="c1">#</span>
<span class="c1"># First name,last Name,Email</span>
<span class="c1"># Abraham,Lincoln,alincoln@gmail.com</span>
<span class="c1"># George,Washington,gwashington@outlook.com</span>

<span class="n">downcase_converter</span> <span class="o">=</span> <span class="nb">lambda</span> <span class="p">{</span> <span class="o">|</span><span class="n">header</span><span class="o">|</span> <span class="n">header</span><span class="p">.</span><span class="nf">downcase</span> <span class="p">}</span>
<span class="n">parsed_csv</span> <span class="o">=</span> <span class="no">CSV</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s1">'/path/to/file.csv'</span><span class="p">,</span> <span class="ss">headers: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">header_converters: </span><span class="n">downcase_converter</span><span class="p">)</span>
<span class="n">parsed_csv</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">row</span><span class="o">|</span>
  <span class="nb">puts</span> <span class="n">row</span><span class="p">[</span><span class="s1">'first name'</span><span class="p">]</span>

  <span class="c1"># =&gt; Abraham</span>
  <span class="c1"># =&gt; George</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Simple as that. You can do anything to the headers here. There are also a couple of built in header converters (<code class="language-plaintext highlighter-rouge">:downcase</code> and <code class="language-plaintext highlighter-rouge">:symbol</code>) that can be used, and an array can be passed as an argument, not just one converter. Converters can also be used for cells in the CSV rows as well, not just headers. The documentation for the Ruby <code class="language-plaintext highlighter-rouge">CSV</code> class is quite clear and helpful, take a look to see all the other myriad options for reading and writing CSVs in Ruby.</p>

<p>Originally, I found this solution and tweaked it a bit from this StackOverflow answer - <a href="https://stackoverflow.com/questions/48894679/converting-csv-headers-to-be-case-insensitive-in-ruby">https://stackoverflow.com/questions/48894679/converting-csv-headers-to-be-case-insensitive-in-ruby</a></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[The CSV library in the Ruby stdlib is a really great and easy to use one, and I’ve often used it for data migrations and imports. When importing data I often find it useful to validate the headers of the imported CSV, to ensure that valid columns are provided. Some users may provide columns in different cases to what you expect or with different punctuation (including spaces etc.). To normalize the headers when parsing a CSV, you can use an option passed to new (other methods such a parse, read, and foreach accept the same options) called header_converters. Here is a simple example of how you can convert the headers of the parsed CSV to lowercase: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ### # Source CSV looks like: # # First name,last Name,Email # Abraham,Lincoln,alincoln@gmail.com # George,Washington,gwashington@outlook.com downcase_converter = lambda { |header| header.downcase } parsed_csv = CSV.parse('/path/to/file.csv', headers: true, header_converters: downcase_converter) parsed_csv.each do |row| puts row['first name'] # =&gt; Abraham # =&gt; George end Simple as that. You can do anything to the headers here. There are also a couple of built in header converters (:downcase and :symbol) that can be used, and an array can be passed as an argument, not just one converter. Converters can also be used for cells in the CSV rows as well, not just headers. The documentation for the Ruby CSV class is quite clear and helpful, take a look to see all the other myriad options for reading and writing CSVs in Ruby. Originally, I found this solution and tweaked it a bit from this StackOverflow answer - https://stackoverflow.com/questions/48894679/converting-csv-headers-to-be-case-insensitive-in-ruby]]></summary></entry><entry><title type="html">Per-page background images using prawn and Ruby</title><link href="http://www.martin-brennan.com/per-page-background-images-using-prawn-ruby/" rel="alternate" type="text/html" title="Per-page background images using prawn and Ruby" /><published>2019-08-06T00:00:00-04:00</published><updated>2019-08-06T00:00:00-04:00</updated><id>http://www.martin-brennan.com/per-page-background-images-using-prawn-ruby</id><content type="html" xml:base="http://www.martin-brennan.com/per-page-background-images-using-prawn-ruby/"><![CDATA[<p><a href="https://github.com/prawnpdf/prawn">Prawn</a> is an excellent PDF generation library for ruby, and we use it for all our PDF needs at work (Webbernet at time of writing). <a href="http://prawnpdf.org/manual.pdf">Their manual</a> is some of the best documentation I have read. Recently, I needed to set a different background image on every page of a PDF I was generating.</p>

<!--more-->

<p>The prawn documentation, while good, only shows how to use a background image for the whole PDF:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="n">img</span> <span class="o">=</span> <span class="s2">"some/image/path.jpg"</span>

<span class="no">Prawn</span><span class="o">::</span><span class="no">Document</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="ss">background: </span><span class="n">img</span><span class="p">,</span> <span class="ss">margin: </span><span class="mi">100</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">pdf</span><span class="o">|</span>
  <span class="n">pdf</span><span class="p">.</span><span class="nf">text</span> <span class="s1">'My report caption'</span><span class="p">,</span> <span class="ss">size: </span><span class="mi">18</span><span class="p">,</span> <span class="ss">align: :right</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>So, I decided to dig into their source code to see how they rendered the background image. After a short search I found what I needed. Turns out, this works for rendering multiple different background images! In prawn you can call <code class="language-plaintext highlighter-rouge">pdf.start_new_page</code> to start a new page, and on each new page I would call the following to set the new background for that page:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="n">background_image_path</span> <span class="o">=</span> <span class="s1">'some/path/for/this/page.jpg'</span>
<span class="n">pdf</span><span class="p">.</span><span class="nf">canvas</span> <span class="k">do</span>
  <span class="n">pdf</span><span class="p">.</span><span class="nf">image</span><span class="p">(</span><span class="n">background_image_path</span><span class="p">,</span> <span class="ss">scale: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">at: </span><span class="n">pdf</span><span class="p">.</span><span class="nf">bounds</span><span class="p">.</span><span class="nf">top_left</span><span class="p">)</span>
<span class="k">end</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I was able to generate the PDF with different background images perfectly with this code.</p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[Prawn is an excellent PDF generation library for ruby, and we use it for all our PDF needs at work (Webbernet at time of writing). Their manual is some of the best documentation I have read. Recently, I needed to set a different background image on every page of a PDF I was generating.]]></summary></entry><entry><title type="html">Prevent remote: true links opening in new tabs or windows in Rails</title><link href="http://www.martin-brennan.com/prevent-remote-true-links-opening-new-tab-rails/" rel="alternate" type="text/html" title="Prevent remote: true links opening in new tabs or windows in Rails" /><published>2019-08-06T00:00:00-04:00</published><updated>2019-08-06T00:00:00-04:00</updated><id>http://www.martin-brennan.com/prevent-remote-true-links-opening-new-tab-rails</id><content type="html" xml:base="http://www.martin-brennan.com/prevent-remote-true-links-opening-new-tab-rails/"><![CDATA[<div class="alert deprecated">
  Since 2019 a few things have changed:

<ul><li>Rails’ UJS stack may no longer use jQuery or $.rails, so this code may break or not even apply.</li>

<li>Monkey-patching internal UJS behavior is brittle and often causes accessibility or maintenance issues.</li>

<li>Better approaches now exist. Provide HTML fallback responses for remote links, intercept clicks via JS rather than removing URLs, or use modern Rails front-end tools (Stimulus, importmaps, non-jQuery UJS) for more robust behavior.</li>

<li>If you’re using Rails 6+ (especially Rails 7) or a JS stack without jQuery, you should not rely on this technique. Consider using click handlers or providing proper responses instead.</li>

<li>Using <code>href='javascript:void(0)'</code> is generally discouraged: it’s not semantic, can be problematic for screen readers, for users who disable JS, etc. It also breaks normal fallback behavior.</li>
</ul>
</div>

<p>In Rails, you can use the option <code class="language-plaintext highlighter-rouge">remote: true</code> on forms and links for Rails to automatically send AJAX requests when the form is submitted or the link is clicked. I plan to write a more in-depth article about this extremely useful feature in time, but essentially you just need to add an <code class="language-plaintext highlighter-rouge">X.js.erb</code> file in your views directory for your controller, where <code class="language-plaintext highlighter-rouge">X</code> is the action, and Rails will deliver this JS file as a response to the AJAX request and execute it. Now, most of the time you will not want these AJAX/JS-only routes to render a HTML view, but by default users can use middle click or open the <code class="language-plaintext highlighter-rouge">remote: true</code> link in a new tab, which will show a <code class="language-plaintext highlighter-rouge">ActionView::MissingTemplate</code> error because there is no <code class="language-plaintext highlighter-rouge">X.html.erb</code> file present.</p>

<!--more-->

<p>To get around this, we can monkey patch the <code class="language-plaintext highlighter-rouge">$.rails.href</code> function which rails uses to get the HREF attribute for <code class="language-plaintext highlighter-rouge">remote: true</code> links, and set the default behaviour of the link to <code class="language-plaintext highlighter-rouge">javascript: void(0)</code>, and store the actual HREF on a data-attribute instead:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="nf">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nf">ready</span><span class="p">(</span><span class="nf">function </span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">remoteLinks</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">a[data-remote=</span><span class="se">\'</span><span class="s1">true</span><span class="se">\'</span><span class="s1">]</span><span class="dl">'</span><span class="p">);</span>

  <span class="k">for </span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">remoteLinks</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">link</span> <span class="o">=</span> <span class="nx">remoteLinks</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
    <span class="nx">link</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">url</span> <span class="o">=</span> <span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>
    <span class="nx">link</span><span class="p">.</span><span class="nf">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">href</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">javascript:void(0);</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">$</span><span class="p">.</span><span class="nx">rails</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nx">el</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="nx">element</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">url</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">element</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This effectively stops users from opening these links in new tabs or windows. Ideally you would want to respond correctly to the HTML content-type requests on those remote routes, but this is a quick and effective way to prevent the error from cropping up for the end-user.</p>

<p>The <code class="language-plaintext highlighter-rouge">$.rails</code> namespace is quite interesting as it contains a lot of functions that Rails uses for this automatic AJAX functionality, and a lot of potential places for JS monkey-patching or function overriding. I’m going to have a further look into the JS functions present there to see if there is any other behaviour that can be changed. Note that if your AJAX request creates more link elements with <code class="language-plaintext highlighter-rouge">data-remote="true"</code> on them, you would need to run the <code class="language-plaintext highlighter-rouge">remoteLinks</code> href swapping code again for the new elements. The <code class="language-plaintext highlighter-rouge">$.rails.href</code> code only needs to be run once per page load.</p>

<p>Originally, I found this solution and tweaked it a bit from this StackOverflow answer - <a href="https://stackoverflow.com/questions/24227586/rails-link-to-with-remote-true-and-opening-it-in-a-new-tab-or-window">https://stackoverflow.com/questions/24227586/rails-link-to-with-remote-true-and-opening-it-in-a-new-tab-or-window</a></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[Since 2019 a few things have changed: Rails’ UJS stack may no longer use jQuery or $.rails, so this code may break or not even apply. Monkey-patching internal UJS behavior is brittle and often causes accessibility or maintenance issues. Better approaches now exist. Provide HTML fallback responses for remote links, intercept clicks via JS rather than removing URLs, or use modern Rails front-end tools (Stimulus, importmaps, non-jQuery UJS) for more robust behavior. If you’re using Rails 6+ (especially Rails 7) or a JS stack without jQuery, you should not rely on this technique. Consider using click handlers or providing proper responses instead. Using href='javascript:void(0)' is generally discouraged: it’s not semantic, can be problematic for screen readers, for users who disable JS, etc. It also breaks normal fallback behavior. In Rails, you can use the option remote: true on forms and links for Rails to automatically send AJAX requests when the form is submitted or the link is clicked. I plan to write a more in-depth article about this extremely useful feature in time, but essentially you just need to add an X.js.erb file in your views directory for your controller, where X is the action, and Rails will deliver this JS file as a response to the AJAX request and execute it. Now, most of the time you will not want these AJAX/JS-only routes to render a HTML view, but by default users can use middle click or open the remote: true link in a new tab, which will show a ActionView::MissingTemplate error because there is no X.html.erb file present.]]></summary></entry><entry><title type="html">ImageMagick unable to load module error on AWS Lambda</title><link href="http://www.martin-brennan.com/imagemagick-unable-to-load-module-error-aws-lambda/" rel="alternate" type="text/html" title="ImageMagick unable to load module error on AWS Lambda" /><published>2019-07-29T00:00:00-04:00</published><updated>2019-07-29T00:00:00-04:00</updated><id>http://www.martin-brennan.com/imagemagick-unable-to-load-module-error-aws-lambda</id><content type="html" xml:base="http://www.martin-brennan.com/imagemagick-unable-to-load-module-error-aws-lambda/"><![CDATA[<div class="alert deprecated">
  This may no longer be relevant in 2025, but it was critical back in 2019. Leaving it as a historical curiosity.
</div>

<p>Last Friday (July 2019) we started seeing an elevated error rate in our AWS Lambda function that converted single page PDFs into images using ImageMagick. We had been seeing the same error crop up randomly in around a two week period before Friday, but we were busy with other things and didn’t look too deeply into it. This was a mistake in retrospect.</p>

<!--more-->

<p>Below is the error in question:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>Error: Command failed: identify: unable to load module `/usr/lib64/ImageMagick-6.7.8/modules-Q16/coders/pdf.la': file not found @ error/module.c/OpenModule/1278.
identify: no decode delegate for this image format `/tmp/BEQj9G8xj1.pdf' @ error/constitute.c/ReadImage/544.
</pre></td></tr></tbody></table></code></pre></div></div>

<!--more-->

<p>To figure out the dimensions of the PDF, to convert it to an image, and to optimize the size we were using <a href="https://aheckmann.github.io/gm/">the gm nodejs package</a>. This is just a friendly wrapper around calling <code class="language-plaintext highlighter-rouge">ImageMagick</code> directly. <code class="language-plaintext highlighter-rouge">ImageMagick</code> version 6.8 is installed on AWS lambda base images by default. It took a while and a lot of googling and experimentation to figure out the error what the error was from. I found <a href="https://stackoverflow.com/questions/57067351/imagemagick-not-converting-pdfs-anymore-in-aws-lambda?noredirect=1#comment100713341_57067351">a StackOverflow question</a> which was pivotal. It held vital information and pointed to a blog post <a href="https://aws.amazon.com/blogs/compute/upcoming-updates-to-the-aws-lambda-execution-environment/">on the AWS blog</a> which talked about upcoming changes to the Lambda execution environment and a migration window. There was only one problem.</p>

<p>We were at the very end of the migration window.</p>

<p>Turns out Amazon likely removed a module referenced by <code class="language-plaintext highlighter-rouge">pdf.la</code>, which makes it so <strong>converting PDFs to images using ImageMagick no longer works on AWS Lambda</strong>. Now, the fix to this was essentially to use GhostScript instead to convert the PDFs to images, and then still use <code class="language-plaintext highlighter-rouge">ImageMagick</code> to resize the images. The steps I followed were (applicable to nodejs):</p>

<ol>
  <li>Include the <code class="language-plaintext highlighter-rouge">bin</code> and <code class="language-plaintext highlighter-rouge">share</code> directories from <a href="https://github.com/sina-masnadi/lambda-ghostscript">https://github.com/sina-masnadi/lambda-ghostscript</a> into our Lambda function, so we had a compiled version of GhostScript that worked on AWS Lambda.</li>
  <li>Change the JS code to call the GhostScript command to convert the PDF (sample below, <a href="https://stackoverflow.com/a/33528730">command here</a>)</li>
  <li>Upload the new code to lambda and make sure everything still worked (it did!)</li>
</ol>

<p>The <a href="https://stackoverflow.com/a/57230609/875941">answer on the StackOverflow question above</a> is similar to the process I followed but I didn’t bother with lambda layers. Here is what our JS function to convert the PDF to image looks like:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c1">// tempFile is the path to the PDF to convert. make sure</span>
<span class="c1">// your path to the ghostscript binary is set correctly!</span>
<span class="kd">function</span> <span class="nf">gsPdfToImage</span><span class="p">(</span><span class="nx">tempFile</span><span class="p">,</span> <span class="nx">metadata</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Converting to image using GS</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">----------------------------</span><span class="dl">'</span><span class="p">)</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">execSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">./bin/gs -sDEVICE=jpeg -dTextAlphaBits=4 -r128 -o </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">tempFile</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">.pdf</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">.jpeg</span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="dl">'</span><span class="s1"> </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">tempFile</span><span class="p">).</span><span class="nf">toString</span><span class="p">());</span>
  <span class="nf">next</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">tempFile</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">.pdf</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">.jpeg</span><span class="dl">'</span><span class="p">),</span> <span class="nx">metadata</span><span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>After I put the fix in place all the errors went away! Lesson learned for next time…pay more attention to the AWS blog! Here is our Lambda function success/error rate chart for last Friday (errors in red). It’s easy to see where the fix went live:</p>

<p><img src="/images/imagemagick_lambda_errors.png" alt="imagemagick lambda errors" /></p>]]></content><author><name>Martin Brennan</name></author><summary type="html"><![CDATA[This may no longer be relevant in 2025, but it was critical back in 2019. Leaving it as a historical curiosity. Last Friday (July 2019) we started seeing an elevated error rate in our AWS Lambda function that converted single page PDFs into images using ImageMagick. We had been seeing the same error crop up randomly in around a two week period before Friday, but we were busy with other things and didn’t look too deeply into it. This was a mistake in retrospect.]]></summary></entry></feed>