Skip to content

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

384 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fragments4k

A framework-agnostic Markdown-based content library for Kotlin/JVM with adapters for HTTP4k, Javalin, Spring Boot, Quarkus, and Micronaut.

Overview

Fragments4k turns a directory of Markdown files with YAML front matter into a fully-featured blog or content site. It handles parsing, routing, pagination, search, RSS, sitemaps, SEO metadata, and more — you bring the framework and templates.

Features

Content

  • Markdown content with YAML front matter
  • Static pages and blog posts with pagination
  • Tag, category, and date-based filtering
  • Full-text search with autocomplete (Lucene)
  • RSS/Atom feed generation
  • XML sitemap generation with image support
  • Content relationships (prev/next, related, translations)
  • Publication lifecycle (draft, review, scheduled, published, archived, expired)
  • Content revisions and audit trail

SEO

  • SeoMetadata with Open Graph, Twitter Card, and JSON-LD generation
  • Canonical URL support on FragmentViewModel
  • /robots.txt route on all adapters
  • Per-page image field for og:image / twitter:image
  • Per-page robots meta tag control

Developer Experience

  • HTMX support for partial rendering
  • Live reload in development
  • CLI tool for project scaffolding
  • Typed front matter accessors (getString, getInt, getBoolean, etc.)
  • Coroutines-based async operations throughout
  • Caching layer with automatic invalidation
  • Image optimization and responsive variants

Framework Adapters

Adapter Template Engine Notes
HTTP4k Any (Any) -> String Template-engine agnostic
Javalin Any via TemplateRenderer Javalin 7+
Spring Boot Thymeleaf (default) @Controller with suspend functions
Quarkus Qute (default) JAX-RS resource
Micronaut Any via Micronaut Views @Controller with suspend functions

Quick Start

1. Add dependency

<dependency>
    <groupId>io.github.rygel</groupId>
    <artifactId>fragments-http4k</artifactId> <!-- or fragments-javalin, fragments-spring-boot, etc. -->
    <version>0.6.2</version>
</dependency>

2. Create content

content/
├── about.md
└── blog/
    ├── 2026-01-15-hello-world.md
    └── 2026-03-20-second-post.md

3. Write front matter

---
title: Hello World
date: 2026-01-15
tags: [kotlin, jvm]
categories: [tutorials]
author: Jane Doe
image: /static/images/hello-cover.jpg
template: blog_post
---

Your Markdown content here.

<!--more-->

Content after the fold (preview stops at the <!--more--> tag).

4. Wire up the adapter

val repository = FileSystemFragmentRepository("./content")
val staticEngine = StaticPageEngine(repository)
val blogEngine = BlogEngine(repository)

// HTTP4k example
val adapter = FragmentsHttp4kAdapter(
    staticEngine = staticEngine,
    blogEngine = blogEngine,
    renderer = myTemplateRenderer,
    searchEngine = LuceneSearchEngine(repository),
    siteUrl = "https://example.com"
)
adapter.createRoutes().asServer(Netty(8080)).start()

See the framework adapter READMEs for framework-specific setup.

Front Matter Reference

All available front matter fields:

Field Type Default Description
title String filename Page title
slug String from filename URL-safe identifier
date Date — Authoring date (yyyy-MM-dd or yyyy-MM-dd'T'HH:mm)
publishDate Date — Scheduled publication date
expiryDate Date — Auto-expire after this date
status String PUBLISHED Lifecycle state (see below)
visible Boolean true Include in visible listings
template String default Template identifier
tags List [] Tag slugs
categories List [] Category slugs
author String — Author name or ID
authorIds List [] Multiple author IDs
image String — Cover image path (e.g. /static/images/cover.jpg)
preview String auto-extracted Custom preview HTML
order Int 0 Sort key for manual ordering
language String en BCP-47 language tag
languages Map {} Translations ({de: "hallo-welt", fr: "bonjour-monde"})
series String — Series slug
seriesPart Int — Position in series (1-based)
seriesTitle String — Display title for this part

Any additional fields are preserved in Fragment.frontMatter and accessible via typed accessors.

Content Lifecycle

Fragments follow a publication lifecycle with guarded transitions:

DRAFT → REVIEW → APPROVED → PUBLISHED
                     ↓
                 SCHEDULED → PUBLISHED (automatic at publishDate)
                                ↓
                            ARCHIVED
                            EXPIRED (automatic at expiryDate)

Manage status programmatically:

repository.updateFragmentStatus("my-post", FragmentStatus.PUBLISHED)
repository.scheduleMultiple(listOf("post-1"), publishDate = myDateTime)
repository.archiveMultiple(listOf("old-post"))

URL Builder

By default, fragment URLs are /<slug>. Use urlBuilder for custom URL schemes:

// Date-based blog URLs: /blog/2026/03/hello-world
val blogRepo = FileSystemFragmentRepository(
    basePath = "./content/blog",
    urlBuilder = { fragment ->
        val date = fragment.date ?: return@FileSystemFragmentRepository "/${fragment.slug}"
        "/blog/${date.year}/%02d/${fragment.slug}".format(date.monthValue)
    }
)

// Prefix-based URLs: /projects/my-project
val projectRepo = FileSystemFragmentRepository(
    basePath = "./content/projects",
    baseUrl = "/projects"
)

Typed Front Matter Accessors

Access custom front matter fields safely without unchecked casts:

// Instead of: fragment.frontMatter["githubRepo"] as? String
fragment.getString("githubRepo")        // String?
fragment.getBoolean("featured")         // Boolean?
fragment.getInt("priority")             // Int?
fragment.getLong("viewCount")           // Long?
fragment.getDouble("rating")            // Double?
fragment.getStringList("customTags")    // List<String>

These handle SnakeYAML type coercion automatically (e.g. Number.toInt() for any numeric type).

SEO

SeoMetadata

Generate complete SEO tags from a fragment:

val seo = SeoMetadata.fromFragment(
    fragment = fragment,
    siteUrl = "https://example.com",
    siteName = "My Site",
    pagePath = "blog/2026/03/hello-world",
    ogType = "article"  // or "website" for static pages
)

// In your template:
seo.generateAllMetaTags()  // outputs <meta>, <link rel="canonical">, OG tags, Twitter Card, JSON-LD

When author is not passed, it defaults from fragment.author. When imageUrl is not passed, it defaults from fragment.image.

Canonical URL on FragmentViewModel

val vm = FragmentViewModel(fragment = fragment, siteUrl = "https://example.com")

// In templates:
// <link rel="canonical" href="${vm.canonicalUrl}">
// <meta property="og:url" content="${vm.canonicalUrl}">

robots.txt

All adapters automatically serve /robots.txt:

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

Search

Full-text search powered by Apache Lucene:

val searchEngine = LuceneSearchEngine(repository)
searchEngine.index()  // build the index

// Simple search
val results = searchEngine.search("kotlin coroutines", maxResults = 20)

// Advanced search
val results = searchEngine.search(SearchOptions(
    query = "kotlin",
    phraseSearch = true,   // exact phrase matching
    fuzzySearch = false,
    maxResults = 10
))

// Autocomplete suggestions
val suggestions = searchEngine.autocomplete("kot", limit = 5)

// Search by tag or category
val kotlinPosts = searchEngine.searchByTag("kotlin")

Template Integration

FragmentViewModel is passed to your templates with these properties:

vm.title              // page title
vm.content            // rendered HTML
vm.preview            // preview HTML (up to <!--more--> tag)
vm.slug               // URL slug
vm.url                // relative URL path
vm.canonicalUrl       // absolute URL (when siteUrl is set)
vm.date               // authoring date
vm.tags               // tag list
vm.categories         // category list
vm.author             // author name
vm.template           // template identifier
vm.readingTime        // ReadingTime(minutes, seconds, text)
vm.formattedReadingTime  // e.g. "3m 12s read"
vm.fragment.image     // cover image path
vm.fragment.frontMatter  // raw front matter map

// Relationships (when loaded)
vm.previousPost       // previous post in chronological order
vm.nextPost           // next post
vm.relationshipRelatedPosts  // related posts by tags/categories
vm.translations       // language → fragment map

Example: JTE template

@param vm: FragmentViewModel

<article>
    <h1>${vm.title}</h1>
    <time>${vm.date}</time>
    <span>${vm.formattedReadingTime}</span>
    
    @if(vm.fragment.image != null)
        <img src="${vm.fragment.image}" alt="${vm.title}">
    @endif
    
    $unsafe{vm.content}
    
    @if(vm.hasPreviousPost)
        <a href="${vm.previousPost.url}">Previous</a>
    @endif
    @if(vm.hasNextPost)
        <a href="${vm.nextPost.url}">Next</a>
    @endif
</article>

Modules

Module Description
fragments-core Domain model, Markdown parsing, repository
fragments-blog-core Blog engine with pagination and filtering
fragments-static-core Static page engine
fragments-seo-core SEO metadata, Open Graph, JSON-LD
fragments-lucene-core Full-text search and autocomplete
fragments-rss-core RSS 2.0 feed generation
fragments-sitemap-core XML sitemap generation
fragments-cache-core Caching decorator for repositories
fragments-navigation-core Menu and archive navigation generators
fragments-chat-core Chat/conversation Markdown extension
fragments-social-core Social media share link generation
fragments-image-optimization-core Image resizing and optimization
fragments-live-reload File-watching live reload for development
fragments-cli CLI tool for scaffolding and dev server
fragments-http4k HTTP4k framework adapter
fragments-javalin Javalin framework adapter
fragments-spring-boot Spring Boot framework adapter
fragments-quarkus Quarkus framework adapter
fragments-micronaut Micronaut framework adapter
fragments-bom Bill of Materials for dependency management

CLI Tool

# Scaffold a new project
java -jar fragments-cli.jar init my-blog --framework=http4k

# Available frameworks: http4k, javalin, spring-boot, quarkus, micronaut

# Run dev server with live reload
java -jar fragments-cli.jar run --watch --content-dir=./content --port=8080

Building

./mvnw verify -T 4

Security Model

Content Sanitization

All Markdown content is sanitized before rendering using an OWASP-style HTML safelist:

  • SanitizerProfile.RELAXED_TRUSTED_AUTHOR (default) — allows class, id, and most HTML tags. Suitable for content written by trusted authors.
  • SanitizerProfile.STRICT — strips class, id, and limits tags to a minimal safe set. Use this when accepting content from untrusted authors.
val parser = MarkdownParser(sanitizerProfile = SanitizerProfile.STRICT)

Content Security Policy

The default CSP is strict and self-only:

default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Override it to allow CDNs or inline styles:

val engine = FragmentsEngine(
    contentSecurityPolicy = "default-src 'self'; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com"
)

Filesystem Trust Boundary

FileSystemFragmentRepository validates that all file operations stay within the configured content root. Symlinks pointing outside the base directory are rejected.

Input Validation

All public route inputs (slugs, tags, categories, search queries, pagination) are validated centrally in RequestValidation. Invalid inputs return a 400 Bad Request with a descriptive error message.

Search Bounds

LuceneSearchEngine enforces maximum query length (500 characters), minimum autocomplete prefix (2 characters), and clamps result limits.

Production Readiness Checklist

Before deploying to production:

  • Set siteUrl to your real domain
  • Review CSP defaults — tighten or override as needed
  • Configure urlBuilder for your URL scheme
  • Choose SanitizerProfile.STRICT if accepting untrusted content
  • Run mvn test and ensure all tests pass
  • Keep dependencies current — check for security advisories
  • Set filesystem permissions on the content directory
  • Remove or protect draft content (set status: DRAFT)
  • Review SECURITY_QUALITY.md for CI scanning configuration

Adapter Consistency

All five framework adapters provide consistent behavior:

Feature HTTP4k Javalin Spring Boot Quarkus Micronaut
JSON error responses Yes Yes Yes Yes Yes
CSP header Yes Yes Yes Yes Yes
Input validation (400) Yes Yes Yes Yes Yes
Missing content (404) Yes Yes Yes Yes Yes
RSS/Sitemap/Robots Yes Yes Yes Yes Yes

Error responses follow a consistent JSON structure:

{"status": 400, "error": "Bad Request", "message": "Invalid slug: contains illegal characters"}

License

Apache License 2.0

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages