encrier — build specification

Encrier Save State Format — This page documents the encrier save state file format. A save state is a portable, self-contained HTML file that preserves a conversation. The format covers structure, metadata, styling, widget embedding, and the Share bar. What follows is the complete format reference.

1. Pre-flight — before you build anything

Do not immediately start generating the HTML file. First, do a quick assessment and tell the user what you're about to do:

Keep this pre-flight to 3–5 sentences. Then proceed to build without waiting for confirmation — unless you flagged a genuine problem.

2. File structure and format

3. Context header — structured JSON metadata

At the very top of the file (before <!DOCTYPE html>), include a JSON metadata block inside an HTML comment:

<!--ENCRIER-SAVE-STATE
{
  "version": "2.0",
  "generated": "YYYY-MM-DD",
  "message_count": 24,
  "topics": ["topic 1", "topic 2"],
  "key_decisions": ["decision 1", "decision 2"],
  "open_questions": ["question 1"],
  "files_generated": ["report.docx"],
  "widgets_embedded": 1
}
-->

Use real values from the conversation. Keep each list to 2–5 items.

4. Content reproduction rules

5. Condensation protocol for long conversations

Under ~25 messages, reproduce everything in full. For longer conversations:

  1. Never condense: User messages (always verbatim), final decisions/deliverables, final code versions.
  2. Condense moderately: Your explanatory responses — tighten prose, preserve all key points.
  3. Condense aggressively: Intermediate iterations, tangential discussions, acknowledgment messages.

When you condense a message, add an italic note: "This response has been condensed. The full version covered [brief description]."

6. Widget embedding — the critical section

Do NOT use <iframe srcdoc='...'> with raw HTML in the attribute. This breaks upload pipelines. Use one of these two approaches:

Approach A — Inline DOM (for widgets with no JavaScript)

Embed the widget directly in the page with scoped CSS classes prefixed ew-[widgetname]-:

<div class="encrier-widget encrier-widget-pricing">
  <!-- widget markup directly in page -->
</div>
<style>
  .encrier-widget-pricing .ew-pricing-tab { ... }
</style>

Approach B — Template + loader (for widgets with JavaScript)

Store the widget HTML inside a <script type="text/encrier-widget"> tag. A loader script injects it into an iframe at runtime:

<script type="text/encrier-widget" id="widget-pricing">
  <!-- full widget HTML/CSS/JS here, no escaping needed -->
</script>
<iframe id="frame-pricing" class="encrier-widget-frame"
  sandbox="allow-scripts" loading="lazy"></iframe>

Include one copy of the loader script at the bottom of the file:

<script>
  document.querySelectorAll('script[type="text/encrier-widget"]')
    .forEach(function(tpl) {
      var id = tpl.id.replace('widget-', 'frame-');
      var frame = document.getElementById(id);
      if (frame) { frame.srcdoc = tpl.textContent; }
    });
</script>

Decision rule: No <script> tags or event handlers in the widget → Approach A. Any JavaScript → Approach B. When in doubt → Approach B. If too complex to embed → use a styled placeholder.

7. Round-trip markup

Add data-encrier-* attributes to every message container for machine parsing:

<div class="msg msg-user"
  data-encrier-role="user"
  data-encrier-index="1">...</div>
<div class="msg msg-assistant"
  data-encrier-role="assistant"
  data-encrier-index="2">...</div>

Also mark: data-encrier-type="widget", data-encrier-type="file-badge" with data-encrier-filename="...", data-encrier-type="image-placeholder", data-encrier-condensed="true".

8. Visual design — use this exact CSS template

Copy this CSS verbatim into the file's <style> block. Do not improvise the design — every encrier save state should look identical.

/* Encrier Save State CSS Template v2 */
:root {
  --bg-page: #f6f3ed; --bg-card-user: #eae7e0; --bg-card-asst: #ffffff;
  --bg-code: #1e1e2a; --text: #1a1612; --text-secondary: #635a52;
  --text-muted: #928980; --text-code: #c8c4bc; --border: #e2dfdb;
  --accent-user: #b8632e; --accent-asst: #3d3680;
  --accent-file: #1a6b5a; --accent-file-bg: #e4f4ee;
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-display: Georgia, 'Times New Roman', serif;
  --font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
}
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
body { font-family:var(--font-body); background:var(--bg-page);
  color:var(--text); line-height:1.7; font-size:15px; }
.encrier-header { background:linear-gradient(135deg,#2a1f3e,#1a1228);
  padding:3rem 2rem; text-align:center; }
.encrier-header h1 { font-family:var(--font-display); color:#fff;
  font-size:1.8rem; font-weight:normal; }
.encrier-header p { color:rgba(255,255,255,0.45); font-size:0.8rem;
  margin-top:0.4rem; letter-spacing:0.04em; }
.encrier-chat { max-width:820px; margin:0 auto; padding:2rem 1.5rem 4rem; }
.msg { margin-bottom:2.5rem; }
.msg-role { font-size:0.7rem; font-weight:600; text-transform:uppercase;
  letter-spacing:0.1em; margin-bottom:0.4rem; display:flex;
  align-items:center; gap:0.5rem; }
.msg-role .dot { width:6px; height:6px; border-radius:50%; display:inline-block; }
.msg-user .msg-role { color:var(--accent-user); }
.msg-user .msg-role .dot { background:var(--accent-user); }
.msg-assistant .msg-role { color:var(--accent-asst); }
.msg-assistant .msg-role .dot { background:var(--accent-asst); }
.msg-body { padding:1.25rem 1.5rem; border-radius:12px; border:1px solid var(--border); }
.msg-user .msg-body { background:var(--bg-card-user); }
.msg-assistant .msg-body { background:var(--bg-card-asst);
  box-shadow:0 1px 3px rgba(0,0,0,0.04); }
.msg-body p { margin-bottom:0.8rem; }
.msg-body p:last-child { margin-bottom:0; }
.msg-body strong { font-weight:600; }
.msg-body h3 { font-size:1rem; font-weight:600; margin:1.2rem 0 0.5rem; }
.msg-body ul,.msg-body ol { margin:0.5rem 0 0.8rem 1.5rem; }
.msg-body li { margin-bottom:0.3rem; }
code { font-family:var(--font-mono); font-size:0.85em;
  background:rgba(0,0,0,0.05); padding:0.15em 0.4em; border-radius:4px; }
pre { background:var(--bg-code); border-radius:8px; padding:1rem 1.25rem;
  overflow-x:auto; margin:0.75rem 0; }
pre code { background:none; padding:0; color:var(--text-code);
  font-size:0.82rem; line-height:1.65; }
.encrier-table { width:100%; border-collapse:collapse; font-size:0.85rem; margin:0.8rem 0; }
.encrier-table th { background:var(--accent-asst); color:#fff;
  padding:0.5rem 0.75rem; text-align:left; font-weight:500; }
.encrier-table td { padding:0.5rem 0.75rem; border-bottom:1px solid var(--border); }
.encrier-file-badge { display:inline-flex; align-items:center; gap:0.4rem;
  background:var(--accent-file-bg); color:var(--accent-file);
  padding:0.35rem 0.8rem; border-radius:6px; font-size:0.8rem;
  font-weight:500; margin:0.5rem 0; }
.encrier-widget-frame { width:100%; border:1px solid var(--border);
  border-radius:10px; min-height:400px; margin:1rem 0; background:#fff; }
.encrier-widget-placeholder { border:1px dashed var(--border); border-radius:10px;
  padding:2rem; text-align:center; margin:1rem 0; background:rgba(0,0,0,0.015); }
.encrier-wp-icon { font-size:1.5rem; color:var(--text-muted); margin-bottom:0.5rem; }
.encrier-wp-title { font-weight:600; font-size:0.9rem; margin-bottom:0.25rem; }
.encrier-wp-note { font-size:0.8rem; color:var(--text-muted); }
.encrier-divider { text-align:center; padding:1.5rem 0; position:relative; }
.encrier-divider::before { content:''; position:absolute; top:50%; left:0; right:0;
  border-top:1px solid var(--border); }
.encrier-divider span { background:var(--bg-page); padding:0 1rem; position:relative;
  font-size:0.7rem; font-weight:600; text-transform:uppercase;
  letter-spacing:0.12em; color:var(--text-muted); }
.encrier-condensed-notice { font-size:0.8rem; font-style:italic;
  color:var(--text-muted); margin-top:0.75rem; padding-top:0.5rem;
  border-top:1px dashed var(--border); }
.encrier-footer { text-align:center; padding:2rem; color:var(--text-muted);
  font-size:0.75rem; border-top:1px solid var(--border); }

/* Share bar */
#encrier-share-bar { position:sticky; top:0; z-index:9999;
  display:flex; align-items:center; justify-content:space-between;
  padding:0.6rem 1.5rem; background:#1a1612; }
.encrier-share-wordmark { font-family:var(--font-display);
  font-style:italic; font-size:1rem; color:#e2dfdb;
  letter-spacing:0.02em; }
.encrier-share-actions { display:flex; align-items:center;
  gap:0.75rem; }
#encrier-share-btn { background:#f6f3ed; color:#1a1612; border:none;
  padding:0.45rem 1.4rem; border-radius:6px; font-size:0.85rem;
  font-weight:600; cursor:pointer;
  font-family:var(--font-body); transition:all 0.15s; }
#encrier-share-btn:hover { background:#ffffff; }
#encrier-share-btn:disabled { opacity:0.7; cursor:default; }
#encrier-share-status { font-size:0.75rem; }
#encrier-share-status a { color:#a0c4a8; text-decoration:none; }
#encrier-share-status a:hover { text-decoration:underline; }

9. Error recovery

10. Share bar — Push Live to encrier

Every save-state file must include a Share bar as the very first element inside <body>. This lets the user publish the conversation to encrier.com with one click.

The Share bar is a slim dark bar pinned to the top of the page. Left side: "encrier" italic wordmark. Right side: a "Share" button. When clicked, it serializes the page HTML (stripping the share bar itself), uploads it to encrier.com, and opens the live page in a new tab.

Include this exact HTML as the first child of <body>:

<!-- encrier Share bar -->
<div id="encrier-share-bar">
  <span class="encrier-share-wordmark">encrier</span>
  <div class="encrier-share-actions">
    <button id="encrier-share-btn" onclick="encrierPublish()">Share</button>
    <div id="encrier-share-status"></div>
  </div>
</div>

<!-- Share bar fallback form (hidden, used when fetch fails on file:// origins) -->
<form id="encrier-push-form"
  method="POST"
  action="https://encrier.com/api/push"
  target="_blank"
  enctype="multipart/form-data"
  style="display:none">
  <textarea name="html" id="encrier-push-payload"></textarea>
</form>

Include this exact JavaScript at the bottom of the file, before the closing </body> tag, AFTER the widget loader script (if any):

<script id="encrier-share-script">
function encrierPublish() {
  var btn = document.getElementById('encrier-share-btn');
  var status = document.getElementById('encrier-share-status');
  btn.textContent = 'Publishing...';
  btn.disabled = true;

  // Serialize HTML, stripping the share bar and its assets
  var clone = document.documentElement.cloneNode(true);
  var strip = ['#encrier-share-bar', '#encrier-push-form', '#encrier-share-script'];
  strip.forEach(function(sel) {
    var el = clone.querySelector(sel);
    if (el) el.parentNode.removeChild(el);
  });
  var html = '<!DOCTYPE html>\n' + clone.outerHTML;

  // Primary path: fetch (works from https:// and blob: origins)
  var file = new File([html], 'conversation.html', {type: 'text/html'});
  var fd = new FormData();
  fd.append('file', file);

  fetch('https://encrier.com/api/upload', {method: 'POST', body: fd})
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (data.url) {
        btn.textContent = 'Published \u2713';
        btn.style.background = '#2d6b4f';
        btn.style.color = '#ffffff';
        status.innerHTML = '<a href="' + data.url
          + '" target="_blank">' + data.url + '</a>';
        window.open(data.url, '_blank');
      } else { throw new Error('no url'); }
    })
    .catch(function() {
      // Fallback: form POST (works from file:// origins)
      document.getElementById('encrier-push-payload').value = html;
      document.getElementById('encrier-push-form').submit();
      setTimeout(function() {
        btn.textContent = 'Published \u2713';
        btn.style.background = '#2d6b4f';
        btn.style.color = '#ffffff';
        status.textContent = 'Opened in new tab';
      }, 800);
    });
}
</script>
Why two paths: When the HTML file is opened from a local file:// URL, browsers block cross-origin fetch() requests. The hidden form bypasses this because HTML forms can POST cross-origin without CORS. The form targets _blank so the live page opens in a new tab. The server responds with a 303 redirect to /c/{token}.

11. Build process (follow this order)

  1. Pre-flight assessment (section 1).
  2. Create the shell: context header, DOCTYPE, head, verbatim CSS template (section 8, including the share bar styles at the end), page header.
  3. Add the share bar as the first element inside <body>, followed by the hidden fallback form (section 10).
  4. Add the encrier-header div with conversation title and metadata.
  5. Add messages iteratively in the encrier-chat div. For large conversations, use multiple tool calls.
  6. Embed widgets per section 6.
  7. Add the widget loader script (if any Approach B widgets were used).
  8. Add the share bar script (section 10) before </body>.
  9. Add the footer.
  10. Save and present the file with a 3–4 sentence summary.

12. Hard rules

13. After building

Present the file with a short summary: message count, condensation status, widget count, anything that couldn't be preserved. Mention that the file has a Share button in the top bar — clicking it publishes the conversation live on encrier.com where anyone with the link can continue the discussion with Claude. If the user wants to upload the file to a future Claude chat instead, suggest renaming it to .txt first.