If you find any issue or missing info, be awesome and edit this document to help others Roqers.

Roq uses Qute templates and a different content model than Jekyll, Hugo, or other static site generators. This guide covers the workflow, syntax mappings, and Roq-specific pitfalls for migrating an existing static site to Roq.

Prerequisites

  • Familiarity with Roq concepts (see the Getting Started guide)

  • An existing static site you want to migrate (Jekyll, Hugo, or similar)

  • The Roq CLI installed (see Getting Started)

  • Java 21+ installed

Using an LLM to accelerate migration

An LLM (Large Language Model) such as Claude, ChatGPT, or a locally hosted model can accelerate the migration of your templates, layouts, and content files. Template conversion is mostly mechanical syntax mapping, which LLMs handle effectively.

To give your LLM full context about Roq, point it to https://iamroq.dev/llms-full.txt.

LLM-assisted migration is not fully automatic. Expect to review and adjust the output. The prompts in this guide are a starting point, refine them as you learn what works for your site.

Overview of the migration process

Migrating a static site to Roq involves converting four categories of files:

  1. Project scaffold — roq create, application.properties, and directory structure

  2. Layouts and templates — Liquid/Jinja/Go templates to Qute templates

  3. Content files — Markdown or AsciiDoc with front matter adjustments

  4. Static assets — JavaScript, SCSS/CSS, images, and data files

Content files typically need minimal changes. Configuration and asset migration require more manual work.

Roq expects content in a content/ directory by default (configurable via site.content-dir). Templates go in templates/layouts/, static files in public/, and data files in a configurable data directory.

Do not attempt to migrate everything at once. Use a phased approach with validation gates:

Phase Scope Gate

A: Foundation

Roq project + one page rendering correctly

Stop if content does not render. Verify quarkus.qute.alt-expr-syntax=true is set so curly braces in code samples are treated as plain text.

B: Styling + scale

JavaScript, CSS/SCSS, all content pages

Stop if build time exceeds 10 minutes or memory exceeds 4 GB

C: Full site

All templates, blog, homepage, static pages, redirects, CI/CD

Production-ready

Commit after each step.

Prepare reference material

Having concrete examples of working Roq templates makes migration smoother (whether you are converting manually or with an LLM).

Collect these files from a working Roq project (the Roq blog is a good source):

  • pom.xml — for Maven dependency structure

  • config/application.properties — for Roq configuration patterns

  • A sample Qute layout (for example, templates/layouts/default.html)

  • A sample content page with front matter (for example, a .md or .adoc file from content/)

If you are using an LLM, attach these files directly or paste them as code blocks. You can also point the LLM to https://iamroq.dev/llms-full.txt for complete Roq documentation.

Create the project scaffold

Before converting any templates, set up the project and validate that a single page renders.

Create a new Roq project with the Roq CLI:

roq create my-site

This creates a project with the default theme (full blog layout, dark mode, sidebar, SEO support). If you want full control over the design and prefer to build your own layouts from scratch, use the base theme instead:

roq create my-site -x theme:base
The base theme provides a minimal HTML structure with just SEO, favicon, and Web Bundler. It is a better starting point if you plan to port your existing site’s design rather than adopt Roq’s default look.

Add any plugins you need:

roq add plugin:asciidoc    # if using AsciiDoc content
roq add plugin:sitemap
roq add plugin:aliases     # for URL redirects
roq add plugin:tagging     # if using tags

LLM prompt for project setup

I am migrating a static website from Jekyll to Roq (a Quarkus-based static
site generator). I have already created the project with `roq create`
and the base theme.

Help me configure application.properties with:
- site.url=https://mysite.example.com
- site.collections.posts.layout=post
- quarkus.qute.alt-expr-syntax=true
- site.slugify-files=false
- quarkus.default-locale=en

Here is my Jekyll _config.yml for reference:
[paste or attach _config.yml]

Roq-specific configuration details

Alternative expression syntax (recommended)

Roq supports an alternative expression syntax where output expressions use {=expr} instead of {expr}. With alt syntax enabled, only {=...} and {#...} are interpreted as Qute expressions. Regular {...} is treated as plain text, so curly braces in code samples and JSON are safe without escaping.

Add to your application.properties:

quarkus.qute.alt-expr-syntax=true
This will become the default syntax for Roq in a future version.

All Qute examples in this guide use the alt syntax ({=expr} for output, {#...} for sections).

Qute escaping (without alt syntax)

If you choose not to enable the alternative expression syntax, Qute treats {...} as template expressions. Content with curly braces (Java code, JSON examples) must be escaped from Qute parsing.

AsciiDoc files: Qute parsing is disabled by default (quarkus.asciidoc.qute=false). Curly braces in AsciiDoc content are safe without any extra configuration. To enable Qute parsing for a specific AsciiDoc file, add the :qute: attribute to the document header.

Markdown and HTML files: Qute parsing is enabled by default. If your Markdown files contain curly braces in code samples, set site.escaped-pages in application.properties:

site.escaped-pages=posts/**

This wraps matched page content with Qute escape markers so curly braces are not parsed as template expressions.

site.collections

Defining any custom collection replaces Roq’s defaults. If you define site.collections.guides.layout=guide, you must also explicitly add site.collections.posts.layout=post — otherwise Roq’s default posts collection is silently dropped.

quarkus.roq.data.dir

Set this to _data to reuse Jekyll’s data directory in place, avoiding the need to move data files.

site.slugify-files

Set to false to preserve original filenames in URLs instead of slugifying them.

Validate the scaffold (Phase A gate)

Copy a single content page into content/ and start dev mode:

roq start

Verify:

  • The page renders with your content (AsciiDoc or Markdown)

  • Curly braces in code samples are treated as plain text (verify quarkus.qute.alt-expr-syntax=true is set)

  • Content files without YAML front matter are recognized as collection members (Roq discovers content by directory, not by front matter presence)

  • Files and directories starting with _ inside content/ (such as _includes/ or _attributes.adoc) are NOT rendered as standalone pages

  • If you use AsciiDoc include:: directives, they resolve correctly

AsciidoctorJ may enforce a security boundary (ROOTDIR) that prevents include:: directives from resolving paths outside the content directory. If includes fail with a security error, move the included files inside content/ or configure AsciidoctorJ’s safe mode.

Do not proceed to templates until this gate passes.

Convert layouts and templates

Layouts are the highest-value conversion target. Jekyll uses Liquid templates; Hugo uses Go templates. Roq uses Qute, which has a different syntax but similar concepts.

If you created your project with the default theme, it already provides main, page, and post layouts with a full blog design. You can override any theme layout by creating a file with the same name in templates/layouts/.

If you used the base theme, you have a minimal HTML structure and will need to create your own layouts to match your existing site’s design.

Key syntax differences (Jekyll Liquid to Qute)

Concept Jekyll (Liquid) Roq (Qute)

Variable output

{{ page.title }}

{=page.title}

Conditional

{% if page.image %}...{% endif %}

{#if page.image}...{/if}

Loop

{% for post in site.posts %}...{% endfor %}

{#for post in site.collections.get('posts')}...{/for}

Include / partial

{% include header.html %}

{#include header.html /}

Layout inheritance

layout: default in front matter

layout: default in front matter (same concept)

Content insertion

{{ content }}

{#insert /}

Date formatting

{{ post.date | date: "%B %d, %Y" }}

{=post.date.format('MMMM dd, yyyy')}

Null / empty check

{% if page.image %}

{#if page.image} (Qute has different null semantics — test carefully)

If you are migrating from Hugo rather than Jekyll, replace the Liquid syntax column with Go template equivalents ({{ .Title }}, {{ if .Params.image }}, {{ range .Pages }}, etc.) and include the Hugo equivalents in your prompt. The Qute (alt syntax) column stays the same.

LLM prompt for layout conversion

I am migrating a static website from Jekyll to Roq (a Quarkus-based static
site generator that uses Qute templates).

Convert the attached Jekyll Liquid layout to a Roq Qute template.

Roq uses the alternative expression syntax: output expressions use {=expr}
instead of {expr}. Section tags ({#if}, {#for}, {#include}) are unchanged.

Key rules:
- Replace Liquid {{ variable }} with Qute {=variable} syntax
- Replace {% if %} with {#if } ... {/if}
- Replace {% for item in collection %} with {#for item in collection} ... {/for}
- Replace {{ content }} with {#insert /}
- Replace {% include file.html %} with {#include file /}
- Replace Liquid filters (| date, | upcase, etc.) with Qute method calls
  (e.g., {=post.date.format('yyyy, MMM dd')})
- Site-level variables use the `site` object (e.g., {=site.title})
- Page-level variables use the `page` object (e.g., {=page.title})
- Collections are accessed via site.collections.get('name')
- Preserve all HTML structure and CSS classes unchanged
- Qute null handling differs from Liquid: test conditionals carefully

Here is my Jekyll layout:
[paste or attach your layout file]

Here is an example of a working Roq Qute layout for reference:
[paste or attach a working Roq layout]

Iterate on complex layouts

Template porting is skilled translation work. Liquid and Qute differ in conditionals, loops, filters, null handling, and partial inclusion syntax. Budget extra time for complex templates like headers, footers, and sidebars.

For layouts with many includes, navigation logic, or pagination:

  1. Convert the main layout first (usually default.html)

  2. Convert each include/partial file separately

  3. Convert pagination logic last (Roq pagination works differently from Jekyll)

After each conversion, test in dev mode (roq start). Qute provides clear error messages with line numbers. Fix errors before moving to the next file.

Convert content files

Content files (blog posts, pages) usually need fewer changes than layouts.

Content that works without changes

  • Markdown files (.md) with YAML front matter work as-is in most cases

  • AsciiDoc files (.adoc, .asciidoc) are supported natively by the quarkus-roq-plugin-asciidoc-jruby plugin

  • AsciiDoc files without YAML front matter are recognized as collection members based on their directory — Roq discovers content by directory, not by front matter presence

  • The layout field in front matter resolves automatically — writing layout: post is sufficient because Roq maps it to templates/layouts/post.html

Front matter differences

Field Jekyll Roq

Layout

layout: post

layout: post (same — Roq resolves to templates/layouts/post.html)

Date

date: 2024-01-15

date: 2024-01-15 (same)

Permalink

permalink: /about/

Use file path or redirect_from for old URLs

Categories

categories: [blog, tech]

Use tags or directory-based collections

Excerpt

excerpt: "…​"

description: "…​"

LLM prompt for content migration

I am migrating content files from Jekyll to Roq.

Convert the YAML front matter in these files to Roq format:
- Keep `layout`, `title`, `date`, and `author` fields unchanged
- Rename `excerpt` to `description`
- Convert `permalink: /path/` to `redirect_from: [/path/]` (Roq uses the
  file path as the URL by default; redirect_from handles old URLs)
- Remove `categories` (use directory structure or `tags` instead)
- Keep the body content unchanged (Markdown and AsciiDoc work as-is)

Here are my content files:
[paste or attach files]
For bulk content migration, write a script (or ask an LLM to generate one) that processes all files in a directory rather than converting files one at a time.

Moving content directories

When moving content from Jekyll directories to Roq’s content/ directory, use git mv instead of cp to preserve file history:

git mv _posts content/posts
If the target directory already exists, git mv moves the source directory into it (for example, content/posts/_posts/). Remove the target first if it was created during earlier testing.

Migrate static assets

JavaScript

Copy JavaScript files to the public/ directory:

mkdir -p public/js
cp path/to/your/javascript/*.js public/js/

Add <script> tags to your root layout, or place JS/CSS sources in web/ to use the built-in Web Bundler (see Styles and Javascript).

SCSS/CSS

Jekyll processes Sass files automatically and supports Jekyll-specific features in SCSS entry points. With Roq, the quarkus-web-bundler extension (a transitive dependency of quarkus-roq) handles CSS/SCSS bundling.

Watch for these Jekyll-specific patterns in your SCSS entry point:

  • Jekyll front matter (a pair of --- lines at the top of .scss files) — remove it

  • Liquid variables (for example, $baseurl: "{{ site.baseurl }}") — replace with hardcoded values

  • @import paths may need updating for the new directory structure

Place SCSS files in the web/ directory. The import hierarchy (@import partials referencing other partials) must be ported as a complete tree — do not split individual files.

Images and other static files

Copy images and other static files (fonts, favicons, etc.) to the public/ directory. Jekyll typically serves these from assets/ or directory-relative paths. In Roq, files in public/ are served at the site root.

cp -r assets/images public/images

Update any hardcoded image paths in templates and content to match the new location.

Data files

Set quarkus.roq.data.dir=_data in application.properties to reuse Jekyll’s data directory in place. Each YAML or JSON file in _data/ automatically registers as a named CDI bean accessible in Qute templates. For example, _data/books.yaml becomes accessible as {=cdi:books}, and nested fields via dot notation such as {=cdi:books.items}.

The file format (YAML/JSON) stays the same. Only the access syntax in templates changes: replace Jekyll’s {{ site.data.books.items }} with Roq’s {=cdi:books.items}.

Convert site configuration

Site configuration does not map cleanly between generators.

Key configuration mappings

Jekyll (_config.yml) Roq (application.properties)

title: My Site

Set in a data file or template partial

url / baseurl

site.url=https://mysite.example.com

permalink: /:categories/:title/

Roq uses file-path-based URLs by default

plugins: [jekyll-sitemap]

roq add plugin:sitemap

collections:

site.collections.<name>.layout=<layout>

exclude: [vendor, node_modules]

site.ignored-files=vendor/**,node_modules/**

LLM prompt for configuration

I am migrating from Jekyll to Roq. Convert my Jekyll _config.yml to Roq
application.properties format.

Key mappings:
- url / baseurl → site.url in application.properties
- permalink → Roq uses file-path-based URLs by default
- plugins → Roq uses plugins (install with `roq add plugin:<name>`)
- collections → site.collections.<name>.layout in application.properties
- exclude → site.ignored-files

Important Roq behaviors:
- Defining ANY custom collection replaces ALL defaults (including posts)
  so always explicitly define site.collections.posts if you have blog posts
- Set quarkus.roq.data.dir=_data to reuse Jekyll's data directory
- Set quarkus.qute.alt-expr-syntax=true (uses {=expr} for output, plain {..} is not parsed)

Here is my Jekyll _config.yml:
[paste or attach _config.yml]

Scale to all content (Phase B gate)

After the foundation works with a single page, move all remaining content directories using git mv (as described in Convert content files) and measure performance:

roq start -Djvm.args="-Xmx4g"

Check:

  • Build time: under 5 minutes is good, under 10 is acceptable, over 10 minutes is a signal to investigate

  • Memory: should stay under 4 GB for most sites

  • Spot-check 5-10 pages across different content types: verify includes, code samples, images, and cross-references

If the build is too slow, try reducing the content to a subset during development (move extra files to a temporary directory outside content/) and build the full site only for production validation.

Handle redirects

Jekyll sites often have permalink-based URLs that differ from Roq’s file-path-based URLs. Use the quarkus-roq-plugin-aliases extension to set up redirects from old URLs to new ones.

The plugin recognizes three equivalent front matter keys: redirect_from, redirect-from, and aliases.

---
title: My Page
redirect_from:
  - /old/permalink/path/
  - /another/old/path/
---

For bulk redirect migration, write a script that reads your Jekyll _config.yml permalink patterns and generates redirect_from front matter for each content file.

Tips

General

Commit after each step

Frequent commits let you roll back if a conversion introduces issues.

Test in dev mode after each change

Run roq start after each conversion. Qute provides clear error messages with line numbers.

When using an LLM

Ask for explanations

When the LLM converts a template, ask it to explain each change. This helps you learn Qute syntax and catch incorrect conversions.

Provide error messages

Paste the full error message from roq start back to the LLM. Qute error messages are specific enough for the LLM to diagnose.

Batch similar files

Group similar templates or content files and convert them together. The LLM produces more consistent output when it can see patterns across files.

Start new conversations for each phase

After 2-3 steps of implementation, build output and file contents fill the LLM context window. A fresh conversation with "Continue from Phase B" works better than pushing through a long session.

Known limitations

  • Liquid filters: Jekyll’s Liquid filters (for example, | date: "%B %d, %Y") do not have direct Qute equivalents. Qute uses method calls instead (for example, {=post.date.format('MMMM dd, yyyy')}). The LLM usually handles common filters, but verify date formats and edge cases.

  • Jekyll plugins: Jekyll plugins cannot be reused in Roq, but Roq has its own plugin ecosystem covering common needs (sitemap, tagging, aliases, RSS, etc.). Check the Plugins & Themes directory and install with roq add plugin:<name>.

  • SCSS processing: Jekyll processes Sass files automatically and supports Jekyll-specific front matter and Liquid variables in SCSS. With Roq, the web-bundler handles SCSS, but you must remove Jekyll-specific syntax from entry points and update import paths. The LLM can help with the syntax changes but not with debugging build tool differences.

  • Data file access: Jekyll’s _data/ files are accessed as {{ site.data.filename.key }} in Liquid. In Roq, data files register as CDI beans and are accessed as {=cdi:filename.key} in Qute templates. The file format (YAML/JSON) stays the same.

  • Build error strictness: Jekyll silently ignores missing includes. Roq and AsciidoctorJ may fail the entire build on a single broken include:: directive. Validate includes early when scaling to all content.

  • AsciidoctorJ security boundaries: AsciidoctorJ may block include:: directives that resolve to paths outside the content directory. If you see security errors, move included files inside content/ or adjust the safe mode configuration.

  • RSS feeds: Jekyll’s feed.xml uses Liquid syntax. Roq has built-in RSS support (see RSS). Create a content/rss.xml with {#include fm/rss.html} and add {#rss site /} to your layout’s <head>.

Clean up Jekyll artifacts

After migration is complete and validated, remove Jekyll-specific files:

  • _config.yml (and any variant config files)

  • _layouts/, _includes/ (top-level Jekyll directories)

  • _sass/ (if ported to web/)

  • _plugins/

  • Gemfile, Gemfile.lock, .bundle/, .ruby-version

  • Any Jekyll serve scripts

Keep files that Roq still uses:

  • _data/ (if quarkus.roq.data.dir=_data is configured)

What’s next