Shank Email Templates
Scope
Shank is not a runtime service. It is a build-time React Email project that compiles JSX templates to static HTML and writes the result into sirloin’s embedded template directory. There is no network surface, no endpoints, no public API.
The “API” of shank is the set of HTML files it produces and the placeholder
tokens those files contain. Sirloin’s internal/pkg/emails package treats
these as plain string templates and substitutes {{TOKEN}} placeholders at
send time (see apps/sirloin/internal/pkg/emails/client.go).
Source location
- React Email project:
apps/shank/react-templates/ - Templates:
apps/shank/react-templates/emails/*.tsx - Export target (committed):
apps/sirloin/internal/pkg/emails/templates/*.html
The export path is hardcoded in apps/shank/react-templates/package.json:
"export": "email export --outDir='../../sirloin/internal/pkg/emails/templates'"Exported templates
| Source TSX | Exported HTML | Sirloin sender |
|---|---|---|
emails/training-done.tsx | templates/training-done.html | Client.SendTrainingDone |
emails/training-failed.tsx | templates/training-failed.html | none — client.go loads the template into c.templateTrainingFailed but no SendTrainingFailed method exists in apps/sirloin/internal/pkg/emails/client.go (verified by grep -rn 'SendTrainingFailed' apps/sirloin, no matches). The template is currently dead code on the sirloin side. |
training-done
- Subject (set by sirloin):
"Training completed for " + characterName + "!" - Preview text:
Your character {{CHARACTER_NAME}} is ready on Foxy AI! - Placeholders:
{{CHARACTER_NAME}}— display name of the trained character{{CHARACTER_ID}}— UUID, used in deep-linkhttps://app.foxy.ai/?trainingSuccessId={{CHARACTER_ID}}
- Sirloin call site:
func (c *Client) SendTrainingDone( ctx context.Context, userID, characterName string, characterID uuid.UUID,) errorCaller: apps/sirloin/internal/app/worker/checkmediageneration.go:262.
training-failed
- Preview text:
Your character {{CHARACTER_NAME}} training failed - Placeholders:
{{CHARACTER_NAME}}only (verified bygrep -oE '\{\{[A-Z_]+\}\}' apps/sirloin/internal/pkg/emails/templates/training-failed.html). The source TSX exportsTrainingDoneEmailas its default — a copy-paste artifact tracked inshank-errors.md; the exported HTML itself does not surface the wrong component name beyond the preview text. - Sirloin sender: none —
apps/sirloin/internal/pkg/emails/client.goloads the template intoc.templateTrainingFailedbut noSendTrainingFailedmethod exists (grep -rn 'SendTrainingFailed' apps/sirloinreturns no matches). The template is currently dead code on the sirloin side.
How sirloin consumes the templates
apps/sirloin/internal/pkg/emails/client.go embeds the template directory
at compile time:
//go:embed templatesvar emailTemplates embed.FSOn NewClient, it reads training-done.html and training-failed.html
into in-memory strings. Each Send* method does straight
strings.ReplaceAll substitution on the {{TOKEN}} placeholders, then
sends via SMTP+TLS using credentials passed to NewClient.
This means:
- Templates are part of the sirloin binary after
pnpm export+go build. - Shank changes are not deployed independently — sirloin must be rebuilt and redeployed to pick them up.
- New placeholders require two coordinated changes: the template (shank) and the substitution call (sirloin).
Adding a new template
- Add
apps/shank/react-templates/emails/<name>.tsx. - Use
{{TOKEN}}placeholders for any dynamic content. Tokens are uppercase snake-case by convention ({{CHARACTER_NAME}},{{CHARACTER_ID}}). - Run
pnpm exportfromapps/shank/react-templates/. - Add a loader branch in
Client.NewClientfor the new HTML file. - Add a
Send<Name>method that does thestrings.ReplaceAllsubstitution and callsc.send. - Wire the caller (typically a worker in
apps/sirloin/internal/app/worker/). - Commit both the source TSX and the exported HTML.
See shank-runbook.md for the full release flow.