# Foundation CSS — Full Documentation > Open-source tools for building websites and emails. This file contains the complete documentation for all Foundation CSS projects. It is intended for AI agents and LLMs to ingest in a single request. --- # Inky — Email Framework # Getting Started ## Installation ### Homebrew (macOS/Linux) ```bash brew install foundation/inky/inky ``` ### npm ```bash npm install -g inky ``` ### Cargo (from source) ```bash cargo install inky-cli ``` ### Direct download ```bash curl -fsSL https://get.inky.email/install.sh | sh ``` ## Your First Email ### 1. Scaffold a project ```bash inky init my-email cd my-email ``` This creates: ```sh my-email/ ├── inky.config.json ├── AGENT.md ├── CLAUDE.md → AGENT.md ├── .cursorrules → AGENT.md ├── .github/ │ └── copilot-instructions.md → AGENT.md ├── src/ │ ├── layouts/ │ │ └── default.html │ ├── styles/ │ │ └── theme.scss │ ├── partials/ │ │ ├── header.inky │ │ └── footer.inky │ ├── components/ │ │ └── cta.inky │ └── emails/ │ └── welcome.inky ├── data/ │ └── welcome.json └── dist/ ``` `AGENT.md` contains project conventions and component syntax for AI coding assistants. The symlinks (`CLAUDE.md`, `.cursorrules`, `copilot-instructions.md`) ensure the instructions are auto-discovered by Claude Code, Cursor, and GitHub Copilot respectively. ### 2. Edit your template ```html

Welcome!

We're glad you're here.

``` ### 3. Build ```bash inky build ``` Output goes to `dist/`. That's it. ## CLI Commands ### `inky build` Transform `.inky` and `.html` files into email-ready HTML. ```bash # Single file (auto-outputs .html alongside .inky) inky build email.inky # Single file to specific output inky build email.inky -o output.html # Directory to directory inky build src/ -o dist/ # Pipe from stdin echo '' | inky build # Skip CSS inlining (on by default) inky build email.inky --no-inline-css # Skip framework CSS injection inky build email.inky --no-framework-css # Custom column count (default: 12) inky build email.inky --columns 16 # Strict mode -- exit 1 on warnings inky build src/ -o dist/ --strict # Hybrid output (div + MSO ghost tables) inky build email.inky --hybrid # Generate plain text version alongside HTML inky build src/ -o dist/ --plain-text # Use per-template data files (data/welcome.json for src/welcome.inky) inky build src/ -o dist/ --data data/ # VML bulletproof buttons for Outlook inky build email.inky --bulletproof-buttons # JSON output (for AI agents and scripts) inky build email.inky --json echo '' | inky build --json ``` ### `inky watch` Rebuild automatically on file changes. ```bash inky watch src/emails -o dist ``` Watches all `.inky` and `.html` files, plus any referenced partials and layouts. When a partial or layout changes, all templates rebuild. When a single template changes, only that file rebuilds. ### `inky validate` Check templates for common email issues. ```bash inky validate email.inky inky validate src/ # Pipe from stdin echo '' | inky validate # JSON output inky validate src/ --json echo '' | inky validate --json ``` | Rule | Severity | What it checks | |------|----------|----------------| | `v1-syntax` | warning | Deprecated v1 syntax | | `missing-alt` | warning | Images without `alt` text | | `generic-alt` | warning | Generic alt text like "image", "logo", or single character | | `button-no-href` | error | Buttons without `href` | | `empty-link` | error/warning | Empty href (error) or placeholder `#` href (warning) | | `insecure-link` | warning | Links using `http://` instead of `https://` | | `bad-shortlink` | warning | URL shorteners that get blocked (bit.ly, youtu.be, t.co, etc.) | | `mailto-in-button` | warning | `mailto:` href on a ` ``` **Data** (`data.json`): ```json { "user": { "name": "Alice" }, "show_coupon": true, "coupon_code": "WELCOME20", "cta_url": "https://example.com/start" } ``` **Build:** ```bash inky build src/emails/welcome.inky --data data.json -o dist/welcome.html ``` The output HTML will have all `{{ }}` and `{% %}` tags evaluated with the provided data. ## CLI Usage ### `inky build` ```bash # Single file with data inky build email.inky --data data.json # Directory with data inky build src/ -o dist/ --data data.json # Pipe from stdin echo '

Hello {{ name }}

' | inky build --data data.json ``` ### `inky watch` ```bash inky watch src/emails -o dist --data data.json ``` In watch mode, Inky also watches the data file. When you edit `data.json`, all templates rebuild automatically. ## Configuration Add `data` to your `inky.config.json` instead of using the `--data` flag: ```json { "src": "src/emails", "dist": "dist", "data": "data.json" } ``` The CLI `--data` flag overrides the config file value. ## Syntax Data merging uses Jinja2 syntax, which is nearly identical to Liquid/Shopify/Nunjucks for common operations. ### Variables ```html {{ user.name }} {{ order.total }} {{ company }} ``` ### Conditionals ```html {% if unsubscribe_url %} Unsubscribe {% endif %} {% if tier == "premium" %}

Thank you for being a premium member!

{% elif tier == "trial" %}

Your trial ends soon.

{% else %}

Upgrade today.

{% endif %} ``` ### Loops ```html {% for item in cart %} {{ item.name }} {{ item.price }} {% endfor %} ``` ### Filters ```html {{ name | upper }} {{ name | lower }} {{ description | truncate(100) }} {{ price | round(2) }} {{ tags | join(", ") }} ``` See the [MiniJinja documentation](https://docs.rs/minijinja/latest/minijinja/syntax/index.html) for the full syntax reference. ## How It Works Data merging runs **after** layout, include, and custom component resolution, but **before** Inky transforms components into table markup. This means you can use template logic around Inky components: ```html {% if show_hero %}

{{ headline }}

{% endif %} ``` The full pipeline order is: 1. Layout resolution (``) 2. Custom component resolution (``) 3. Include resolution (``) 4. **Data merge** (MiniJinja) 5. SCSS extraction + framework CSS injection 6. Inky component transformation 7. CSS inlining ## Missing Keys By default, missing keys render as empty strings (lenient mode). This means `{{ undefined_var }}` produces no output rather than an error. ## Language Bindings ### Node.js (WASM) ```js const inky = require("inky"); const html = inky.transformWithData( '', JSON.stringify({ url: "https://example.com", text: "Click" }) ); ``` ### PHP / Python / Ruby (FFI) All FFI bindings expose `inky_transform_with_data(html, data_json)` where `data_json` is a JSON string. ```python import inky html = inky.transform_with_data( '', '{"url": "https://example.com", "text": "Click"}' ) ``` ## When NOT to Use Data Merging If you're sending emails through an ESP (SendGrid, Mailchimp, Postmark, etc.) that handles its own template merging, **don't use `--data`**. Instead, let Inky pass your merge tags through untouched (the default behavior), and let the ESP fill in the data at send time. Data merging is best for: - **Previewing** emails with sample data during development - **Generating** final static HTML when you handle sending yourself - **Testing** that templates render correctly with different data --- # Hybrid Output Mode Hybrid mode generates `
`-based layouts for modern email clients with Outlook-specific `` fallbacks wrapped in MSO conditional comments. This produces cleaner HTML, better accessibility, and smaller file sizes compared to pure table-based output. **Hybrid mode is off by default.** The default output uses pure table-based markup for maximum compatibility. ## Quick Start ```bash # CLI flag inky build src/ -o dist/ --hybrid # Or in inky.config.json {"src": "src/emails", "dist": "dist", "hybrid": true} ``` ## How It Works In table mode (default), a container outputs: ```html
``` In hybrid mode, the same container outputs: ```html
...
``` Modern email clients (Apple Mail, Gmail, Yahoo, etc.) use the `
` with CSS. Microsoft Outlook (which uses the Word rendering engine) sees the `` inside the `
Left
Right
``` ## CLI Usage ```bash # Build with hybrid output inky build email.inky --hybrid # Watch with hybrid output inky watch src/ -o dist/ --hybrid # Serve with hybrid output inky serve src/ --hybrid ``` ## Configuration Add `hybrid` to `inky.config.json`: ```json { "src": "src/emails", "dist": "dist", "hybrid": true } ``` The `--hybrid` CLI flag overrides the config file. ## Language Bindings ### Node.js (WASM) ```js const inky = require("inky"); const html = inky.transformHybrid('Hello'); ``` ### FFI (PHP, Python, Ruby) ```python import inky html = inky.transform_hybrid('Hello') ``` ## When to Use Hybrid Mode **Use hybrid mode when:** - You want cleaner, more semantic HTML - Accessibility is a priority (screen readers handle `
` better than nested tables) - You need smaller file sizes (important for Gmail's 102KB clipping limit) - Your audience primarily uses modern email clients **Stick with table mode when:** - You need maximum compatibility with older/niche email clients - You're targeting environments where Outlook is the primary client (tables render more predictably in Outlook) - You're migrating from v1 and want identical output behavior --- # Language Bindings Inky provides official bindings for Node.js, PHP, Python, and Ruby. All bindings expose the same core API surface. For complete working examples with build scripts and email sending, see: [Node.js](https://github.com/foundation/inky-example-node) | [PHP](https://github.com/foundation/inky-example-php) | [Python](https://github.com/foundation/inky-example-python) | [Ruby](https://github.com/foundation/inky-example-ruby) | [Go](https://github.com/foundation/inky-example-go) ## Common API Every binding provides these functions: | Function | Description | |----------|-------------| | `transform(html, columns?)` | Transform Inky HTML into email-safe table markup | | `transformInline(html)` | Transform and inline CSS from ` `); // Transform with data merge const merged = inky.transformWithData( '', JSON.stringify({ url: "https://example.com", text: "Click" }) ); // Migrate v1 to v2 const migrated = inky.migrate('Content'); // Migrate with change details const result = inky.migrateWithDetails('Content'); // result.html => 'Content' // result.changes => [' -> ', ...] // Validate const diagnostics = inky.validate(''); // [{ severity: "error", rule: "button-no-href", message: "..." }] const diagnostics16 = inky.validate(html, { columns: 16 }); // Version console.log(inky.version()); // "2.0.0" ``` ### TypeScript Type definitions are included. Key types: ```ts interface TransformOptions { columns?: number; } interface ValidateOptions { columns?: number; } interface Diagnostic { severity: "warning" | "error"; rule: string; message: string; } interface MigrateResult { html: string; changes: string[]; } ``` --- ## PHP **Package:** `foundation/inky` on Packagist **Engine:** Native shared library via FFI (or PECL extension) **Requires:** PHP >= 8.1 ### Install ```bash composer require foundation/inky ``` You also need the `libinky` shared library available. Build it from source: ```bash cargo build -p inky-ffi --release # produces target/release/libinky.dylib (macOS) or libinky.so (Linux) ``` ### Driver Setup The PHP package auto-detects the best available driver: | Priority | Driver | Mechanism | Best For | |----------|--------|-----------|----------| | 1 | PECL Extension | `ext-inky` | Shared hosting, production | | 2 | FFI | `ext-ffi` + `libinky` | Local dev, self-managed servers | **FFI setup** -- enable in `php.ini`: ```ini # Option A: Enable globally (dev) ffi.enable = true # Option B: Preload mode (production, more secure) ffi.enable = preload opcache.preload = /path/to/vendor/inky/preload.php ``` Note: `ffi.enable` is a `PHP_INI_SYSTEM` directive and cannot be changed with `ini_set()`. ### API ```php use Inky\Inky; // Transform $html = Inky::transform(''); $html = Inky::transform('Content', columns: 16); // Transform + inline CSS $html = Inky::transformInline(''); // Transform with data merge $html = Inky::transformWithData( '', json_encode(['url' => 'https://example.com', 'text' => 'Click']) ); // Migrate $html = Inky::migrate('Content'); // Migrate with details $result = Inky::migrateWithDetails('Content'); // $result['html'] => 'Content' // $result['changes'] => [' -> ', ...] // Validate $diagnostics = Inky::validate(''); // [['severity' => 'error', 'rule' => 'button-no-href', 'message' => '...']] // Version echo Inky::version(); // "2.0.0" ``` --- ## Python **Package:** `inky-email` on PyPI **Engine:** Native shared library via ctypes **Requires:** Python >= 3.8 ### Install ```bash pip install inky-email ``` You also need the `libinky` shared library. Build from source: ```bash cargo build -p inky-ffi --release ``` The library searches these paths automatically: 1. `target/release/` (development) 2. Bundled with the package 3. `/usr/local/lib/` 4. `/usr/lib/` ### API ```python import inky # Transform html = inky.transform('') html = inky.transform('Content', columns=16) # Transform + inline CSS html = inky.transform_inline('') # Transform with data merge html = inky.transform_with_data( '', '{"url": "https://example.com", "text": "Click"}' ) # Migrate html = inky.migrate('Content') # Migrate with details result = inky.migrate_with_details('Content') # result['html'] => 'Content' # result['changes'] => [' -> ', ...] # Validate diagnostics = inky.validate('') # [{'severity': 'error', 'rule': 'button-no-href', 'message': '...'}] # Version print(inky.version()) # "2.0.0" ``` Note: Python uses `snake_case` -- `transform_inline`, `migrate_with_details`. --- ## Ruby **Package:** `inky-email` on RubyGems **Engine:** Native shared library via Fiddle **Requires:** Ruby >= 2.7 ### Install ```bash gem install inky-email ``` Or in your `Gemfile`: ```ruby gem "inky-email" ``` You also need the `libinky` shared library. Build from source: ```bash cargo build -p inky-ffi --release ``` The library searches these paths automatically: 1. `target/release/` (development) 2. Bundled with the gem 3. `/usr/local/lib/` 4. `/usr/lib/` ### API ```ruby require "inky" # Transform html = Inky.transform('') html = Inky.transform('Content', columns: 16) # Transform + inline CSS html = Inky.transform_inline('') # Transform with data merge html = Inky.transform_with_data( '', '{"url": "https://example.com", "text": "Click"}' ) # Migrate html = Inky.migrate('Content') # Migrate with details result = Inky.migrate_with_details('Content') # result[:html] => 'Content' # result[:changes] => [' -> ', ...] # Validate diagnostics = Inky.validate('') # [{severity: "error", rule: "button-no-href", message: "..."}] # Version puts Inky.version # "2.0.0" ``` --- ## Building the Shared Library PHP, Python, and Ruby bindings all depend on the `libinky` shared library from `inky-ffi`. To build it: ```bash cd /path/to/inky cargo build -p inky-ffi --release ``` This produces: - **macOS:** `target/release/libinky.dylib` - **Linux:** `target/release/libinky.so` - **Windows:** `target/release/inky.dll` For production, copy the library to a system path (`/usr/local/lib/`) or bundle it with your package. ## Building the WASM Module The Node.js binding uses the WASM module from `inky-wasm`: ```bash cd crates/inky-wasm wasm-pack build --target nodejs ``` This generates the `.wasm` file and JS/TS wrapper in `pkg/`. --- # Email Development Guide A practical guide to building HTML emails that render correctly across every major client. These tips apply whether you're using Inky's CLI, language bindings, or writing raw HTML. --- ## How Email HTML Differs from Web HTML Email clients are not browsers. Most strip `