Getting started
Aegis goes into the page your server already renders. There is nothing to install on the server, nothing to compile: one <script type="importmap"> in the base template, one <script type="module"> per island, and a data-aegis attribute where the page has to react.
1. The base template#
Pick your stack. Each file is complete; the only Aegis-specific lines are the import map and the module script.
Django#
templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<script type="importmap">
{
"imports": { "aegis": "https://aegisjs.com/0.7.0/aegis.min.js" },
"integrity": { "https://aegisjs.com/0.7.0/aegis.min.js": "sha384-IlZI8EunTsg50qmYrO6AIH/tIHqkkD1bZZIIln8MQrwlRKLz8NuE+aW5umJBGDgu" }
}
</script>
<script type="module" src="{% static 'js/islands.js' %}"></script>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>templates/products.html:
{% extends "base.html" %}
{% block content %}
<h1>Products</h1>
<div data-aegis="likes" data-id="{{ product.id }}" data-count="{{ product.likes }}">
<button>{{ product.likes }} likes</button>
</div>
{% endblock %}CSRF: configure({ csrf: 'django' }) once in islands.js reads the csrftoken cookie and sends X-CSRFToken on every api.post().
Laravel#
resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>@yield('title')</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<script type="importmap">
{
"imports": { "aegis": "https://aegisjs.com/0.7.0/aegis.min.js" },
"integrity": { "https://aegisjs.com/0.7.0/aegis.min.js": "sha384-IlZI8EunTsg50qmYrO6AIH/tIHqkkD1bZZIIln8MQrwlRKLz8NuE+aW5umJBGDgu" }
}
</script>
<script type="module" src="{{ asset('js/islands.js') }}"></script>
</head>
<body>
@yield('content')
</body>
</html>resources/views/products/show.blade.php:
@extends('layouts.app')
@section('content')
<h1>{{ $product->name }}</h1>
<div data-aegis="likes" data-id="{{ $product->id }}" data-count="{{ $product->likes }}">
<button>{{ $product->likes }} likes</button>
</div>
@endsectionCSRF: configure({ csrf: 'laravel' }) reads the csrf-token meta tag and sends X-CSRF-TOKEN.
Rails#
app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= yield(:title) %></title>
<%= csrf_meta_tags %>
<script type="importmap">
{
"imports": { "aegis": "https://aegisjs.com/0.7.0/aegis.min.js" },
"integrity": { "https://aegisjs.com/0.7.0/aegis.min.js": "sha384-IlZI8EunTsg50qmYrO6AIH/tIHqkkD1bZZIIln8MQrwlRKLz8NuE+aW5umJBGDgu" }
}
</script>
<script type="module" src="<%= asset_path('islands.js') %>"></script>
</head>
<body>
<%= yield %>
</body>
</html>app/views/products/show.html.erb:
<h1><%= @product.name %></h1>
<div data-aegis="likes" data-id="<%= @product.id %>" data-count="<%= @product.likes %>">
<button><%= @product.likes %> likes</button>
</div>CSRF: configure({ csrf: 'rails' }) reads csrf-token and sends X-CSRF-Token. Turbo pages work unchanged: islands that arrive with a Turbo visit are hydrated by hydrate(document, { watch: true }).
Go#
templates/base.html with html/template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ .Title }}</title>
<script type="importmap">
{
"imports": { "aegis": "https://aegisjs.com/0.7.0/aegis.min.js" },
"integrity": { "https://aegisjs.com/0.7.0/aegis.min.js": "sha384-IlZI8EunTsg50qmYrO6AIH/tIHqkkD1bZZIIln8MQrwlRKLz8NuE+aW5umJBGDgu" }
}
</script>
<script type="module" src="/static/js/islands.js"></script>
</head>
<body>
{{ template "content" . }}
</body>
</html>templates/product.html:
{{ define "content" }}
<h1>{{ .Product.Name }}</h1>
<div data-aegis="likes" data-id="{{ .Product.ID }}" data-count="{{ .Product.Likes }}">
<button>{{ .Product.Likes }} likes</button>
</div>
{{ end }}CSRF: configure({ csrf: { header: 'X-CSRF-Token', cookie: '_csrf' } }) for gorilla/csrf or any cookie + header pair.
2. The first island#
static/js/islands.js, the same file for every stack:
import { island, mutation, api, configure } from 'aegis';
configure({ csrf: 'django' }); // or 'laravel', 'rails', { header, cookie }
island('likes', ({ props, signal, html }) => {
const count = signal(props.count);
const like = mutation(() => api.post(`/api/products/${props.id}/like`), {
optimistic: () => { count.value++; },
onError: () => { count.value--; },
});
return html`<button @click=${like} ?disabled=${like.pending}>${count} likes</button>`;
}, { types: { id: Number, count: Number } });What happens: the server sends <button>12 likes</button>, search engines and users see it at once; when the module loads, island() finds every [data-aegis="likes"], reads the typed data-* props and replaces the markup with the live template, without a flash. A click writes the count first and sends the request after; a failed request rolls back.
3. Where it goes from here#
- A form: keep your server-rendered
<form>and addwireForm(formEl): live validation from the browser's own rules, server 422 errors landing on the right fields. Forms, example. - A table with search:
resource(url)for the data,list()for the rows. Example. - A fragment from the server:
swap(el, await fetch(url), { mode: 'morph' })morphs server HTML into place; islands inside it come alive. With htmx, example. - The ten names you need: the canonical API. Everything else is optional.
Versions and hashes#
/0.7.0/aegis.min.js never changes; the hash in the import map makes the browser refuse anything else. Upgrading is changing two strings in the base template. Installation lists every version with its hash, Versioning explains what 0.x means.