MoreRSS

site iconGood EnoughModify

Good Enough LLC, Made Pika, Letterbird, Yay.Boo, Album Whale, and more.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Good Enough

Barry is Good Enough

2025-04-15 08:00:00

We put a lot of ourselves into our work, and it occurs to us that you, dear reader, might not know much about us. So we’re continuing our Q&A column to introduce ourselves, one-at-a-time. Today, we’re meeting the biggest Pika fan, Barry Hess…

Who are you? (Who, who, who, who?)
My name is Barry Hess. I grew up in a rural part of Minnesota where the population of the entire county is less than 10,000 people. After getting a college degree in Computer Science, I moved to a metropolis of 25,000 people in southern Minnesota. I live here with my wife and three daughters, though our children are actively growing and slowly moving out into the world to create their own lives. 😭

I’ve been coding off and on for nearly thirty years now, though there was a bit of a dalliance in management for a while.

What do you do at Good Enough?
I started Good Enough with Shawn. For the past year my primary work has been Pika, which is near and dear to my heart. An illustration of me even sits on the Pika homepage, yet somehow Pika still keeps attracting new customers. Baffling! To accomplish my work, I mostly code Ruby on Rails while sometimes talking to AIs to help me write JavaScript. I also try to do some marketing work as best I can, write a bit, and help out with our other products.

What are the tools of your trade?
Ruby, Rails, JavaScript, repeat. I’ve been using RubyMine as my editor for the past year, along with Cody as my AI assistant. (I’m thinking of taking a month to try using a more integrated AI code editor.) While AI is more of a sounding board for my Rails work, it has become key for me to be able to accomplish anything at all in the JavaScript world. Aside from that, it’s my trusty MacBook Pro, a browser, Terminal, and reading the docs.

What’s your origin story?
I entered college as a Physics major, but knew enough about myself to take a Computer Science class in my first semester. I think I changed my major within a week. Though I loved computers, there wasn’t much programming in my life before college. My coding through college was primarily C++ for class, and I loved iterating on the little projects to make them more and more user friendly. From there I got into web development of the basic sort: HTML, CSS, SSI, and eventually PHP.

My career started at an insurance company, which onboarded new employees with an intensive, three-month COBOL training class. So, yes, I spent a couple of years being paid to code COBOL before transitioning to a Java team. After about six years, I had an early-life crisis and left that job for parts unknown. (Literally, I had no new job lined up, and my wife was pregnant with our second child. Do not try this at home!)

I was very fortunate to run into the Harvest founders, and was able to start working with them from (nearly) the beginning of that product. It was there that I was able to experience the tiny, scrappy startup; the growing, scrappy startup; and eventually the medium-sized company, established in its niche. I went from coding many of the original features to helping build a team to managing the entire technical team.

Eventually, it was time to move on to a new experience, and here I am at Good Enough, trying something rather different!

Barry Hess
Me and my family overlooking Florence

What’s new? How are things going?
Oh, you know, life keeps barreling on! As I mentioned above, our children continue to grow. We have a tight-knit family and love to hang out together. We also love to travel together! We just got back from visiting our oldest while she was studying abroad in Florence, Italy. It was fantastic.

Our middle child is heading to college in the fall, and we’ll continue to adapt. As you can imagine, this is all bittersweet, but we are doing our best to cherish the moments and enjoy watching our children grow into lovely adults.

What’s your drink of choice?
In the past few years, I’ve gotten into making cocktails. I don’t make a cocktail often, but I really enjoy the variety of flavors that exist out there—I had no idea until I started making them! I love a good Negroni. Paper Planes are awesome, as are Last Words and Enzonis. If I’m being honest, though, the most frequently drunk cocktail in the Hess household is the Gin & Tonic.

Any parting words of wisdom for our dear readers?
It’s okay to be mildly addicted to Fortnite if you take care to play with family and friends. It’s a fun game that’s always changing. The fun is enhanced by planning together and succeeding (or more likely failing) together.

When you play too much alone, and spend all of your “content viewing time” watching YouTube videos about how to play better (even though you never really get that good because you can’t put that much time into training), then perhaps you’ve went too far.

I may or may not be giving this speech to myself. Don’t worry, I have a totally balanced Fortnite life these days!

Where can you be found on the internet?
This blog, Pika’s blog, and my blog on Pika. I’m also haltingly involved in various social sites @bjhess:


Want to know other things about our team? Let us know what really interests you!

There's no “A” or “I” in “Jelly”

2025-01-17 08:00:00

The second half of 2024 was definitely an inflection point in the world of software. Large Language Models (LLMs) and generative AI started to permeate products everywhere, from chatbots to operating systems, and at times it felt like everyone was taking part in a race to integrate some AI feature or other into their product.

This seems to have been particularly true in the world of customer support. Whole businesses seem to have pivoted, turning AI into their central feature as if their very lives depended on it. Some taglines from well-known companies leave no doubt:

The best AI Agent and AI-first Customer Service Platform

Try our new AI integration!

AI-first service.

... and I can see the appeal for some businesses.

But personally, I hate talking to bot or AI customer service tools. Is there anything more frustrating than carefully explaining your issue, then inexplicably being railroaded through some set of pointless questions or regurgitated knowledge-base articles, desperately hoping that if you can only jump through all these hoops like a good little boy, you might be able to eventually get in touch with an actual person who can actually read and understand your question and actually help you at the end of the tortuous process? It makes my blood boil!

And as these big players double down on AI, it feels clearer than ever that they are really only focussed on customers who are so big that they don’t need to care how frustrating their support processes are. Companies for whom support is a cost centre they are trying to minimise.

Jelly takes a different position. Jelly is about connecting actual people having actual conversations — support requests, questions, and all other kinds of collaboration.

We're a small company, too. We know that the communication between us and our customers, existing or potential, will be one of the biggest factors in our success. There's no way we want an AI agent representing us in those vital conversations. Our bet is that there are thousands of other small companies and groups who neither need nor want an AI agent sitting between them and the people they want to communicate with.

If you've been looking for a way for your team to share an inbox and work together to talk to your users, customers, clients, collaborators, and anyone else -- try Jelly. It's the simplest, most elegant, most humane way to work on email as a team.

TIL: Tiptap Excerpt Extension with Rails

2025-01-16 08:00:00

While building Pika’s Stream of posts layout, we had need to add the capability to manage excerpts in the Pika editor. These excerpts would be used to show a small portion of your post in a post stream while offering a “continue reading” link for readers to click to read the rest of your post. To add this capability we had to dig into extending the base open source library for our editor, Tiptap.

First the Tiptap part

Below is the full code of the extension. Primarily the extension detects if the user types {{ excerpt }}, {{ more }}, or WordPress’s <!--more--> and replaces that text with:

<div data-type="excerpt" class="post-excerpt" contenteditable="false" draggable="true">↑ Excerpt ↑</div>

With that HTML, we use CSS to style things like so:

Pika blog post with an excerpt indicated within the editor by a dashed line between the excerpted text and the rest of the blog post

This extension is smart enough to know if an excerpt already exists in the editor, in that case disallowing another excerpt being created.

The extension:

import { Node, InputRule, nodeInputRule, mergeAttributes } from '@tiptap/core'

/**
* Look for on a line by itself, with all the whitespace permutations
*/

export const excerptInputRegex = /^\s*{{\s*excerpt\s*}}\s*$/i

/**
* Look for on a line by itself, with all the whitespace permutations
*/

export const moreInputRegex = /^\s*{{\s*more\s*}}\s*$/i

/**
* Look for classic WordPress <!--more--> tag
*/

export const wpMoreInputRegex = /^\s*<*![-—]+\s*more\s*[-—]+>\s*$/i

const excerptText = '↑ Excerpt ↑'

/**
* Used to detect if an excerpt already exists in the editor
*/

const hasExistingExcerpt = (state) => {
let hasExcerpt = false
state.doc.descendants(node => {
if (node.type.name === 'excerpt') {
hasExcerpt = true
return false // stop traversing
}
})
return hasExcerpt
}

/**
* Disable excerpt button in toolbar if excerpt already exists in
* the editor. Note that we use Tiptap with the Rhino editor for
* Rails, which explains the rhino-editor selectors. Rhino:
* https://rhino-editor.vercel.app/
*/

const setExcerptButtonState = (editor) => {
const button = editor.view.dom.closest('rhino-editor').querySelector('rhino-editor button[data-excerpt]')
if (button) {
button.classList.toggle('toolbar__button--disable', hasExistingExcerpt(editor.state))
button.disabled = hasExistingExcerpt(editor.state)
}
}

/**
* This custom InputRule allows us to make a singleton excerpt node
* that short-circuits if an excerpt node already exists.
*/

export function excerptInputRule(config) {
return new InputRule({
find: config.find,
handler: ({ state, range, match }) => {
if (hasExistingExcerpt(state)) {
return
}

const delegate = nodeInputRule({
find: config.find,
type: config.type,
})

delegate.handler({ state, range, match })
},
})
}

export const Excerpt = Node.create({
name: 'excerpt',
group: 'block',
content: 'inline+',
inline: false,
isolating: true,
atom: true,
draggable: true,
selectable: true,

onCreate() {
setExcerptButtonState(this.editor)
},

onUpdate() {
setExcerptButtonState(this.editor)
},

parseHTML () {
return [{ tag: 'div[data-type="excerpt"]' }]
},

renderHTML ({ HTMLAttributes }) {
return ['div', mergeAttributes({ 'data-type': 'excerpt', class: 'post-excerpt' }, HTMLAttributes), excerptText]
},

/**
* Add insertExcerpt command that we can call from our custom
* toolbar buttons. This command checks for an existing excerpt
* before inserting a new one.
*/

addCommands() {
return {
insertExcerpt: () => ({ state, commands }) => {
if (hasExistingExcerpt(state)) {
return false
}

return commands.insertContent({
type: this.name,
content: [{
type: 'text',
text: excerptText
}]
})
},
}
},

/**
* Set up various detection for {{ excerpt }} etc text.
*/

addInputRules() {
return [
excerptInputRule({
find: excerptInputRegex,
type: this.type,
}),
excerptInputRule({
find: moreInputRegex,
type: this.type,
}),
excerptInputRule({
find: wpMoreInputRegex,
type: this.type,
}),
]
},

})

Now for the Rails part

This will obviously need modified depending on your Tiptap environment. In our case, using the Rhino editor with Rails, here’s what we do…

app/javascript/controllers/extensions/excerpt.js is where our extensions directory lives. We use importmaps to manage our JavaScript, so we need to pin our extensions directory there:

pin_all_from "app/javascript/extensions", under: "extensions"

We have a Stimulus controller for all of our Rhino enhancements. We need to import our extension there:

import { Excerpt } from "extensions/excerpt"

To add the extension, we do this in the function that we use to do all of our Rhino initialization (we pass the Rhino editor into this function):

initializeEditor(rhino) {
// snip
rhino.addExtensions(Excerpt)
// snip
}

And we add one function, which we will later call with our new Rhino toolbar button. This function calls the insertExcerpt command that we defined in our extension.

insertExcerpt() {
this.element.editor.chain().focus().insertExcerpt().run()
}

And finally here’s the button we add to our Rhino toolbar. Notice the data-action="click->rhino#insertExcerpt", which is calling the function above:

<button slot="after-increase-indentation-button" class="rhino-toolbar-button toolbar__button--excerpt" type="button" data-role-tooltip="rhino-insert-excerpt" data-action="click->rhino#insertExcerpt" data-excerpt data-role="toolbar-item" tabindex="-1">
<role-tooltip class="toolbar__tooltip" id="rhino-insert-excerpt" part="toolbar__tooltip toolbar__tooltip--create-excerpt" exportparts="base:toolbar__tooltip__base, arrow:toolbar__tooltip__arrow">
Create Excerpt
</role-tooltip>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" part="toolbar__icon" viewBox="0 0 24 24" width="24" height="24" style="pointer-events: none;">
<path d="SNIP"/>
</svg>
</button>

This is by no means a drop-in extension, but hopefully it helps someone else who is wanting to add this excerpt functionality to their Tiptap editor.

You Complete Me

2024-12-04 08:00:00

When Good Enough was in its infancy as a truly American LLC (formed in Delaware and representing one or two people who were only semi-serious about a business), it was fun to play around with building websites. Shawn and I were truly just playing and exploring, more than anything reminding ourselves that building software could be a satisfying activity. After a year of goofing around we were still enjoying it, but we were also running up against our limitations. Some things we were okay at, but many of our skills just weren’t that impressive.

So began the journey to Good Enough’s next phase: a collective of Good Enough people. We could make some cool, if janky, web toys alone, but with a few more people to play with…

Along came Lettini and Patrick and James and Cade. Each of us with a different set of skills and a different set of weaknesses.

Things definitely did become a lot more interesting once we teamed up! When my weaknesses got in the way, there was someone else to step into that gap and show me how it’s done. Hopefully others agree that I’m able to help them in some of the areas where I have a little more experience. 🤞

That’s enough reading for you; now it’s time to listen. Lettini, James, and I were recently asked to have a conversation on the IndieRails podcast. We are very thankful to Jeremy and Jess for this opportunity to talk about some of Good Enough’s short history. And luckily for you, we hardly talk about Rails at all!

Throughout our lovely discussion, the power of a team filled with complimentary skills kept resurfacing in my head. This experience cannot be recreated as a solo dev or by working on some project in my garage. The times where our skills don’t overlap makes this whole Good Enough experiment lovely and worthwhile. To my teammates, I thank you. You complete me!

Jerry Maguire saying 'You complete me.'

Jelly was #1 on Hacker News

2024-11-13 08:00:00

Yesterday, Lettini took a chance and posted about Jelly on Hacker News, a discussion site notorious for it's mercurial population of tech-maybe-too-saavy experts. Jelly is a tough sell for some of them, those with the technical skill to pipe email at a low level through custom-built filters running on their own cloud servers.

I'm not going to lie to you. I was pretty nervous.

And yet...

Jelly spread on top of the toast of tech
Jelly at the top of Hacker News last night. At the time of writing, we've had over 100 comments and 281 "points"

We got a really lovely response! It was also a great opportunity for us to practice talking about Jelly, about why we built it, what it stands for, and why people should consider it over other tools or workflows.

It gave us an opportunity to talk about our philosophy on pricing:

For us, affordability is part of the product itself.

We’re specifically building this not to hoover up every dollar on the table, but to serve smaller groups that have been left out in the cold by "bigger" tools, and who get screwed by per-seat pricing. We believe there are enough teams who fit this profile to be profitable.

There’s a difference between making profit and maximizing profit. the capitalists will call us crazy, but we're not here to maximize profit.

This really resonated:

I love this. Seriously.

This is such a refreshing perspective! I've always wondered if there's room for craftsmen to build quality products for smaller groups. Your focus on simple, well-designed software really resonates with me. Thanks for showing us a viable path.

A lot of people really got the product and the design choices we've been making:

I'm really liking the UX there! In sports-speak there's the "Whose got the ball" method to identify who is managing a topic...and the way this is executed - from what i saw in the video - seems really straight-forward to help answer that.

I really like the way this landing page is designed. And I think it really highlights one of the sales points, which is that you are decent and reasonable. Good stuff. I'm going to send this around to some people.

Of course, there were plenty of people offering their home-brewed alternatives that cover some of what Jelly does, setting up filters and forwarding and even using labels to "claim" messages.

It's fascinating to see how other people have approached this, and the existence of so many different "solutions" demonstrates, to me, that this is a problem that really exists in the world, and that really needs a Good Enough solution that works for people whether they are tech-saavy or not.

Anyway. Go try Jelly. It's approved by the smart folks at Hacker News. What are you waiting for?

TIL: Fixing Broken Social Share Images in Twitter/X

2024-11-12 08:00:00

I have an admission to make. Social share images for Pika were broken on Twitter/X, LinkedIn, and Apple Messages for months. And it made me sad.

But in the past few months we got it fixed. And that made me very happy!

We really love our Pika social share images. They are pretty. They are readable. They reflect the theme chosen by the blogger. They're great! When they work.

One day we went to share one of our blog posts and noticed that Twitter/X wasn’t playing nicely with Pika. At the time that service had been repeatedly mucking with how it displayed links. Seemingly every week there was a new change. So we sat on it for a bit, but eventually we decided it was probably us, not them. Especially since a couple other services were also having issues.

We tried many and various things to fix it. I’ll share those below so we can get to the point…

The fix in our case was to make sure our server was returning the correct headers:

Content-Type: text/html; charset=utf-8

We arrived at this fix when a customer pointed out that our Content-Type differed from other working services. Using the service HTTP Header Check, they shared output from our server that showed:

Content-Type: */*; charset=utf-8

Most services were fine figuring this out. Twitter/X, LinkedIn, and Apple Messages were not fine figuring this out. Naturally I introduced this bug when fixing another bug. 🤦

What other things did we try?

Our twenty-nine-comment thread on Basecamp proves we tried just about everything.

  • We compared line-by-line our META tags between Pika and various other blogs (including this one right here)
  • We tweaked those META tags about fifteen different ways for each little discrepancy that we detected
  • We read all the documents
  • We tried the card validator
  • We tried LinkedIn's post inspector
  • We tried LinkedIn's support (they sent us to their developer forums who in turn said “not our problem” and sent us to an abandoned area of Stack Overflow 🙄)
  • We tweaked our robots.txt
  • We played with charset=uft-8 vs charset=us-ascii