I’m a UX designer and front-end developer. These days, I spend my time designing and building website front-ends. I came to the web through a fascination with media, publishing, and information design. Recently, I’ve followed my interest in sustainable systems design into studying permaculture and landscape design.

Currently, I’m consulting through my studio, Lasser Design. I design and develop digital products, workflows, and strategies after understanding my clients’ needs and goals. I love working with small businesses, startups, mission-driven organizations and online communities. If this sounds like you, drop me a line.

This website is many things. It’s a digital garden, it’s a zettelkasten, it’s where I hone my craft. Feel free to poke around, and thanks for stopping by.

  • I built a fantastic application layout today. Lightweight, responsive, rock-solid, uncomplicated; it just works. I feel immense satisfaction when I can solve a problem, completely, in the fewest possible steps. It was just a just few hours of work, supported by a few additional hours of sketching and mocking, but possible because of the thousands of hours I’ve spent solving this kind of problem over the last decade.

  • In the fall I built a compost bin. Having a healthy balance of green (nitrogen-rich) and brown (carbon-rich) compost promotes decomposition; an ideal ratio is about 4 parts brown to 1 part green. One issue I have is that our kitchen and garden compost produces plenty of green matter, in the form of veggie scraps, weeds, and trimmings, but not nearly enough brown matter to balance it out. Fortunately, my neighbor just down the alley is a woodworker, and every week she puts out a garbage bag full of sawdust. After chatting with her, I now have a ready supply of sawdust, which is a wonderful brown compost additive: its small particles have a high surface area, so it decomposes faster and absorbs more moisture from the greens. Now I’m helping my neighbor to divert some of her waste while I’m putting it towards building better soil and, with any luck, growing better vegetables.

  • Avoid using React’s `useFormStatus`

    Server actions are the new shiny toy in Reactland, and for good reason: they make handling form submissions more straightforward than they’ve even been for front-end developers. That being said, they’re still in an experimental state, so the APIs for handling them shouldn’t be taken for granted.

    So far, I’ve found the useFormStatus hook, as it currently works, to be all but useless. While its intended purpose is to return the form’s pending state, this comes with a huge caveat: it only works when called from within a form child. Any examples I can find for its use all copy the React documentation, which disables a submit button. Besides problems in its implementation, its current design is flawed:

    • Placing a submit <button> outside a form is valid HTML, as long as the button references the form’s id. The child-component limitation on useFormStatus makes it harder to progressively enhance a form that works natively using browser defaults.
    • Requiring the calling component to be a child of a <form> means that any other form elements, like inputs, can’t be disabled while the form is submitting unless they’re wrapped in their own components. This undoes much of the simplicity gained by using server actions with native HTML forms.

    Here’s how I’m tracking a form’s pending state instead:

    useForm.ts
    import { SyntheticEvent, useTransition } from "react";
    import { useFormState } from "react-dom";
    
    export interface UseFormHook<FormState> {
      formState: FormState;
      isPending: boolean;
      formAction: (payload: FormData) => void;
      onSubmit: (event: SyntheticEvent<HTMLFormElement>) => void;
    }
    
    export function useForm<FormState>(
      action: (
        formState: Awaited<FormState>,
        formData: FormData
      ) => Promise<FormState>,
      initialState: Awaited<FormState>
    ): UseFormHook<FormState> {
      const [isPending, startTransition] = useTransition();
      const [formState, formAction] = useFormState(action, initialState);
    
      function onSubmit(event: SyntheticEvent<HTMLFormElement>) {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        startTransition(async () => {
          await formAction(formData);
        });
      }
    
      return {
        formState,
        isPending,
        formAction,
        onSubmit,
      };
    }
    

    Basically, I avoid useFormStatus entirely in favor of useTransition, which tracks the form action’s pending state without blocking other user interactions. Since the transition calls the function provided by useFormState, the formState value updates with the result of the form action.

    With this, here’s what a form could look like:

    'use client';
    
    import {myAction} from 'src/actions'
    import {useForm} from 'src/hooks/useForm'
    
    export default function MyForm() {
      const {isPending, formState, formAction, onSubmit} = useForm(myAction, {});
    
      return (
        <>
          <form id="myForm" action={formAction} onSubmit={onSubmit}>
            <input name="field1" disabled={isPending} />
          </form>
          <button form="myForm" type="submit" disabled={isPending}>Submit</button>
        </>
      );
    }
    

    The only downside is that now forms override their native functionality with an onSubmit handler. But the form will still work without JavaScript enabled since the server action is still being passed to the form’s action attribute.

    Given how I’m basically wrapping useFormState, I’m very surprised that this API doesn’t already provide tracking for the server action’s pending state. As these experimental features continue to evolve, I hope the React team takes this feedback under consideration.

  • Keeping a daily journal with Notion

    I’ve been a big fan of Notion for years, since I first learned about it in late 2018. I run my freelancing business on Notion, using it to keep track of all my meeting notes, project details, client directories, task lists, and development timelines.

    I’ve also extended it into my personal life for daily journaling and habit-tracking. A habit tracker is a case study in how Notion databases can be configured to create adaptable, powerful tools. Here’s how you can create one, too:

    1. Create a new database. Call it “Journal,” “Habits,” or whatever else you’d like.
    2. Create a new template for your database. Templates can be created from the menu under the dropdown arrow next to the “New” button in the upper right of the database.
    3. In the new template, title it as “@Today,” then select the “Date when duplicated” option. This will set the title of each entry to the date it’s created, giving each journal entry a unique title that’s easy to find when searching.
    4. Add a “Date” type property. Like with the title, select “Date when duplicated” as its value. This will help with filtering and sorting views to allow you to view entries by day, week, month, or year.
    5. Leave the template editor, then again open the template menu from the dropdown menu next to the “New” button. Open the menu for the “@Today” template you just created by clicking its ••• button. Select “Repeat,” then configure the template to repeat daily at whatever time you’d like (I chose 7 AM).

    Finally, I set up three views on my database:

    1. a table view that gives me a reverse-chronological list of all my entries, which is great for getting a high-level look at my habit-building progress;
    2. a weekly calendar view;
    3. and a monthly calendar view.

    With this, every day a new entry will automatically appear in the database that’s automatically set to the date it was created. From here, add new properties to your database for tracking habits. Since each entry also has its own free-form body, this serves as a blank page for a daily journaling habit, as well.

    For example, my database has a checkbox property for “Did I exercise today?” Inspired by the word-counting calendar Robert Caro kept while writing The Power Broker, I also keep track the number of words I write each day.

    A page from Caro’s word-counting diary was exhibited as part of ”‘Turn Every Page’: Inside the Robert A. Caro Archive,” which is still on display as of the time I’m writing. Good on Bob for taking Sundays off.

    New York Historical Society Museum & Library

    Like I said, this is a great case study for the power and flexibility Notion affords. To me, their stated mission—“to make toolmaking ubiquitous”—continues on the tradition established by the founders of personal computing. It’s not the perfect tool for every situation, but it’s filled so many niches that it’s hard to imagine working without it.

  • Rules for remote work

    I’ve worked remotely since 2017, both as an employee and as a founder. When we all locked down in 2020, I saw everyone thrown into chaotic remote working situations they had no time to prepare for. At that time, nearly two-thirds of employees polled “felt like the cons outweigh the pros” and a third considered quitting altogether. Four years later, remote work is now an intentional choice made by many organizations—but I still hear my friends complain about their continually dysfunctional remote cultures.

    I think that’s a shame, since I’ve come to love the benefits of being remote: working wherever and whenever I’m at my most productive and creative. I’m able to collaborate with teams spread across the country, with people living fascinating lives on their own terms, and maintain a healthy balance between my work and my life. The benefits of eliminating a daily commute cannot be understated, either.

    When a company doesn’t have a strong remote culture, avoidable stresses and conflicts become inevitable. There’s no training for an office that suddenly shifts from in-person to on-line when managers are learning in realtime along with their employees. The work never stops, either. There’s no chance to regroup and rebuild a remote culture from the ground up. Changing a company’s culture from the grassroots is difficult when you not only need buy-in from managers, but need them to be the ones leading by example.

    Here’s what I’ve learned about how to build a positive, functional remote work culture. These lessons cobbled together from personal experience building remote workplaces, informed other remote-first companies like Automattic and 37signals (a note to remote managers: there are books written about this and you should be reading them!). In retrospect, the best time to have have written these down would have been April 2020. But, as the old saying goes, the next best time to have written them down is now.

    1. Escalate mode, not tone.

    If there’s a golden rule for remote working, it’s this one. There’s a nasty cognitive bias where we read negative emotion into innocuous messages. Like a ship’s crew maintaining their boat against salty seas, workplaces need to be constantly counteracting the corrosive effects of negative intensification bias.

    Fortunately, we have the tools to avoid this. As soon as you start to read negative emotion into a text conversation, switch to an audio call. When you switch into a higher-resolution medium, you can hear someone else’s tone, instead of trying to infer it. If a phone call becomes testy, escalate it into video. The more you can see and hear somebody, from audio to video to an in-person meeting, the easier it is to build empathy, resolve problems, and keep your relationship constructive.

    This isn’t to say that tension and negativity may be avoided altogether. But if a discussion is on a sour subject, or has the potential to curdle, start it from the highest-resolution medium available and deescalate down from there.

    2. Limit communication to specific, enforced hours.

    One of the huge upsides of remote work is the flexibility to work when you’re at your most productive. One of the huge downsides is feeling like you’re on-call at all hours of the day. In the US we paradoxically view overwork as an elite signifier; in reality, the overworked are “less efficient and less effective,” more likely to feel depressed and anxious, and experience declines in creativity and judgement. It’s a fallacy to think that working at all hours demonstrates a commitment to success; it only leads to burnout.

    Let work happen at any time, but try to limit communication to an agreed-upon window of time. Try to avoid sending emails or messages during this time, and don’t expect any responses if you’re communicating outside that window. Unless one of your specific responsibilities is monitoring resources that need to always be available, the business won’t fail because you waited until tomorrow morning to reply to an email. This is critically true during vacations or holidays when an office is meant to be closed.

    A friend of mine here in Indianapolis (Eastern time zone) was just telling me how she works remotely for a company in Portland, Oregon. Her office’s hours are 9am Pacific to 5pm Eastern, which sounds amazing. This policy respects everyone’s schedule while expecting everyone to be available and responsive during the same daily window of time.

    3. Use (a)synchronicity to your advantage.

    At some point, we’ve all left a meeting with the same feeling: this should have just been an email. In truth, an email isn’t enough, but it can turn an hour meeting into one that’s over in fifteen minutes.

    There’s a story, beloved by the startup community, about how Amazon holds “silent meetings” that begin by reading a six-page memo that narratively describe the idea or problem under discussion. There’s many aspects of Amazon’s culture that should be avoided, but this one is actually pretty good. It maximizes the advantages of writing and speaking while minimizing the disadvantages of each.

    Writing helps articulate your argument, instantly share it, and affords consideration and thoughtful response from readers. Meetings are a great way to synchronously communicate, build consensus, and create opportunities for dialogue and debate. Try to use each to its greatest advantage: don’t introduce new ideas in a call, and don’t expect an immediate response in writing.

    Another way to successfully combine writing and meeting could be to speed up a morning stand-up routine. Having everyone write up what they’re working on massively speeds up the process, so that you can focus on discussing blockers and how to overcome them.

    4. Don’t backchannel.

    Backchannelling fractures and fragments understanding, like looking at your organization through a broken mirror. Nobody is able to see the whole picture, just shards that look different based on their perspective. Keep communication out in the open as much as possible, whether it’s by email, chat, voice, or video. Direct messaging should only be used for “closed-door” situations: providing feedback, expressing concerns, and planning surprise parties.

    While it may feel more immediate and secure, these are false positives. It’s no more immediate than sharing the same information in a public. As long as the information being shared isn’t personally private, trust your team to be able to handle it. Tools like Slack and Discord use “channels” to help keep conversations on-topic—channels often provide enough focus to keep the conversation on a need-to-know basis without making it secret, and allowing employees to opt-in to discussions on their own. As you solve problems out in the open, you’ll build build understanding and consensus.

    5. Call freely; decline freely.

    Talk is cheap. It’s also information dense, synchronous, and empathic. Getting into a habit of simply calling someone when you need to think through a problem can be a powerful force-multiplier. But feeling pressure to answer every call can destroy your focus and productivity. Create an understanding that it’s up to the receiver to answer if they have the time and attention to. If it’s really important, call again. If it can wait, call back or fall back to a more asynchronous mode.

    Tools like Discord, Slack, and Teams provide flexible audio/video channels for jumping in and out of quick conversations. The power of a simple phone call shouldn’t be overlooked. Voice memos and voice mails can be easily substituted if you just want to give a quick report. The point is that you’re actually talking to one another like actual human beings, not just typing at one another.

    6. Chat is a watercooler. Don’t lean too much on it or things will get messy.

    Chat is hard to search, hard to follow, and hugely distracting. It’s a bad format for building shared knowledge and a bad medium for synchronously communicating. When use chat, you let your thoughts guide your speech. Many ideas require more development, more editing, and a slower pace than chat affords. Yet, it’s come to dominate remote workplace culture because it’s so low friction.

    Chat works best as a social glue, facilitating low-stakes discussion, scheduling, and sharing for things like quick status updates, dropping interesting links, and sending photos of your pets. Never expect somebody to instantly reply to a DM or a chat, treat it as the lowest-stake mode of communication (see rules ##1, ##3, and ##5). Any important information—a company policy change, or an important deadline—needs to be communicated outside a chat (ideally, as a memo or a meeting, depending on the urgency).

    7. Prioritize spending time together, in-person and on-line.

    Remote work is inherently isolating. It often involves closing yourself in a quiet room and endlessly looking at your computer. Sociologist Ray Oldenburg calls work the “second place” where people spend most of their time socializing outside their home. Employees suffer when their second place vanishes into the cloud, especially after their “third places” have already disappeared.

    Creating spaces and times to socialize and gather, even through a video call, are essential for building the culture of a remote workplace. Bots that schedule random coffee chats can encourage coworkers to better know one another, while making space in the day for conversation and downtime. Incorporating a creative prompt into a daily stand-up routine can introduce spontaneity into an otherwise rote process. An occasional lunch talk, happy hour or game time can gather everyone for low-stakes, informal hangout (as long as it’s within the office hours established as part of ##2).

    8. ‌Managers must lead by example.

    This rule applies to the least number of people, but is no less essential. None of these guidelines will work if the people in charge are shooting off 11 p.m. emails that demand an immediate response, feel insecure when their calls go unanswered, and talk shit in DMs. If you’re a manager leading a remote workplace, establish guidelines, follow them yourself, and constructively reenforce them. Managers set the example, reinforce the culture, and have the power to spoil a healthy culture or reboot one that’s crashed.


    Remote working is amazing, as long as its under the right conditions. This isn’t an exhaustive list, and may need to be tweaked and adapted for the particulars of the remote workplace you find yourself in. These principles have, however, helped me to guide frustrating conversations towards constructive outcomes, create a shared sense of purpose, and protected my most precious resource: uninterrupted focus.

  • The understated genius of mid-sized cities

    Craig Mod’s submission to the New York Times’s “52 Places to Go in 2024”, Yamaguchi, is a spiritual successor to his submission last year, Morioka. In his most recent Ridgeline newsletter, Mod explains why he keeps submitting mid-sized cities with populations in the hundreds of thousands, instead of multi-million metropoli like Kyoto and Tokyo:

    Cities like Yamaguchi and Morioka occupy an Other space — an alternative space between the disappearing, somewhat unsustainable, countryside villages, and the non-stop megalopolises. If Kyoto and Kanazawa and Hiroshima are the A-sides of Japan, Yamaguchi and Morioka are B-sides — the side often containing the understated genius of a record. I see a city like Yamaguchi and I think: Good life is possible here, full life, on a human scale, operating within the bounds of a warm community, feeling your own small contributions meaningfully add up in the lives of people around you. When I travel I’m not looking for the most delicious bowl of ramen or the perfect croissant, but rather archetypes of ways of living that set my imagination ablaze, that make me grateful to have encountered those people, and grateful for the social and political infrastructure that exists, allowing these people to live in their own, additive ways.

    When I read this, I’m immediately reminded of life in Indianapolis. I’ve always said it feels more like a big town than a small city. It’s just big enough to support the kind of vibrant creative scene that keeps life interesting, with local music, art and food that are worth celebrating. But the city still small enough that those same creative people can make a living doing what they love. What we have here are communities living together, building culture together, and supporting themselves in the process.

    Bike Party, May ’23

    Indianapolis is not really a city with “the Nation’s best…” anything, and I think that’s true for so many Midwest cities that are both beloved by their residents and looked down on by outsiders. It’s that feeling of superiority perfectly captured by the Talking Heads’s “The Big Country,” where David Byrne’s character looks down on the country from inside an airplane. The chorus goes, “I wouldn’t live there if you paid me / I wouldn’t live like that, no siree / I wouldn’t do the things the way those people do / I wouldn’t live there if you paid me to.”

    Reflecting on the song for a piece in Slate a few years ago, Byrne is quoted as saying:

    “If you look, the lyrics in the verses are more or less objective—there’s no disdain for the folks down there. It’s pure description; in fact, one could even say it’s sweet and bucolic, which makes the venom of the chorus more surprising (in my view). To me this song is about revealing the irrationality of that venom.”

    That venom makes no sense because that kind of distanced observation doesn’t tell you much. Places only reveal themselves upon close inspection. It’s the same reason why, when people talk about driving across Kansas and Nebraska as “long, flat, and boring,” I know they’re talking more about their experience inside the car than the landscape they’re driving through. Long? Sure. Flat? Mostly. Boring? You’re not looking close enough.

    A lush green landscape with a grain silo in the distance, framed by gathering storm clouds.

    If you think the tallest buildings in Kansas are the grain silos, that tells me you’ve never been to Kansas.

    I had to learn this, too. Before I moved to Indianapolis, I had the same opinion of the city as Byrne’s narrator: “why in the world would anybody live there?” But staying in any one place long enough means becoming a part of it; and it, of you. The longer the stayed, the more I came to appreciate the culture that’s grown up in Indianapolis: WQRT, Kan-Kan, BUTTER, First Fridays, Feast of Lanterns, and on and on.

    Indianapolis isn’t ranking as of the Best Places to Travel, but that also makes it a pretty damn nice place to live. It’s a creative city, but only because it’s also modest, affordable, and sustainable. It’s one of hundreds of similar mid-sized cities around the world. I love that Craig Mod knows to shine the blinding spotlight of a New York Times feature on a thriving city that lives in the shadows of its neighbor.

    Yamaguchi isn’t Kyoto, Morioka isn’t Tokyo, and Indianapolis isn’t Chicago. But it’s for exactly that reason that they’re worth checking out.

  • Programming Note

    Building my own publishing system has been a fun distraction. This is for better and worse. it keeps my programming skills sharp, but it gets in the way of actually publishing anything. To quote from one of my first notes, “the human condition, the condition of the tool-using animal, is to be perpetually vulnerable to mistaking instruments for ends”. I’m as guilty of that as ever.

    Anyway, I am about to deploy a new branch that rejiggers my RSS feed. It makes my highlights a second-class data type and elevating the posts I’m actually writing myself. I’m still trying to find the balance for what I want to capture vs. what’s truly publishable.

    As a result, anyone (anyone?) subscribed to my site’s feed may get a big chunk of old posts that will appear unread. It should be a one-time thing, until the next time I decide to flip the table and rebuild my publishing system yet again.

  • AI and Trust

    This transcript of Bruce Schneier’s lunchtime lecture at the Kennedy School is worth reading in full. He presents a loose case for how and why to regulate the deployment of AI:

    In this talk, I am going to make several arguments. One, that there are two different kinds of trust—interpersonal trust and social trust—and that we regularly confuse them. Two, that the confusion will increase with artificial intelligence. We will make a fundamental category error. We will think of AIs as friends when they’re really just services. Three, that the corporations controlling AI systems will take advantage of our confusion to take advantage of us. They will not be trustworthy. And four, that it is the role of government to create trust in society. And therefore, it is their role to create an environment for trustworthy AI. And that means regulation. Not regulating AI, but regulating the organizations that control and use AI.

    The ability to access to free, public events like this lunch is one of the things I miss most about living in Boston area.

  • First, some complaints about automating Apple stuff:

    • Why are there no Smart Mailboxes or Mail Rules on iOS? Why can’t that be synced through iCloud or similar? My rules only get run once I open Mail on my laptop.
    • The performance of Shortcuts on iOS 17 is terrible and I haven’t seen this discussed much. I’m talking like a 5-second delay between touch and response, it’s embarrassing.
    • Creating a Shortcut that makes a HTTP request is pretty challenging. There’s no good way to handle errors, so you’ve just gotta cross your fingers that everything goes 200.

    Now, some praise for Apple automations:

    • Once I’ve fought Shortcuts and gotten my endpoints working, the ability to fire off arbitrary API requests from my phone is super cool and powerful. I’m writing this in Drafts and publishing it with a Shortcut. That rules!
    • The ability to compose Shortcuts together means it’ll be easier to add new commands that do web stuff, once I have some utilities to handle the nasty bits.
    • I haven’t dipped into Scriptable yet, but it looks really cool. I’m hoping that if I can get out of the mess of the Shortcuts app, I’ll be able to work faster by just writing commands with JavaScript.
  • I just asked an LLM for help writing some AppleScript and—holy moly—it was so much better than trying to write AppleScript myself. Now that I don’t need to learn the syntax, I feel like a world of automation has just opened up to me.

  • Generating Feeds with Next.js Route Handlers

    Since I’ve started collecting notes and highlights here, I’ve been meaning to return them as formatted feeds, RSS being the main one. Well, I got around to it. It was way easier than I remembered, and I even got bonus Atom and JSON feeds out of it.

    I’m using Next 13.2 and its new App Directory to generate the site, so this made feeds delightfully simple to implement. In fact, it may be the best experience I’ve ever had for developing content feeds like these. I want to share my walkthrough and results since this is a pretty common task when setting up a new project with Next, and all the existing examples were based in Next’s older pages generation system.

    How to Generate RSS, Atom, and JSON Feeds with Markdown content using Next.js App Directory Route Handlers

    I started from the point of already having data-fetching functions for getting all my notes from my CMS (the aptly named getAllNotes and getNoteTitle).

    When adding a new function to generate the feed, it simply has to set the top-level properties then run over the notes to add them as entries. I author and store all my notes as Markdown, so for each note I render its body into HTML. Each feed format then gets its own Route Handler, which calls the generator function for the formatted feed. Finally, I update the top-level metadata to include links to the newly added feeds.

    Create a Site URL

    I quickly realized I needed a little utility function to get the canonical site URL. Since I build and host using Vercel, I want to make sure my site URL corresponds with its preview deploy URL. I used a combination of environment variables to figure that out, using a dedicated SITE_URL variable with Vercel’s system environment variables to figure out the build’s context and dedicated URL.

    src/utils/getSiteUrl.ts
    export default function getSiteUrl() {
      let protocol = "https";
      let domain = process.env.SITE_URL;
      switch (process.env.VERCEL_ENV) {
        case "preview":
          domain = process.env.VERCEL_URL;
          break;
        case "development":
        case undefined:
          protocol = "http";
          break;
      }
      return `${protocol}://${domain}`;
    }

    Render Markdown to HTML

    To render Markdown into HTML, I used the unified library with the plugins:

    1. remark-parse to parse the Markdown string into an AST
    2. remark-rehype to convert the Markdown into HTML
    3. rehype-sanitize to ensure the HTML is safe to render
    4. rehype-stringify to turn the AST back into a string

    This string was then passed as the content value for each feed item.

    src/utils/markdownToHtml.ts
    import { unified } from "unified";
    import remarkParse from "remark-parse";
    import remarkRehype from "remark-rehype";
    import rehypeSanitize from "rehype-sanitize";
    import rehypeStringify from "rehype-stringify";
    
    export default async function markdownToHtml(input: string) {
      const file = await unified()
        .use(remarkParse)
        .use(remarkRehype)
        .use(rehypeSanitize)
        .use(rehypeStringify)
        .process(input);
    
      return file;
    }

    Create the Feed

    With other site generation frameworks I’ve used, generating feeds has meant writing a template XML file and filling in dynamic values with curly-braced variables, usually with that format’s spec open alongside. This time, I was able to use the feed package for all the XML authoring. As a result, generating multiple feed formats became a matter of making a function call.

    The generateFeed function is based on an example provided by Ashlee M Boyer. It creates a feed with proper metadata, then generates each post. Since the Markdown generation runs asynchronously, adding entries needs to happen inside a Promise.all call. This way, generateFeed waits to return the feed object until all content has finished generating.

    src/utils/generateFeed.ts
    import { Feed } from "feed";
    import smartquotes from "smartquotes";
    import getAllNotes from "src/data/getAllNotes";
    import getNoteTitle from "src/data/getNoteTitle";
    import markdownToHtml from "./markdownToHtml";
    import getSiteUrl from "./getSiteUrl";
    
    export default async function generateFeed() {
      const notes = await getAllNotes();
      const siteURL = getSiteUrl();
      const date = new Date();
      const author = {
        name: "Allan Lasser",
        email: "allan@lasser.design",
        link: "https://allanlasser.com/",
      };
      const feed = new Feed({
        title: "Allan Lasser",
        description: "Thoughts, reading notes, and highlights",
        id: siteURL,
        link: siteURL,
        image: `${siteURL}/logo.svg`,
        favicon: `${siteURL}/favicon.png`,
        copyright: `All rights reserved ${date.getFullYear()}, Allan Lasser`,
        updated: date,
        generator: "Feed for Node.js",
        feedLinks: {
          rss2: `${siteURL}/feeds/rss.xml`,
          json: `${siteURL}/rss/feed.json`,
          atom: `${siteURL}/rss/atom.xml`,
        },
        author,
      });
      await Promise.all(
        notes.map(
          async (note) =>
            new Promise<void>(async (resolve) => {
              const id = `${siteURL}/notes/${note._id}`;
              const url = note.source?.url ? note.source.url : id;
              const content = String(await markdownToHtml(smartquotes(note.body)));
              feed.addItem({
                title: smartquotes(getNoteTitle(note)),
                id,
                link: url,
                content,
                date: new Date(note._createdAt),
              });
              resolve();
            })
        )
      );
      return feed;
    }

    Create the Feed Endpoints

    Now here comes the fun part. Creating feed endpoints becomes so simple it’s silly. Using Route Handlers introduced in Next.js 13.2, adding a new endpoint is as simple as creating a folder in the App Directory with the name of the feed file, then creating a route.ts file inside it.

    So, to add the RSS feed, I create the folder src/app/feeds/rss.xml and then create route.ts inside it.

    src/app/feeds/rss.xml/route.ts
    import generateFeed from "src/utils/generateFeed";
    
    export async function GET() {
      const feed = await generateFeed();
      return new Response(feed.rss2(), {
        headers: { "Content-Type": "application/rss+xml" },
      });
    }

    To create the Atom and JSON feeds, I follow the same process ensuring that the appropriate method and content type are used in the format’s route handler.

    src/app/feeds/atom.xml/route.ts
    import generateFeed from "src/utils/generateFeed";
    
    export async function GET() {
      const feed = await generateFeed();
      return new Response(feed.atom1(), {
        headers: { "Content-Type": "application/atom+xml" },
      });
    }
    src/app/feeds/feed.json/route.ts
    import generateFeed from "src/utils/generateFeed";
    
    export async function GET() {
      const feed = await generateFeed();
      return new Response(feed.json1(), {
        headers: { "Content-Type": "application/json" },
      });
    }
    

    Adding alternates to site metadata

    The last step is updating the site’s <head> to reference these feeds to make them more discoverable to readers. This is made even easier using the App Directory’s Metadata APIalso new to Next.js 13.2. In the top-most page or layout file in my app directory, I add an alternates property to the exported metadata object:

    src/app/layout.tsx
    import { Metadata } from "next";
    import getSiteUrl from "src/utils/getSiteUrl";
    
    export const metadata: Metadata = {
      title: "Allan Lasser",
      viewport: { width: "device-width", initialScale: 1 },
      icons: [{ type: "image/x-icon", url: "/static/favicon.ico" }],
      alternates: {
        canonical: getSiteUrl(),
        types: {
          "application/rss+xml": `${getSiteUrl()}/feeds/rss.xml`,
          "application/atom+xml": `${getSiteUrl()}/feeds/atom.xml`,
          "application/json": `${getSiteUrl()}/feeds/feed.json`,
        },
      }
    }

    That’s it!

    Now after running next dev, I can see I have feed files generated at /feeds/rss.xml, /feeds/atom.xml, and /feeds/feed.json. I’ve gotten feeds in three different formats with only a few libraries and simple, easily testable functions.

    After deploying to production, you can now follow my new notes via:

    The flourishing, decentralized Web

    The level of productivity I feel when using Next.js, Vercel, and GitHub together is really hard to beat. It feels like the tools are getting out of my way and letting me developer smaller PRs faster.

    I’m still a daily RSS user. It’s my preferred way to read on the web. I’m glad to see that there’s still robust library support for RSS and feed generation, at least within the Node ecosystem at least. I don’t think RSS is going anywhere, especially since it powers the entire podcasting ecosystem. It’s great to see the longevity of these open standards.

    Speaking of open standards, integrating an ActivityPub server into a Next.js application is something I’m interested in exploring next. It’d be very cool to have a site generated out of an aggregation of one’s own ActivityPub feeds, for example combinining posts from personal micro.blog, Mastodon and Pixelfed into a single syndicated feed.

    Seeing all of the recent progress in decentralizing important services has felt so cool. We can still keep the Web wild and weird, empower individuals with more tools for expressing themselves online, and have it all be user-friendly. Content feeds are an important force for good here, so I’m very glad how easy it is these days for even a novice developer to publish them.

  • Attention Gardening

    In summing up the unlikely, 30-year story of how Yellowstone’s algae inspired the invention of PCR (the biochemical technique used for COVID testing), Clive Thompson writes:

    I think I’m so smitten by this story — with its mix of deep curiosity into seemingly pointless subjects, followed by the discovery that this “pointless” material is wildly useful in a new domain — because it dovetails with my interest in “rewilding” one’s attention.

    I’ve written a bunch about “rewilding” (essays here), which is basically the art of reclaiming one’s attention from all the forces that are trying to get you to obsess over the same stuff that millions of other people are obsessing over. Mass media tries to corral your attention this way; so do the sorting-for-popularity algorithms of social media.

    Now, sometimes that’s good! It’s obviously valuable, and socially and politically responsible, to know what’s going on in the world. But our media and technological environment encourages endless perseveration on The Hot Topic of Today, in a way that can be kind of deadening intellectually and spiritually. It is, as I’ve written, a bit like “monocropping” your attention. And so I’ve been arguing that it’s good to gently fight this monocropping — by actively hunting around and foraging for stuff to look at, read, and see that’s far afield, quirkier, and more niche.

    This “rewilding” is the same sort of shift in attention away from commercial platforms that Jenny Odell argues for in her book. She uses the exact same analogy to “monocropping”:

    It’s important for me to link my critique of the attention economy to the promise of bioregional awareness because believe that capitalism, colonialist thinking, loneliness, and an abusive stance toward the environment all coproduce one another. It’s also important because of the parallels between what the economy does to an ecological system and what the attention economy does to our attention. In both cases, there’s a tendency toward an aggressive monoculture, where those components that are seen as “not useful” and which cannot be appropriated (by loggers or by Facebook) are the first to go.

    A monoculture is an illuminating frame for considering attention. Created in an attempt to achieve economies of scale, monocultures reduce biodiversity and exhaust their soil. To make up for this, they’re covered in heavy amounts of fertilizer and pesticide to maintain their productivity. The analogs to commercial social media are clear. Whether they’re lying about their metrics, unfairly compensating their creators, or simply moderating your timelines without explanation or accountability, commercial social media companies create toxic social conditions in order to establish themselves as places for huge numbers of people to sink their attention. Once they have it, they turn the screws to maximize value for their owners despite the damage it does to their ecosystems.

    In resisting this monoculture, I think Thompson misses a helpful middle-ground between a monocrop and a wilderness. In-between lies a garden: small-scale, intentional, low-impact cultivation of attention. A great garden takes time to establish, but once it does it can live by itself, supported by its rich diversity and interdependency.

    When I think about the ways I focus my attention, I’ve already established a few gardens. My library of books. My collection of RSS feeds. My relationships. My actual garden! All of these contribute to a diverse, interconnected space of shared ideas that help me understand and appreciate the world in new ways.

  • Thinking through a system for reading

    I want my website/homepage to give me ways to keep track of bookmarks, notes and highlights in the things that I’m reading. I’m typically creating these on my phone or tablet, and the kind of data varies with the kind of reading:

    • when browsing the web, I’m saving tagged bookmarks into Pinboard. This is like my private search engine, where it’s easy to recall things I have seen in the past that I wanted to remember.
    • when reading on my phone or tablet, it’s usually RSS or Instapaper–in that case, I’m typically highlighting passages and marking posts as favorites. I want to be capturing my highlights and favs as content in Sanity.
    • I don’t typically read books on computer, but I also want to be capturing highlights and tracking favs as content in Sanity.

    Seems like we have a system starting to come together:

    1. bookmarks can continue living in Pinboard, and I can provide a way to browse and search these on my personal site.
    2. I have the same workflow triggers for my reading. I want to save favorite articles or books into Sanity, and I want to capture highlights or reading notes on those entities.

    Are a highlight and a note the same? I think so, because they’re both text. That text could be anchored to a page or other location, but it’s all just text content at the end of the day. I think I would want to create multiple entries, with one for each note/highlight. This also allows for a note to be combined with a highlight if a passage triggers a thought.

    This is exactly the same kind of Markdown I’d be capturing in Drafts. I could easily turn this into a JSON payload and send it off.

    Since I’m starting from a place where I’m capturing notes, it makes sense that notes would have an association with a source. The source could be a web article, a book, a movie, anything really. I could put a type field on sources to distinguish between mediums, if necessary. I could also pretty easily generate a citation for each note if I know the page and the source!

    If I’m capturing a note on a web article, I’m probably going to need to create the source at the same time that I create the note. If I’m reading a book, there’s a good chance that I’ll need to find an existing source for the note.

    At the time I’m creating a note, there’s two ways that I will be associating it with a source. I will either need to be creating the source at the same time, or I’ll need to find an existing source.

    This workflow suggests that I’ll want to:

    1. Capture the highlight and create a draft entry in Sanity
    2. Get the URL for the newly created entry, then open its page in the browser
    3. From there, I can associate the newly created note with a source and publish it.

    I also want to be able to create a source from a URL or from an ISBN. This is a convenience feature and can come later!