marquery

A compile-time markdown file query engine for Crystal. Parses markdown files with optional YAML frontmatter.

Marquery

A compile-time markdown file query engine for Crystal. Parses markdown files with optional YAML frontmatter at compile time and provides a query interface with pagination-friendly navigation.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      marquery:
        codeberg: fluck/marquery
    
  2. Run shards install

Usage

Require the shard in your app:

require "marquery"

Setting up a query

Create a query class and include Marquery:

class Blog::PostQuery
  include Marquery
end

This will look for markdown files in data/blog_post/*.md, derived from the class name (without the Query suffix). For example:

  • Blog::PostQuerydata/blog_post/*.md
  • ItemQuerydata/item/*.md
  • News::ArticleQuerydata/news_article/*.md

Markdown files

Entries are markdown files with a date-prefixed filename:

data/blog_post/20260320_first_post.md

The date (YYYYMMDD) and name are extracted from the filename. An optional YAML frontmatter block can override the title and add additional fields:

---
title: The very first post
description: >-
  This is the first post.
active: true
---

The body of the post goes here.

Custom models

By default, entries are deserialized into Marquery::Entity. To use a custom model, define a struct that includes Marquery::Model and declare it in the query:

struct Blog::Post
  include Marquery::Model
end

class Blog::PostQuery
  include Marquery

  model Blog::Post
end

Marquery::Model includes JSON::Serializable and defines the base fields:

Field Type Default
slug String
title String
description String? nil
content String
date Time
active Bool true

Additional fields can be added to the custom model and populated through frontmatter.

Sort order

Entries are sorted by date in descending order by default. Use order_by to change the field or direction:

class Blog::PostQuery
  include Marquery

  order_by date, Marquery::Order::ASC
end

Or sort by a different field:

class Blog::PostQuery
  include Marquery

  order_by title
end

Querying

query = Blog::PostQuery.new

# Get all entries
query.all

# Find by slug (slugs are always hyphenated)
query.find("first-post")   # raises if not found
query.find?("first-post")  # returns nil if not found

# Navigate between entries
query.previous(post)  # previous entry in the list, or nil
query.next(post)      # next entry in the list, or nil

Filtering

Use filter to narrow down entries. It takes a block and is chainable:

Blog::PostQuery.new
  .filter(&.active)
  .filter { |post| post.date >= 1.month.ago }
  .all

Since filter accepts any block that returns a Bool, you can express any condition without being limited to a predefined set of operators.

Pagination

The all method returns a plain Array, so it works with any array-based pagination solution.

Lucky

Lucky has built-in array pagination with paginate_array:

class Blog::Index < BrowserAction
  get "/blog" do
    pages, posts = paginate_array(Blog::PostQuery.new.all)
    html Blog::IndexPage, posts: posts, pages: pages
  end
end

Other frameworks

For Kemal and other frameworks, pager is a good option:

require "pager/collections/array"

get "/blog" do |env|
  current_page = env.params.query["page"]?.try(&.to_i) || 0
  posts = Blog::PostQuery.new.all.paginate(current_page, 10)
  # ...
end

Contributing

We use conventional commits for our commit messages, so please adhere to that pattern.

  1. Fork it (https://codeberg.org/fluck/marquery/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'feat: new feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • Wout - creator and maintainer
Repository

marquery

Owner
Statistic
  • 1
  • 0
  • 0
  • 0
  • 2
  • about 3 hours ago
  • March 28, 2026
License

Links
Synced at

Sat, 28 Mar 2026 19:32:31 GMT

Languages