Coming from VitePress
If you know VitePress, you’ll appreciate Ruby2JS’s Vue template support combined with ActiveRecord-like content queries.
Table of Contents
- What You Know → What You Write
- Quick Start
- Content Collections
- Vue Components in Ruby
- Query API
- Theme Customization
- Why Ruby2JS for VitePress?
- Vite Configuration
- Migration Path
- Next Steps
What You Know → What You Write
| VitePress | Ruby2JS |
|---|---|
.md with Vue components |
Same, plus .vue.rb components |
{{ frontmatter.title }} |
Same (Vue syntax unchanged) |
data/*.js loaders |
Content adapter via virtual:content |
| Custom Vue components | .vue.rb components with Ruby |
| Vite plugins | Add vite-plugin-ruby2js + content-adapter/vite |
Quick Start
Add Ruby2JS to your VitePress config:
// .vitepress/config.js
import ruby2js from 'vite-plugin-ruby2js';
import content from '@ruby2js/content-adapter/vite';
export default {
vite: {
plugins: [
ruby2js(),
content({ dir: 'content' })
]
}
}
Create a Ruby Vue component:
# components/PostList.vue.rb
@posts = []
def mounted
@posts = Post.where(draft: false).order(date: :desc).limit(5)
end
__END__
<div>
<article v-for="post in posts" :key="post.slug">
<h2></h2>
<time></time>
</article>
</div>
Content Collections
Directory Structure
content/
posts/
getting-started.md
advanced-usage.md
authors/
alice.md
docs/
index.md
guide/
introduction.md
.vitepress/
config.js
theme/
index.js
Importing Collections
Use the virtual:content module in your components:
# components/RecentPosts.vue.rb
import { Post } from 'virtual:content'
@recent = Post.where(draft: false).order(date: :desc).limit(5)
__END__
<aside class="recent-posts">
<h3>Recent Posts</h3>
<ul>
<li v-for="post in recent" :key="post.slug">
<a :href="`/posts/${post.slug}`"></a>
</li>
</ul>
</aside>
In Data Loaders
Use the content adapter in VitePress data loaders:
// posts/[slug].paths.js
import { Post } from 'virtual:content';
export default {
paths() {
return Post.where({ draft: false }).toArray().map(post => ({
params: { slug: post.slug },
content: post
}));
}
}
Vue Components in Ruby
Script Setup Style
# components/PostCard.vue.rb
@props = { post: Object }
def formatted_date
@post.date.to_date.strftime("%B %d, %Y")
end
__END__
<article class="post-card">
<h2></h2>
<time></time>
<p></p>
</article>
Reactive State
# components/SearchPosts.vue.rb
import { Post } from 'virtual:content'
@query = ""
@results = []
def search
return @results = [] if @query.length < 2
@results = Post.where(title: @query).limit(10).toArray()
end
__END__
<div class="search">
<input v-model="query" @input="search" placeholder="Search posts...">
<ul v-if="results.length">
<li v-for="post in results" :key="post.slug">
<a :href="`/posts/${post.slug}`"></a>
</li>
</ul>
</div>
Query API
The content adapter provides ActiveRecord-like queries:
import { Post, Author } from 'virtual:content'
# Filtering
Post.where(draft: false)
Post.where(author: 'alice')
Post.where().not(draft: true)
# Ordering
Post.order(date: :desc)
Post.order(title: :asc)
# Pagination
Post.limit(10).offset(20)
# Finding
Post.find('hello-world') # by slug
Post.find_by(title: 'Welcome')
Post.first
Post.last
# Counting
Post.count
Post.where(draft: false).count
# Chaining
Post.where(draft: false)
.where(author: 'alice')
.order(date: :desc)
.limit(5)
Theme Customization
Custom Layout
# .vitepress/theme/Layout.vue.rb
import { Post } from 'virtual:content'
@recent_posts = Post.where(draft: false).order(date: :desc).limit(3)
__END__
<div class="layout">
<header>
<nav>
<a href="/">Home</a>
<a href="/guide/">Guide</a>
</nav>
</header>
<main>
<Content />
</main>
<aside>
<h3>Recent Posts</h3>
<ul>
<li v-for="post in recentPosts" :key="post.slug">
<a :href="`/posts/${post.slug}`"></a>
</li>
</ul>
</aside>
</div>
Theme Setup
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme';
import PostList from './components/PostList.vue';
import PostCard from './components/PostCard.vue';
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('PostList', PostList);
app.component('PostCard', PostCard);
}
}
Why Ruby2JS for VitePress?
Content as Data
Query your markdown content like a database:
# Instead of manual file processing
Post.where(category: 'tutorials')
.where(draft: false)
.order(date: :desc)
Relationships
Authors, tags, categories—all linked automatically:
post = Post.find('getting-started')
post.author.name # Resolves from authors collection
post.tags # Array of Tag objects
Ruby in Vue
Write Vue components with Ruby syntax:
# Blocks become arrow functions
posts.map { |p| p.title } # → posts.map(p => p.title)
# snake_case becomes camelCase
@is_loading # → isLoading
Vite Configuration
// .vitepress/config.js
import { defineConfig } from 'vitepress';
import ruby2js from 'vite-plugin-ruby2js';
import content from '@ruby2js/content-adapter/vite';
export default defineConfig({
title: 'My Docs',
description: 'Documentation with Ruby2JS',
vite: {
plugins: [
ruby2js({
filters: ['Functions', 'ESM', 'CamelCase']
}),
content({
dir: 'content'
})
]
}
});
Migration Path
- Add plugins: Install
vite-plugin-ruby2jsand@ruby2js/content-adapter - Configure Vite: Add plugins to
.vitepress/config.js - Create content: Add markdown files to
content/directory - Write components: Create
.vue.rbfiles for Ruby Vue components - Import collections: Use
virtual:contentin components
Your existing VitePress markdown and Vue components continue to work unchanged.
Next Steps
- Coming from Vue - Vue component patterns
- Coming from Nuxt - Full-stack Vue
- Coming from 11ty - Liquid templates
🧪 Feedback requested — Share your experience