Coming from 11ty

If you know 11ty, you’ll appreciate Ruby2JS’s ActiveRecord-like queries over your content collections.

Table of Contents

What You Know → What You Write

11ty Ruby2JS
_data/posts.js _data/site.js with createCollection
{% for post in posts %} Same (Liquid unchanged)
JavaScript data files Content adapter scans markdown
Manual filtering Post.where(draft: false)
collection.getFilteredByTag() Post.where(tag: 'ruby')
Computed data Relationships resolve automatically

Quick Start

Install the content adapter:

npm install @ruby2js/content-adapter

Create a data file that loads your content:

// src/_data/site.js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
import { createCollection } from '@ruby2js/content-adapter';

export default function() {
  const posts = loadMarkdownFiles('content/posts');
  return {
    posts,
    Post: createCollection('posts', posts)
  };
}

Use it in templates:

{% for post in site.posts %}
  {% unless post.draft %}
    <article>
      <h2>{{ post.title }}</h2>
      <time>{{ post.date | date: "%B %d, %Y" }}</time>
    </article>
  {% endunless %}
{% endfor %}

The Content Adapter

ActiveRecord-like Queries

The content adapter provides familiar Rails query methods over your markdown files:

// In your data file
const Post = createCollection('posts', posts);

// These queries work:
Post.where({ draft: false })
Post.where({ author: 'alice' }).order({ date: 'desc' })
Post.find('hello-world')  // by slug
Post.find_by({ title: 'Welcome' })
Post.first()
Post.last()
Post.count()
Post.limit(10).offset(5)

Relationships

Define relationships between collections:

const Author = createCollection('authors', authors);
const Post = createCollection('posts', posts);

Post.belongsTo('author', Author);

// Now post.author resolves automatically
const post = Post.first();
console.log(post.author.name);  // "Alice"

Convention-Based Inference

If your post has author: alice and you have an authors/alice.md file, the relationship is inferred automatically.

Content Structure

content/
  posts/
    2024-01-15-welcome.md
    2024-01-20-getting-started.md
  authors/
    alice.md
    bob.md
src/
  _data/
    site.js
  _includes/
    layout.liquid
  index.liquid

Markdown with Front Matter

---
title: Welcome to the Blog
date: 2024-01-15
author: alice
draft: false
tags: [ruby, javascript]
---

Your content here...

Slug Extraction

Slugs are extracted from filenames automatically:

Filename Slug
2024-01-15-hello-world.md hello-world
alice.md alice

Query API Reference

Method Description
where(conditions) Filter by attributes
where().not(conditions) Exclude by attributes
order({ field: 'asc' }) Sort results
limit(n) Limit result count
offset(n) Skip first n records
find(slug) Find by slug
find_by(conditions) Find first matching
first() / last() Get first/last record
count() Count records
exists() Check if any exist
toArray() Execute and return array

Why Ruby2JS for 11ty?

Familiar Query Syntax

Instead of writing custom JavaScript filter logic:

// Before: Custom filtering
const published = posts.filter(p => !p.draft)
  .sort((a, b) => new Date(b.date) - new Date(a.date))
  .slice(0, 10);

Write Rails-style queries:

// After: ActiveRecord-like
Post.where({ draft: false }).order({ date: 'desc' }).limit(10)

Automatic Relationships

No manual data joining:

// Before: Manual lookup
const authorData = authors.find(a => a.slug === post.author);

// After: Automatic resolution
post.author.name  // Just works

Same Collections, More Power

Your existing content structure works unchanged. The adapter adds query capabilities without requiring migration.

Full Data File Example

// src/_data/site.js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
import { createCollection } from '@ruby2js/content-adapter';

const contentDir = path.resolve(process.cwd(), 'content');

function loadCollection(name) {
  const dir = path.join(contentDir, name);
  if (!fs.existsSync(dir)) return [];

  return fs.readdirSync(dir)
    .filter(f => f.endsWith('.md'))
    .map(f => {
      const content = fs.readFileSync(path.join(dir, f), 'utf-8');
      const { data, content: body } = matter(content);
      const slug = f.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace('.md', '');
      return { ...data, slug, body: marked(body) };
    });
}

export default function() {
  const posts = loadCollection('posts');
  const authors = loadCollection('authors');

  const Post = createCollection('posts', posts);
  const Author = createCollection('authors', authors);

  // Wire relationships
  Post.belongsTo('author', Author);

  return { posts, authors, Post, Author };
}

Template Examples

List Published Posts

{% for post in site.posts %}
  {% unless post.draft %}
    <article>
      <h2><a href="/posts/{{ post.slug }}/">{{ post.title }}</a></h2>
      <time>{{ post.date | date: "%B %d, %Y" }}</time>
      <p>{{ post.excerpt }}</p>
    </article>
  {% endunless %}
{% endfor %}

Posts by Tag

{% assign ruby_posts = site.posts | where: "tags", "ruby" %}
{% for post in ruby_posts %}
  <li>{{ post.title }}</li>
{% endfor %}

Author Page

{% assign author = site.authors | where: "slug", page.author | first %}
<div class="author">
  <h2>{{ author.name }}</h2>
  <p>{{ author.bio }}</p>
</div>

Migration Path

  1. Install adapter: npm install @ruby2js/content-adapter
  2. Create data file: Add src/_data/site.js with content loading
  3. Update templates: Reference site.posts, site.authors, etc.
  4. Add queries: Use site.Post.where(...) for complex filtering

Your existing markdown files and Liquid templates require no changes.

Next Steps

🧪 Feedback requestedShare your experience

Back to Juntos/coming-from