jekyll-documents
Turn files in assets/documents/ into browsable document pages.
Requirements: Ruby 3.4+ • Jekyll 4.4+
Features: Auto-collection • File icons • Categories • Search
Quick Start
# Gemfile
gem "jekyll-documents"# _config.yml
plugins:
- jekyll-documents
documents:
icon_set: "color" # color, lines, minimal, ultra-minimal# Add files
assets/documents/reports/2026-03-01_Annual_Report.pdf
assets/documents/minutes/2026-02-15_Board_Meeting.docx
<!-- Use in templates -->
{% include latest_documents.html %}
{% include documents_list.html %}
{% include category_list.html %}File Naming
Format: YYYY-MM-DD_Title.ext
assets/documents/reports/2026-03-01_Annual_Report.pdf
Date → YYYY-MM-DD • Title → underscores to spaces • Category → folder name
Icons
4 icon sets: color (default), lines, minimal, ultra-minimal
documents:
icon_set: "color"Render a document icon in templates with the context-aware tag:
{% document_icon page %}The tag applies the configured icon set and site baseurl automatically. The file_type_icon and
file_type_icon_tag filters remain available for lower-level or Ruby-side use.
Usage
Includes
{% include latest_documents.html count=5 %}
{% include documents_list.html %}
{% include documents_search.html %}Liquid Tags
Link to a document by title or slug (partial match, case-insensitive):
{% doc_link "Annual Report" %}
{% doc_link "annual" text:"Read the report" %}
{% doc_link "board-meeting" icon:false size:false %}
{% doc_link path:"Reports/2026-03-01_Annual_Report.pdf" %}Renders an <a> tag with the file type icon, title, and human-readable file size.
Use text:"..." to override the link text, icon:false to hide the icon,
or size:false to hide the file size. A path: lookup is exact and case-sensitive after
normalizing / and \ separators; use it when a title or slug matches multiple documents.
Link to or list a category:
{% doc_category "reports" %}
{% doc_category "reports" text:"All reports" %}
{% doc_category "reports" list:true limit:5 %}
{% doc_category path:"Departments/Europe/Reports" list:true %}
{% doc_category "reports" aggregate:true list:true %}Link mode renders an <a> tag to the category page. List mode renders a <ul> sorted by date.
Use path: for an exact, case-sensitive category path, limit:N to cap the list, and
aggregate:true list:true to explicitly combine repeated short category names.
List the most recent documents:
{% latest_documents %}
{% latest_documents count:3 %}
{% latest_documents count:3 category:"minutes" %}count: defaults to latest_default_count (5); category: filters to a single category.
Unlike {% include latest_documents.html %}, the tag reads its defaults from site config.
Document template customization
The document layout provides two optional include hooks. Both are empty by default, and the
LayoutRegistrar copies them only when the site does not already provide a file with that name:
-
_includes/document_metadata_extra.htmlrenders inside the document metadata paragraph, after the date and category. -
_includes/document_download_attributes.htmlrenders inside the opening tag of the document download link. It should output zero or more valid HTML attributes, including any leading spaces.
For example, a site can put a downloads label or count display in the metadata include, and add analytics attributes to the download link without copying the full document layout. These hooks are provider-neutral; the site owns their content and any associated JavaScript.
Optional GoatCounter click events
If the site loads GoatCounter's count.js, it can record clicks on the document-page download link
using the attributes include. Use a stable, unique event name for each document:
data-goatcounter-click="download-{{ page.source_path | url_encode | escape }}"
data-goatcounter-no-session="1"data-goatcounter-no-session="1" counts repeat clicks by the same visitor. GoatCounter counts a
click attempt, not a confirmed file transfer. This hook covers the download link on the document
page only; direct file links in site-authored listings or posts need the same event attributes of
their own. The gem does not load GoatCounter, fetch event totals, or combine historical baselines.
Those remain optional site-level analytics behavior.
Configuration
documents:
root: "assets/documents"
icon_set: "color"
include_extensions: [".pdf", ".docx", ".pptx", ".xlsx", ".odt", ".ods", ".odp"]
strict_filename: true
strict_extensions: true
resolution_mode: "warn"resolution_mode controls ambiguous or unknown path references. The default warn mode logs
all matching candidates and renders nothing. Set it to strict to log the same diagnostic and
abort the build.
Generated documents expose source_path, category_path, and category_slug. Category mappings
check the complete category_path before falling back to its final directory component.
Permalinks support :category, :category_path, :slug, :source_path, :date, :year,
:month, and :day. For example:
documents:
permalink: "/documents/:category_path/:date/:slug/"
category_map:
Departments/Europe/Research: "European Research"
Research: "Research"The default permalink remains backward compatible. Any duplicate final permalink is fatal and
the build error lists every conflicting source_path.
See configuration.rb for all options.
Icon sizing
Icons default to 1em (line-height) so they scale with surrounding text.
A framework-agnostic CSS file with fixed-size utility classes is included:
<link rel="stylesheet" href="{{ '/assets/css/documents.css' | relative_url }}">| Class | Size |
|---|---|
| (default) |
1em — scales with line height |
icon-x1 |
16px |
icon-x2 |
32px |
icon-x3 |
48px |
icon-x4 |
64px |
icon-x5 |
96px |
icon-x6 |
128px |
icon-x7 |
150px |
icon-x8 |
256px |
icon-x9 |
512px |
Usage with the document_icon tag:
{% document_icon page class:"document-file-icon icon-x2" %}
{% document_icon doc alt:"Board minutes" %}Or with any <img>:
<img src="..." class="document-file-icon icon-x3" />The CSS uses display: inline-block and vertical-align: middle — no
dependency on Bulma, Bootstrap, Tailwind, or any other framework.
Search Integration
Documents are compatible with jekyll-client-search
for client-side search. Each document has categories (plural array) and
searchable content baked in at generation time, so search plugins can
index uploaded files alongside posts.
To include documents in the search index, add documents to the
collections list in _config.yml:
client_search:
collections:
- posts
- documentsIcons and file-type metadata in search results
jekyll-documents bakes file_type, icon_url, and icon_set into each
document's data at generation time. When jekyll-client-search is installed
and documents is in the search collections, these fields are
auto-injected into the search index — no extra configuration needed:
client_search:
collections:
- posts
- documentsThis makes jekyll-client-search render:
-
data-file-type,data-icon-setattributes on each result<article>(for CSS-based badges and theme-aware styling) - An
<img class="client-search-result-icon">before the title, using the icon from the configuredicon_set(color, lines, minimal, or ultra-minimal)
The icon automatically matches the icon_set configured in your documents
section — no extra configuration needed for theme consistency.
Field renaming (for integration with other search conventions):
client_search:
passthrough_fields:
- file_type: doctype # rename in the search index
- icon_url: thumbnail
icon_field: thumbnailCSS examples:
/* File-type badge */
.client-search-result[data-file-type="pdf"]::before {
content: "PDF";
background: #e74c3c; color: white;
padding: 0 0.3em; font-size: 0.7em; margin-right: 0.3em;
}
/* Theme-aware icon sizing */
.client-search-result[data-icon-set="color"] .client-search-result-icon { width: 2em; }
.client-search-result[data-icon-set="ultra-minimal"] .client-search-result-icon { width: 1em; }See the jekyll-client-search README for all CSS customization options.
Text Extraction
Extract text from PDF/DOCX/XLSX/PPTX/ODT/ODS/ODP files so search engines can index document contents, not just metadata.
Setup
Add the optional plaintext gem
to your Gemfile:
# Gemfile
gem "plaintext", group: :jekyll_pluginsEnable extraction in _config.yml:
documents:
extract_text: trueHow it works
- Extracted text is stored in
doc.content, whichjekyll-client-searchindexes automatically — no extra configuration needed - Text is cached in
.cache/jekyll-documents/(in your site source, not.jekyll-cache/), so it survivesjekyll clean - Cache invalidation uses SHA-256 file digests — only changed files are re-extracted
- Stale cache entries for deleted files are cleaned up automatically at the end of each build
- Falls back to metadata-only content (title, category, file type, date)
if the
plaintextgem is missing or extraction fails
Configuration
documents:
extract_text: true # Enable text extraction
text_max_bytes: 500000 # Truncate extracted text (default 500KB)
text_cache_dir: ".cache/jekyll-documents" # Cache directory in site sourceAdd the cache directory to .gitignore:
.cache/jekyll-documents/
CLI tool dependencies
The plaintext gem uses the rubyzip Ruby gem for Office formats (no CLI
tools needed). PDF extraction shells out to a system command:
| Format | Tool |
|---|---|
pdftotext (poppler-utils) |
|
| DOCX/PPTX/XLSX | rubyzip (Ruby gem, no CLI needed) |
| ODT/ODS/ODP | rubyzip (Ruby gem, no CLI needed) |
Install pdftotext for PDF extraction:
# macOS
brew install poppler
# Debian/Ubuntu
sudo apt-get install poppler-utilsOn macOS and other non-standard installations, the executable is discovered
from PATH. You can also configure a custom pdftotext command through the
plaintext gem configuration.
Development
rake # Ruby quality checks
rake test # Rebuild/install gem + Ruby and browser tests
rake browser_test # Browser search tests (Node.js + Chromium required)
rake quick # Fast check (style + markdown + tests)
rake help # Show commandsQuality: ≥98% coverage • no unallowlisted vulnerabilities • 0 offenses
See README.Development.md for details.
DeepWiki: https://deepwiki.com/gundestrup/jekyll-documents
Release
bundle exec rake "version:bump[patch]"
# Edit CHANGELOG.md
git add lib/jekyll/documents/version.rb CHANGELOG.md
git commit -m "Release X.Y.Z: summary"
git tag -a vX.Y.Z -m "Release X.Y.Z"
git push origin main
git push origin vX.Y.ZLicense
AGPL-3.0-only • Icons by SVG Repo