celestite

Crystal + Svelte = :zap:

Celestite allows you to use the full power of Svelte reactive components in your Crystal web apps. It's a drop-in replacement for your view layer -- no more need for intermediate .ecr templates. With celestite, you write your backend server code in Crystal, your frontend client code in JavaScript & HTML, and everything works together seamlessly...and fast.

Introduction

Read the full introductory blog post here.

Requirements

Installation

THIS IS PREVIEW / EARLY ALPHA SOFTWARE

This is not much more than a proof-of-concept at the moment, but it does work! Standard warnings apply - it will likely break/crash in spectacular and ill-timed glory, so don't poke it, feed it past midnight, or use it for anything mission-critical (yet).

Celestite has been developed / tested with Kemal, but there's no reason it won't work with Amber, Lucky, Athena, etc. (but no work integrating with those has been done yet.) The steps below assume you'll be working with Kemal.

1. Add celestite to your application's shard.yml and run shards install

dependencies:
  celestite:
    github: noahlh/celestite
    version: ~> 0.2.0

The postinstall hook will automatically install JavaScript dependencies via Bun.

2. Include the helper:

For Kemal:

require "celestite"
include Celestite::Adapter::Kemal

3. Add initialization code

Create an initializer file (e.g., /config/initializers/celestite.cr):

require "celestite"

Celestite.initialize(
  engine: Celestite::Engine::Svelte,
  component_dir: "#{Dir.current}/src/views/",
  build_dir: "#{Dir.current}/public/celestite/",
  port: 4000,
  vite_port: 5173,
)

See example config for more options.

4. Add a static route for your build_dir

For Kemal:

# myapp.cr

add_handler Kemal::StaticFileHandler.new("./public/celestite")

5. Add your .svelte files and start building!

Name your root component index.svelte (all lowercase).

Usage

celestite_render

celestite_render(component : String?, context : Celestite::Context?, layout : String?)

Call this where you'd normally call render in your controllers.

Example

<!-- src/views/layouts/layout.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- CELESTITE HEAD -->
    <!-- The above comment is actually needed - Celestite looks for it and injects optional svelte:head content -->
  </head>
  <body>
    <div id="celestite-app">
      <!-- CELESTITE BODY -->
      <!-- The above comment is also actually needed - Celestite looks for it and injects the server-side rendered component -->
    </div>
    <!-- CELESTITE CLIENT -->
    <!-- The above comment is also also actually needed - Celestite looks for it and injects the client-side bundle -->
  </body>
</html>
# myapp.cr
get "/test" do
  context = Celestite::Context{ data: "Hello from Crystal!" }
  celestite_render(component:"Home.svelte", context: context, layout: "layout.html")
end

Accessing Context in Svelte

<script>
  let { context } = $props();
</script>

<h1>Result: {context.data}</h1>

Server vs Client Rendering

Your .svelte components are automatically rendered server-side before being sent to the client, then hydrated on the client for interactivity.

Code that relies on browser-specific APIs (like document or window) must be wrapped in Svelte's onMount() or otherwise guarded.

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    // Browser-only code here
    console.log(window.location);
  });
</script>

or

<script>
  let isBrowser = false;

  if (typeof window !== 'undefined') {
    isBrowser = true;
  }
</script>

{#if isBrowser}
  <!-- Browser-specific content -->
{/if}

HTTPS/SSL Support for Development

Celestite supports running the Vite dev server over HTTPS, useful for tunneled connections (ngrok, localtunnel, etc.).

Setup

  1. Install mkcert:

    brew install mkcert  # macOS
  2. Install the local CA:

    sudo mkcert -install
  3. Generate certificates:

    mkcert -key-file dev.key -cert-file dev.crt localhost 127.0.0.1 ::1
  4. Enable in configuration:

    Celestite.initialize(
      dev_secure: true,
      # ... other config
    )

Production Builds

For production, Svelte components must be pre-built using Vite.

Building

From your app's root directory:

# Build client bundles
COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite \
  bunx --bun vite build --config /path/to/lib/celestite/vite.config.js

# Build SSR bundles
COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite \
  bunx --bun vite build --config /path/to/lib/celestite/vite.config.js --ssr

Or use the Makefile target:

cd /path/to/lib/celestite/src/svelte-scripts
make build COMPONENT_DIR=/path/to/views BUILD_DIR=/path/to/public/celestite

Build Output

Testing Production Builds Locally

NODE_ENV=production NODE_PORT=4000 \
  COMPONENT_DIR=/path/to/views \
  LAYOUT_DIR=/path/to/views/layouts \
  BUILD_DIR=/path/to/public/celestite \
  bun run /path/to/lib/celestite/src/svelte-scripts/vite-render-server.js

Configuration Options

| Option | Default | Description | | ----------------------- | -------- | ---------------------------------------- | | engine | Svelte | Rendering engine (currently only Svelte) | | component_dir | - | Path to your Svelte components | | layout_dir | - | Path to HTML layout templates | | build_dir | - | Output directory for production builds | | port | 4000 | Bun SSR server port | | vite_port | 5173 | Vite dev server port (development only) | | dev_secure | false | Enable HTTPS for dev server | | disable_a11y_warnings | false | Suppress Svelte accessibility warnings |

Roadmap

Contributing

Contributions are welcome! This is an open source project and feedback, bug reports, and PRs are appreciated.

  1. Fork it (https://github.com/noahlh/celestite/fork)
  2. Create your feature branch (git checkout -b my-feature)
  3. Write tests!
  4. Commit your changes (git commit -am 'Add feature')
  5. Push to the branch (git push origin my-feature)
  6. Create a Pull Request

Contributors