marquery
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
-
Add the dependency to your
shard.yml:dependencies: marquery: codeberg: fluck/marquery -
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::PostQuery→data/blog_post/*.mdItemQuery→data/item/*.mdNews::ArticleQuery→data/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.
- Fork it (https://codeberg.org/fluck/marquery/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'feat: new feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Wout - creator and maintainer
marquery
- 1
- 0
- 0
- 0
- 2
- about 3 hours ago
- March 28, 2026
Sat, 28 Mar 2026 19:32:31 GMT