Skip to content

Shank Errors and Pitfalls

Shank does not run at request time, so “errors” here are split into two buckets: build/export errors that surface during pnpm dev / pnpm export, and render errors that surface only once sirloin substitutes placeholders and a client renders the HTML.

  • Port already in use. email dev defaults to 3000. Kill the other process or run pnpm dev -- --port 3001.
  • react/react-dom version drift. react-templates/package.json pins react@19.0.0 and react-dom@19.0.0 and depends on react-email 3.0.6 and @react-email/components 0.0.32. If a transitive bump pulls React 18, JSX from @react-email/components will throw at render time. Run pnpm install from apps/shank/react-templates/ (not from the monorepo root) to get the locally pinned versions.

pnpm export writes nothing or writes to the wrong place

Section titled “pnpm export writes nothing or writes to the wrong place”
  • The export path is hardcoded relative to apps/shank/react-templates/: --outDir='../../sirloin/internal/pkg/emails/templates'.
  • If you run email export from a different directory, the output lands in the wrong tree. Always run from apps/shank/react-templates/.
  • If the path is missing (e.g. fresh checkout that pruned the sirloin tree), the command silently fails to create directories. Verify with git status apps/sirloin/internal/pkg/emails/templates/.

Tailwind classes do not appear in exported HTML

Section titled “Tailwind classes do not appear in exported HTML”
  • react-email inlines styles at export time via the Tailwind wrapper component. If you import a component outside of <Tailwind>{...}</Tailwind>, classes are dropped. Existing templates wrap their entire body in <Tailwind> — keep that pattern.

Placeholder appears literally in the email

Section titled “Placeholder appears literally in the email”

Symptom: the recipient sees {{CHARACTER_NAME}} instead of the character’s name.

  • The token in the TSX must match the constant in apps/sirloin/internal/pkg/emails/client.go exactly. Sirloin uses strings.ReplaceAll(body, "{{CHARACTER_NAME}}", characterName) — a typo on either side leaves the literal token in the output.
  • Add new tokens in both places (template and Send* method) in the same change.

Sirloin passes raw strings to ReplaceAll. If a caller passes "", the placeholder is replaced with an empty string and the email renders with a gap where the value should be. Validate inputs in the caller, not in the template.

Preview text (<Preview>...</Preview>) is interpolated with the same {{CHARACTER_NAME}} token. Some inboxes display preview text in notifications. Avoid putting anything more sensitive than a character name into preview strings. See docs/src/content/docs/standards/security-model.md for the canonical PII classes.

  • Outlook (desktop) renders via Word. Avoid CSS not supported there: flexbox, grid, position: absolute, custom fonts. Stick to the components from @react-email/components — they ship Outlook-safe markup.
  • Dark-mode handling: react-email does not auto-invert images. Provide light-on-transparent assets where possible. Existing templates use https://images.foxy.ai/logo1_*.png which is light.
  • Litmus / Email-on-Acid: not currently wired into the build (no external visual-check tooling appears in apps/shank/react-templates/ or .github/workflows/). TODO(unassigned): document whether designers run external visual checks before merging; if so, add the checklist link to shank-runbook.md.

Run pnpm dev and inspect the live preview before exporting. The preview matches the exported HTML 1:1 except for the unsubstituted {{TOKEN}} placeholders.