Repository navigation
Copy as code: copy a code-managed template as its HTML file #509
Copy link
Copy link
Closed
Labels
Description
Map: Map: send code-managed email templates with one PostHog call
Spec: Spec: copy-as-code and open-a-PR for code-managed templates (revision 2), section "Copy HTML file (stack A)"
Cast: Opus 5.5, high
Repository: PostHog/posthog (frontend only)
Stack: A, second slice
Question
Can a person copy a code-managed template as the exact .html file posthog.emails.templateFile() reads?
Delivers end to end.
- Menu item. The side menu of "Edit with your agent" offers "Copy HTML file".
- Encoder. A pure encoder turns the published template into the file:
- the subject goes into the first
<title>and the preheader into the first<meta name="preheader">, each added when missing; - a tag whose parsed value already matches stays as it is;
- a blank preheader removes its tag;
- Liquid stays literal;
- the document is always the published HTML.
- the subject goes into the first
- Toast. The spec's toast after copying, plus the text-part note when the saved template has a nonempty text part. Copy is never blocked.
- Event:
message template html file copied{template_id, dropped_text_part}.
Test seams.
- The encoder against a vector table. Its expected parse results and hashes are recorded once from the SDK's
parseTemplateFileandemailContentHashat feat(node): send transactional email with the flat SDK contract PostHog/posthog-js#5312's head, including the research report's synthetic vector (v1:b967355e…). Cover:- the parse round trip;
- idempotence;
- an SDK-uploaded file encoding to identical bytes;
- a missing head, title or meta;
- entities in the title;
- a blank preheader;
- Liquid.
- Kea logic test: the clipboard holds the encoded file and no request leaves the page.
- Prior art:
messageTemplateLogic.test.ts.
Write scope. products/workflows/frontend/TemplateLibrary/: a new encoder module and its test, MessageTemplateMenu.tsx, messageTemplateLogic.ts, templateCopy.ts.
Proof.
- Red: the "SDK file encodes to identical bytes" vector fails.
- Green: the encoder and logic tests pass.
- Local:
- upload a template from a file through posthog-node, copy it, and
diffthe clipboard against the file (no difference); - upload one through REST with a separate subject, preheader and text part, copy it, check the title and meta, and screenshot the text-part note.
- upload a template from a file through posthog-node, copy it, and
- Screenshots at 1280px with the side panel open and at full width.
Acceptance criteria
- Copying an SDK-uploaded template gives back the original file byte for byte.
- The SDK's parser reads the copied file's subject and preheader as the template's.
- Screenshots in the PR.
Blockers
Blocked by #503 (the button and its side menu).
How to build (map #461)
/tddred-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.- Repo skills apply where the diff touches their area:
/writing-tests,/writing-ui-components,/writing-kea-logics,/writing-user-facing-copy,/writing-code-comments,/improving-drf-endpoints,/routing-outbound-api-calls. - Read code from
origin/spec/code-managed-templates(the fork's integration branch for feat(messaging): upload, send and lock code-managed email templates PostHog/posthog#118924), not frommaster. - Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use
example.com. The private note the map points at is background only; never quote it. - Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge. The conductor names the stack's integration branch.
Activity
Silthus commentedon Oct 17, 2026
Runtime: T3 child task
Task:
node:delegated-task:command%3Amcp%3A1778d880-5a25-404c-80d0-25fc5ccca3b2%3Adelegate-task%3Awf-email-impl-509-1Cast: Opus 5.5, high. Cap 180 minutes.
Silthus commentedon Oct 17, 2026
Note
🤖 Opus 5.5 responding on behalf of Michael
Resolution
Landed
8a50f6ef11conspec/template-agent-editas a fast-forward from62c695af6cb. The ship ticket #516 owns the PR and the review.templateHtmlFile.ts, pure) turns the published subject, preheader and HTML into the fileposthog.emails.templateFile()reads:<title>. A missing title is added at the start of<head>, and a missing<head>is added too;<meta name="preheader">. A missing tag is added after the title;ésurvives;message template html file copied{template_id, dropped_text_part}.Products/Workflows/Template library/Edit with your agentgainsCopy menu open.Proof
Red (red.log): the final test table against an encoder that always rewrites the title and meta.
1 failed, 10 passed. The failing case isgives back the file the SDK uploaded byte for byte: an author's own entities, whitespace and attribute order: the output losesCaféand the author's single-quoted meta. The logic red (red-logic.log):TypeError: logic.actions.copyHtmlFile is not a function.Green (green.log): every suite under
TemplateLibrary/pluslib/components/Giton the landed tree.8 suites, 101 passed, nothing skipped.templateHtmlFile: 3 files uploaded by the SDKv1:b967355e…; an author's entities, whitespace and attribute order; a commented-out title before the real one, with no preheadertemplateHtmlFile: 8 encoded templates>, a subject that matches after whitespace collapsesmessageTemplateLogic, API mocked at HTTP, with and without a text partPATCHorPOST; the exact toast; the event withdropped_text_partI recorded each row's expected parse and hash once with the SDK's own
parseTemplateFileandemailContentHash, at PostHog/posthog-js#5312's head87e9e95ca65(script). The server's Python hash agrees on the SDK rows.Property check, not committed (script, result): 10,560 combinations of document shapes, subjects and preheaders. The cases include comments, an SVG
<title>, entities, quotes, Liquid and blank values. For each, the real SDK parser read the encoded file back as the template's subject and preheader, and a second encode changed nothing.failed 0.Local gate on the landed tree:
tsgo --noEmit --checkers 1infrontend/throughfleet heavy: exit 0 (log).pnpm --filter=@posthog/frontend fixthroughfleet heavy: exit 0, no further changes.messageTemplateLogic.tsmatches the committed inline types.hogli ci:preflight --strict):"failures": 0(log).build:openapior mypy run.Browser round trip (Storybook from this worktree, real
Appscene, API mocked at HTTP, headless Chromium with clipboard access, log):parseTemplateFile's subject and preheader, with the whole file as HTML. After "Copy HTML file",diffbetween the clipboard (560px, 1440px) and the file finds no difference.<head><title>Your weekly summary</title><meta name="preheader" content="Three things changed this week"></head>. The SDK parses it back to that subject and preheader (parse). The toast carries the text-part note./api/requests after any copy click.Screenshots (MockHog project,
example.comrepository, invented copy, no customer data). Narrow is a 560px window, about the scene of a 1280px window with the side panel open, as #503 used. Wide is 1440px.Committed story: Copy menu open.
Gaps
template_uploads._email), so the page gets the same template. Ship stack A: edit a code-managed template with your agent #516 can run the real upload on the PR head.Calls made
EditWithAgentButton. It follows the precedent ofworkflow-publishinWorkflowSceneHeader.tsx(sideActionwith aLemonMenuOverlay).MessageTemplateMenu.tsxandtemplateCopy.tsare unchanged: the overflow menu is not the button's side menu, andtemplateCopyis the Duplicate payload. The encoder got its own module.scanHead: comments and titles hide tags, the first title and preheader win, and a malformed tag stops the scan. The frontend can't import posthog-node, so about 40 lines repeat its rules. The vector table pins them to Think through the Elements tab PostHog/posthog#5312's head.{{ … }}and{% … %}. A preheader whose Liquid holds"gets a single-quotedcontent='…'. Otherwisedefault: "there"would turn into"and break the Liquid when the HTML renders. When the Liquid holds both quote kinds, the encoder escapes everything, because the attribute can't hold both raw.<meta name="preheader" content="…">. Other attributes on that one tag go away.content=""copies back unchanged.<title>(template_missing_subject), so the missing-title cases are REST-shaped templates, not SDK files.dropped_text_partis true when the saved text part is nonempty after trimming.copyToClipboard's toasts, and the event fires only on success.anyparameter types in Edit with your agent: copy a prompt for a code-managed template's file #503's selector types with their real types.pnpm install --frozen-lockfilewas needed in this worktree for Edit with your agent: copy a prompt for a code-managed template's file #503's new devDependencies. The lockfile is unchanged.Follow-ups
message template duplicatedevent (carried over from Edit with your agent: copy a prompt for a code-managed template's file #503).parseTemplateFilebefore Think through the Elements tab PostHog/posthog#5312 merges, re-record the vector table withrecord-sdk-parse.tsagainst the new head.