Coming from Astro
If you know Astro, you’ll appreciate Ruby2JS’s approach to multi-framework components with a focus on minimal JavaScript.
Table of Contents
- What You Know → What You Write
- The
.astro.rbFormat - Template Syntax
- Instance Variables and Params
- Model Imports
- Component Patterns
- Methods → Functions
- Multi-Framework Components
- Why Ruby2JS for Astro?
- File Extensions
- Deployment
- Migration Path
- Next Steps
What You Know → What You Write
| Astro | Ruby2JS |
|---|---|
.astro components |
.astro.rb components |
--- frontmatter |
Ruby code before __END__ |
{expression} |
{ruby_expression} (auto-converted) |
{items.map(i => <jsx>)} |
{items.map { \|i\| <jsx> }} |
client:load |
Preserved |
| File-based routing | Same convention |
The .astro.rb Format
Write Astro components in Ruby. The format mirrors Astro’s structure:
@post = Post.find_by(slug: @@slug)
@related = Post.where(category: @post.category).limit(3)
__END__
<Layout title={post.title}>
<article>
<h1>{post.title}</h1>
<div set:html={post.body} />
</article>
<aside>
<h2>Related Posts</h2>
<ul>
{related.map { |p| <li><a href={"/posts/#{p.slug}"}>{p.title}</a></li> }}
</ul>
</aside>
</Layout>
Template Syntax
Ruby Blocks → Arrow Functions
The key transformation: Ruby blocks with JSX become JavaScript arrow functions:
# Ruby syntax in template
{posts.map { |post| <Card post={post} /> }}
# Becomes JavaScript
{posts.map(post => <Card post={post} />)}
This works for all iteration patterns:
{items.each { |item| <li>{item.name}</li> }} # → items.map(...)
{users.select { |u| u.active }} # → users.filter(...)
{data.map { |d, idx| <Row data={d} index={idx} /> }}
Snake Case → Camel Case
Ruby conventions automatically convert to JavaScript:
@user_name = "Alice"
@is_loading = false
__END__
<p show_loading={is_loading}>{user_name}</p>
Astro Directives Preserved
All Astro-specific attributes work as expected:
__END__
<Counter initial={count} client:load />
<Chart data={data} client:visible />
<ReactComponent client:only="react" />
<div set:html={raw_content} />
<pre is:raw>{code}</pre>
Instance Variables and Params
Instance Variables → Const
Instance variables in the Ruby code become const declarations:
@title = "Hello"
@count = 0
@posts = Post.all
__END__
<h1>{title}</h1>
Route Params with @@ Sigil
Use the @@ sigil for concise route parameter access:
@post = Post.find(@@id)
__END__
<h1>{post.title}</h1>
The @@ sigil automatically converts snake_case to camelCase:
| Ruby | JavaScript |
|---|---|
@@id |
Astro.params.id |
@@user_id |
Astro.params.userId |
@@post_slug |
Astro.params.postSlug |
Explicit Params Access
You can also access params explicitly via Astro.params:
# src/pages/posts/[id].astro.rb
@id = Astro.params[:id]
@post = Post.find(@id)
__END__
<h1>{post.title}</h1>
Model Imports
Model references are automatically detected and imported:
@post = Post.find(1)
@comments = Comment.where(post_id: @post.id)
__END__
...
Generates:
import { Post } from '../models/post'
import { Comment } from '../models/comment'
Component Patterns
Layout Component
@title = "Home"
@description = "Welcome to my site"
__END__
<Layout title={title} description={description}>
<main>
<h1>Welcome</h1>
</main>
</Layout>
Named Slots
__END__
<Layout>
<div slot="sidebar">
{sidebar_content}
</div>
<main>
Main content
</main>
</Layout>
Interactive Islands
@initial_count = 5
__END__
<Counter initial={initial_count} client:load />
<HeavyChart data={chart_data} client:visible />
Methods → Functions
Methods defined in Ruby become JavaScript functions:
def format_date(date)
date.strftime("%Y-%m-%d")
end
__END__
<time>{format_date(post.created_at)}</time>
Multi-Framework Components
Use .astro.rb for pages, and framework-specific extensions for components:
src/
pages/
index.astro.rb # Astro page (Ruby)
posts/[slug].astro.rb # Dynamic route (Ruby)
components/
Counter.jsx.rb # React island (Ruby)
Form.vue.rb # Vue island (Ruby)
Animation.svelte.rb # Svelte island (Ruby)
Why Ruby2JS for Astro?
Astro’s content-focused approach pairs well with Rails’ data patterns:
Full-Stack Ruby
Same language in your Astro frontmatter and your backend. Rails models work directly:
# Backend model (Rails)
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :featured, -> { where(featured: true) }
end
# Astro page (Ruby2JS)
@posts = Post.published.order(date: :desc)
@featured = Post.featured.limit(3)
Built-in ORM
Direct database access in frontmatter—no API endpoints, no fetch:
# Instead of:
# const posts = await fetch('api/posts').then(r => r.json())
# Write:
@posts = Post.published.order(date: :desc).limit(10)
@categories = Category.with_post_counts
@author = User.find_by(slug: @@author)
Rails Ecosystem
ActiveRecord queries, associations, scopes—all in your Astro pages:
@post = Post.find_by(slug: @@slug)
@comments = @post.comments.includes(:author)
@related = Post.where(category: @post.category).limit(3)
Syntax Benefits
Ruby syntax in your frontmatter:
# String interpolation
{"Hello, #{user.name}!"}
# Blocks in templates
{posts.map { |p| <Card post={p} /> }}
File Extensions
| Astro | Ruby2JS |
|---|---|
.astro |
.astro.rb |
.jsx |
.jsx.rb |
.vue |
.vue.rb |
.svelte |
.svelte.rb |
Deployment
Ruby2JS supports the same deployment targets:
- Vercel Edge -
packages/ruby2js-rails/targets/vercel-edge/ - Cloudflare Workers -
packages/ruby2js-rails/targets/cloudflare/ - Node.js -
packages/ruby2js-rails/targets/node/
Caching (ISR)
For pages that benefit from caching, add a pragma comment:
# Pragma: revalidate 60
@posts = Post.published
__END__
<Layout>
{posts.map { |p| <Card post={p} /> }}
</Layout>
See Vercel Deployment or Cloudflare Deployment for details.
Migration Path
- Rename files:
.astro→.astro.rb - Move frontmatter:
---block → Ruby code before__END__ - Convert expressions: JavaScript → Ruby syntax
- Add models: Replace API calls with direct database access
Next Steps
- Vue Components - Vue component support
- Svelte Components - Svelte component support
- React Components - React component support
🧪 Feedback requested — Share your experience