Pasting into multiple fields at once

I recently came across a situation where I wanted to extract YouTube video data into a form to feed to an LLM to extract some information. I wanted to collect the video title, description, and transcript. I wrote a simple Chrome extension to extract this information from the current video in the tab and copy it as a json object to the clipboard.

let title = await getVideoTitle();
let description = await getVideoDescription();
let transcript = await getVideoTranscript();

let data = JSON.stringify({title, description, transcript})

await navigator.clipboard.writeText(data);

Each of these async functions uses css selectors and document.querySelector to get a handle on the appropriate elements, click them if needed, and then essentially get the text via element.textContent. The end result is that I have a json object copied to my clipboard.

I have a simple form in my Remix app to submit this data. It looks something like this:

<Form method="post">
    <Label>Title</Label>
    <Input name="title" />

    <Label>Description</Label>
    <Input name="description" />

    <Label>Transcript</Label>
    <Input name="transcript" />

    <Button>Submit</Button>
</Form>

Now let's get fancy.

We're going to add an onPaste to fill out this form all at once from our json data.


function YouTubeForm() { let getFormInput = (name: string): HTMLInputElement => { return document.querySelector(`[name="${name}"]`)!; }; let handlePaste: ClipboardEventHandler<HTMLInputElement> = (e) => { let pasted = e.clipboardData.getData("text"); try { let data = JSON.parse(pasted); getFormInput("title").value = data.title; getFormInput("description").value = data.description; getFormInput("transcript").value = data.transcript; // prevents the json from being pasted into the input e.preventDefault(); } catch (e) { // ignore and paste normally } }; return ( <Form method="post"> <Label>Title</Label> <Input name="title" onPaste={handlePaste} /> <Label>Description</Label> <Input name="description" onPaste={handlePaste} /> <Label>Transcript</Label> <Input name="transcript" onPaste={handlePaste} /> <Button>Submit</Button> </Form> ) }

Boom! Now we have a pretty slick and easy way to fill out our form in one go that seems a little magical to those aren't aware of what's happening behind the scenes. You can check out a simple codesandbox demo of this idea here.

This combo of using a Chrome extension to copy the data and then this paste event to fill out all the fields at once has saved me a ton of time. If you don't want the hassle of creating a Chrome extension you can go the simpler route and create a Chrome Snippet. It's more cumbersome to use since you have to pop open the dev tools, locate your snippet, and then click to run it but it'll get the job done.

Hope you enjoyed this and got your wheels turning on how and where you can implement this concept to benefit you and/or your app's users.

Maybe don't navigate(-1) in Remix/React Router

Remix/React Router 7 has a handy hook for navigating without the user interacting. It covers use cases like:

  • Navigating based on complex logic
  • Navigating to logout after inactivity
  • Implementing "back" links

You can give it a route path:

const navigate = useNavigate()

const goSomewhere = () => navigate('/some/place/in/your/app')

Or you can give it an integer to navigate the user's history stack:

const goBack = () => navigate(-1) // go the previous browser location

At first glance, this seems pretty handy for implementing a "Back" link. For example, if you have a list of todos and have the ability for the user to navigate to a single todo. You might want to have a convenient "Back" link to direct the user back to the list of todos. This is especially useful if you have multiple ways to navigate to a single todo:

  • View all todos -> click on a single todo
  • View users todos -> click on a single todo
  • View todos of a certain category -> click on a single todo

"Back" can mean different things in the single todo route so you wouldn't want to just hardcode a link:

<Link to='/todos'>Back</Link>

This would always assume you navigated from a the same place, the /todos route. No bueno. You could, however, slap an event handler on it and make use of navigate(-1):

<Link
  to='/todos'
  onClick={(e) => {
    e.preventDefault()
    navigate(-1)
  }}
>
 Back
</Link>

This seems better but there's another problem...

Browser context

The problem with navigate(-1) is that it isn't limited to your app's history stack. It's the browser tab's history stack. The Remix docs give you some words of caution about this behavior:

Note that this may send you out of your application since the history stack of the browser isn't scoped to just your application.

I can't think of any web application that I use where I want that behavior. I'm of the mind that a "Back" link in a web application should be contained to the application. I don't want to click a "Back" link in an app and get bounced out of the app.

Let's make it better

Remix/React Router 7 gives us a handy way pass data during a navigation via the state property of the <Link> component.

<Link to={`/todos/${todoId}`} state={{back: '/todos'}}>Todos</Link>

And then if the user clicks on that <Link>, in the todos.$id route component you can read that state:

const location = useLocation()
let back = location.state?.back // '/todos'

We just need to make use of that in a "Back" link:

<Link
  to='/todos'
  onClick={e => {
    if (back) {
      e.preventDefault()
      navigate(back)
    }
  }}
>
  Back
</Link>

Now, if the user click on a link with back state defined, we'll use that. Otherwise, we'll let the <Link> navigate to to. Nice.

Let's make it easy

We can define some custom hooks to reuse some of this logic and make things a bit easier.

First, we'll define a simple hook to return the current route url with search params to use as the back state value.

function useCurrentURL() {
  let location = useLocation()
  return location.pathname + location.search
}

Using it would look like something like that.

const currentURL = useCurrentURL()

// ...

<Link to={`/todos/${todoId}`} state={{back: currentURL}}>Back</Link>

Then, we can create a hook to handle this state.

import { type LinkProps, useLocation, useNavigate } from '@remix-run/react'

export function useBackNavigation() {
  let navigate = useNavigate()
  let location = useLocation()

  let handleClick: LinkProps['onClick'] = (e) => {
    let back = location.state?.back
    if (back) {
      e.preventDefault()
      navigate(back)
    }
  }
  return handleClick
}

Using it would look something like this.

const handleBack = useBackNavigation()

// ...

<Link to='/todos' onClick={handleBack}>Back</Link>

Much better. Now we can navigate back based on the presence of a back navigation state value, otherwise we'll fall back to the default link's location. 🎉

Alright, that was a pretty lengthy post about "Back" links but hey, we sweat the small stuff 'round here 🍻

Failure to Launch (A Cautionary Shopify App Tale)

I get inspired by people like Pieter Levels Ruben Gamez, DHH, and Rob Walling, among many others. I recently read Rob Walling's The Stair Step Method of Bootstrapping and thought I'd give that a shot with building a Shopify plugin. I don't know the first thing about effective marketing. I don't have a huge twitter audience or mailing list. The built-in marketing from the Shopify app store was really appealing to me and my lack of marketing skills.

The Idea

Most Shopify product pages have a gallery of images for each product. A set of photos for the same product. Shopify will use the first of those images for the Open Graph images. These photos are also usually portrait oriented(taller than they are wide). This tends to result in the images getting cropped in funky ways leave you with social impressions that leave a lot to be desired. Here's an example.

What you see on social media:

And the actual product:

I thought this would be an interesting problem to tackle so I began to build an app the would generate branded OG images for each product automatically that actually showcased the product in a much more flattering way.

The development

I was really attracted to Shopify because it meant I could use Remix, the react framework I most comfortable and productive with.

The Remix aspect was great. Learning the Shopify ecosystem and sdk's was like drinking from a disjointed fire hose though. I encountered several really confusing things:

  • webhooks
    • subscribing via yaml and/or the shopifyApp remix utility
    • at the time debugging payloads was a headache. It looks like that's gotten better.
    • unsubscribing
      • I would get development emails about webhook subscriptions failing but never knew how to unsubscribe those.
  • apis
    • some things are in the REST api
    • other things are in the GraphQL api
  • development via tunnel
    • sometimes this would just fail to launch so I'd need to restart it randomly
    • you can't develop offline because it gets rendered into the Shopify admin site
    • when you start the app in development a new tunnel is created and this automatically updates your app's configuration to point to it. You have to remember to manually switch this over to your production url or else you're at risk of the reviewer using your development tunnel
  • billing
    • you have to implement your own UI to change plans even though you're supposed to define them in your app config and in the listing
    • billing.request()'s returnUrl was not documented at all. This looks like it's been updated in the docs.
  • the authenticate. vs unauthenticated api's and when to use each

It's probably just the nature of Shopify's ecosystem having grown to be so big, but I found myself bouncing around all over completely different sets of docs.

Injecting meta tags

By default, in the themes I came across there is a meta-tags.liquid snippet that renders the meta tags for og:image, title, description, etc. This gets used in the main theme.liquid file.

In my app's onboarding, I was instructing the user to edit their meta-tags.liquid file to use OG images from my app rather than what's used by default, the shopify cdn.

I found no other way to programmatically update the OG images. Shopify doesn't have a means for users, or developers, to assign specific OG images per product which was very surprising to me. You can set a custom OG image but the same image would be used for every link to your store. Oh well, I'll just settle on instructing the user how to update their theme file I thought... (foreboding music begins to play)

Getting ready

Finally, after spending several evenings getting an app together I was ready to submit the app for review. This involved putting together an app listing page. On the surface that doesn't sound like a big task but when you're lacking in the marketing department it can be tough coming up with app visuals, blurbs, feature lists, logos, app icons, etc. There's a lot more to it than I initially anticipated.

App review

A few days later after I submitted the app I received my first kickback from Shopify which I expected. There's a lot of requirements, not just in the app listing but the app itself. They have an actual person review the listing and attempt to use your app in a demo store.

There were some easy to fix app listing issues I tackled and some billing items that I needed to flesh out more. No problem. The biggest issue was the reviewer telling me that instructing the user to edit theme files to enable my app is not a good user experience and to use "app embed blocks". I hadn't come across them yet and it looked like exactly what I needed. Sweet.

I quickly implemented an app embed block that would inject markup into the <head>. This markup would contain the meta tags for the OG images but there was a problem... When using app embed blocks, your app's markup gets appended to the existing content in the <head>. It ends up looking like this

<head>
    <meta charset="utf-8">

    <!-- BEGIN rendering `meta-tags.liquid` -->
    <meta property="og:image" content="https://cdn.shopify.com/s/files/1/2341/3995/files/some-product-image.jpg?v=1720565083" />
    <meta property="description" content="blah blah blah">
    <!-- more stuff... -->
    <!-- END rendering `meta-tags.liquid` -->

    <!-- BEGIN app block: shopify://apps/your-apps-name/blocks/your_block_name/e512386f-5389-4282-aadb-21c442089ea -->
    <meta property="og:image" content="https://your-app-domain.com/images/product-123" />
    <meta property="description" content="blah blah blah">
    <!-- END app block -->
</head>

The show stopping problem is that social sites only use the first og:image that's rendered and if you don't stop the meta-tags.liquid file from doing that then that's what's going to be used. The app embed block is going to be worthless unless I can prevent Shopify's theme from rendering the og:image tag.

So armed with that knowledge, I updated the onboarding to comment out a single line from the meta-tags.liquid file. This would allow my app embed block's og:image tag to actually be used since it would be the only one rendered in the markup. My new onboarding instructions and image:

  1. Comment out the line
  2. Save the theme file

I deployed my changes and then sent off a reply explaining everything I fixed and how I was now using an app embed block like he suggested but I still had to instruct the user to comment out a line in one of their theme files.

I received a reply and it was at this point that I knew this project was headed for the bin. The reviewer turned into a parrot who kept repeating the same thing during a series of back and forth emails: I needed to not instruct the user to edit theme code and instead accomplish it via app embed code. I repeatedly attempted to explain my dilemma and asked for guidance but was simply told to use the help forum and that reviewers are not responsible for help with code but that I could apply to be able to use the Assets API which allows apps to automatically update theme files. That went nowhere. The back and forth with the reviewer became almost like talking to AI chatbot who kept repeating the same recommendation of using app embed blocks and not instructing the user to edit a theme file even though I've repeatedly explained why that was insufficient.

Even more frustrating is the fact that there are apps in the app store that provide a means to update OG images. When I brought this up to the reviewer I was simply told that the rules have changed. That's a pretty amazing moat for those older apps...

Lessons learned

It's really easy to get incredibly frustrated by this situation but I have to remind myself that no matter what new thing I'm trying, I'm always learning and adding to my toolkit.

  • I've checked another idea off the list of things to try. I'm a doer, not a talker.
  • I now have some familiarity with the Shopify development ecosystem.
  • I've given my marketing muscle a workout when putting together the listing page.
  • I've experienced first-hand why should launch ASAP and fail fast so you can move on to the next thing.
  • I've gained an even deeper respect for indie developers who are actually to make this work.
  • I've learned about MX Route(not affiliated) and now have a lifetime email service for any future app ideas I want to attempt to make work. I've eased that inevitable friction a bit.

If by the off chance someone from Shopify reads this, I would love to be part of a pilot program that allows developers to update OG images per product when that get's implemented. 😀 Or, if anyone knows of someway to do this that I somehow missed, I would love to chat. Reach out to me here, on 𝕏, or Bluesky!