github.com/noahlh/celestite

Beautifully reactive, server-side rendered Svelte apps w/ a Crystal backend

239 stars
0 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  celestite:
    github: noahlh/celestite
    version: ~> 0.2.3

Then run:

shards install

shard.yml

Crystal
>= 1.0.0
License
MIT
Author
Noah Lehmann-Haupt <nlh@nlh.me>

Dependencies

This version declares no dependencies.

README

![](https://github.com/noahlh/celestite/workflows/crystal%20spec/badge.svg?branch=master)

# celestite

<img src="https://crystal-lang.org/assets/media/crystal_icon.svg?sanitize=1" height=21> Crystal + <img src="https://upload.wikimedia.org/wikipedia/commons/1/1b/Svelte_Logo.svg" height=16> Svelte = :zap:

Celestite allows you to use the full power of [Svelte](https://svelte.dev) reactive components in your [Crystal](https://crystal-lang.org) 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.](https://nlh.me/projects/celestite)

### Requirements

- Crystal 1.0.0+
- Bun 1.0+ (for the SSR render server)

## 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](http://kemalcr.com/), but there's no reason it won't work with [Amber](https://amberframework.org), [Lucky](https://luckyframework.org/), [Athena](https://athenaframework.org), 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`

```yaml
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:

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

### 3. Add initialization code

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

```crystal
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](/config/celestite_init.example.cr) for more options.

### 4. Add a static route for your build_dir

For Kemal:

```crystal
# 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

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

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

- `component` - The Svelte component to render (without `.svelte` extension)
- `context` - A `Celestite::Context` hash with data to pass to your component
- `layout` - Optional HTML layout file from your layout_dir

### Example

```html
<!-- 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>
```

```crystal
# 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

```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.

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

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

or

```svelte
<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:

   ```bash
   brew install mkcert  # macOS
   ```

2. Install the local CA:

   ```bash
   sudo mkcert -install
   ```

3. Generate certificates:

   ```bash
   mkcert -key-file dev.key -cert-file dev.crt localhost 127.0.0.1 ::1
   ```

4. Enable in configuration:
   ```crystal
   Celestite.initialize(
     dev_secure: true,
     # ... other config
   )
   ```

Celestite reads `dev.key` and `dev.crt` from `ROOT_DIR` when `dev_secure` is enabled.

For sandboxed or proxied development, "set" means exporting the variables in the
parent app process environment before Celestite initializes.

Use:

```bash
DEV_CLIENT_BASE=/_vite
DEV_CLIENT_PROTOCOL=https
```

For example:

```bash
ENV=development DEV_CLIENT_BASE=/_vite DEV_CLIENT_PROTOCOL=https crystal run src/trader.cr
```

or, if your shell already has those variables exported:

```bash
./cmon trader
```

`DEV_CLIENT_BASE=/_vite` tells Celestite to load Vite modules from the same public
origin under `/_vite/...` instead of from a separate `:5173` origin. In authenticated
previews, that keeps module loads and HMR on the authenticated origin.

`DEV_CLIENT_PROTOCOL=https` is the correct companion setting when the public app URL
is HTTPS.

## Production Builds

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

### Building

From your app's root directory:

```bash
# 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:

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

### Build Output

- `BUILD_DIR/client/` - Client-side JS/CSS with content hashes
- `BUILD_DIR/client/.vite/manifest.json` - Asset manifest for hydration
- `BUILD_DIR/server/` - SSR modules for server-side rendering

### Testing Production Builds Locally

```bash
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              |
| `dev_client_protocol`   | -        | Override the browser-facing dev protocol |
| `dev_client_base`       | -        | Prefix browser-facing dev assets         |
| `disable_a11y_warnings` | `false`  | Suppress Svelte accessibility warnings   |

## Roadmap

- [x] Svelte 5 support with Vite
- [x] Hot Module Reloading (HMR)
- [x] Production builds with content hashing
- [ ] Example/demo project

## 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

- Noah Lehmann-Haupt (nlh@nlh.me / [noahlh](https://github.com/noahlh)) - creator, maintainer