← Back to home

TagWise AI · Personal project · Beta

AI tagging that waits for a yes

TagWise is a Shopify app that writes and scores SEO tags for products. Merchants told me they didn't want AI changing their live store without seeing it first, so every AI tag lands as a draft they can edit, and nothing is saved until they confirm.

It also scores existing tags out of 100, using my own rules and keyword lists for 13 industries, and tells merchants what to fix.

40%
Lower perceived latency
60fps
At 100+ products
13
Industry keyword lists
6
Scoring checks
Interaction designUX researchReactRemixShopify PolarisOpenAI API
TagWise dashboard with a product list, tags, scores, and suggestions
The TagWise dashboard: search, filter, generate tags, and score them.

The problem

Tagging a big catalog by hand doesn't work

Shopify merchants use tags for collections, filters, and search, so bad tags cause real problems. On a big catalog, tagging by hand takes forever and gets inconsistent over time. People also reach for vague tags like "hot," "new," or "best-seller," which don't help anyone find the product.

AI can write tags quickly, but merchants were nervous about letting it change their live store. That worry ended up shaping the whole app.

Decision 1

Adding a review step on purpose

Merchants cared more about control than speed

My first sketch had no review step: pick products, click Generate, and the tags go straight to the store. Talking to merchants changed that. Speed wasn't what worried them. Control was.Tags run their collections, filters, and search, so an AI quietly changing them is exactly the kind of automation they'd learned not to trust. They wanted to see what it would do before it did it.

Considered

Fully automatic

The fastest option, and the one merchants said they wouldn't use. One bad batch could quietly break collections and filters.

Considered

Confirm every tag

Safe in theory, but on a big catalog the pop-ups turn into noise, and people click through without reading.

Shipped

Batch review

Generate tags for up to 10 products at once. They show up as drafts that look different from live tags, and one Confirm saves the whole batch.

I bet that one clear review step would earn more trust than no step at all, and be less tiring than confirming everything. That choice drove the rest of the UI. Draft tags are orange and live tags are gray, and the state doesn't rely on color alone: drafts have an "×" to remove them and an input to add more. Saving only happens when you press Confirm. There's no autosave.

How it works

Generating and reviewing tags

AI suggests, the merchant decides

Pick up to 10 products and click "Generate AI Tags." The OpenAI API reads each product's title and description and suggests tags for things like material, use, and who it's for. Nothing is saved yet.

The new tags show up in orange next to the existing gray ones. Merchants can delete any they don't like, fix the wording, or type in their own. Only "Confirm Tags" calls Shopify's productUpdate mutation. There's also an option to replace all existing tags with the AI ones, and Cancel throws away every change.

A product with orange AI draft tags next to gray live tags
Gray tags are already live. Orange tags are AI drafts you can edit. Nothing saves until you click Confirm.

Drafts stay in the app

AI tags live in the app's state until Confirm. Shopify isn't touched before that.

The merchant has the last word

Each tag has an "×" to remove it, and there's an input for adding your own.

Batches of 10

Generate and save tags for up to 10 products at a time.

Decision 2

Scoring tags by industry

A score, plus what to fix

Select products and click "Score Selected Tags." Each product gets a score out of 100 across six checks: how many tags it has (5 to 10 is the target), duplicates, weak words like "hot" or "new," whether the tags cover function, material, and use, whether they match the title, and how varied they are.

One set of rules can't work for every store. A good tag for a snowboard looks nothing like a good tag for a lab instrument. So I wrote keyword lists for 13 industries, including fashion, electronics, industrial, beauty, sports, and food, each split into function, material, and use keywords. That way "waterproof" counts for sports gear, and food products don't get marked down for missing it.

Scores and the top suggestion show up right in the product list. "View Report" opens the full breakdown with up to three suggestions, like "Contains weak words (e.g., 'hot'). Try using more specific terms."

Product list with SEO scores and suggestions
Full score report with suggestions
Left: scores and suggestions in the product list. Right: the full report.

How the 100 points break down

20
Tag count
5 to 10 tags
15
No duplicates
Each tag is unique
10
No weak words
No vague or spammy tags
15
Keyword coverage
Function, material, and use
20
Matches the title
Tags reflect the title
20
Variety
Covers different angles

The details

Finding the products that need work

Search, filter, and sort a large catalog

Merchants can search by product name, filter by tags a product already has, filter by collection (loaded live from Shopify GraphQL), and sort by last updated or by name. Together that makes it practical to find a group of products and fix them in batches.

Each row shows the product image, name, description, current tags, SEO score if it has one, the top suggestion, and a "View Report" link. Products with low scores or missing info are easy to spot.

Dashboard with search, sort, tag filter, and collection filter
Search by name, sort, filter by existing tags, and filter by collection.
Technical deep dive
1

Three tag states

The app keeps three versions of a product's tags at once: the original tags from Shopify, the AI drafts, and the final tags that get saved. That covers the messy cases, like generating again for a product that already has drafts, mixing in manual tags, or going back to the originals on Cancel.

// Original -> AI draft (review) -> Final (saved)
// productUpdate only runs when the merchant clicks "Confirm Tags"
const mutation = `
  mutation productUpdate($input: ProductInput!) {
    productUpdate(input: $input) {
      product { id, tags }
      userErrors { field, message }
    }
  }
`;
2

Keyword lists for 13 industries

Fashion, electronics, industrial, beauty, baby, sports, pet, food, craft, digital, home and kitchen, adult, and general. Each list is split into function, material, and use keywords, and the scorer picks the list based on product type.

// 13 industry keyword lists
const KEYWORD_MAP = {
  fashion: { function, material, scene },
  electronics: { function, material, scene },
  industrial: { function, material, scene },
  // ...10 more
};
// The scorer picks the list for the product type
const keywords = KEYWORD_MAP[productType] || generalKeywords;
3

Collecting every tag in the store

The tag filter needs every unique tag in the store. A server-side loop asks Shopify for products 100 at a time with cursor pagination and adds each tag to a Set to drop duplicates. It runs in a Remix loader, so the browser isn't doing the work.

// Collect every unique tag in the store, 100 products at a time
const allTags = new Set();
while (hasNextPage) {
  const data = await admin.graphql(query, {
    variables: { first: 100, after: cursor }
  });
  for (const edge of data.products.edges) {
    edge.node.tags.forEach(tag => allTags.add(tag));
    cursor = edge.cursor;
  }
  hasNextPage = data.products.pageInfo.hasNextPage;
}
4

Keeping keys on the server

All OpenAI calls and Shopify admin requests run in Remix loaders and actions, so API keys never reach the browser. Shopify App Bridge handles the session, and Prisma stores settings and tag history. TypeScript types describe the GraphQL responses, so shape mismatches show up while I'm writing code.

What I learned

The biggest lesson was that an AI feature needs extra care in the interface. I wanted the flow to be "click a button, get tags, done." Merchants didn't trust that. The review step adds one click, and it's the reason they're willing to use the tool at all.

The scoring taught me how much the domain matters. Writing 13 keyword lists was tedious, but without them the score would just be a number nobody trusts.