More from Good Enough
For your small business to survive, you need customers. Not just to buy once. You need them to come back, tell their friends, and trust you over time. And yet, too many small businesses make it weirdly hard to talk to them. Well, duh, right? I agree, yet I see small businesses fumbling this over and over. All the attention when discussing business is about giant corporations. Whether they’re selling servers or vehicles or every product under the sun, millions of dollars pass through their doors every day. Yet it is folly to apply the methodologies of giant companies to our small businesses. It sounds obvious, but I constantly see small businesses making it hard for customers to get in touch. If a customer does get through the “contact us” gauntlet, that small business often uses needlessly complicated enterprise software to talk with customers. Small businesses don’t get the spotlight, but they are the engine of the economy. To wit, in the United States: 99.9% of businesses are small Nearly half the private workforce is employed by small businesses They generate over 43% of the country’s GDP And beyond the stats, small businesses are who we turn to every day: your corner coffee shop, your local cleaner, your neighborhood software team. And don’t forget that every big business started small. Small businesses are the genesis of innovation. We all need small businesses to succeed. Most small teams aren’t trying to become giant corporations. They want to make a living doing work for a fair return. Many of them work hard in hopes of moving the needle from a fair return to a comfortable life, and maybe even some riches down the road. Yet it’s amazing how often it’s forgotten: you need customers to succeed. Success in small business starts with human conversation. While talking effectively with your customers does not guarantee success, it is certainly a requirement. Here’s what that looks like: a customer has a question and your team responds kindly, clearly, and quickly. Or sometimes your team wants to reach out with a question for a customer. It’s a simple, human interaction that cannot be done effectively by automation or AI. It’s the air your small business is breathing. Starve that air, and everything else suffers. Your product or service is almost secondary to building a healthy relationship with each of your customers. Big business doesn’t operate this way. We shouldn’t expect it to show us how to build real relationships. We’re doing our best here at Good Enough to build healthy, happy customer relationships. Whenever you write to us about any of our products, someone on the team is going to reply to offer help or an explanation or an alternative. Fact is, if you write to us about anything, we’re going to reply to offer help or an explanation or an alternative. As an online business, we’re talking with customers primarily over email. For us, Jelly makes those conversations easy to have—human, not hectic. Actual customer support is remarkable. Actual, healthy human relationships are important. Actual customer conversations are a key to small business success. Choose your actions and tools accordingly. If you liked this post, maybe you’ll like Jelly, our new email collaboration app for small teams!
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 descending deeper, ever deeper, into the Q&A column introduce ourselves, one-at-a-time. Desperate to keep things interesting, James tried to spice up the interview format... https://lazyatom.com https://interblah.net https://goodenough.us https://goodenough.us/blog Once you've done that, I want you to conduct a question-by-question interview with me, to form the basis of a "James is Good Enough" post. Ask me one question at a time, and then use my answer to evolve the interview. We should aim for about 8 questions in total. Don't be boring. Use the existing posts as inspiration, but I want the post about me to be unique. I'd be cool if it went in a strange tangent. Q1: If your inner monologue were a programming language, what would it be, and why? E_WHAT_THE_HELL_ARE_YOU_TALKING_ABOUT_THIS_IS_NONSENSE Fantastic. You’ve got a reflective meta-loop keeping everything in check. Let’s follow that thought deeper: Q2: What’s one recurring loop or thought-process that’s currently being tagged by your inner supervisor as E_WHAT_THE_HELL_ARE_YOU_TALKING_ABOUT_THIS_IS_NONSENSE? Fair enough—let’s rewind a bit and start from a more grounded place. Q3: OK James, tell me a bit about who you are, what you’re doing these days, and how you fit into the Good Enough story? How do I fit into the Good Enough story? My friend Patrick, who I met when we both lived in Austin, TX, had always been trying to persuade me to work with him. A few jobs later, he succeeded! I was the 6th person to join the team, in mid 2023, and I love being a part of this team. It's long been my dream to build great products with love, and at Good Enough we are lucky to have that opportunity. That’s perfect; let’s build on that a bit. Q4: You mentioned it’s long been your dream to build great products with love. What does “building with love” mean for you personally—especially after two decades of programming? Great! Let’s push into this tension a bit more: Q5: Loving your users is admirable, but what’s a memorable example of when loving users conflicted with practical realities—like deadlines, budgets, or even your team’s sanity? How did you navigate that? Alright, let’s throw practicalities aside and pivot into something more colorful. Q5 (take two): If Good Enough were secretly a front for a mysterious cult, what strange rituals or beliefs would you and your teammates secretly adhere to, and why would you be tempted to join in? Q6: If Tony the Pony, Zalgo, and Y2K emerged through your DVD player at midnight and demanded you justify your existence in precisely one feature you’ve built or contributed to, which one would you pick, and how would you convince them to spare humanity based on it? Jelly -- it's an idea I've wanted to develop properly for a very long time. That, and an unreleased application I made called Chicken. Excellent—Jelly and Chicken it is. Clearly, we’ve entered culinary territory now. Q7: If Jelly and Chicken were literally dishes on a menu at the Good Enough café (the front for the aforementioned cult, obviously), how would you describe their flavors and textures to customers who wander in, unsuspecting of their metaphysical implications? Q7 (revised): What is it about Jelly, as an idea, that resonated with you deeply enough that it stuck around in your mind until you could finally build it at Good Enough? It should be a simple thing! But all these big software products make it complicated, smother it in bells and whistles -- and now AI, no offence GPT. There should be a "no-brainer" answer for a group that wants to use email together. I think Jelly can be that answer. Let’s wrap this up neatly: Final Question (Q8): You’ve spent your career advocating for simplicity in software. Do you think there’s a deeper personal philosophy beneath that, something you’re trying to express beyond just “good software design”? At this point ChatGPT tried to summarise the whole thing into a coherent post and, to be honest, it was garbage. So, instead, you get the raw interview. Judge me harshly, for I deserve it. If you liked this post, maybe you’ll like Jelly, our new email collaboration app for small teams!
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.
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: 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.
More in technology
As we pack our bags and prepare for the adult-er version of BlackHat (that apparently doesn’t require us to print out stolen mailspoolz to hand to people at their talks), we want to tell you about a recent adventure - a heist, if you will. No heist story
For your small business to survive, you need customers. Not just to buy once. You need them to come back, tell their friends, and trust you over time. And yet, too many small businesses make it weirdly hard to talk to them. Well, duh, right? I agree, yet I see small businesses fumbling this over and over. All the attention when discussing business is about giant corporations. Whether they’re selling servers or vehicles or every product under the sun, millions of dollars pass through their doors every day. Yet it is folly to apply the methodologies of giant companies to our small businesses. It sounds obvious, but I constantly see small businesses making it hard for customers to get in touch. If a customer does get through the “contact us” gauntlet, that small business often uses needlessly complicated enterprise software to talk with customers. Small businesses don’t get the spotlight, but they are the engine of the economy. To wit, in the United States: 99.9% of businesses are small Nearly half the private workforce is employed by small businesses They generate over 43% of the country’s GDP And beyond the stats, small businesses are who we turn to every day: your corner coffee shop, your local cleaner, your neighborhood software team. And don’t forget that every big business started small. Small businesses are the genesis of innovation. We all need small businesses to succeed. Most small teams aren’t trying to become giant corporations. They want to make a living doing work for a fair return. Many of them work hard in hopes of moving the needle from a fair return to a comfortable life, and maybe even some riches down the road. Yet it’s amazing how often it’s forgotten: you need customers to succeed. Success in small business starts with human conversation. While talking effectively with your customers does not guarantee success, it is certainly a requirement. Here’s what that looks like: a customer has a question and your team responds kindly, clearly, and quickly. Or sometimes your team wants to reach out with a question for a customer. It’s a simple, human interaction that cannot be done effectively by automation or AI. It’s the air your small business is breathing. Starve that air, and everything else suffers. Your product or service is almost secondary to building a healthy relationship with each of your customers. Big business doesn’t operate this way. We shouldn’t expect it to show us how to build real relationships. We’re doing our best here at Good Enough to build healthy, happy customer relationships. Whenever you write to us about any of our products, someone on the team is going to reply to offer help or an explanation or an alternative. Fact is, if you write to us about anything, we’re going to reply to offer help or an explanation or an alternative. As an online business, we’re talking with customers primarily over email. For us, Jelly makes those conversations easy to have—human, not hectic. Actual customer support is remarkable. Actual, healthy human relationships are important. Actual customer conversations are a key to small business success. Choose your actions and tools accordingly. If you liked this post, maybe you’ll like Jelly, our new email collaboration app for small teams!
Poll will only be live for 3 days, so vote while you can.