This Astro integration adds Lune.js to your project so that you can use Lune.js anywhere on your page.
npm install @lune-js/astro lune-jsRegister the integration in your Astro config:
// astro.config.mjs
import { defineConfig } from "astro/config";
import lune from "@lune-js/astro";
export default defineConfig({
integrations: [lune()]
});Write Lune directives directly in your markup — no <script> tag and no createApp() call. The integration boots a single app per page and mounts it to every top-level lu-scope.
---
// src/pages/index.astro
---
<html>
<head>
<title>Lune 🌙 x Astro 🚀</title>
</head>
<body>
<div lu-scope="{ count: 1 }">
<p lu-text="count"></p>
<button @click="count++">increment</button>
<button @click="count--">decrement</button>
</div>
</body>
</html>Note
Astro's template syntax claims single braces, so {{ }} interpolation does not work inside .astro files. Use lu-text, or set custom $delimiters that Astro leaves alone.
Point entrypoint at a module that configures Lune before it mounts.
// astro.config.mjs
export default defineConfig({
integrations: [lune({ entrypoint: "/src/entrypoint" })]
});The path may be a root-relative import specifier (/src/entrypoint) or relative to your project root (./src/entrypoint). The module may provide either or both of the exports below; if it provides neither, the integration warns in development and mounts an unmodified app.
A data export is passed straight to createApp(), becoming the app's root scope. Every lu-scope on the page inherits from it, which makes it the place for shared scope factories and for options Lune reads at creation time, such as $delimiters.
// src/entrypoint.ts
export const data = {
$delimiters: ["[[", "]]"],
Counter: (start = 0) => ({
count: start,
get double() {
return this.count * 2;
},
increment() {
this.count++;
}
})
};<div lu-scope="Counter(5)">
<p>[[ count ]]</p>
<p lu-text="double"></p>
<button @click="increment">increment</button>
</div>Because this object is a real module export rather than serialized config, it can hold methods and getters — not just JSON. Custom $delimiters are worth calling out: they are read only at creation time, so this export is the only way to set them, and picking delimiters Astro ignores makes interpolation usable inside .astro files.
The default export receives the app instance after it is created but before it mounts. Use it to register custom directives and install plugins.
// src/entrypoint.ts
import type { App } from "lune-js";
export default (app: App) => {
app.directive("focus", ({ el }) => el.focus());
};The integration exposes window.Lune as an escape hatch for inline scripts, carrying reactive, effect, nextTick, and createApp.
window.Lune.createApp() is not Lune's bare createApp — it is the integration's, so any app you create from it gets the same data and customizations as the one the integration mounts for you. Anything you pass takes precedence over the entrypoint's data:
window.Lune.createApp({ user }).mount(el);That matters with Astro's <ClientRouter />. The integration mounts on DOMContentLoaded, which does not fire on client-side navigations, so re-mount on astro:after-swap and the new app keeps your entrypoint:
<script>
if (!window.__luneRemount) {
window.__luneRemount = true;
document.addEventListener("astro:after-swap", () => window.Lune.createApp().mount());
}
</script>The guard matters: listeners on document survive swaps, so registering unconditionally would stack a listener on every navigation.
Full guide: lune-js.com/integrations/astro