## Using Roq (copy this section to your project's CLAUDE.md / AGENTS.md) Roq is a static site generator built on Quarkus. It uses the Qute template engine with FrontMatter headers (YAML between `---` delimiters). ### Quick Start 1. Install via JBang: `curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --force roq@quarkiverse/quarkus-roq` 2. Create a site: `roq create my-site` (adds the default theme with example content) 3. Start dev mode: `cd my-site && roq start` (live-reload on http://localhost:8080, most changes are picked up automatically, use `-p 9090` for a custom port, press `s` to force a soft restart if needed) 4. Build static site: `roq generate` (output in `target/roq/`), preview with `roq serve` 5. Add a plugin or theme: `roq add plugin:tagging`, `roq add theme:default` 6. Update to latest versions: `roq update` For a minimal HTML structure without the default theme: `roq create my-site -x theme:base`. Use `--no-code` to skip example content, `--gradle` for Gradle. Available commands: `roq create`, `roq start`, `roq generate`, `roq serve`, `roq add`, `roq update`, `roq blog`. There is NO `roq dev` command, use `roq start` for dev mode. ### AI Agent Integration (Quarkus Agent MCP) AI coding agents (Claude Code, VS Code Copilot, Cursor, etc.) can manage Roq dev servers using [Quarkus Agent MCP](https://github.com/quarkusio/quarkus-agent-mcp). It handles lifecycle (start, stop, soft restart via `s`), log capture, and proxies to Dev MCP tools in the running app. **Install via JBang:** ``` jbang app install --fresh --force quarkus-agent-mcp@quarkusio ``` **Configure in Claude Code:** ``` claude mcp add quarkus-agent -- jbang quarkus-agent-mcp@quarkusio ``` **Configure in VS Code / Cursor (`.vscode/mcp.json`):** ```json { "servers": { "quarkus-agent": { "type": "stdio", "command": "jbang", "args": ["quarkus-agent-mcp@quarkusio"] } } } ``` Once configured, the agent uses `quarkus_start` to launch the Roq dev server, `quarkus_restart` for soft restart, `quarkus_logs` to read output, and `quarkus_searchTools` / `quarkus_callTool` to interact with the running app. ### Directory Structure ``` my-site/ ├── content/ # Pages, collections (posts/, etc.), index.html (required) ├── templates/ │ ├── layouts/ # Page layouts (page.html, post.html) │ └── partials/ # Reusable template fragments ├── data/ # Structured data files (YAML/JSON), accessible via {=cdi:filename.property} ├── public/ # Static assets served as-is (images, PDFs) ├── web/ # JS/CSS sources (bundled by Quarkus Web Bundler) └── config/ └── application.properties # Site config with site.* prefix ``` ## Installing Roq Skill Files for AI Coding Assistants Roq extensions ship detailed reference docs as skill files inside their deployment JARs at `META-INF/quarkus-skill.md`. Follow these steps to install them in your project. ### 1. Find your Roq extensions and version ``` mvn dependency:list -DincludeGroupIds=io.quarkiverse.roq -DoutputAbsoluteArtifactFilename=true ``` ### 2. Extract the relevant skills Extract from the matching JAR in your local Maven repository: ``` unzip -p ~/.m2/repository/io/quarkiverse/roq/ARTIFACT_ID/VERSION/ARTIFACT_ID-VERSION.jar META-INF/quarkus-skill.md > .claude/skills/SHORT_NAME.md ``` Install to the appropriate skills directory (`.claude/skills/` for Claude Code), or append to your CLAUDE.md/AGENTS.md. Install a skill for each `*-deployment` JAR listed in the dependency output. ### 3. Keep skills in sync After running `roq update`, re-extract the skill files to match the new version. Compare the Roq version in your dependencies with the version of the installed skill to detect staleness. **Migrating to Roq:** - [Migrating to Roq](https://iamroq.dev/docs/migrating/): phased workflow, syntax mappings, configuration reference, and LLM prompts for migrating a Jekyll/Hugo site to Roq **Skill files (latest version for reference):** - [quarkus-roq-frontmatter-deployment](https://raw.githubusercontent.com/quarkiverse/quarkus-roq/main/roq-frontmatter/deployment/src/main/resources/META-INF/quarkus-skill.md): base reference (FrontMatter pages, layouts, collections, pagination, template variables, Qute syntax, built-in tags, data files, template extensions, configuration, common pitfalls) - [quarkus-roq-deployment](https://raw.githubusercontent.com/quarkiverse/quarkus-roq/main/roq/deployment/src/main/resources/META-INF/quarkus-skill.md): full Roq extras (directory structure, themes, RSS, LLMs.txt, static generation, testing, CLI commands) - [quarkus-roq-data-deployment](https://raw.githubusercontent.com/quarkiverse/quarkus-roq/main/roq-data/deployment/src/main/resources/META-INF/quarkus-skill.md): data file mapping with @DataMapping and CDI --- The following is the content of this blog site: # Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. > An Open Source static site generator (SSG) that makes it fun and easy to build websites and blogs. It's built with Java and Quarkus under the hood. ## Marketplace ### [Tailwind CSS](/web/tailwind-css/) Add Tailwind CSS support to your Roq project. Write utility-first CSS classes directly in your templates, with automatic purging of unused styles for optimized production builds. Configuration Tailwind configuration is automatic (detecting site content and templates) via the Quarkus Web Bundler. Getting started After installing, create a CSS file that imports Tailwind: @import "tailwindcss"; @plugin "@tailwindcss/typography"; The @tailwindcss/typography plugin provides the prose class for beautifully styled content rendering (used by Roq for markdown and AsciiDoc output). Then use Tailwind classes in your templates: <div class="flex items-center gap-4 p-6 bg-white dark:bg-gray-800 rounded-lg shadow"> <h2 class="text-xl font-bold text-gray-900 dark:text-white">Hello Tailwind</h2> </div> ### [Svelte](/web/svelte/) Add Svelte component support to your Roq project. Build interactive UI components with Svelte's reactive framework and embed them in your static pages. Getting started Create .svelte files in your web/ directory: <!-- web/components/Counter.svelte --> <script> let count = 0; </script> <button on:click={() => count++}> Clicks: {count} </button> Then mount the component in your templates using Web Bundler's script injection. ### [Sass](/web/sass/) Add Sass/SCSS support to your Roq project. Use variables, nesting, mixins, and all Sass features to write maintainable stylesheets. Sass is the default Web Bundler preprocessor. It is included automatically when using the Web Bundler without Tailwind. Getting started Create .scss files in your web/ directory: // web/style.scss $primary: #3b82f6; $radius: 0.5rem; .card { border-radius: $radius; background: white; &-title { color: $primary; font-weight: 600; } } You can then reference the bundled stylesheet (i.e /static/bundle/app-xxxxx.css), via the bundle user tag (already included in base and other themes): <script type="module" src="/static/bundle/app-UNTE27YL.js"></script> <link rel="stylesheet" href="/static/bundle/app-4KONIKMC.css" /> See the related documentation. ### [mvnpm](/web/mvnpm/) Add mvnpm support to your Roq project. Import npm packages directly through Maven coordinates, with no Node.js or npm installation required. mvnpm bridges Maven and npm, converting npm packages into Maven artifacts that the Web Bundler can resolve and bundle. Getting started Add npm packages as Maven dependencies in your pom.xml: <dependency> <groupId>org.mvnpm</groupId> <artifactId>htmx.org</artifactId> <version>2.0.4</version> <scope>provided</scope> </dependency> Then import them in your JavaScript: import 'htmx.org'; Browse available packages at mvnpm.org. ### [TOC](/plugin/toc/) Generate a table of contents from the headings of your Markdown and AsciiDoc pages. The plugin reads the rendered HTML of each page, collects the headings (h1 to h6) that carry an id, nests them by level, and exposes the result to your Qute templates as structured data or as a ready-made <nav> block. Everything is rendered on the server, so search engines and AI crawlers see the outline without running JavaScript. All output is HTML-escaped. The plugin is not part of the quarkus-roq aggregate extension, so add it explicitly. Installation roq add plugin:toc Or add the Maven dependency: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-toc</artifactId> <version>${quarkus.roq.version}</version> </dependency> Render the default markup Add {page.tocHtml} to a layout: <!-- templates/layouts/post.html --> --- layout: default --- {page.tocHtml} {#insert /} This produces a <nav class="roq-toc" aria-label="Table of contents"> element with nested <ul> lists and anchor links. Each <li> carries a data-level attribute with the 0-indexed depth (h1 is 0, h2 is 1, and so on), the same convention as the default theme's toc.js: <nav class="roq-toc" aria-label="Table of contents"> <ul> <li data-level="1"><a href="#introduction">Introduction</a></li> <li data-level="1"><a href="#getting-started">Getting Started</a> <ul> <li data-level="2"><a href="#prerequisites">Prerequisites</a></li> <li data-level="2"><a href="#installation">Installation</a></li> </ul> </li> </ul> </nav> Style it through the .roq-toc class, or target a depth with [data-level="N"]. Render custom markup For full control over the markup, iterate over {page.toc}. Bind it once with {#let}, because every evaluation parses the page again: {#let toc=page.toc} {#if toc.size > 0} <aside class="my-toc"> <h2>On this page</h2> <ul> {#for entry in toc} <li> <a href="#{entry.id}">{entry.title}</a> {#if entry.children.size > 0} <ul> {#for child in entry.children} <li><a href="#{child.id}">{child.title}</a></li> {/for} </ul> {/if} </li> {/for} </ul> </aside> {/if} {/let} This example renders two heading levels. Nest more {#for} loops for deeper levels, or use {page.tocHtml}, which renders the whole hierarchy. Put the expressions in a layout. They also work inside the page content itself, but then the plugin renders that content one more time to find the headings. Configuration Tune the TOC per page with these front matter keys. They are the keys the default theme's client-side TOC reads, so both implementations share one configuration. Key Default Description content-toc true Set to false to suppress the TOC on a page. Applies to both {page.toc} and {page.tocHtml}. content-toc-levels 6, or toclevels + 1 on AsciiDoc pages Maximum heading level to include (1 to 6). For example, 3 keeps h1, h2, and h3, matching the default theme's JavaScript TOC. content-toc-title Table of contents The aria-label of the rendered <nav> element. When content-toc-levels is absent on an AsciiDoc page, the plugin uses the document's toclevels attribute plus one, because toclevels counts section depth while sect1 renders as h2. It looks in the asciidoc-attributes front matter map, then in the attributes Roq parsed from the document header, then at a :toclevels: entry in the header itself. With neither set, AsciiDoc pages show two section levels, which is the Asciidoctor default; other pages include all six levels. --- title: My Page content-toc: false --- Combine with the default theme's JavaScript TOC Because {page.tocHtml} emits the same data-level convention as the default theme's toc.js, you can place the server-rendered TOC inside the theme's <aside class="content-toc"> wrapper. Crawlers get the full outline; visitors with JavaScript get scroll tracking and active-section highlighting on top: {#if page.data.content-toc??} <aside class="content-toc" data-title="Contents" data-levels="2"> {page.tocHtml} </aside> {/if} Template API Expression Returns Description {page.toc} List<TocEntry> The nested TOC entries of the page. Empty when the page has no headings with an id, or when content-toc is false. {page.tocHtml} RawString The <nav class="roq-toc"> block described above. Empty under the same conditions. Each TocEntry has these properties: Property Type Description id String The heading's id, used as the anchor fragment. title String The heading text. level int The heading level, 1 to 6. children List<TocEntry> The nested entries. ### [Tagging](/plugin/tagging/) Generate a dynamic (derived) collection based on a given collection's tags. For example, if multiple posts have tags: guide, a /posts/tag/guide page is generated listing all matching posts. This works for any collection. If you are using a theme that supports it (includes a tagging layout), you should now have tags pages available for all the tags in your posts! You can use theme override to customize the theme tagging layout. To enable tagging without a theme, create a layout template and add tagging: [collection id] in FM. As a result you will have access to a new derived collection named tagCollection: templates/layouts/tag.html: --- layout: main tagging: posts --- {#for post in site.collections.get(page.data.tagCollection)} <div>{=post.title}</div> {/for} This also supports pagination. Since tagging already specifies the target collection, pagination can be enabled with paginate: true in FM: templates/layouts/tag.html: --- layout: main tagging: posts paginate: true --- {#for post in site.collections.get(page.data.tagCollection).paginated(page.paginator)} <div>{=post.title}</div> {/for} Accessing tags There is also a site.tags property, which enables this syntax in templates: {#for entry in site.tags} {#for entry in site.tags} Tag: {=entry.key} has {=entry.value.size} pages {/for} {/for} Or with sorting: {#let tag_words=site.tags.entrySet.sort('key')} {#for entry in tag_words} <a href="/blog/tag/{=entry.key}">{=entry.key}</a> ({=entry.value.size}) {/for} {/let} Template Extensions Usage Description collection.allTags Returns a list of all tags from the collection, each tag slugified collection.tagsCount Returns a list of all tags slugified (name) with their count (count) in the collection site.tags Returns a map of all tags in the site and pages with that tag, each tag slugified ### [Sitemap](/plugin/sitemap/) Easily create a sitemap.xml for your site. Create a new sitemap file: {#include fm/sitemap.xml /} To remove pages from the sitemap, use sitemap: false in the FM data. Browse http://localhost:8080/sitemap.xml to verify. ### [Series](/plugin/series/) Join multiple posts into a series with automatic series headers. Edit the layout for your posts, for example when using the roq-default theme: <!-- templates/layouts/post-series.html --> --- theme-layout: post --- {#include partials/roq-series /} {#insert /} Then use this layout and add the series attribute in the Front Matter of the posts you want to join: --- layout: post-series title: Assemble your blog post in a series description: Automatically series header for your posts tags: plugin, frontmatter, guide, series author: John Doe series: My series Title --- Use the exact same series title for all documents in the series. ### [QR Code](/plugin/qr-code/) Add QR codes to your website. Create a template and add the #qrcode tag to it, then style and size it as you want. By default, the plugin produces HTML output compatible with both HTML and Markdown templates. To use the plugin with AsciiDoc, set the asciidoc attribute to true. {#qrcode value="https://luigis.com/menu/" alt="Luigi's Menu" foreground="#000066" background="#FFFFFF" width=300 height=300 /} {#qrcode value="https://luigis.com/menu/" alt="Luigi's Menu" foreground="#000066" background="#FFFFFF" width=300 height=300 asciidoc=true /} ### [OG Card](/plugin/og-card/) Generate 1200×630 Open Graph preview cards from Qute SVG templates at build time. Injects og-image frontmatter so {#seo /} emits og:image and twitter:card=summary_large_image. Installation roq add plugin:og-card Or add the Maven dependency: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-og-card</artifactId> <version>${quarkus.roq.version}</version> </dependency> Configuration At least one of collections or include-paths is required: quarkus.roq.plugin.og-card.collections=posts quarkus.roq.plugin.og-card.exclude-paths=/posts/tag/ quarkus.roq.plugin.og-card.include-paths=/about/ quarkus.roq.plugin.og-card.template=og-card/default-card.svg quarkus.roq.plugin.og-card.site-name=My Site quarkus.roq.plugin.og-card.output-prefix=/og quarkus.roq.plugin.og-card.max-text-width=-1 collections — generate cards for all documents in named collections (e.g. posts → /og/posts/{slug}.png) include-paths — explicit normal pages (e.g. /about/ → /og/about.png) exclude-paths — when using collections, skip path prefixes such as /posts/tag/ (not applied to include-paths) max-text-width — horizontal pixels for text wrapping (-1 auto-computes from card width); reduce when graphics occupy part of the card Pages with image, img, or picture frontmatter are skipped by default (skip-if-image-set=true). Map title, description, and optional kicker / eyebrow from page frontmatter into the card. Card template Place custom templates under templates/og-card/. The plugin passes a card object with pre-wrapped titleLines and descriptionLines for multi-line SVG text: <text x="72" y="170" font-size="52"> {#for line in card.titleLines} <tspan x="72" dy="{line_isFirst ? '0' : '62'}">{line}</tspan> {/for} </text> Bundled default: og-card/default-card.svg. Viewing your cards In dev mode, open http://localhost:8080/og/about.png. With generator batch enabled, PNGs are written to target/roq/og/: QUARKUS_ROQ_GENERATOR_BATCH=true mvn package quarkus:run For CI and containers, set JAVA_TOOL_OPTIONS=-Djava.awt.headless=true before the build (Batik uses Java2D/AWT). ### [Markdown](/plugin/markdown/) Process .md and .markdown files using CommonMark Java. Markdown plugin is already included in Quarkus Roq extension. No separate installation needed unless you removed it. Every file with .md or .markdown extension will be processed. Collapsible sections Use standard HTML <details> and <summary> tags in your Markdown files for collapsible content. The default theme styles them automatically. <details> <summary>Click to reveal</summary> Hidden content with **Markdown formatting**. </details> See the Markdown markup test for a live example. ### [Lunr Search](/plugin/lunr-search/) Enable search for your site without the need for external, server-side, search services. Setup Add the search index JSON: <!-- content/search-index.json --> {#include fm/search-index.json} Inject the search script in the <head> of your layout. For example with the default theme: <!-- templates/layouts/default.html --> --- theme-layout: default --- {#insert /} {#head} {#search-script /} {/} Inject the search overlay in the <body> and search button in the navigation: <!-- templates/layouts/main.html --> --- theme-layout: main --- {#search-overlay /} {#insert /} {#menu} {#search-button /} {#include partials/roq-default/sidebar-menu menu=cdi:menu.items /} {/} Custom search trigger The <button type="button" id="search-button" class="search-button" aria-label="Search"></button> component renders a plain <div id="search-button" class="search-button">. You can replace it with your own HTML element as long as it has id="search-button" — the click handler binds to that ID. <button id="search-button" class="my-search-btn" aria-label="Search">⌘K</button> The search overlay also responds to the Cmd+K (macOS) / Ctrl+K (Windows/Linux) keyboard shortcut out of the box. Controlling indexing You can prevent content from being indexed: --- title: I don't want to be indexed search: false --- You can also boost specific pages or layouts in the results using search-boost: --- title: Important Page search-boost: 1.2 --- How boost works Search relevance is calculated using the BM25 algorithm. The search-boost value is a multiplier on the BM25 score. The default is 1. Use values between 0 and 2: Value Effect 0.5 Demote a page in results 1 Default (no boost) 1.2 Gentle boost (recommended for reference pages) 1.5 Moderate boost 2 Maximum recommended boost BM25 term frequency saturates quickly (controlled by k1=1.2). This means the relevance advantage from having more keyword matches is bounded: Matches BM25 score Ratio vs 1 match 1 1.00 1.00 2 1.38 1.38 3 1.57 1.57 5 1.77 1.77 9 1.94 1.94 A page with 9 matches scores ~1.94x higher than one with 1 match. If boost exceeds this ratio, it overrides keyword relevance. That is why values above 2 are not recommended: they would make boost more important than actual keyword matches. With a boost of 1.2, a boosted page only outranks a non-boosted page when their keyword relevance is within 20% of each other. Stronger keyword matches always win. Full pages receive a 10% boost (×1.1) so they always rank above their own sections. Sections are slightly demoted (h2: ×0.96, h3: ×0.95, down to h6: ×0.92), keeping them ordered by heading level within the same page. ### [Hybrid](/plugin/hybrid/) Build Quarkus applications with Roq static content. Pages are rendered once and cached (in memory or on disk), making subsequent requests as fast as serving static files while still supporting dynamic CDI content. Cache Modes Set globally or per-page via frontmatter: # application.properties site.hybrid.cache-mode=lazy # Per-page override in frontmatter --- title: My Dynamic Page cache: false --- lazy (default): rendered on first request, cached for subsequent requests. Supports cache-ttl. startup: pre-rendered at application startup, cached until restart. TTL is ignored. false: never cached, rendered on every request Cache Stores Memory (default) Caffeine-backed in-memory cache with LRU eviction. site.hybrid.cache-store=memory site.hybrid.cache-max-size=1000 site.hybrid.cache-ttl=5m Filesystem Rendered HTML is written to disk. Survives restarts, low memory usage. site.hybrid.cache-store=filesystem site.hybrid.cache-dir=/path/to/cache Future Pages Future-dated pages are automatically included and date-checked at runtime. A page with a future date returns 404 until its scheduled date, then becomes available without a rebuild. Cache Management Service Inject RoqCacheManager to manage the cache from your application code: @Inject RoqCacheManager cacheManager; // Clear the entire cache cacheManager.invalidateAll(); // Invalidate a specific page by its cache key (output path) cacheManager.invalidate("posts/my-post.html"); // Check cache stats long size = cacheManager.size(); Caching is automatically disabled in dev mode so that template changes are always reflected immediately. Set site.hybrid.cache-in-dev-mode=true to test caching during development. ### [Faker](/plugin/faker/) Populate your site with realistic fake content during development. Generates posts with random titles, descriptions, authors, dates, tags, and images so you can test layouts, pagination, and styling without writing real content. Configuration Add the number of fake documents to generate per collection in application.properties: quarkus.roq.faker.count.posts=20 This generates 20 fake posts in the posts collection. You can target any collection: quarkus.roq.faker.count.posts=20 quarkus.roq.faker.count.docs=5 What's Generated Each fake document includes: Field Content title Random book title with genre and author description Random sentence author Random author name date Random date within the last 2 years tags 1 to 4 random tags from a curated list image Random image from a set of 10 bundled photos content 2 to 5 paragraphs of lorem ipsum The generated posts use the collection's configured layout (e.g., post for the posts collection) and respect all theme styling. Dev Mode Only Faker is designed for development. The generated content is not persisted to disk. It exists only in the dev server's memory and is regenerated on each restart. ### [Diagram](/plugin/diagram/) Diagram-as-code support by leveraging Kroki.io. It delegates image rendering to Kroki either by using a provided server or by popping a dev service. Please take a look at the full Kroki reference documentation. Use it in your content: {#diagram asciidoc=true language="pikchr" alt="Impossible trident" width=500 height=500 diagramOutputFormat="svg"} scale = 1.0 eh = 0.5cm ew = 0.2cm ... {/} You can either use a deployed server or let the dev services provide one for you, but in this case you won't have all languages available. ### [AsciiDoc](/plugin/asciidoc/) Fast Java-based AsciiDoc processor (based on Yupiik asciidoc-java). Provides fast startup but does not support all AsciiDoc options yet. For the full feature set, see AsciiDoc JRuby. Add the .adoc or .asciidoc file extension to pages and they will be processed. Use Qute in AsciiDoc files Qute parsing is disabled by default on AsciiDoc files, to enable it: quarkus.asciidoc.qute=true You can also use the :qute: AsciiDoc header attribute to enable Qute parsing (or not :qute: false) per page. AsciiDoc includes You may use includes from anywhere in the site directory. Make sure the included file is ignored by Roq by prefixing the file or directory with _. include::_includes/attributes.adoc[] Headers AsciiDoc headers are parsed by Roq and used as page data: = Title is used as page title author is available through page.data.author and page.data.author-email revision is available through page.data.revision.number, page.data.revision.date and page.data.revision.remark attribute :description: is used as page description attributes starting with page- will be used as page data (:page-image: becomes image in the data) all other header attributes are also available in page.data.attributes You can also use FrontMatter headers to set the page data like any other page. Roq attributes Name Description {site-url} The full site url (e.g. https://my-site.com/blog/) {site-path} The site path (e.g. /blog/) {page-url} The full page url (e.g. https://my-site.com/blog/about/) {page-path} The page path (e.g. /blog/about) Collapsible sections Use the %collapsible option on an example block to create collapsible content. The default theme styles them automatically. .Click to expand [%collapsible] ==== Hidden content here. ==== Add the .result role for a distinct output/result look: .Show result [%collapsible.result] ==== Result content with a background. ==== See the AsciiDoc markup test for a live example. AsciiDoc attributes configuration Attributes can be configured globally: quarkus.asciidoc.attributes.source-highlighter=highlight.js quarkus.asciidoc.attributes.icons=font Or as an include file in the AsciiDoc headers, or as part of the Frontmatter data asciidoc-attributes in a page or layout: --- asciidoc-attributes: notitle: true --- Table of Contents (TOC) To add a Table of Contents, use the page-content-toc attribute in your AsciiDoc header: :page-content-toc: true :page-content-toc-title: Contents :page-content-toc-levels: 2 This works with the default Roq theme and creates a dynamic sidebar TOC that highlights the current section as you scroll. AsciiDoc Data Conversion Convert data containing AsciiDoc into HTML using the asciidocToHtml template extension: --- bar: | == Hello * that's nice * I can use asciidoc in the data --- {=page.data.bar.asciidocToHtml} ### [AsciiDoc JRuby](/plugin/asciidoc-jruby/) Full-featured Asciidoctor implementation based on AsciidoctorJ. Offers the complete AsciiDoc feature set including all extensions. Slower startup than the Java variant but covers all advanced AsciiDoc options. Add the .adoc or .asciidoc file extension to pages and they will be processed using Asciidoctor. Use Qute in AsciiDoc files Qute parsing is disabled by default on AsciiDoc files, to enable it: quarkus.asciidoc.qute=true You can also use the :qute: AsciiDoc header attribute to enable Qute parsing (or not :qute: false) per page. AsciiDoc includes You may use includes from anywhere in the site directory. Make sure the included file is ignored by Roq by prefixing the file or directory with _. include::_includes/attributes.adoc[] Headers AsciiDoc headers are parsed by Roq and used as page data: = Title is used as page title author is available through page.data.author and page.data.author-email revision is available through page.data.revision.number, page.data.revision.date and page.data.revision.remark attribute :description: is used as page description attributes starting with page- will be used as page data (:page-image: becomes image in the data) all other header attributes are also available in page.data.attributes You can also use FrontMatter headers to set the page data like any other page. Roq attributes Name Description {site-url} The full site url (e.g. https://my-site.com/blog/) {site-path} The site path (e.g. /blog/) {page-url} The full page url (e.g. https://my-site.com/blog/about/) {page-path} The page path (e.g. /blog/about) AsciiDoc attributes configuration Attributes can be configured globally: quarkus.asciidoc.attributes.source-highlighter=highlight.js quarkus.asciidoc.attributes.icons=font Or as an include file in the AsciiDoc headers, or as part of the Frontmatter data asciidoc-attributes in a page or layout: --- asciidoc-attributes: notitle: true --- Table of Contents (TOC) To add a Table of Contents, use the page-content-toc attribute in your AsciiDoc header: :page-content-toc: true :page-content-toc-title: Contents :page-content-toc-levels: 2 This works with the default Roq theme and creates a dynamic sidebar TOC that highlights the current section as you scroll. AsciiDoc Data Conversion Convert data containing AsciiDoc into HTML using the asciidocToHtml template extension: --- bar: | == Hello * that's nice * I can use asciidoc in the data --- {=page.data.foo.asciidocToHtml} ### [Aliases](/plugin/aliases/) Create one or many aliases (redirections) for a page. Add aliases: [your-alias-here, another-alias-here] in the Front Matter to access the page using a customized URL. # content/posts/2024-08-29-welcome-to-roq.md --- layout: post title: "Welcome to Roq!" date: 2024-08-29 13:32:20 +0200 description: This is the first article ever made with Quarkus Roq tags: blogging aliases: [first-roq-article-ever] --- Now, when you access http://localhost:8080/first-roq-article-ever, you will be redirected to the 2024-08-29-welcome-to-roq blog post. You can use link templating in aliases. ### [Resume Theme](/theme/resume-theme/) Build a personal resume or CV with a data-driven YAML configuration. Data Files Add your resume info in the data/ directory: profile.yml firstName: Ada lastName: Lovelace jobTitle: Computational Pioneer city: London country: United Kingdom bio: | Ada Lovelace was a 19th-century mathematician known for her visionary work on Charles Babbage's Analytical Engine. bio.yml - title: Experience items: - header: "1842 - 1843" title: "Mathematician · Self-initiated · London" content: | Translated and annotated Luigi Menabrea's paper on Charles Babbage's Analytical Engine. Added extensive original notes, including the first published algorithm designed for a machine. - title: Education items: - header: "1830 - 1835" title: "Private Tutoring" content: | Studied mathematics and science under Augustus De Morgan and Mary Somerville. The bio data supports hierarchical items with subItems, collapsible/collapsed flags, ruler separators, and logo objects with label, imageUrl, and link. social.yml - name: LinkedIn url: https://www.linkedin.com/in/ada-lovelace/ - name: X url: https://x.com/ada-lovelace Color Themes The theme comes with 6 pre-configured color schemes (Purple, Blue, Emerald, Amber, Rose, Cyan). To use an alternate theme, import it in your web/style.css: /* Available: _theme-blue.css, _theme-emerald.css, _theme-amber.css, _theme-rose.css, _theme-cyan.css */ @import "./_theme-blue.css"; You can also create a custom color scheme by overriding the theme variables. The theme uses Tailwind CSS v4 color palettes. ### [Linktree Theme](/theme/linktree-theme/) Build a personal link-tree site with data-driven YAML configuration, social icons, and auto-generated QR codes. Layouts The theme provides three layouts: Layout Purpose linktree-home Home page with profile, social icons, and your main links from data/profile.yml linktree Auto-generated page for each tree in data/trees/, with profile and profile links at the bottom linktrees Gallery listing all trees with QR codes and download buttons Data Files data/profile.yml contains your identity and main links: name: Ada Lovelace handle: "@adalovelace" title: Computational Pioneer image: ada.png social: - name: GitHub url: https://github.com/adalovelace icon: github-logo - name: LinkedIn url: https://www.linkedin.com/in/adalovelace icon: linkedin-logo links: - name: The Analytical Engine url: https://en.wikipedia.org/wiki/Analytical_engine description: The machine that started it all icon: gear - name: Quarkus url: https://quarkus.io description: Supersonic Subatomic Java framework icon: lightning data/trees/*.yml are additional link pages (one file per tree): # data/trees/research.yml title: Research description: Ada Lovelace's writings and legacy links: - name: Notes on the Analytical Engine url: https://en.wikipedia.org/wiki/Ada_Lovelace description: The first published algorithm icon: note-pencil Features Data-driven: profile and links defined in YAML, mapped to typed Java records via @DataMapping Multiple trees: drop a new YAML file in data/trees/ and a page is auto-generated with its own QR code Profile links on tree pages: tree pages show the profile and main links at the bottom (configurable) Social icons: Phosphor Icons for social media links (GitHub, LinkedIn, Bluesky, and more) QR codes: built-in QR code generation with SVG download Tailwind CSS: all styles use @apply with lt-* class names for easy customization Page Data Frontmatter keys for tree pages (linktree layout): Key Description Default show-profile Show profile section at the bottom of tree pages true profile-links Append profile links below the profile on tree pages true Frontmatter keys for the gallery page (linktrees layout): Key Description Default qr-foreground QR code foreground color (hex) #0e4a5c qr-background QR code background color (hex) #FFFFFF Icons Icons use Phosphor Icons. Browse the catalog and use the icon name in your YAML: icon: github-logo # renders as <i class="ph ph-github-logo"> icon: lightning # renders as <i class="ph ph-lightning"> Template Components The theme provides reusable components you can override: partials/roq-linktree/profile.html: profile card with avatar, name, handle, title, and social icons tags/roq-linktree/linkCard.html: link card with icon, name, description, and arrow CSS Customization Override theme styles in web/_custom.css (included by default). All component styles use lt-* class names (e.g. lt-avatar, lt-link-card, lt-profile) defined with @apply, so you can restyle any component. You can also replace the theme CSS entirely by creating your own web/linktree.css. ### [Default Theme](/theme/default-theme/) The default Roq theme for blogs and sites (used on this site). Built with Tailwind CSS, featuring dark mode, responsive design, sidebar navigation, and social media links. The accent color palette can easily be customized with your own colors or any existing Tailwind color palette. Site Data Configure your site through the index page frontmatter (e.g. content/index.html): Key Description name Site name displayed in the sidebar simple-name Short name used in the copyright notice logo Logo image path displayed in the sidebar (falls back to image) description Site tagline shown below the logo theme-color Color used for the browser address bar on mobile (default: #263959) Analytics analytics: ga4: G-XXXXXXXXXX Social Brands Add social media links to your site through the index page frontmatter: social-github: quarkiverse social-twitter: quarkusio social-linkedin: john-doe social-mastodon: https://mastodon.social/@username Available keys: social-twitter, social-github, social-linkedin, social-linkedin-company, social-facebook, social-youtube, social-discord, social-email, social-bluesky, social-mastodon, social-slack, social-whatsapp, social-instagram, social-telegram. For Mastodon and Slack, you must provide the complete URL as these platforms don't have a standard prefix. Menu Define navigation menus in data/menu.yml. Each key becomes a menu section in the sidebar: nav: - title: "Home" path: "/" icon: "fa-solid fa-house" - title: "Blog" path: "/blog" icon: "fa-regular fa-newspaper" doc: - title: "Getting Started" path: "/docs/getting-started/" icon: "fa fa-bolt" Each item supports title, path, icon (Font Awesome class), and optionally target (e.g. _blank for external links). Authors Define authors in data/authors.yml to display author info on blog posts: ada: name: "Ada Lovelace" avatar: "https://example.com/ada.png" job: Software Pioneer profile: "https://x.com/ada" nickname: "ada" bio: "Passionate about algorithms and analytical engines." Then reference an author in a post's frontmatter with author: ada. Layouts Theme layouts are automatically available: use layout: foo and it resolves local first, then falls back to the theme. To override a theme layout, create your own layout file and use theme-layout: foo to extend from the original. default // Base HTML structure ├── main // Shared site layout (sidebar, nav, footer) │ ├── home // Home page │ ├── blog // Blog listing with pagination │ ├── page // Generic page │ ├── post // Blog post with author and tags │ └── tag // Tag archive page └── 404 // Error page Page Data Frontmatter keys available to control page behavior per layout. All layouts Key Description Default body-class Custom CSS class on the body element page-class CSS class for page-specific styling robots Value rendered as <meta name="robots"> via the built-in {#seo /} tag. Use noindex to keep drafts/internal/staging pages out of search engines. Page / Post Key Description Default show-header Show the page header true show-header-date Show the date in the header true show-header-intro Show the description in the header true post-date-style Date format style: short, medium, long, or full (uses Java's FormatStyle, locale-aware) medium content-toc Enable table of contents false content-toc-title TOC section title Contents content-toc-levels Heading levels to include in TOC 2 Post Key Description Default author Author key from data/authors.yml tags List of tags for the post fig-caption Caption for the post cover image Blog Key Description Default featured Number of featured posts Partials Override any theme partial by creating a file with the same name in templates/partials/roq-default/: partials/roq-default/ ├── 404.html ├── head.html ├── head-scripts.html ├── page-header.html ├── page-toc.html ├── pagination.html ├── sidebar-about.html ├── sidebar-contact.html ├── sidebar-copyright.html ├── sidebar-darkmode.html └── sidebar-menu.html Qute User-Tags The theme provides reusable Qute user-tags for building pages: roq/hero Hero section for the home page. {#roq/hero logo="roq-logo.svg"} {#title}My Site{/title} {#tagline}A tagline for my site{/tagline} {#subtitle}Some extra info{/subtitle} {/roq/hero} roq/featureCard Feature card, typically used on the home page. {#roq/featureCard icon="fa-solid fa-bolt" title="Fast" link="/docs/" link-text="Learn more" highlighted=true} Feature description here. {/roq/featureCard} roq/postCard Blog post preview card. Used in blog and tag layouts, can also be used in custom pages. {#roq/postCard post=myPost /} roq/authorCard Author profile card. {#roq/authorCard name="Ada Lovelace" avatar="ada.png" profile="https://example.com" nickname="ada"} Author bio here. {/roq/authorCard} roq/terminal Terminal emulator component for displaying commands. {#roq/terminal title="Getting Started"} {#commands} {#command} {#prompt}${/prompt} {#cmd}quarkus{/cmd} {#args}create app my-site -x roq{/args} {/command} {/commands} {/roq/terminal} CSS Customization Create a web/_custom.css file in your site to override theme styles. This file is processed by Tailwind, so you can use Tailwind utilities, @apply, @theme, and other Tailwind features. Other CSS files added to web/ are bundled as plain CSS without Tailwind processing. Color Palettes The theme is built on three color palettes. Override any combination to completely transform the look and feel of your site: Palette Role Default accent Structure: headings, links, page headers, sidebar slate pop Energy: buttons, hover effects, gradients, icons sky neutral Text and backgrounds: body text, cards, sidebar background gray To swap a palette, map all 11 shades (50 through 950) in a @theme block in your web/_custom.css. You can use any Tailwind color or custom hex values: @theme { /* Accent: indigo instead of slate */ --color-accent-50: var(--color-indigo-50); --color-accent-100: var(--color-indigo-100); --color-accent-200: var(--color-indigo-200); --color-accent-300: var(--color-indigo-300); --color-accent-400: var(--color-indigo-400); --color-accent-500: var(--color-indigo-500); --color-accent-600: var(--color-indigo-600); --color-accent-700: var(--color-indigo-700); --color-accent-800: var(--color-indigo-800); --color-accent-900: var(--color-indigo-900); --color-accent-950: var(--color-indigo-950); /* Pop: rose instead of sky */ --color-pop-50: var(--color-rose-50); --color-pop-100: var(--color-rose-100); --color-pop-200: var(--color-rose-200); --color-pop-300: var(--color-rose-300); --color-pop-400: var(--color-rose-400); --color-pop-500: var(--color-rose-500); --color-pop-600: var(--color-rose-600); --color-pop-700: var(--color-rose-700); --color-pop-800: var(--color-rose-800); --color-pop-900: var(--color-rose-900); --color-pop-950: var(--color-rose-950); /* Neutral: stone instead of gray */ --color-neutral-50: var(--color-stone-50); --color-neutral-100: var(--color-stone-100); --color-neutral-200: var(--color-stone-200); --color-neutral-300: var(--color-stone-300); --color-neutral-400: var(--color-stone-400); --color-neutral-500: var(--color-stone-500); --color-neutral-600: var(--color-stone-600); --color-neutral-700: var(--color-stone-700); --color-neutral-800: var(--color-stone-800); --color-neutral-900: var(--color-stone-900); --color-neutral-950: var(--color-stone-950); } Changing all three palettes gives your site a completely different identity while keeping the same layout and structure. You can also override just one or two palettes. The Roq blog itself uses a custom cyan for accent and orange for pop. Sidebar The sidebar is fully customizable through theme variables in web/_custom.css. Color variables are defined in @theme and can be used as Tailwind utilities (e.g. text-sidebar, bg-sidebar-subtle): Variable Role Default Utility --color-sidebar Main text color neutral-300 text-sidebar --color-sidebar-heading Site name, headings white text-sidebar-heading --color-sidebar-muted Secondary text, separators neutral-500 text-sidebar-muted --color-sidebar-subtle Subtle borders, hover backgrounds rgba(255,255,255,0.06) border-sidebar-subtle --color-sidebar-border Sidebar right border neutral-700 border-sidebar-border --sidebar-bg Background (supports gradients) neutral 800→900 gradient By default the sidebar is dark in both modes. To create a light sidebar in light mode with a dark sidebar in dark mode: @theme { --color-sidebar: var(--color-indigo-800); --color-sidebar-heading: var(--color-indigo-950); --color-sidebar-muted: var(--color-indigo-500); --color-sidebar-subtle: rgba(0, 0, 0, 0.08); --color-sidebar-border: var(--color-indigo-200); --sidebar-bg: linear-gradient(180deg, var(--color-indigo-50) 0%, var(--color-indigo-100) 100%); } .dark { --color-sidebar: var(--color-indigo-200); --color-sidebar-heading: white; --color-sidebar-muted: var(--color-indigo-400); --color-sidebar-subtle: rgba(255, 255, 255, 0.06); --color-sidebar-border: var(--color-indigo-800); --sidebar-bg: linear-gradient(180deg, var(--color-indigo-950) 0%, var(--color-indigo-900) 100%); } For best dark mode legibility with colored sidebars, test your chosen palette carefully. Alternatively, omit the .dark block to fall back to the default dark sidebar. Dark Mode Dark mode is built-in with automatic system preference detection and a toggle in the sidebar. No configuration needed. SEO The theme includes built-in SEO support with meta tags, Open Graph, Twitter cards, favicon auto-discovery, and RSS. For more details, see the SEO, Favicon, Analytics, and RSS documentation. ### [Base Theme](/theme/base-theme/) The base theme is a minimal starting point included with Roq. It provides the essential HTML structure with SEO, favicon, and Web Bundler support, giving you full control over your site's design. This is the ideal choice when you want to build a fully custom site from scratch. Layouts default // Base HTML structure (SEO, favicon, bundle) ├── page // Simple page with title └── post // Post with title and date The default layout provides the HTML skeleton with: {#seo /} for meta tags, Open Graph, and Twitter cards {#favicon /} for automatic favicon discovery {#bundle /} for CSS and JS bundling via Web Bundler The page and post layouts extend default with minimal markup (title, content, and date for posts). Customization Since the base theme provides only the HTML structure, you style everything through your own CSS in web/app.css. The starter CSS includes basic variables for colors, typography, and a simple card layout that you can replace entirely. See the Favicon, SEO, and Analytics documentation for configuring the built-in tags. ## Posts ### [Comparing Roq with Hugo, Jekyll, and JBake: A Feature Breakdown](/posts/comparing-roq-with-hugo-jekyll-and-jbake-a-feature-breakdown/) Here’s a feature comparison with some popular SSGs to highlight how Roq stacks up. Feature Roq Hugo Jekyll JBake Build Perf Fast Extremely fast (written in Go) Slower due to Ruby and plugins Slower, runs on Java with Freemarker/Groovy templates Dev Perf Instant hot reload with Quarkus dev-mode Fast rebuilds Slow rebuilds on large sites Manual rebuild required Templating Qute (simple & readable) Go templates (powerful but complex) Liquid (easy but limited) Freemarker, Groovy, Thymeleaf... Extensibility Rich plugin marketplace built on Quarkus extensions Batteries-included, Hugo Modules Large plugin ecosystem (mostly stale) Limited, Java-based plugins Setup Just install the CLI Single binary install Requires Ruby & Bundler Requires Java & Gradle/Maven Content Editor Built-in Notion-like editor with rich text and Markdown None (use external editors) None (use external editors) None (use external editors) Search Built-in client-side search (Lunr plugin) Requires external setup Requires plugins or external service No built-in support Dynamic Features Can integrate with Quarkus for hybrid use Mostly static, some JS workarounds Plugins enable some dynamic behavior Fully static Migration Tools Built-in Jekyll-to-Roq converter Hugo import (Jekyll only) Importers via separate gems No migration tooling CSS/Bundling Built-in Tailwind and JS bundling (no Node.js) Built-in asset pipeline, Tailwind requires npm Requires plugins No built-in support AI Support Built-in llms.txt generation and MCP server Community modules for llms.txt Community gems for llms.txt None Community Growing, part of Quarkus ecosystem Large, well-established Large, long history Niche, mostly inactive Learning Curve Beginner friendly, easier for Java developers Can be difficult due to Go templates Complex to setup and update Moderate, depends on template engine Why Roq? Roq is the only SSG where you can write a blog post in a Notion-like editor, preview it instantly with hot reload, and deploy a fully static site with built-in search, SEO, and Tailwind, all without touching Node.js. Need more? Since Roq is built on Quarkus, you can add REST endpoints, database access, or any Quarkus extension to go hybrid. The very large quarkus.io website, with its thousands of pages and multiple versioned documentation sets, has been fully migrated to Roq with success (last few tweaks in progress, online version should be updated soon). Coming from Jekyll? A built-in converter handles front matter, Liquid-to-Qute templates, and configuration mapping. Check the migration guide for details. Image processing is still in the works (Issue #42). Conclusion Jekyll is widely used but comes with the complexity of setting up Ruby environments, which often causes headaches. Performance can be an issue for large sites. JBake was the only Java-based SSG before Roq, but it has not kept up with modern alternatives. Roq has grown into a complete, modern, Java-friendly SSG that brings the ease of Jekyll, the speed of Hugo, and the flexibility of Quarkus, with unique features like its built-in editor and rich plugin ecosystem. ### [Smarter Search Ranking](/posts/smarter-search-ranking/) Searching for "qr code" on this site used to show the plugin reference page first, with the blog post buried far below. The reference page had search-boost: 20, making it score 20x higher regardless of keyword matches. Blog posts about QR codes could never compete. Why boost was too strong Lunr uses BM25 for scoring, where term frequency saturates quickly. A page with 9 keyword matches scores only ~1.94x higher than one with 1 match. Any search-boost above 2 overrides keyword relevance entirely. What changed Boost values reduced to the BM25 range. Marketplace pages now use search-boost: 1.2 instead of 20. The reference doc still ranks above unrelated pages, but a blog post with strong keyword matches now appears right after it. Section heading boost fixed. h2 sections now rank above h6 (was inverted). Sections are slightly demoted (h2: ×0.96, h6: ×0.92) while full pages get a 10% boost, ensuring pages always rank above their own sections. Field boosts rebalanced. All field boosts (title, tags, content) are now between 1 and 2, letting BM25 handle relevance naturally. Search results now show the page URL, making it easier to see where each result links before clicking. Using search-boost Keep values between 0 and 2. A boost of 1.2 gives a gentle advantage. Above 2, boost starts overriding keyword matches. See the Lunr Search plugin docs for details. ### [Add Comments to Your Blog with a Web Component (30min)](/posts/add-comments-web-component/) Your Roq blog generates static pages. Fast, simple, easy to deploy. But what about comments? You could use a third-party widget, but why not build your own? In this tutorial, you'll create a Lit web component for comments backed by a Quarkus REST API in a separate microservice. The blog pages stay static. The comments are fully dynamic, loaded and posted via JavaScript from a dedicated comments server, no page reload needed. This is the "web component" approach (as opposed to the hybrid mode approach). Same result, different architecture: here, the blog stays 100% static and the interactivity lives in a custom HTML element served by a separate Quarkus app. Note Prerequisites: A working Roq blog from the blog tutorial or the from-scratch tutorial. You should have at least one blog post. Keep the blog running on port 8080. Tip For the best development experience, install the Quarkus IDE tooling for your editor (VS Code, IntelliJ, or Eclipse). You get config autocompletion, validation, and Qute template completion. 1. Create the comments project The comments service is a separate Quarkus app that runs alongside your blog. Generate it from code.quarkus.io with the extensions: REST Jackson, Hibernate ORM Panache, JDBC H2, and Web Bundler. Extract it next to your blog directory. Then add the Lit dependency to comments/pom.xml: <!-- Lit web component library (bundled by Web Bundler) --> <dependency> <groupId>org.mvnpm</groupId> <artifactId>lit</artifactId> <version>3.2.1</version> <scope>provided</scope> </dependency> Then configure the app in src/main/resources/application.properties: quarkus.http.port=7070 quarkus.web-bundler.bundle-redirect=true quarkus.datasource.db-kind=h2 quarkus.hibernate-orm.database.generation=drop-and-create quarkus.http.cors.enabled=true quarkus.http.cors.origins=http://localhost:8080,http://localhost:7070 The comments app runs on port 7070 so it doesn't conflict with your blog. CORS is enabled so the blog can load the web component script and call the API. If your blog runs on a different port, adjust the cors.origins accordingly. If you don't have the Quarkus CLI yet, install it with JBang (same as Roq): jbang app install --fresh --force quarkus@quarkusio Start the comments app in dev mode: cd comments quarkus dev 🚀 The app should start on port 7070 with no errors. Note The Lit dependency has scope: provided because Web Bundler bundles the JavaScript at build time. The library isn't needed at runtime in the Java classpath, only during the bundling step. This is how mvnpm works: npm packages as Maven dependencies, bundled via esbuild. 2. Create the Comment entity Just like in the hybrid tutorial, we need a database model for comments. Panache makes this simple. ››› CODING TIME Create src/main/java/org/acme/Comment.java as a Panache entity with author, content, createdAt, and postSlug fields. Add a query method to find comments by post slug, and a method that persists a new comment and returns the updated list. See hint Extend PanacheEntity for a free id field and CRUD methods. Use @Entity from Jakarta Persistence. Add findByPost(String slug) using list("postSlug", Sort.by("createdAt").descending(), slug). The "persist and return list" method is useful for the API to return the updated comment list after a POST. See solution Create src/main/java/org/acme/Comment.java: package org.acme; import java.time.LocalDateTime; import java.util.List; import jakarta.persistence.Entity; import io.quarkus.hibernate.orm.panache.PanacheEntity; import io.quarkus.panache.common.Sort; @Entity public class Comment extends PanacheEntity { public String author; public String content; public LocalDateTime createdAt; public String postSlug; public static List<Comment> findByPost(String slug) { return list("postSlug", Sort.by("createdAt").descending(), slug); } } 3. Create the REST API The web component will talk to a REST API. We need two endpoints: GET to fetch comments for a post, and POST to add a new one. ››› CODING TIME Create src/main/java/org/acme/CommentResource.java with a GET endpoint at /api/comments/{postSlug} and a POST endpoint at /api/comments. The POST should return the updated list of comments so the web component can refresh immediately. See hint Use @Path("/api/comments"), @GET with @Path("{postSlug}") for fetching, and @POST with @Consumes(APPLICATION_JSON) for creating. Mark the POST method @Transactional. Set createdAt = LocalDateTime.now() before persisting. Return Comment.findByPost(comment.postSlug) after persisting so the client gets the full updated list. See solution Create src/main/java/org/acme/CommentResource.java: package org.acme; import java.time.LocalDateTime; import java.util.List; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/api/comments") @Produces(MediaType.APPLICATION_JSON) public class CommentResource { @GET @Path("{postSlug}") public List<Comment> getComments(@PathParam("postSlug") String postSlug) { return Comment.findByPost(postSlug); } @POST @Consumes(MediaType.APPLICATION_JSON) @Transactional public List<Comment> addComment(Comment comment) { comment.id = null; comment.createdAt = LocalDateTime.now(); comment.persist(); return Comment.findByPost(comment.postSlug); } } 🚀 Test the API with curl: curl -s http://localhost:7070/api/comments/hello-world | jq . You should get an empty JSON array []. The API works! 4. Build the Lit web component This is where it gets fun. We'll create a custom HTML element <comments-section> that fetches and displays comments, and includes a form to post new ones. All client-side, no page reload. ››› CODING TIME Create src/main/resources/web/comments-section.js as a Lit component. It should: Accept a post-slug attribute to know which post's comments to load Fetch comments from /api/comments/{postSlug} when connected to the DOM Render a list of existing comments (author, date, content) Show a form with name and comment fields POST to the API on submit and refresh the list See hint Import LitElement, html, and css from lit. Define a class that extends LitElement. Use static properties to declare postSlug (attribute: post-slug), serverUrl (attribute: server-url), and comments (reactive state). In connectedCallback(), call fetchComments(). Use ${this.serverUrl}/api/comments/... in fetch calls so the component works cross-origin. The render() method returns a template literal with html\...`. Use @clickfor the submit button handler. After POST, setthis.comments` to the response to trigger a re-render. See solution Create src/main/resources/web/comments-section.js: import { LitElement, html, css } from 'lit'; class CommentsSection extends LitElement { static properties = { postSlug: { type: String, attribute: 'post-slug' }, serverUrl: { type: String, attribute: 'server-url' }, comments: { state: true }, _author: { state: true }, _content: { state: true }, }; static styles = css` :host { display: block; font-family: inherit; } .comments-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; } .comment-card { padding: 1rem; border-radius: 0.5rem; border: 1px solid #e2e8f0; background: #fff; } @media (prefers-color-scheme: dark) { .comment-card { border-color: #334155; background: #1e293b; } .comment-meta { color: #94a3b8; } .comment-body { color: #cbd5e1; } input, textarea { background: #1e293b; border-color: #475569; color: #e2e8f0; } } .comment-meta { display: flex; justify-content: space-between; font-size: 0.75rem; color: #64748b; margin-bottom: 0.5rem; } .comment-meta strong { color: #1e293b; } @media (prefers-color-scheme: dark) { .comment-meta strong { color: #e2e8f0; } } .comment-body { font-size: 0.875rem; color: #334155; } .empty { font-size: 0.875rem; color: #94a3b8; font-style: italic; } .form { display: flex; flex-direction: column; gap: 0.75rem; } label { font-size: 0.875rem; font-weight: 500; } input, textarea { width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 0.5rem; font-family: inherit; font-size: 0.875rem; box-sizing: border-box; } input:focus, textarea:focus { outline: none; border-color: #0ea5e9; box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.3); } button { align-self: flex-start; padding: 0.5rem 1rem; border: none; border-radius: 0.5rem; background: #0284c7; color: white; font-weight: 500; cursor: pointer; font-size: 0.875rem; } button:hover { background: #0369a1; } `; constructor() { super(); this.serverUrl = ''; this.comments = []; this._author = ''; this._content = ''; } connectedCallback() { super.connectedCallback(); this.fetchComments(); } fetchComments() { fetch(`${this.serverUrl}/api/comments/${this.postSlug}`) .then(r => r.json()) .then(data => this.comments = data); } postComment() { if (!this._author.trim() || !this._content.trim()) return; fetch(`${this.serverUrl}/api/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ postSlug: this.postSlug, author: this._author, content: this._content, }), }) .then(r => r.json()) .then(data => { this.comments = data; this._author = ''; this._content = ''; }); } render() { return html` <h2>Comments (${this.comments.length})</h2> <div class="comments-list"> ${this.comments.length === 0 ? html`<p class="empty">No comments yet. Be the first!</p>` : this.comments.map(c => html` <div class="comment-card"> <div class="comment-meta"> <strong>${c.author}</strong> <span>${new Date(c.createdAt).toLocaleString()}</span> </div> <div class="comment-body">${c.content}</div> </div> `)} </div> <div class="form"> <div> <label>Name</label> <input type="text" .value=${this._author} @input=${e => this._author = e.target.value}> </div> <div> <label>Comment</label> <textarea rows="3" .value=${this._content} @input=${e => this._content = e.target.value}></textarea> </div> <button @click=${this.postComment}>Post comment</button> </div> `; } } customElements.define('comments-section', CommentsSection); 🚀🔑 This is a web component. It's a custom HTML element (<comments-section>) that encapsulates its own rendering, styles, and behavior. The styles inside static styles are scoped to the component (Shadow DOM), so they won't leak into the rest of your page. Lit makes reactive updates automatic: change this.comments and the list re-renders. Note The file lives in src/main/resources/web/ because Web Bundler automatically picks up all JS files there and bundles them via esbuild. No extra configuration needed. 5. Embed the component in the blog The web component is ready. Now we need to load it in the blog and use it in the post template. Since the comments app runs on port 7070, we load the bundled script cross-origin. ››› CODING TIME Add a <script> tag loading the component from the comments server, and a <comments-section> element to your post layout. See hint If you used the default theme (Tutorial 1a), create templates/layouts/post.html to override the theme's post layout. Use theme-layout: post. If you built from scratch (Tutorial 1b), edit your existing templates/layouts/post.html. Add the script tag and the component after {#insert /}. The post slug is available as page.slug. See solution (default theme) Create templates/layouts/post.html: --- theme-layout: post --- {#insert /} <script crossorigin src="http://localhost:7070/static/bundle/app.js" type="module"></script> <comments-section post-slug="{=page.slug}" server-url="http://localhost:7070"></comments-section> See solution (from-scratch theme) In your existing templates/layouts/post.html, add the script and component after the article content: <script crossorigin src="http://localhost:7070/static/bundle/app.js" type="module"></script> <comments-section post-slug="{=page.slug}" server-url="http://localhost:7070"></comments-section> 🚀 Open the blog (on port 8080), navigate to a blog post. You should see the comments section at the bottom with a form. Try posting a comment! 🤩 The comment appears instantly, no page reload. The Lit component posted to the API, got the updated list back, and re-rendered. The blog page itself is still static HTML, served by Roq. The interactivity is entirely in the web component. 6. Add sample data Let's seed some comments so you can see how the list looks with content. ››› CODING TIME Create a startup observer that inserts sample comments in dev mode. See hint Use a CDI bean with @Observes StartupEvent, check LaunchMode.DEVELOPMENT, and persist a few comments with postSlug matching your existing posts. See solution Create src/main/java/org/acme/SampleData.java: package org.acme; import java.time.LocalDateTime; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.transaction.Transactional; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.StartupEvent; @ApplicationScoped public class SampleData { @Transactional void onStart(@Observes StartupEvent event) { if (LaunchMode.current() != LaunchMode.DEVELOPMENT) { return; } Comment c1 = new Comment(); c1.author = "Ada"; c1.content = "Great first post! Welcome to the blogging world."; c1.postSlug = "hello-world"; c1.createdAt = LocalDateTime.now().minusHours(2); c1.persist(); Comment c2 = new Comment(); c2.author = "Grace"; c2.content = "I love how simple Roq makes this. Nice work!"; c2.postSlug = "hello-world"; c2.createdAt = LocalDateTime.now().minusHours(1); c2.persist(); } } 🚀 Restart dev mode and navigate to your first post. The sample comments should appear in the list. 7. 🚀🔑 How it all fits together Take a step back and look at what you've built: Roq generates static HTML pages with a <comments-section> custom element tag The blog loads the bundled Lit component from the comments server via a <script> tag The browser sees the custom element and the component takes over Lit fetches comments from the REST API (cross-origin), renders them, and handles form submission Quarkus REST + Panache serves the API and talks to H2 No hybrid mode needed. The blog stays fully static. The dynamic part is a clean separation: a REST API + a web component in a separate microservice. In production, the blog would be generated with roq build and deployed as static files (GitHub Pages, Netlify, etc.), while the comments service runs as a standalone Quarkus app wherever you host it. 🤩 You've built a full-stack comment system with two apps: a static blog and a comments microservice with a reactive web component. What's next? Style it further: the component uses Shadow DOM, so you can freely edit the static styles without affecting the rest of your site Add validation: use @NotBlank on the REST endpoint and show errors in the component Switch to PostgreSQL: replace H2 with quarkus-jdbc-postgresql for production Add markdown support: use markdown-it to render comment content as Markdown Add relative timestamps: use @github/relative-time-element to show "2 hours ago" instead of raw dates Deploy: the static pages deploy to GitHub Pages, but the API needs a server (e.g. Fly.io, Railway, or any container host) ### [Add Comments to Your Blog with Hybrid Mode (30min)](/posts/add-comments-hybrid/) Your Roq blog generates static pages. That's great for speed and deployment, but what about dynamic features like comments? You'd normally need a third-party service or a separate backend. With hybrid mode, you can keep your Roq blog and add dynamic content backed by a real database. Pages render on demand with access to live CDI beans, while everything else stays static. Best of both worlds. In about 30 minutes, you'll add a comment system to your blog with an H2 database, Panache entities, and a form that saves comments per post. Note Prerequisites: A working Roq blog from the blog tutorial or the from-scratch tutorial. You should have at least one blog post. Tip For the best development experience, install the Quarkus IDE tooling for your editor (VS Code, IntelliJ, or Eclipse). You get config autocompletion, validation, and Qute template completion. 1. Add hybrid mode Roq's hybrid plugin makes pages render dynamically per request instead of being pre-generated at build time. Each page can choose its caching strategy via frontmatter. Add the hybrid plugin: roq add plugin:hybrid Then add the backend dependencies to your pom.xml: <!-- Database: Hibernate ORM Panache + H2 --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-orm-panache</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-h2</artifactId> </dependency> <!-- REST endpoint for form handling --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest</artifactId> </dependency> Then configure the database in config/application.properties (or src/main/resources/application.properties): quarkus.datasource.db-kind=h2 quarkus.hibernate-orm.database.generation=drop-and-create 🚀 Restart dev mode. The app should start with no errors. 🚀🔑 With the hybrid plugin active, every page is now served dynamically. By default, pages use cache: lazy caching (rendered on first request, cached for subsequent ones). We'll set cache: lazy with a short TTL on the post layout so comments stay fresh. 2. Create the Comment entity Comments need a database model. We'll use a Panache entity, which is Hibernate ORM with a simplified API: public fields, built-in query methods, no boilerplate. ››› CODING TIME Create src/main/java/io/acme/Comment.java as a Panache entity with fields for author, content, createdAt, and postSlug (to link comments to specific posts). Add a query method to find comments by post slug. See hint Extend PanacheEntity to get a free id field and built-in CRUD methods. Use @Entity from Jakarta Persistence. The postSlug field links a comment to a blog post by its URL slug. Add a static method findByPost(String slug) that uses list("postSlug", Sort.by("createdAt").descending(), slug). See solution Create src/main/java/io/acme/Comment.java: package io.acme; import java.time.LocalDateTime; import java.util.List; import jakarta.persistence.Entity; import io.quarkus.hibernate.orm.panache.PanacheEntity; import io.quarkus.panache.common.Sort; @Entity public class Comment extends PanacheEntity { public String author; public String content; public LocalDateTime createdAt; public String postSlug; public static List<Comment> findByPost(String slug) { return list("postSlug", Sort.by("createdAt").descending(), slug); } } 🚀 Save and let dev mode pick up the new entity. No errors means Hibernate created the table. 3. Make comments available in templates Qute templates can access CDI beans with the @Named annotation. We'll create a bean that provides comments for the current page. ››› CODING TIME Create src/main/java/io/acme/CommentService.java as a @Named CDI bean with a method that takes a post slug and returns its comments. See hint Use @Named("comments") and @ApplicationScoped so it's accessible in templates as cdi:comments. Add a method forPost(String slug) that calls Comment.findByPost(slug). Also add a count(String slug) method for displaying the comment count. See solution Create src/main/java/io/acme/CommentService.java: package io.acme; import java.util.List; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Named; @Named("comments") @ApplicationScoped public class CommentService { public List<Comment> forPost(String slug) { return Comment.findByPost(slug); } public long count(String slug) { return Comment.count("postSlug", slug); } } 4. Set post pages to dynamic rendering For comments to show up, the post layout must render dynamically instead of being pre-generated at build time. We'll use cache: lazy with a short TTL so pages are cached but refresh every 30 seconds. This is a good balance: most visitors see a fast cached page, and new comments appear within half a minute. ››› CODING TIME Edit your post layout and add cache: lazy with cache-ttl: 30s to the frontmatter. See hint If you used the default theme (Tutorial 1a), you need to override the post layout by creating templates/layouts/post.html with theme-layout: post, cache: lazy, and cache-ttl: 30s. If you built from scratch (Tutorial 1b), just add those fields to your existing templates/layouts/post.html. See solution (default theme) Create templates/layouts/post.html to override the theme: --- theme-layout: post cache: lazy cache-ttl: 30s --- {#insert /} See solution (from-scratch theme) Edit templates/layouts/post.html and add caching to the frontmatter: --- layout: default cache: lazy cache-ttl: 30s --- 🚀🔑 cache: lazy renders the page on first request, then caches it for the duration of cache-ttl. After 30 seconds, the next request gets a fresh render with the latest comments. For development, you can use cache: false to see changes instantly. Note Other cache options: cache: false (render fresh on every request, good for development) and cache: startup (pre-render at startup, cache forever, good for pages that never change). 5. Add the comment form and list Now let's display comments and a submission form on every post. We'll create a reusable partial so the comments section can be included in any layout. ››› CODING TIME Create a templates/partials/comments.html partial with a list of existing comments and a form that posts to /api/comments. Then include it in your post layout. See hint Use {#for comment in cdi:comments.forPost(page.slug)} to iterate over comments for the current post. The form should use method="POST" with action="/api/comments" and include hidden fields postSlug and redirectUrl. Then include the partial in post.html with {#include partials/comments /}. See solution Create templates/partials/comments.html: <!-- Comments section --> <section class="mt-12 border-t border-slate-200 dark:border-slate-700 pt-8"> <h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6"> Comments ({=cdi:comments.count(page.slug)}) </h2> <!-- Comment list --> <div class="space-y-4 mb-8"> {#for comment in cdi:comments.forPost(page.slug)} <div class="p-4 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700"> <div class="flex items-center justify-between mb-2"> <span class="font-medium text-sm text-slate-800 dark:text-slate-200">{=comment.author}</span> <time class="text-xs text-slate-500 dark:text-slate-400">{=comment.createdAt.format('MMM d, yyyy HH:mm')}</time> </div> <p class="text-sm text-slate-700 dark:text-slate-300">{=comment.content}</p> </div> {#else} <p class="text-sm text-slate-500 dark:text-slate-400 italic">No comments yet. Be the first!</p> {/for} </div> <!-- Comment form --> <form method="POST" action="/api/comments" class="space-y-4"> <input type="hidden" name="postSlug" value="{=page.slug}"> <input type="hidden" name="redirectUrl" value="{=page.url}"> <div> <label for="author" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Name</label> <input type="text" id="author" name="author" required class="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500"> </div> <div> <label for="content" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Comment</label> <textarea id="content" name="content" rows="3" required class="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500"></textarea> </div> <button type="submit" class="px-4 py-2 rounded-lg bg-sky-600 text-white font-medium hover:bg-sky-700 transition-colors cursor-pointer"> Post comment </button> </form> </section> Then update templates/layouts/post.html (default theme): --- theme-layout: post cache: lazy cache-ttl: 30s --- {#insert /} {#include partials/comments /} Or for the from-scratch theme, add {#include partials/comments /} after your post content in templates/layouts/post.html. 🚀 Refresh a blog post. You should see a "Comments (0)" section with a form at the bottom. The form won't work yet since we haven't created the endpoint. 6. Create the form handler The comment form submits via POST. We need a REST endpoint that saves the comment and redirects back to the post (the POST/redirect/GET pattern). ››› CODING TIME Create src/main/java/io/acme/CommentResource.java as a JAX-RS resource that handles the form POST at /api/comments. See hint Use @Path("/api/comments") and a @POST method with @Consumes(MediaType.APPLICATION_FORM_URLENCODED). Receive form fields with @FormParam. Create a new Comment, set its fields including createdAt = LocalDateTime.now(), call .persist(), and return Response.seeOther(URI.create(redirectUrl)) to redirect back. Mark the method @Transactional. See solution Create src/main/java/io/acme/CommentResource.java: package io.acme; import java.net.URI; import java.time.LocalDateTime; import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @Path("/api/comments") public class CommentResource { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Transactional public Response addComment(@FormParam("author") String author, @FormParam("content") String content, @FormParam("postSlug") String postSlug, @FormParam("redirectUrl") String redirectUrl) { Comment comment = new Comment(); comment.author = author; comment.content = content; comment.postSlug = postSlug; comment.createdAt = LocalDateTime.now(); comment.persist(); return Response.seeOther(URI.create(redirectUrl)).build(); } } 🚀 Go to a blog post, fill in the form, and click "Post comment". The page should reload and show your comment! 🤩 You just added dynamic comments to a static site generator. The post page renders dynamically with cache: lazy, queries the database for comments, and displays them. The form saves to H2 via Panache, and the redirect brings you right back to the post. 7. Add sample data for development It's nice to have some comments pre-loaded in dev mode so you can see how the layout looks with content. ››› CODING TIME Create a startup observer that seeds the database with a few sample comments in dev mode. See hint Use a @Startup CDI bean with @Transactional. Check for LaunchMode.DEVELOPMENT before inserting. Create 2-3 comments with different post slugs matching your existing posts. See solution Create src/main/java/io/acme/SampleData.java: package io.acme; import java.time.LocalDateTime; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.transaction.Transactional; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.StartupEvent; @ApplicationScoped public class SampleData { @Transactional void onStart(@Observes StartupEvent event) { if (LaunchMode.current() != LaunchMode.DEVELOPMENT) { return; } Comment c1 = new Comment(); c1.author = "Ada"; c1.content = "Great first post! Welcome to the blogging world."; c1.postSlug = "hello-world"; c1.createdAt = LocalDateTime.now().minusHours(2); c1.persist(); Comment c2 = new Comment(); c2.author = "Grace"; c2.content = "I love how simple Roq makes this. Nice work!"; c2.postSlug = "hello-world"; c2.createdAt = LocalDateTime.now().minusHours(1); c2.persist(); } } 🚀 Restart dev mode. Navigate to your first post. You should see the sample comments already there. What you've learned Hybrid mode turns Roq from a static site generator into a dynamic server with smart caching cache: lazy with cache-ttl renders pages dynamically with smart caching Panache entities give you a database model with zero boilerplate @Named beans make Java services available in Qute templates via cdi: prefix POST/redirect/GET is the standard pattern for form handling in server-rendered apps What's next? Next in the series: Add Comments with a Web Component for an alternative approach using a Lit web component and a separate microservice. Add validation: use @NotBlank on form fields and show error messages Add HTMX: replace the full page reload with hx-post for instant comment submission (see the Quarkus Web Lab for HTMX patterns) Switch to PostgreSQL: replace H2 with quarkus-jdbc-postgresql for production Add moderation: add an approved boolean field and only show approved comments Try cache: false: for instant comment visibility during development, switch to cache: false (renders fresh on every request) ### [Create a Link-Tree with Roq (45min)](/posts/create-a-link-tree-with-roq/) Link-tree sites are everywhere: a clean page with your photo, a few links, maybe some social icons. Simple enough that you could build one in plain HTML, but what if you want multiple link pages, QR codes, and zero-effort deployment? In this tutorial you'll build a personal link-tree from scratch with Roq and Tailwind CSS. No pre-built theme, just you, data files, layouts, and reusable template components. By the end you'll have a polished site with typed data, icon sets, auto-generated pages, and downloadable QR codes. Note Prerequisites: Install the Roq CLI by following the Getting Started guide. Roq uses JBang, so no JDK installation is needed. Verify your setup with: roq --version Tip For the best development experience, install the Quarkus IDE tooling for your editor (VS Code, IntelliJ, or Eclipse). You get config autocompletion, validation, and Qute template completion. Tip The finished project is available at github.com/ia3andy/roq-linktree-tuto if you get stuck. 1. Create the project Open a terminal and create a new Roq project with the base theme (no styling, just the essentials): roq create my-linktree -x theme:base The base theme gives you SEO tags, favicon support, and CSS/JS bundling, but no visual styling. That's intentional: we'll bring our own with Tailwind. cd my-linktree Now add the Tailwind CSS extension: roq add web:tailwindcss Start dev mode: roq start 🚀 Hit w or open http://localhost:8080. You should see a bare-bones page with no styling. That's expected, we'll fix that next. Here's what was generated: my-linktree/ ├── content/ # Your pages │ └── index.html # Home page ├── data/ # Data files (YAML/JSON) ├── public/ # Static assets (images, favicon…) ├── web/ # CSS and JS (bundled automatically) │ └── app.css └── pom.xml content/ is where you write your pages. data/ holds structured data (YAML/JSON) accessible from templates. We'll use this for profile and links. web/ is for CSS and JS, bundled automatically by Web Bundler. public/ holds static files served as-is (images, fonts). templates/ doesn't exist yet, but this is where you'll create your own layouts, partials, and tags to override or extend the theme. 2. Set up Tailwind and component styles The base theme already provides a default layout with the HTML skeleton, SEO tags, favicon, and {#bundle /}. We need to set up our CSS with Tailwind, icon imports, and reusable component classes. First, add the Phosphor Icons npm dependency to your pom.xml: <dependency> <groupId>org.mvnpm.at.phosphor-icons</groupId> <artifactId>web</artifactId> <version>2.1.2</version> <scope>provided</scope> </dependency> Then replace the contents of web/app.css with Tailwind, icon imports, and all the component styles we'll use throughout the tutorial. Instead of writing Tailwind utility classes directly in the HTML, we define semantic class names using @apply to keep our templates clean and maintainable: See the full CSS @import "tailwindcss"; @import "@phosphor-icons/web/regular"; @import "@phosphor-icons/web/fill"; /* Layout */ .lt-page { @apply flex items-start justify-center pb-6 pt-12; } .lt-page-relative { @apply flex items-start justify-center pb-6 pt-12 relative; } .lt-container { @apply w-full max-w-md mx-auto space-y-8 px-4; } /* Profile */ .lt-profile { @apply text-center space-y-3; } .lt-avatar { @apply w-32 h-32 mx-auto rounded-full bg-white dark:bg-slate-800 p-2 shadow-md; } .lt-name { @apply text-2xl font-bold tracking-tight; } .lt-name-text { @apply text-slate-900 dark:text-white; } .lt-handle { @apply text-sky-500 dark:text-sky-400; } .lt-title { @apply text-sm text-slate-500 dark:text-slate-400; } /* Social */ .lt-social { @apply text-center space-y-3; } .lt-social-icons { @apply flex items-center justify-center gap-5; } .lt-social-link { @apply text-slate-500 dark:text-slate-400 hover:text-sky-600 dark:hover:text-sky-400 transition-colors; } /* Link cards */ .lt-links { @apply space-y-3; } .lt-link-card { @apply flex items-center justify-between px-5 py-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-sky-400 dark:hover:border-sky-500 hover:shadow-md transition-all duration-200; } .lt-link-content { @apply flex items-center gap-3; } .lt-link-icon { @apply text-sky-500 dark:text-sky-400; } .lt-link-name { @apply text-sm font-medium text-slate-800 dark:text-slate-100; } .lt-link-desc { @apply block text-xs text-slate-500 dark:text-slate-400; } .lt-link-arrow { @apply text-slate-300 dark:text-slate-600 transition-colors; } .group:hover .lt-link-arrow { @apply text-sky-500 dark:text-sky-400; } /* Profile links separator */ .lt-profile-links { @apply pt-2 pb-10 space-y-3; } .lt-separator { @apply text-center text-slate-300 dark:text-slate-600; } /* Trees gallery */ .lt-trees { @apply space-y-6; } .lt-tree-card { @apply text-center space-y-4 p-5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800; } .lt-tree-title { @apply text-lg font-bold text-slate-800 dark:text-slate-100; } .lt-tree-desc { @apply text-xs text-slate-500 dark:text-slate-400; } .lt-qr-wrap { @apply inline-block p-3 bg-white rounded-xl; } .lt-tree-actions { @apply flex items-center justify-center gap-4; } .lt-tree-action { @apply text-sm text-sky-600 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-200 transition-colors; } .lt-tree-action-btn { @apply text-sm text-sky-600 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-200 transition-colors cursor-pointer; } /* Home link */ .lt-home-link { @apply flex items-center justify-center gap-2 text-base text-slate-500 dark:text-slate-400 hover:text-sky-600 dark:hover:text-sky-400 transition-colors; } /* Navigation */ .lt-nav-trees { @apply absolute top-4 right-4 text-slate-400 dark:text-slate-500 hover:text-sky-500 dark:hover:text-sky-400 transition-colors; } /* Section heading */ .lt-heading { @apply text-center space-y-2; } .lt-heading-title { @apply text-2xl font-bold tracking-tight text-slate-900 dark:text-white; } Note Replacing the CSS will break the look of the initial content from the codestart. That's expected: we build our own design in the following steps. 🚀 Refresh your browser to make sure the CSS compiles without errors. 🚀🔑 The {#bundle /} tag is the magic glue. It tells Roq's Web Bundler to compile everything in web/ (CSS, JS, npm packages) and inject it into the page. Tailwind is processed at build time, so only the classes you actually use end up in the final CSS. The @apply directives let us define semantic class names (lt-avatar, lt-link-card, etc.) that map to Tailwind utilities, keeping our templates clean. 3. Create the profile data Link-tree sites are all about you. Let's define your profile as structured data that templates can access. The profile includes your info, social links, and your main set of links. ››› CODING TIME Create data/profile.yml with your name, handle, title, bio, profile image, a list of social links, and a list of links. Each social entry should have a name, url, and icon. Each link should have a name, url, description, and icon. See hint YAML files in data/ are automatically available in templates via cdi: prefix. For the icon fields, we'll use Phosphor Icons names. Pick names like github-logo, linkedin-logo, butterfly (for Bluesky), lightning, terminal, etc. See solution Create data/profile.yml: name: Groot handle: "@iamgroot" title: I am Groot. image: groot.png bio: I am Groot. I am Groot. I am Groot! social: - name: GitHub url: https://github.com/iamgroot icon: github-logo - name: LinkedIn url: https://www.linkedin.com/in/iamgroot icon: linkedin-logo - name: Bluesky url: https://bsky.app/profile/iamgroot.bsky.social icon: butterfly links: - name: Guardians HQ url: https://marvel.com/guardians description: Where we save the galaxy icon: shield-star - name: Quarkus url: https://quarkus.io description: Supersonic Subatomic Java framework icon: lightning - name: Roq url: https://iamroq.dev description: Static site generator with a Java soul icon: terminal - name: Tree Care Tips url: https://en.wikipedia.org/wiki/Groot description: I am Groot icon: tree Drop a profile image as public/images/groot.png (or any image you like). 4. Map the profile data to Java Raw YAML data works in templates, but Roq can do better. With @DataMapping, you map your YAML to a typed Java record. This gives you compile-time safety and auto-completion. ››› CODING TIME Create src/main/java/io/acme/Profile.java as a Java record annotated with @DataMapping("profile"). Include fields for name, handle, title, bio, image, a List<Social> and a List<Link>. See hint @DataMapping("profile") tells Roq to map data/profile.yml to this record. The field names must match the YAML keys. For nested lists, use nested record types. Import from io.quarkiverse.roq.data.runtime.annotations.DataMapping. See solution Create src/main/java/io/acme/Profile.java: package io.acme; import io.quarkiverse.roq.data.runtime.annotations.DataMapping; import java.util.List; @DataMapping("profile") public record Profile(String name, String handle, String title, String bio, String image, List<Social> social, List<Link> links) { public record Social(String name, String url, String icon) {} public record Link(String name, String url, String description, String icon) {} } 🚀🔑 The @DataMapping annotation is the bridge between your YAML data and your templates. Once mapped, you access the profile in templates with cdi:profile.name, cdi:profile.handle, etc. The cdi: prefix means it's a CDI bean, which is how Quarkus manages dependency injection. 5. Create reusable template components Before building the full page, let's create two reusable components: a profile partial (avatar, name, social icons) and a linkCard tag (a single link card). We'll reuse these across all our layouts. ››› CODING TIME Create templates/partials/profile.html that displays the avatar, name, handle, title, and social icons from cdi:profile. Use the lt-* CSS classes from step 2. See hint Partials are included with {#include partials/profile /} and share the parent template's context. Use site.image(cdi:profile.image) for the avatar URL. Loop through cdi:profile.social for the social icons with ph-fill ph-{=s.icon} classes. See solution Create templates/partials/profile.html: <div class="lt-profile"> {#if cdi:profile.image} <img src="{=site.image(cdi:profile.image)}" alt="{=cdi:profile.name}" class="lt-avatar"> {/if} <h1 class="lt-name"> {#if cdi:profile.name}<span class="lt-name-text">{=cdi:profile.name}</span>{/if} {#if cdi:profile.handle}<span class="lt-handle">{#if cdi:profile.name} {/if}{=cdi:profile.handle}</span>{/if} </h1> <p class="lt-title">{=cdi:profile.title}</p> </div> {#if cdi:profile.social??} <div class="lt-social"> <div class="lt-social-icons"> {#for s in cdi:profile.social} <a href="{=s.url}" target="_blank" rel="noopener noreferrer" aria-label="{=s.name}" class="lt-social-link"> <i class="ph-fill ph-{=s.icon}" style="font-size: 24px;"></i> </a> {/for} </div> </div> {/if} Now create the link card tag. ››› CODING TIME Create templates/tags/linkCard.html that renders a single link as a styled card with an icon, name, description, and arrow. See hint Tags are called with {#linkCard link=myLink /}. The link variable is passed as a parameter. Use the group class on the <a> element alongside lt-link-card for the hover effect on the arrow. See solution Create templates/tags/linkCard.html: <a href="{=link.url}" target="_blank" rel="noopener noreferrer" class="group lt-link-card"> <span class="lt-link-content"> {#if link.icon} <i class="ph ph-{=link.icon} lt-link-icon" style="font-size: 20px;"></i> {/if} <span> <span class="lt-link-name">{=link.name}</span> <span class="lt-link-desc">{=link.description}</span> </span> </span> <span class="lt-link-arrow"> <i class="ph ph-arrow-right" style="font-size: 16px;"></i> </span> </a> 🚀🔑 Partials share the calling template's context (they can access site, page, etc.), while tags receive data explicitly through parameters. Use partials for sections that always render the same way, and tags for components you call multiple times with different data. 6. Build the home page Now let's create a layout for the home page. The linktree-home layout shows your profile, social icons, and your links from the profile data. It also adds a navigation icon to the trees gallery. ››› CODING TIME Create templates/layouts/linktree-home.html that extends default. It should display the profile partial, loop through cdi:profile.links using the linkCard tag, and include a navigation icon to /trees. See hint Use layout: default in the frontmatter to extend the base layout. Include the profile with {#include partials/profile /} and loop through links with {#for link in cdi:profile.links}{#linkCard link=link /}{/for}. Use lt-page-relative and lt-nav-trees for the trees icon. The {#insert /} slot lets content pages add extra content. See solution Create templates/layouts/linktree-home.html: --- layout: default --- {@io.quarkiverse.roq.frontmatter.runtime.model.Page page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <div class="lt-page-relative"> <a href="/trees" class="lt-nav-trees" title="All trees"> <i class="ph ph-tree-structure" style="font-size: 24px;"></i> </a> <div class="lt-container"> {#include partials/profile /} {#if cdi:profile.links??} <div class="lt-links"> {#for link in cdi:profile.links} {#linkCard link=link /} {/for} </div> {/if} {#insert /} </div> </div> Now update your home page to use this layout. Replace the content of content/index.html: --- layout: linktree-home title: Groot --- 🚀 Refresh your browser. You should see your full link-tree: avatar in a circle, name in dark text, handle in sky blue, social icons with hover effects, and a list of link cards with icons and arrows. All from a three-line content file! 🤩 That's a complete link-tree, and it's all driven by YAML data. Want to change a link? Edit data/profile.yml. Want to add one? Add a line. No template changes needed. 7. Create a secondary tree Your home page shows your main links from the profile. But what if you want a separate link-tree for a specific topic? You can create additional trees as YAML files in data/trees/. Why a directory instead of a single file? Because you might want multiple link-trees: one for personal links, one for work, one for a specific event or conference. Each YAML file in data/trees/ becomes its own page with its own QR code (we'll set that up in steps 9 and 10). ››› CODING TIME Create data/trees/guardians.yml with a title, description, and a list of links related to a different topic. See solution Create data/trees/guardians.yml: title: Guardians of the Galaxy description: I am Groot's team resources links: - name: Guardians HQ url: https://marvel.com/guardians description: Where we save the galaxy icon: shield-star - name: Milano Ship Manual url: https://en.wikipedia.org/wiki/Guardians_of_the_Galaxy description: How to fly the Milano icon: rocket - name: Groot's Wikipedia url: https://en.wikipedia.org/wiki/Groot description: Everything about me icon: tree 8. Map the trees data to Java Just like the profile, we'll map the trees data to a typed Java record. The difference is that trees live in a directory (data/trees/), so we use DataMapping.Type.OBJECT_DIR to load all files in that directory as a map. ››› CODING TIME Create src/main/java/io/acme/Trees.java with @DataMapping(value = "trees", type = DataMapping.Type.OBJECT_DIR). The record should contain a Map<String, Tree> where each key is the file name (e.g. guardians) and Tree has title, description, and List<Link>. See hint Use DataMapping.Type.OBJECT_DIR to tell Roq that data/trees/ is a directory of objects, not a single file. The map key comes from the file name. Tree and Link are nested records inside Trees. See solution Create src/main/java/io/acme/Trees.java: package io.acme; import io.quarkiverse.roq.data.runtime.annotations.DataMapping; import java.util.List; import java.util.Map; @DataMapping(value = "trees", type = DataMapping.Type.OBJECT_DIR) public record Trees(Map<String, Tree> map) { public record Tree(String title, String description, List<Link> links) {} public record Link(String name, String url, String description, String icon) {} } 🚀🔑 OBJECT_DIR is powerful. Drop a new YAML file in data/trees/ and it automatically becomes a new entry in the map. No config changes needed. 9. Generate pages from data Now let's auto-generate a page for each tree. Configure the collection in config/application.properties: site.collections.trees.layout=linktree site.collections.trees.from-data.id-key=_key site.collections.trees.link=/trees/:name This tells Roq: "create a trees collection, generate one page per data entry, use the linktree layout, and use the YAML file name as the page ID." ››› CODING TIME Create templates/layouts/linktree.html that extends default and renders a tree page. Show the tree's title and description at the top, then its links. Below a chevron separator, show the profile and append the profile's main links. See hint Use page.data.title, page.data.description, and {#for link in page.data.links} for the tree's own data. Reuse the profile partial and linkCard tag. The show-profile frontmatter key lets tree YAML files hide the profile section (defaults to true). The profile-links key controls whether profile links are appended. See solution Create templates/layouts/linktree.html: --- layout: default robots: noindex sitemap: false --- {@io.quarkiverse.roq.frontmatter.runtime.model.Page page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <div class="lt-page-relative"> <a href="/trees" class="lt-nav-trees" title="All trees"> <i class="ph ph-tree-structure" style="font-size: 24px;"></i> </a> <div class="lt-container"> <div class="lt-heading"> <h1 class="lt-heading-title">{=page.title}</h1> {#if page.description} <p class="lt-title">{=page.description}</p> {/if} </div> <div class="lt-links"> {#for link in page.data.links} {#linkCard link=link /} {/for} </div> {#if page.data('show-profile', true)} <div class="lt-separator"> <i class="ph ph-caret-double-down" style="font-size: 20px;"></i> </div> <div class="lt-profile-links"> {#include partials/profile /} </div> {#if page.data('profile-links', true) and cdi:profile.links??} <div class="lt-links"> {#for link in cdi:profile.links} {#linkCard link=link /} {/for} </div> {/if} {/if} </div> </div> 🚀 Navigate to http://localhost:8080/trees/guardians/. You should see the Guardians tree with its links at the top, then a chevron separator, your profile, and your main links below. The tree icon in the top-right links to the gallery page (we'll build that next). 🚀🔑 This is the key insight: drop a new YAML file in data/trees/ and Roq generates a new page automatically. The profile and main links appear at the bottom, giving visitors a path back to your other content. Add show-profile: false in a tree's YAML to hide the profile section on that specific page. 10. Add QR codes Let's build a gallery page that lists all your link-trees with downloadable QR codes. This is great for sharing at events or printing on business cards. First, add the QR code plugin: roq add plugin:qrcode ››› CODING TIME Create templates/layouts/linktrees.html that extends default, shows the profile, a link back home, and loops through all trees to display each as a card with a QR code. See hint Use {#for tree in site.collections.trees} to loop through the collection. The QR code tag is {#qrcode value=tree.url.absolute alt=tree.data.title foreground="#0e4a5c" background="#FFFFFF" width=200 height=200 /}. Access tree data with tree.data.title and tree.data.description. Link to the tree page with tree.url. See solution Create templates/layouts/linktrees.html: --- layout: default --- {@io.quarkiverse.roq.frontmatter.runtime.model.Page page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <div class="lt-page"> <div class="lt-container"> {#include partials/profile /} <a href="/" class="lt-home-link"><i class="ph ph-house" style="font-size: 16px;"></i> Profile</a> <div class="lt-heading"> <h1 class="lt-heading-title">{=page.title}</h1> </div> <div class="lt-trees"> {#for tree in site.collections.trees} <div class="lt-tree-card"> <h2 class="lt-tree-title">{=tree.data.title}</h2> <p class="lt-tree-desc">{=tree.data.description}</p> <div class="lt-qr-wrap qr-wrap" data-filename="qr-{=tree.data.title.slugify}.svg"> {#qrcode value=tree.url.absolute alt=tree.data.title foreground="#0e4a5c" background="#FFFFFF" width=200 height=200 /} </div> <div class="lt-tree-actions"> <a href="{=tree.url}" class="lt-tree-action">Open</a> <button onclick="downloadQR(this)" class="lt-tree-action-btn">Download QR</button> </div> </div> {/for} </div> {#insert /} </div> </div> Now add the QR download script. Create web/scripts.js: window.downloadQR = function(btn) { var wrap = btn.closest('.lt-tree-card').querySelector('.qr-wrap'); var img = wrap.querySelector('img'); if (!img) return; var filename = (wrap.dataset.filename || 'qr-code.svg').toLowerCase(); var svgText = atob(img.src.split(',')[1]); var doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); var svg = doc.querySelector('svg'); svg.setAttribute('viewBox', '0 0 ' + svg.getAttribute('width') + ' ' + svg.getAttribute('height')); svg.setAttribute('width', '400'); svg.setAttribute('height', '400'); var blob = new Blob([new XMLSerializer().serializeToString(svg)], { type: 'image/svg+xml' }); var a = document.createElement('a'); a.download = filename; a.href = URL.createObjectURL(blob); a.click(); URL.revokeObjectURL(a.href); }; Finally, create the gallery content page. Create content/trees.html: --- layout: linktrees title: All Trees --- 🚀 Navigate to http://localhost:8080/trees. You should see your profile at the top, a "Profile" link back to the home page, and each tree displayed as a card with a QR code. Click "Download QR" to save it as an SVG. 🤩 Your link-tree site is complete! A profile card, social icons, link cards with hover effects, auto-generated pages from YAML data, and a QR code gallery. All styled with @apply CSS, all driven by data, all built with reusable layouts and components. What's next? Next in the series: Add Comments with Hybrid Mode to add dynamic features to your Roq blog with a database and server-rendered templates. Here are a few ideas to keep going: Add more trees: drop a new YAML file in data/trees/ (e.g. work-links.yml) and it's instantly available with its own QR code. Deploy to GitHub Pages: your project already includes a .github/workflows/deploy.yml. Push to GitHub, enable Pages in Settings, and you're live. Add analytics: set analytics.ga4: G-XXXXX in your site index frontmatter and add {#ga4 /} to the layout. Make it your own: swap the color palette (try indigo/rose), change the card style, add animations. It's just Tailwind, go wild. Explore the docs: Roq the basics covers collections, custom data, templates, and much more. Switch to the Linktree theme Now that you understand how everything works under the hood, you can switch to the pre-built Linktree theme which provides all the layouts, partials, tags, and styles you just built. Run: roq add theme:linktree Then delete the files that the theme now provides: templates/, web/app.css, and web/scripts.js. Your data files (data/profile.yml, data/trees/) and content files (content/index.html, content/trees.html) stay the same, and you can customize the theme through web/_custom.css. Happy linking! ### [Create a Blog from Scratch with Roq (45min)](/posts/create-a-blog-from-scratch-with-roq/) The first tutorial showed you how to create a blog using the default theme. Everything was styled and ready to go. But what if you want to understand how it all works under the hood? What if you want full control over every layout, every class, every pixel? That's what this tutorial is about. You'll build a blog from scratch using Roq's base theme, which gives you the foundation (SEO, favicon, bundling) but zero styling. You'll create your own layouts, wire up collections, add pagination and tags, all while learning how Roq's template system works. Note Prerequisites: Install the Roq CLI by following the Getting Started guide. Roq uses JBang, so no JDK installation is needed. Verify your setup with: roq --version Tip For the best development experience, install the Quarkus IDE tooling for your editor (VS Code, IntelliJ, or Eclipse). You get config autocompletion, validation, and Qute template completion. 1. Create the project Create a new Roq project with the base theme (no pre-built styling): roq create my-blog -x theme:base The base theme provides three things: {#seo /} for meta tags, {#favicon /} for favicon discovery, and {#bundle /} for CSS/JS bundling. Everything else is up to you. Now add Tailwind CSS: cd my-blog roq add web:tailwindcss Start dev mode: roq start 🚀 Hit w or open http://localhost:8080. You should see a basic page with minimal styling and some placeholder content. We'll replace all of it. Here's what was generated: my-blog/ ├── content/ # Your pages │ └── index.html # Home page ├── data/ # Data files (YAML/JSON) ├── public/ # Static assets (images, favicon…) ├── web/ # CSS and JS (bundled automatically) │ └── app.css └── pom.xml content/ is where you write. Markdown, AsciiDoc, or HTML. data/ holds structured data (YAML/JSON) accessible from templates. web/ is for CSS and JS, bundled automatically by Web Bundler. public/ holds static files served as-is (images, fonts, robots.txt). templates/ doesn't exist yet, but this is where you'll create your own layouts, partials, and tags to override or extend the theme. 2. 👀 Explore the base theme In Roq, a theme is a Maven dependency that provides layouts, partials, styles, and much more. Your project already includes the base theme as a dependency in pom.xml. You can browse its source on GitHub. It provides three layouts you can extend: default: the HTML skeleton with <head> (SEO, favicon, bundle) and <body>. This is the root of the layout chain. page: extends default, adds a simple <main> with an <h1> title. For generic pages. post: extends default, same as page but adds the post date. For blog posts. These layouts are intentionally minimal. They use {#insert /} as a content slot. When your page says layout: default, Roq injects your page content into that slot. 🚀🔑 The layout chain works like inheritance: your content page declares a layout, that layout can declare its own parent layout, all the way up to the base default. Each level wraps the previous one. 3. Create your default layout The base theme's default layout works, but it has no visual structure. Let's create our own that extends it and adds a site header, navigation, and footer. ››› CODING TIME Create templates/layouts/default.html that extends the base theme's default. Add Tailwind classes for a slate/sky color scheme, a header with your site name, and a footer. See hint Use theme-layout: default in the frontmatter to extend the base theme's default (instead of layout: default which would create a loop). The base theme's {#insert head /} slot lets you add extra content to <head>. The main {#insert /} slot is where your body content goes. Add a <header>, <main>{#insert /}</main>, and <footer>. See solution Create templates/layouts/default.html: --- theme-layout: default --- {@io.quarkiverse.roq.frontmatter.runtime.model.Page page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <div class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200"> <header class="border-b border-slate-200 dark:border-slate-800"> <div class="max-w-3xl mx-auto px-4 py-4 flex items-center justify-between"> <a href="/" class="text-lg font-bold text-slate-900 dark:text-white hover:text-sky-600 dark:hover:text-sky-400 transition-colors"> {=site.title} </a> <nav class="flex gap-4 text-sm"> <a href="/" class="text-slate-600 dark:text-slate-400 hover:text-sky-600 dark:hover:text-sky-400 transition-colors">Home</a> <a href="/about" class="text-slate-600 dark:text-slate-400 hover:text-sky-600 dark:hover:text-sky-400 transition-colors">About</a> </nav> </div> </header> <main class="max-w-3xl mx-auto px-4 py-8"> {#insert /} </main> <footer class="border-t border-slate-200 dark:border-slate-800 mt-12"> <div class="max-w-3xl mx-auto px-4 py-6 text-center text-sm text-slate-500 dark:text-slate-400"> Built with <a href="https://iamroq.dev" class="text-sky-600 dark:text-sky-400 hover:underline">Roq</a> </div> </footer> </div> 🚀 Refresh your browser. The page now has a header with your site name, a navigation bar, and a footer. The content area is centered at max-w-3xl. 🚀🔑 Notice the theme-layout: default in the frontmatter. This tells Roq to extend the base theme's default layout directly, not your own. Without theme-layout:, using layout: default would create a self-referencing loop. This is how you override a theme layout while still inheriting its <head> setup (SEO, favicon, bundle). 4. Set up the CSS The Tailwind extension replaced web/app.css with Tailwind imports. Let's also add the Tailwind typography plugin for styling prose content (blog posts rendered from Markdown). ››› CODING TIME Open web/app.css and make sure it imports Tailwind and the typography plugin. See hint The file should already have @import "tailwindcss". Add @plugin "@tailwindcss/typography" for the prose class that styles rendered HTML from Markdown. See solution Edit web/app.css: @import "tailwindcss"; @plugin "@tailwindcss/typography"; Note Replacing the CSS will break the look of the initial content from the codestart. That's expected: we build our own design in the following steps. 🚀 Save and verify the page still loads with Tailwind classes applied. 5. Create the home page Let's replace the placeholder index with a proper home page that will later show a list of blog posts. ››› CODING TIME Replace content/index.html with a home page that has a title, a short intro, and a placeholder for the blog listing (we'll add that after creating some posts). See hint Set layout: default and give it a title and description in the frontmatter. The title from your index page becomes site.title, which the header already displays. Use Tailwind classes for spacing and typography. See solution Replace content/index.html: --- layout: default title: My Blog description: A blog built from scratch with Roq and Tailwind CSS. --- <div class="space-y-6"> <div class="text-center space-y-2"> <h1 class="text-3xl font-bold text-slate-900 dark:text-white">Welcome</h1> <p class="text-slate-500 dark:text-slate-400">Thoughts on code, craft, and everything in between.</p> </div> <section> <h2 class="text-xl font-semibold text-slate-800 dark:text-slate-100 mb-4">Latest posts</h2> <p class="text-slate-500 dark:text-slate-400 italic">No posts yet. Create one and come back!</p> </section> </div> Also create a simple about page. Create content/about.md: --- layout: default title: About --- # About this blog This blog is built from scratch with [Roq](https://iamroq.dev) and [Tailwind CSS](https://tailwindcss.com). No pre-built theme, just custom layouts and Markdown content. 🚀 Check the home page and click "About" in the nav. Both pages should render with your layout. 6. Create the post layout Before writing blog posts, we need a layout that knows how to display them: title, date, tags, and the rendered Markdown content. ››› CODING TIME Create templates/layouts/post.html that extends your default layout. Display the post title, date, tags, and content. Use the prose class from Tailwind Typography to style the Markdown output. See hint Use layout: default to inherit your custom layout. Declare the page type as {@io.quarkiverse.roq.frontmatter.runtime.model.DocumentPage page} (DocumentPage, not Page, because posts are collection documents with extra fields like date). Access page.title, page.date.longDate, page.data.tags.asStrings. The content slot {#insert /} renders the Markdown body. Wrap it in <div class="prose dark:prose-invert"> for typography styling. See solution Create templates/layouts/post.html: --- layout: default --- {@io.quarkiverse.roq.frontmatter.runtime.model.DocumentPage page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <article class="space-y-6"> <header class="space-y-2"> <h1 class="text-3xl font-bold text-slate-900 dark:text-white">{=page.title}</h1> {#if page.date} <time class="text-sm text-slate-500 dark:text-slate-400">{=page.date.longDate}</time> {/if} {#if page.data.tags} <div class="flex gap-2"> {#for tag in page.data.tags.asStrings} <span class="text-xs bg-sky-100 dark:bg-sky-900 text-sky-700 dark:text-sky-300 px-2 py-0.5 rounded">{=tag}</span> {/for} </div> {/if} </header> <div class="prose dark:prose-invert max-w-none"> {#insert /} </div> <footer class="border-t border-slate-200 dark:border-slate-800 pt-4 flex justify-between text-sm text-slate-500 dark:text-slate-400"> {#if page.previous} <a href="{=page.previous.url}" class="hover:text-sky-600 dark:hover:text-sky-400">← {=page.previous.title}</a> {#else}<span></span>{/if} {#if page.next} <a href="{=page.next.url}" class="hover:text-sky-600 dark:hover:text-sky-400">{=page.next.title} →</a> {/if} </footer> </article> 🚀🔑 The {@io.quarkiverse.roq.frontmatter.runtime.model.DocumentPage page} declaration is important. A DocumentPage is a page that belongs to a collection (like posts). It has extra fields: date, next, previous, collectionId. A regular Page doesn't have these. 7. Write your first post Time to create some content. Blog posts live in content/posts/ and follow a date-based naming convention. ››› CODING TIME Create your first post at content/posts/2026-07-01-hello-world/index.md with a title, description, tags, and some Markdown content. See hint The directory name 2026-07-01-hello-world gives Roq the date and slug. Add frontmatter with title, description, and tags. You don't need to specify layout because the collection config already sets it to post. Write a few paragraphs of Markdown. See solution Create content/posts/2026-07-01-hello-world/index.md: --- title: "Hello, World!" description: "My very first blog post, built from scratch with Roq." tags: [hello, roq] --- ## Welcome This is my first blog post. I built this blog from scratch using [Roq](https://iamroq.dev) and Tailwind CSS. No pre-built theme. Just custom layouts, a posts collection, and Markdown content. Here's what I learned: - **Layouts are just HTML with Qute tags.** You extend them with `layout:` in frontmatter. - **Collections are directories.** Drop a Markdown file in `content/posts/` and it becomes a blog post. - **Live-reload is instant.** Save the file, see the result. More posts coming soon! Create a second post so we have something to paginate later: --- title: "Learning Roq Layouts" description: "How template inheritance works in Roq." tags: [roq, layouts] --- ## The layout chain Every page in Roq goes through a layout chain. Your content page declares a `layout`, that layout can declare its own parent `layout`, and so on up to the base `default`. This is how you get consistent headers, navs, and footers across your entire site without repeating yourself. Save this as content/posts/2026-07-02-learning-roq-layouts/index.md. 🚀 Navigate to /posts/hello-world/ and /posts/learning-roq-layouts/. You should see your posts rendered with the title, date, tags, and prose-styled content. The previous/next links at the bottom should connect them. 8. Add the blog listing Now let's update the home page to list your posts with pagination. ››› CODING TIME Update content/index.html to iterate over the posts collection and display each post as a card with title, date, description, and a link. See hint Add paginate: posts to the frontmatter to enable pagination. Then use {#for post in site.collections.posts.paginated(page.paginator)} to loop through posts. Access post.title, post.date.longDate, post.description, post.url. For pagination links, use {#include fm/pagination.html /}. See solution Replace content/index.html: --- layout: default title: My Blog description: A blog built from scratch with Roq and Tailwind CSS. paginate: collection: posts size: 5 --- <div class="space-y-8"> <div class="text-center space-y-2"> <h1 class="text-3xl font-bold text-slate-900 dark:text-white">My Blog</h1> <p class="text-slate-500 dark:text-slate-400">Thoughts on code, craft, and everything in between.</p> </div> <div class="space-y-4"> {#for post in site.collections.posts.paginated(page.paginator)} <a href="{=post.url}" class="block p-5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-sky-400 dark:hover:border-sky-500 hover:shadow-md transition-all duration-200"> <h2 class="text-lg font-semibold text-slate-900 dark:text-white">{=post.title}</h2> <time class="text-xs text-slate-500 dark:text-slate-400">{=post.date.longDate}</time> {#if post.description} <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">{=post.description}</p> {/if} </a> {/for} </div> {#include fm/pagination.html /} </div> 🚀 Go to the home page. Your posts should appear as clickable cards, sorted by date (newest first). With only two posts you won't see pagination yet, but the mechanism is in place. 🤩 You've built a working blog from scratch. Posts, layouts, a listing page with pagination. All with Tailwind and zero pre-built theme code. 9. Add tag support Tags let readers browse posts by topic. The tagging plugin auto-generates a page for each tag. roq add plugin:tagging ››› CODING TIME Create templates/layouts/tag.html that displays all posts for a given tag, with pagination. See hint Use tagging: posts and paginate: true in the frontmatter to wire up the tag page. The current tag is in page.data.tag. The tagged posts are in site.collections.get(page.data.tagCollection). Use .paginated(page.paginator) for pagination. See solution Create templates/layouts/tag.html: --- layout: default tagging: posts paginate: true --- {@io.quarkiverse.roq.frontmatter.runtime.model.NormalPage page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} <div class="space-y-6"> <div class="text-center space-y-2"> <h1 class="text-2xl font-bold text-slate-900 dark:text-white">Tag: {=page.data.tag}</h1> </div> <div class="space-y-4"> {#for post in site.collections.get(page.data.tagCollection).paginated(page.paginator)} <a href="{=post.url}" class="block p-5 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 hover:border-sky-400 dark:hover:border-sky-500 hover:shadow-md transition-all duration-200"> <h2 class="text-lg font-semibold text-slate-900 dark:text-white">{=post.title}</h2> <time class="text-xs text-slate-500 dark:text-slate-400">{=post.date.longDate}</time> </a> {/for} </div> {#include fm/pagination.html /} </div> Now update the tag spans in your post.html layout to be clickable links: See hint Replace the <span> tags with <a> links pointing to {=site.url('/posts/tag', tag.slugify)}. See solution In templates/layouts/post.html, replace the tags section: {#if page.data.tags} <div class="flex gap-2"> {#for tag in page.data.tags.asStrings} <a href="{=site.url('/posts/tag', tag.slugify)}" class="text-xs bg-sky-100 dark:bg-sky-900 text-sky-700 dark:text-sky-300 px-2 py-0.5 rounded hover:bg-sky-200 dark:hover:bg-sky-800 transition-colors">{=tag}</a> {/for} </div> {/if} 🚀 Click on a tag in a blog post. You should land on a page like /posts/tag/roq/ listing all posts with that tag. 10. Add an RSS feed RSS lets readers subscribe to your blog. Roq has built-in support. ››› CODING TIME Create content/rss.xml and add the RSS auto-discovery tag to your default layout. See hint Create content/rss.xml with {#include fm/rss.html /} inside. Then add {#rss site /} in your default.html layout, inside the {#head} insert block, to add the <link rel="alternate" type="application/rss+xml"> tag to the page head. See solution Create content/rss.xml: {#include fm/rss.html /} Update templates/layouts/default.html to add RSS discovery. Add this block after the opening ---: {#head} {#rss site /} {/head} So the top of your default.html becomes: --- theme-layout: default --- {@io.quarkiverse.roq.frontmatter.runtime.model.Page page} {@io.quarkiverse.roq.frontmatter.runtime.model.Site site} {#head} {#rss site /} {/head} <div class="min-h-screen bg-slate-50 dark:bg-slate-900 ..."> 🚀 Navigate to /rss.xml in your browser. You should see a valid RSS feed with your blog posts. 11. Deploy to GitHub Pages Your project already includes a .github/workflows/deploy.yml file that handles deployment. ››› CODING TIME Push your blog to GitHub and enable GitHub Pages. See hint Create a repository on GitHub, push your code, then go to Settings > Pages and set the source to "GitHub Actions". See solution git init git add . git commit -m "Initial blog" gh repo create my-blog --public --source=. --push Then in your repository settings: Go to Settings > Pages Set Source to GitHub Actions Your blog will be live at https://your-username.github.io/my-blog/ within a couple of minutes. 🚀 The first run will likely fail because GitHub Pages is not yet enabled. Go to Settings > Pages, set Source to GitHub Actions, then re-run the workflow from the Actions tab. Once it passes, visit your live URL. 🤩 You built a blog from scratch. Custom layouts, a posts collection, pagination, tags, RSS, and deployment. All from a blank base theme, styled with Tailwind, and deployed in one push. What's next? Next in the series: Create a Link-Tree with Roq to build a different kind of site with typed data and auto-generated pages. Here are a few ideas to keep going: Add more posts: create new directories in content/posts/ or use the Editor (press m in the dev terminal). Add a sitemap: roq add plugin:sitemap and it's done. Add search: roq add plugin:lunr for full-text search. You'll need to add three tags to your default.html layout: See hint Create content/search-index.json with {#include fm/search-index.json /} to generate the search index. Then add {#search-script /} in the {#head} slot, {#search-overlay /} at the top of the body, and {#search-button /} in your nav bar. See the Lunr Search plugin docs for details. See solution Create content/search-index.json: {#include fm/search-index.json /} In templates/layouts/default.html, add: {#head}{#search-script /}{/head} {#search-overlay /} And in your <nav>: <nav class="flex gap-4 text-sm items-center"> <a href="/" ...>Home</a> <a href="/about" ...>About</a> {#search-button /} </nav> Press Cmd+K (or Ctrl+K) to try it out. Add images to posts: drop an image in the post directory and set image: photo.jpg in frontmatter. Dark mode toggle: the CSS classes are already in place (dark:), add a JavaScript toggle button. Explore the docs: Roq the basics covers collections, custom data, templates, and much more. Happy building! ### [Create your own Blog with Roq (30min)](/posts/create-a-blog-with-roq/) So you want a blog. Not a WordPress behemoth, not a JavaScript-heavy framework that needs a PhD to configure. Just a clean, fast, good-looking blog that you can write in Markdown and deploy anywhere. That's exactly what Roq is for. It's a static site generator powered by Quarkus, with blazing-fast live-reload, a beautiful default theme, and zero configuration to get started. In about 30 minutes, you'll go from nothing to a fully customized blog deployed on GitHub Pages. Let's go! Note Prerequisites: Install the Roq CLI by following the Getting Started guide. Roq uses JBang, so no JDK installation is needed. Verify your setup with: roq --version Tip For the best development experience, install the Quarkus IDE tooling for your editor (VS Code, IntelliJ, or Eclipse). You get config autocompletion, validation, and Qute template completion. 1. Create your blog Open a terminal and run: roq create my-blog This scaffolds a complete blog project with the default theme, example content, and everything you need. Now start it: cd my-blog roq start 🚀 Hit w or open http://localhost:8080 in your browser. You should see a hero page with a mascot, feature cards, and a sidebar with navigation. Not bad for two commands! Note roq start starts Quarkus in dev mode with live-reload. Every change you save is instantly reflected in the browser, no manual refresh needed. 2. 👀 Explore the generated structure Your project uses the default theme, a Maven dependency that provides layouts, partials, styles, and much more. You don't need to look at the theme source to use it, but it helps to know it's there. Before changing anything, take a minute to look at what was generated: my-blog/ ├── content/ # Your pages and blog posts │ ├── index.html # Home page (also holds site-wide data) │ ├── blog.html # Blog listing page │ ├── about.md # About page │ ├── 404.html # Error page │ └── posts/ # Blog post collection │ └── 2024-10-13-the-first-roq/ │ ├── index.md # Post content │ └── blog.avif # Post image ├── data/ # Data files (YAML/JSON) │ ├── menu.yml # Navigation menu │ └── authors.yml # Author profiles ├── public/ # Static assets (images, favicon…) │ └── images/ │ ├── logo.svg │ └── mascot.svg ├── web/ # CSS and JS (bundled automatically) │ └── _custom.css # Your color overrides ├── pom.xml # Maven build file └── .github/workflows/ └── deploy.yml # GitHub Pages deployment (ready to go!) Here's the mental model: content/ is where you write. Markdown, AsciiDoc, or HTML. Pages at the root, posts in posts/. data/ feeds the sidebar and templates. The menu, authors, and any structured data you want. web/ is for styling. The theme provides the base CSS; _custom.css is your override layer. public/ holds static files served as-is (images, fonts, robots.txt). templates/ doesn't exist yet, but this is where you can create your own layouts, partials, and tags to override the theme. 🚀🔑 This is one of the key things to remember: content goes in content/, looks go in web/, data goes in data/. 3. Make it your own The sidebar displays your site name, description, logo, and social links. All of this comes from the frontmatter in content/index.html, which acts as the site-wide data source. ››› CODING TIME Open content/index.html and personalize the sidebar: Change name to your blog's name Update description with a short tagline about you or your blog Find a nice avatar image (or generate one with AI!), save it as public/images/avatar.png, and update the logo field Update the social-* fields with your own accounts (or remove the ones you don't use) Also open data/authors.yml and update the default author with your own info: name, avatar, bio, and links. See hint The key frontmatter fields for the sidebar are: name (displayed as site name), description (shown below the name), logo (sidebar image, references a file in public/images/), and social-twitter, social-github, social-linkedin (contact icons). For the avatar, try generating one with an AI image tool, or grab a photo and drop it in public/images/. See solution Edit the frontmatter in content/index.html: --- title: Jane's Dev Blog — Thoughts on code, coffee, and building things that work. description: Software developer, open source enthusiast, and occasional writer. name: Jane's Dev Blog simple-name: Jane's Blog image: avatar.png logo: avatar.png social-twitter: janecodes social-github: janecodes social-linkedin: janecodes layout: home --- Edit data/authors.yml: jane: name: Jane Doe nickname: janecodes job: Software Developer avatar: https://i.pravatar.cc/300 profile: https://github.com/janecodes bio: Software developer who loves Java, open source, and writing about what I learn. Drop your avatar image in public/images/avatar.png. 🚀 The sidebar should now show your name, your description, your avatar, and your social links. Looking good! Note The title field in content/index.html is used for SEO (page title, Open Graph, etc.), while name is what appears in the sidebar. You'll typically want title to be a full sentence and name to be short. 4. Customize the navigation menu The sidebar menu on the left comes from data/menu.yml. Right now it has two entries: Blog and About. ››› CODING TIME Open data/menu.yml and add a "Projects" link that points to an external URL. See hint Each menu item has title, path, and icon. For external links, use a full URL. Icons use Font Awesome classes. Add target: _blank for links that should open in a new tab. See solution Edit data/menu.yml: items: - title: Blog path: /blog icon: fa-solid fa-newspaper - title: Projects path: https://github.com/your-username icon: fa-solid fa-code target: _blank - title: About path: /about icon: fa-solid fa-user 🚀 Check the sidebar in your browser. The new links should appear instantly thanks to live-reload. 5. Edit the home page content The body of content/index.html defines what visitors see when they land on your site. Right now it has the default Roq hero with a mascot. Let's make it yours. ››› CODING TIME Open content/index.html and customize the hero section. Change the title, tagline, subtitle, and buttons to reflect your blog's personality. See hint The hero uses the {#roq/hero} tag with nested sections: {#title}, {#tagline}, {#subtitle}, and <a> buttons with btn btn-primary or btn btn-secondary classes. You can remove the logo= attribute if you don't want the mascot image. See solution Replace the body of content/index.html (everything between --- and the end): {#roq/hero} {#title}Welcome to my <span class="shimmer">blog</span>!{/title} {#tagline}Code, coffee, and curiosity{/tagline} {#subtitle}I write about software development, open source projects, and things I learn along the way. Glad you're here.{/subtitle} <a href="/blog" class="btn btn-primary">Read the blog <i class="fa-solid fa-arrow-right"></i></a> <a href="/about" class="btn btn-secondary"><i class="fa-solid fa-user"></i> About me</a> {/} <div class="roq-features"> {#roq/featureCard icon="fa-solid fa-pencil" title="Fresh Articles"} Regular posts about Java, Quarkus, and web development. Short, practical, and straight to the point. {/} {#roq/featureCard icon="fa-brands fa-github" title="Open Source"} I contribute to open source and share what I build. Check out my projects on GitHub. {/} {#roq/featureCard icon="fa-solid fa-mug-hot" title="Coffee Chats"} Sometimes I write about life, productivity, and the things that keep me going as a developer. {/} </div> 🚀 Refresh your browser and admire your personalized home page! 6. Change the colors The default theme ships with warm brown accent colors and sky blue "pop" colors (used for buttons, links, and gradients). All of this is controlled by CSS custom properties in web/_custom.css. ››› CODING TIME Open web/_custom.css and change the accent color to indigo (a modern purple-blue) and the pop color to emerald (a fresh green for energy elements like buttons and links). See hint The theme includes all Tailwind CSS colors as built-in variables: var(--color-indigo-500), var(--color-emerald-300), etc. Replace the hex values in _custom.css with these variable references for each shade (50 through 950). Add a --color-pop-* block for the pop color. See solution Replace the content of web/_custom.css: /* Theme customization: /theme/default/#css-customization */ @theme inline { /* Accent: indigo (sidebar, headings, borders) */ --color-accent-50: var(--color-indigo-50); --color-accent-100: var(--color-indigo-100); --color-accent-200: var(--color-indigo-200); --color-accent-300: var(--color-indigo-300); --color-accent-400: var(--color-indigo-400); --color-accent-500: var(--color-indigo-500); --color-accent-600: var(--color-indigo-600); --color-accent-700: var(--color-indigo-700); --color-accent-800: var(--color-indigo-800); --color-accent-900: var(--color-indigo-900); --color-accent-950: var(--color-indigo-950); /* Pop: emerald (buttons, links, shimmer) */ --color-pop-50: var(--color-emerald-50); --color-pop-100: var(--color-emerald-100); --color-pop-200: var(--color-emerald-200); --color-pop-300: var(--color-emerald-300); --color-pop-400: var(--color-emerald-400); --color-pop-500: var(--color-emerald-500); --color-pop-600: var(--color-emerald-600); --color-pop-700: var(--color-emerald-700); --color-pop-800: var(--color-emerald-800); --color-pop-900: var(--color-emerald-900); --color-pop-950: var(--color-emerald-950); } 🚀 Your site should now have a completely different vibe! Try a few other color combos: rose + amber, cyan + orange, violet + lime... go wild. Now click the moon icon in the top-right corner to toggle dark mode. The theme handles both modes automatically. 🤩 Your blog already looks nothing like the default. And you haven't written a single line of Java. 7. Write your first blog post Time to actually write something. You have two ways to create a post. Option A: The Roq Editor (recommended) In the terminal where roq start is running, press m (for Manage). This opens the Roq Editor in your browser, a rich-text editor with Markdown support, right inside the dev experience. From the editor, click New Post, give it a title, write your content, and save. The file is created for you in content/posts/. Option B: Create the file manually Create a new directory in content/posts/ following the naming pattern YYYY-MM-DD-slug/: mkdir -p content/posts/2026-07-05-my-first-post ››› CODING TIME Create content/posts/2026-07-05-my-first-post/index.md with YAML frontmatter and some Markdown content. Include at least a title, description, tags, and author. See hint Every post starts with YAML frontmatter between --- markers. The key fields are: title (string): the post title description (string): shown in previews and SEO tags (comma-separated): used for categorization date (YYYY-MM-DD or full datetime): publication date author (string): matches a key in data/authors.yml image (string): header image (URL or local file in the same directory) See solution Create content/posts/2026-07-05-my-first-post/index.md: --- title: "My First Blog Post" description: "Hello world! This is my very first post on my brand new blog." tags: hello, blogging date: 2026-07-05 author: jane --- ## Hello, World! Welcome to my blog. I built this site with [Roq](https://iamroq.dev), a static site generator powered by Quarkus. It took me about 30 minutes to go from `roq create` to a fully customized blog. Here's what I like about it so far: - **Live-reload** makes writing a joy. Save the file, see it instantly. - **Markdown** keeps things simple. No complex editors, just text. - **The default theme** looks professional out of the box. I'll be writing about code, open source, and whatever else catches my attention. Stay tuned! 🚀 Head to http://localhost:8080/blog. Your new post should appear in the listing! 🚀🔑 Click on it. Notice how the theme automatically renders the reading time, the date, the author info (pulled from data/authors.yml), the tags, and even social sharing buttons. All from a simple Markdown file with a few frontmatter fields. Note You can also add an image to your post. Drop an image file (e.g. cover.jpg) in the same directory as your index.md and add image: cover.jpg to the frontmatter. It becomes the post header image and the social media preview. 8. Power up with plugins Roq has a plugin system. Each plugin is a Quarkus extension you can add with a single command. Let's add three useful ones. Tagging If your posts use tags in frontmatter (yours already does!), the tagging plugin auto-generates tag pages so visitors can browse posts by topic. roq add plugin:tagging That's it. The default theme already includes a tag layout, so tag pages are immediately available. 🚀 Click on a tag in your blog post. You should land on a page listing all posts with that tag, for example /posts/tag/blogging. Faker (development helper) Writing a blog with just one post makes it hard to see how pagination and layouts work at scale. The faker plugin generates realistic fake posts during development. roq add plugin:faker Then add this to config/application.properties (or src/main/resources/application.properties): quarkus.roq.faker.count.posts=20 🚀 Refresh your blog listing. You should now have 20+ posts with random titles, images, tags, and lorem ipsum content. The fake posts only exist in dev mode and are never published. 🤩 Scroll through your blog. With pagination, tags, and a pile of content, your blog is starting to feel like a real site! Search (optional) For full-text search powered by Lunr.js, add the search plugin: roq add plugin:lunr ››› CODING TIME Override the theme's main.html layout to add the search overlay and button. See hint You need two things: a content/search-index.json file that generates the search index, and a templates/layouts/main.html override with three Qute tags: {#search-overlay /} for the modal, {#search-button-input /} for the trigger in the sidebar, and {#search-script /} in the head. See the Lunr Search plugin docs for details. See solution Create content/search-index.json: {#include fm/search-index.json /} Create or edit templates/layouts/main.html: --- theme-layout: main --- {#head}{#search-script /}{/head} {#search-overlay /} {#insert /} {#menu} {#search-button-input /} {#include partials/roq-default/sidebar-menu menu=cdi:menu.items /} {/} 🚀 Refresh your blog and press Cmd+K (or Ctrl+K). A search overlay should appear, letting you search across all your posts. 9. Deploy to GitHub Pages Your blog already includes a .github/workflows/deploy.yml file that handles everything. All you need is a GitHub repository. ››› CODING TIME Push your blog to GitHub and enable GitHub Pages. See hint Create a new repository on GitHub (e.g. my-blog) Push your code using git init, git add, git commit, and git push In the repository settings, go to Pages and set the source to GitHub Actions See solution git init git add . git commit -m "Initial blog" gh repo create my-blog --public --source=. --push Then go to your repository on GitHub: Click Settings > Pages Under "Build and deployment", set Source to GitHub Actions The workflow triggers automatically on push to main. It also runs daily at 5:00 UTC to publish any scheduled content (posts with a future date). Your blog will be live at https://your-username.github.io/my-blog/ within a couple of minutes. 🚀 The first run will likely fail because GitHub Pages is not yet enabled. Go to Settings > Pages, set Source to GitHub Actions, then re-run the workflow from the Actions tab. Once it passes, visit your live URL. 🤩 Your blog is live on the internet. You built it from scratch, customized the theme, wrote your first post, and deployed it. All in about 30 minutes. What's next? Next in the series: Create a Link-Tree with Roq or go deeper with Create a Blog from Scratch to learn how layouts, collections, and templates work under the hood. Here are a few ideas to keep going: Write more posts! Just create new directories in content/posts/ or use the Editor (m). Add an RSS feed: create content/rss.xml with {#include fm/rss.html /} inside. Add a sitemap: roq add plugin:sitemap and it's done. Override theme templates: create templates/layouts/post.html to customize the post layout. The theme is your base, not your cage. Explore the docs: Roq the basics covers collections, custom data, templates, variables, and much more. Happy blogging! ### [Collapsible Sections: Hide and Reveal Content in Your Posts](/posts/collapsible-sections-hide-and-reveal-content-in-your-posts/) The Roq default theme now includes styled collapsible sections using the standard HTML <details> and <summary> elements. They work in both Markdown and AsciiDoc content, with a pill-shaped toggle that expands on open, an animated arrow, and a fade-in effect. In Markdown Use standard HTML <details> and <summary> tags directly in your .md files: <details> <summary>Click to reveal</summary> Your hidden content here. **Markdown formatting** works inside. </details> Here is how it looks: Click to reveal Your hidden content here. Markdown formatting works inside. Tutorial hints and solutions Collapsible sections are a great fit for tutorials where you want to give readers a chance to try on their own before revealing the answer: Hint Use the @Path and @GET annotations on a resource class. Return a plain String. Solution @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello from Roq!"; } } Step-by-step instructions Multiple consecutive collapsible sections stack with a small gap between them: Step 1: Create the project quarkus create app my-app Step 2: Add an extension quarkus ext add rest-jackson Step 3: Start dev mode quarkus dev Open http://localhost:8080/hello to see your endpoint. In AsciiDoc AsciiDoc content has its own collapsible styling using the %collapsible option on an example block: .Click to expand [%collapsible] ==== Hidden content here. ==== Add the .result role for a distinct output/result look: .Show result [%collapsible.result] ==== Result content with a background. ==== Both Markdown and AsciiDoc collapsible sections are styled by the default theme with no extra configuration needed. See them in action on the Markdown markup test and AsciiDoc markup test pages. ### [Generate Open Graph Images for Social Sharing with Roq](/posts/generate-open-graph-images-for-social-sharing-with-roq/) Social networks and chat apps use Open Graph metadata to build link previews. Roq already renders {#seo /} tags from frontmatter — the OG Card plugin closes the loop by generating 1200×630 PNG cards at build time and injecting og:image metadata for pages you choose. Cards are rendered from Qute SVG templates via Apache Batik at build time and published as static PNG files under /og/. Installation Add the plugin dependency: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-og-card</artifactId> <version>${quarkus-roq.version}</version> </dependency> Or use the Roq CLI: roq add plugin:og-card Configuration Choose which pages get generated cards: quarkus.roq.plugin.og-card.collections=posts quarkus.roq.plugin.og-card.include-paths=/about/ quarkus.roq.plugin.og-card.template=og-card/default-card.svg quarkus.roq.plugin.og-card.site-name=My Site quarkus.roq.plugin.og-card.output-prefix=/og collections — generate cards for posts in named collections include-paths — generate cards for standalone pages (e.g. /about/) template — Qute SVG template under templates/og-card/ site-name — branding text on the card Pages with an existing image:, img:, or picture: frontmatter are skipped by default (skip-if-image-set=true). Custom card template Create templates/og-card/my-card.svg with a fixed 1200×630 viewBox. The plugin passes a card data object with pre-wrapped line arrays for multi-line rendering: <text x="72" y="170" font-size="52"> {#for line in card.titleLines} <tspan x="72" dy="{line_isFirst ? '0' : '62'}">{line}</tspan> {/for} </text> <text x="72" y="260" font-size="28"> {#for line in card.descriptionLines} <tspan x="72" dy="{line_isFirst ? '0' : '34'}">{line}</tspan> {/for} </text> <text x="72" y="582">{card.siteName}</text> Set max-text-width to limit how wide text can flow before wrapping — useful when graphics occupy part of the card. The Roq blog dogfoods a branded roq-card.svg that inlines the Roq mascot SVG paths and sets max-text-width=700 to keep text clear of the mascot (Batik cannot resolve external image URLs during render). Viewing your cards In dev mode, browse to a generated PNG directly: http://localhost:8080/og/about.png With generator batch enabled, PNGs land on disk: QUARKUS_ROQ_GENERATOR_BATCH=true mvn package quarkus:run ls target/roq/og/ For the full configuration reference, check out the doc. ### [Generate first class citizen pages from your data](/posts/generate-first-class-citizen-pages-from-your-data/) You have all that nicely structured content in yaml ou json, and all you would like is to generate static pages out of it. May that be events, catalog or any type of roq collection really. However, if it was already possible to declare data using Roq via roq-data, for which it is even possible to have strong typing, it was not possible to extract pages directly from them. One could create pages by hand that used the data, or, as Stéphane Philippart did, generate pages programmatically from these…​ The first solution is useful if you only want a single page containing all the content. This is the case, for example, in this blog for the list of events that I gave. However, it is not possible to send a specific link to a particular talk. Since Roq version 2.1, it is possible to automatically generate a page for each collection item simply by declaring it. In the configuration file we just need to add: application.properties site.collections.events.layout=event (1) site.collections.events.from-data.id-key=id (2) 1 Every collection item will be rendered using the events layout, which source can be found under <root_dir>\templates\layouts\events.html. 2 The property name to be used as identifiant In this case one item of this collection would be found under the https://iamroq.dev/events/<slugified form of the title>. And that’s all. There is NOTHING else to do. Ok, maybe the layout is still work to do. Ok. A minor peculiarity, for the moment item will be made available for the templating through the page data, even you have specified @DataMapping. To access it in your Qute template you will have to use {=page.data.title} (or {page.data.title} if you are not using the alternative expression syntax). The feature is beeing particularly usefull in this AI era where LLM are so keen to generate structured output. We do not particularly like generated post, but it can be very usefull for summaries, check https://devoured.fyi/ that make good use of this feature !!! Full documentation is already available. Illustration by Lucas Santos ### [Devoured: My Healthy Instagram for Tech News](/posts/devoured-my-healthy-instagram-for-tech-news/) I wanted to stay up to date in this new AI era where everything is changing so fast. But there is so much information that keeping up feels impossible. I saw the TLDR newsletter popping up on Instagram and thought it looked promising. But I'm not a fan of email newsletters, and I found the format hard to digest. Then it hit me: I spend way too much time on Instagram swiping through content. What if I could channel that energy into something actually useful? What if I could build my own healthy Instagram for tech news, powered by AI and Roq? That's how devoured.fyi was born. The idea Take the best tech feeds, let AI digest them even further, remove ads and noise, and serve it all as a clean, swipeable daily digest. Roughly 15 to 20 condensed articles per day, incremental enough that I can skim the one-liners but still dive deeper when something catches my eye. How it works A GitHub Action runs every morning. It fetches RSS feeds from multiple sources (AI, Tech, DevOps, Data, Design), processes them through a JBang script, and uses the Gemini free tier to generate structured summaries for each article. The output is a JSON file per day, committed directly to the repository. It took a while to tweak the GitHub Action to perfection, mainly to avoid going over the Gemini free tier limits. I ended up using Mutiny for reactive batching with controlled concurrency, combined with caching and careful rate limiting. Gemini initially gave pretty poor quality results. I spent a lot of time comparing its output against Claude's to identify the gaps, then slowly iterated on the prompts until Gemini's summaries reached a quality level comparable to Claude. It's a good reminder that prompt engineering matters as much as model choice. Roq data as the engine This is where Roq shines. Each daily digest is a JSON file in the data/digest-posts/ directory (e.g. data/digest-posts/2026-05-05.json). Roq's data feature turns that directory into two collections automatically: one aggregated collection containing all digests, and individual entries for each day. No Java code needed for the basic setup, just drop JSON files and they're available in your templates. Each JSON file contains frontmatter-like fields (title, date, layout, tags) alongside the digest content (sections with articles, summaries, one-liners, and decoder entries). Roq maps it all to template variables accessible through Qute: { "title": "Devoured - May 05, 2026", "layout": "digest-post", "date": "2026-05-05", "sections": [ { "name": "AI", "articles": [ { "title": "Anthropic and OpenAI Launch Enterprise AI Ventures", "one-liner": "Both are launching enterprise AI joint ventures...", "summary": { "what": "...", "why": "...", "takeaway": "..." } } ] } ] } Making static feel dynamic with localStorage One challenge with a static site: how do you track what the user has already read? The answer is localStorage. Devoured uses it to remember which articles you've seen, your preferred sections, and your reading preferences. All client-side, no backend, no accounts. The site feels dynamic and personalized while remaining fully static. The result After weeks of tweaking the prompts, the visual layout, and the content density, I now have exactly what I wanted: a quick daily swipe through the most relevant tech news, condensed by AI, with the option to go deeper on anything interesting. It's free, open source, and built entirely with Roq. Links: devoured.fyi Source on GitHub Roq data documentation TLDR newsletter (the original inspiration) ### [How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq](/posts/how-ai-helped-me-rebuild-my-blog-and-move-from-jekyll-to-quarkus-roq/) Introduction I don't blog that often. It's not because I don't have ideas, quite the opposite. I have lots of them: ideas I want to share, experiments I want to document, things I want to remember. The real problem has always been time. And yet, a personal blog is such a great place for all of this: sharing ideas, writing things down before they're forgotten, and keeping track of what you've learned along the way. How many times have you been in this situation? "I've already used this technology before… but it was on a customer project." Three years later, you're asked to work on it again, and you have absolutely no idea how you did things back then. A blog is a memory extension. A personal site is also a good way to keep your CV up to date: to show what you've worked on, what you've learned, and how your experience evolves over time. It's not just about writing posts, it's about telling your professional story. Another thing that slowed me down was language. I'm never fully sure whether I should write in English or French. English makes more sense if I want to reach more people, but it's not my native language, so I've always felt a bit uncomfortable writing long-form content in it. On top of that, my blog itself felt old. The design was… okay-ish. Good enough when I started. I used a Jekyll theme made by someone else (Freshman21), and I'm genuinely thankful to the author for that work, it helped me get started. But over time, I realized the site didn't really match my personality anymore. The theme felt too "lambda" (too generic). On the technical side, Jekyll with GitHub Pages works fine, but as soon as you want to customize things, you start feeling a bit naked. I also kept wishing for a site generator more aligned with my daily work, something Java-based. So yeah: time, language, visual identity, and technology were all reasons why I kept postponing this refresh. But recently, AI changed the equation. This is what my blog looked like before the redesign and migration. The old Jekyll version is now archived at https://sunix.github.io/old-jekyll-blog.sunix.org/ Keeping Jekyll, Redoing the Design (Without a Theme) Let's be honest: I'm not very good at design stuff. I may have ideas, but CSS and I… we don't really get along. And it's not only about CSS. Whether it's Jekyll with Ruby, or even Hugo, I only know the basics. Once you want to go a bit further, things quickly become harder, and progress slows down. At some point, I seriously considered switching technologies. I had heard about Quarkus Roq some time ago, and the idea of a Java-based static site generator was very appealing. But there was one big question in my head: How am I going to migrate my existing Jekyll theme to Roq? After discussing with Andy, the creator of Roq, it became clear pretty quickly that this wouldn't be easy, at least not at my level. Roq uses Qute as its templating engine, which is very different from what Jekyll uses. I took a look at how a Roq theme is structured, and honestly, it's quite straightforward. But migrating the Freshman21 Jekyll theme to Roq? That felt like a rabbit hole I really didn't want to go down. So I gave up. That was last summer. A Real Project, at the Right Time Pretty much at the same time, I got a call from my tennis club asking for help. Recently, the FFT (French Tennis Federation) shut down all CMS-based websites for many tennis clubs across France. Since I'm part of one of those clubs, I volunteered to rebuild a brand-new website from scratch. It turned out to be the perfect opportunity to finally try Roq. This time, I didn't have the Jekyll theme migration problem. No legacy constraints, no existing templates to port. I could start fresh. So I asked ChatGPT to generate a pure HTML/CSS website that could later be moved easily to any static site generator. The result was simple and clean: Tailwind CSS for styling Two static HTML pages: a main page with different sections an article page with a real content structure Once I had that, moving it to Roq was surprisingly straightforward. I extracted the common parts (header, footer, navigation) into templates/partials HTML fragments, then created proper layouts for: the main pages the article/post pages Each layout simply includes the relevant partials. For some parts of the site, I needed dynamic content. That's where I really enjoyed working with Roq. Instead of fighting a plugin system, I could just write Java. For example, I wanted file and image attachments to be automatically added to certain pages for download. This wasn't available out of the box, but creating a Qute template extension in Java was trivial. I ended up with a small helper like this: https://github.com/tc11-fr/tc11.fr/blob/main/src/main/java/fr/tc11/FilesViewHelpers.java Which is then used directly inside templates, for example here: https://github.com/tc11-fr/tc11.fr/blob/main/templates/layouts/post.html#L55 An example article page displaying an image and a downloadable attachment, powered by a custom Java extension. That moment really clicked for me. I wasn't hacking around limitations anymore, I was extending the system in a clean, explicit way, using a language I'm comfortable with. The Paris 11 Tennis Club website 🎾 is now live: 🌐 Website: https://tc11.fr/ 💻 GitHub: https://github.com/tc11-fr/tc11.fr Coming Back to My Blog, Starting With the Design With the Paris 11 Tennis Club website done, I finally had my first real Roq static website in production. At that point, I knew two important things. First, I wouldn't need to rely on an existing theme to get a nice design anymore. Second, I could rely on AI to handle most of the CSS part, which, for me, is a huge relief. I also realized that my initial idea, migrating an existing Jekyll theme to Roq, was simply not the right strategy. The theme itself was the problem. Migrating it would take a lot of time, and I'd rather spend that time building my own design that fits my personality. Getting rid of the Freshman21 Jekyll theme on my blog would make the transition from Jekyll to Roq much easier. The theme was the real blocker, not the content. So I came up with a simple plan. The first step was to completely drop the existing Jekyll theme and ask ChatGPT to generate a fresh website skeleton with my design. Just like I did for the tennis club site, the idea was to start with a pure HTML/CSS static website. From there, I could extract reusable HTML fragments and make them work with Jekyll. Once I had the new design running with Jekyll, without any theme, I knew the final step would be much simpler: moving the site to Quarkus Roq. Finding a Visual Direction (With a Lot of Help) When it came to design, I had ideas… but nothing very precise. My very first prompt was extremely simple: "I'd like to refresh my blog website from a design point of view. Can you propose something cool?" ChatGPT came back with several design directions. They were all fine, but nothing really clicked. Everything felt a bit too safe, a bit too generic. So I pushed further: "I want something with a lot of colors, but still tech-oriented. Something that inspires joy." It generated a few concepts, but they didn't really feel joyful. After asking for something colorful and tech-oriented, I wondered what would happen if I went even further: "Ok, what would a street art style look like?" The results were interesting: bold, very expressive, lots of neon colors and heavy contrast. After ruling out the street art direction, I tried to refine the idea instead of pushing it further: "Street art that inspires creativity… but also code." Once again, ChatGPT proposed several examples. Most of them were interesting, but still not there. And then one image caught my eye. It wasn't loud. It wasn't aggressive. But it was expressive, colorful, and clearly modern. After a bit of digging, I realized it came from an article called "How To Design the Perfect Hero Image": https://htmlburger.com/blog/hero-image-guide/ From Inspiration to a First Hero Illustration Attempt I really liked the hero image style I had discovered, so the next step felt obvious: try to generate my own version of it. I described what I had in mind like this: "A banner with a drawing similar to the 'How To Design The Perfect Hero Image' example: mostly black-and-white with a few elements in color. But instead of a woman on a couch, a developer wearing headphones, coding, and generating code that transforms into something (I don't know what yet)." The idea was clear: mostly black and white a calm, focused developer a splash of color to represent creativity code flowing out and turning into ideas ChatGPT generated an image… and it was close. Very close. But not quite what I wanted. Narrowing the Style: Less Noise, More Intention At that point, I realized something important: the problem wasn't the idea, it was the style. So I copied the reference image and told ChatGPT, very clearly: "No, in this style." That made all the difference. The illustration wasn't black and white as I originally suggested, but the result was even better: a pastel style with controlled colors. And the developer really looked like me… except for the missing glasses. Then I added one last detail: "And with glasses." 👓 And honestly… that was it. It felt modern, joyful, and clearly tech-oriented. Most importantly, it finally felt like my blog. From a Pretty PNG to a Working Website Skeleton Once the hero illustration direction was clear, I wanted to stop "designing in my head" and start moving pixels on a real page. So I asked for what I actually needed: not another image, not a moodboard, but a concrete, runnable mockup. "Can you make me a complete mockup in this style?" "Make me an HTML/CSS/Tailwind template, ready to plug in." That changed the workflow completely. Instead of iterating on vague concepts, I now had a full HTML page with: a navbar (Articles, Tags, About…) a hero section designed around the illustration basic typography, spacing, and layout rules a structure reusable for posts and content pages The first version used Tailwind via CDN, which was perfect for prototyping: copy/paste, open a browser, iterate fast. Getting the Hero Illustration Into the Skeleton (The "Transparent Background" Trap) My skeleton was working, but it was still missing the most important part: the hero illustration. So I tried the obvious next step: ask ChatGPT to generate the same hero image again, but with a transparent background. In theory: perfect. In practice: not really. The new image had a transparent background, as requested, but some details had disappeared, the mouth was missing, and the rocket and the light bulb looked unfinished. It felt more like a draft than the polished illustration I had before. It was a good image… just not the one. So I went back to the previous "perfect" image. AI got me 90% of the way, and for the last 10%, I brought out the classic: GIMP. I extracted the character + desk + rainbow flow, cleaned the edges, removed the background, and exported a version ready to be integrated into the HTML page. Not glamorous, but effective. And once it was done… huhey! 🎉 I finally had the hero image exactly the way I wanted, in a format that actually works on the web. At that point, the design was no longer just an idea. I had the hero illustration, the HTML layout, and the Tailwind CSS, something real I could build on. The next step was simple: make it work with Jekyll first. I wasn't going to redesign and migrate to Quarkus Roq all at once. I preferred small steps, each one reducing risk and keeping things manageable. GitHub Issues Driven Development (a.k.a. Coding Without an IDE) Recently, I adopted a new way of building applications with AI, and without even opening an IDE. I call it GitHub Issues Driven Development. The idea is simple: instead of starting in my IDE, I start with GitHub Issues. For each new feature or bug fix, I create an issue, assign it to @Copilot, and let it handle the first iteration. Copilot creates a pull request and does the work. I don't sit there watching it, I just go on with my day. Then, during my next coffee break, I come back to review the PR. If something isn't right, I leave feedback directly in the PR comments, mentioning @copilot. It adjusts the code, I review it again, and we iterate like that. Short cycles, low mental load, very little context switching. Most of the time, I end up merging the PR without ever opening my IDE. I've already written more details about this workflow in a previous post: 👉 https://blog.sunix.org/posts/building-a-gift-card-management-app-with-github-copilot-my-first-completed-side-project/ Applying This Workflow to My Blog Redesign So once I had the HTML/CSS skeleton and the hero illustration ready, I didn't start manually refactoring files. Instead, I went back to GitHub. In my blog repository, I created a new issue with a very explicit description: Title: Redesign the website Remove the current theme Use a custom design inspired by the provided HTML/CSS Include the new hero illustration on the main page I pasted: the generated HTML/CSS the hero illustration That was enough. Copilot picked up the issue and did the heavy lifting: removed the existing Jekyll theme reorganized the layouts integrated the new design wired everything together so the site still builds correctly You can see the issue here: 👉 https://github.com/sunix/blog.sunix.org/issues/44 And the resulting pull request here: 👉 https://github.com/sunix/blog.sunix.org/pull/45 Of course, it wasn't perfect on the first try. I had to adjust a few minor things, fix small issues, and guide Copilot through comments. But overall, it did the job. More importantly, it fit perfectly within my constraints: limited time, short bursts of focus, and the desire to keep momentum without mentally reopening a big "side project." Previewing a Pull Request (Without an IDE) With this workflow, working mostly from GitHub and without an IDE, it's hard to validate changes just by looking at a diff in a pull request. Sure, GitHub Copilot runs tests (and you can trust them… to a point), but when you're working on a website, you really want to see the result. With Quarkus Roq in a local development environment, I would normally just run: roq But here, I'm only using the GitHub UI and reviewing a PR. So I needed a way to preview what Copilot generated. The /preview Command On several GitHub Pages projects I've worked on recently, I set up a simple but very effective mechanism: a /preview comment on a pull request. When I comment /preview on a PR, it triggers a GitHub Action that: checks out the PR branch builds the static site deploys it to surge.sh (a static site hosting service with a free plan, more than enough for preview environments) posts the preview URL directly as a comment on the PR adds a clear banner to indicate that the preview site is not the production version Technically, the deployment is very simple: build the site, then run the surge command. This gives me a real, clickable version of the site to review during my coffee break, exactly what I need when I'm not opening an IDE. I simply comment /preview on the PR, and the preview site is automatically deployed and made available. If the deployment fails, I can investigate by checking the GitHub Actions workflow logs. In some projects, I also added a banner to clearly indicate that this is only a preview site, with a link back to the corresponding pull request. Bootstrapping the Feature With… an Issue And yes, I use the same GitHub Issues Driven Development workflow to set this up. When I want this feature in a new GitHub Pages project, I usually create an issue like this: Title: Add a preview command in GitHub PR comments to have a preview on surge.sh Description: Getting inspiration from this PR: https://github.com/SCIAM-FR/sciam-fr.github.io/pull/151 Assignee: @copilot Most of the time, Copilot proposes a pull request that includes: the GitHub Action workflow the /preview command handling the deployment logic From there, I review it, tweak it if needed, and merge it. This preview mechanism is what makes the whole workflow viable. Without it, reviewing HTML/CSS changes blindly would be frustrating. With it, I can confidently validate design changes, layouts, and content, even when everything happens through GitHub. Moving from Jekyll to Quarkus Roq At this point, I had a Jekyll site without a theme. No complex plugins, no hidden magic, just content, layouts, and HTML. So I thought: This should be easy now. Time to move to Quarkus Roq. And of course, I followed the same workflow as before: create a GitHub issue and assign it to @copilot. Since I had already built a GitHub Pages site with Roq for the tennis club, I simply reused that repository as a reference. Issue: https://github.com/sunix/blog.sunix.org/issues/60 Title: Replace Jekyll with Quarkus Roq Description: Getting inspiration from https://github.com/tc11-fr/tc11.fr Copilot picked it up and produced a pull request: 👉 https://github.com/sunix/blog.sunix.org/pull/61 At first glance, it looked good. The project structure was there, the site was building, and the content was being rendered. But… it didn't work out of the box. Reality Check: Migration Is Never Just One Issue Once I started testing things more carefully, a few problems surfaced. So I did what I now do instinctively: I created more issues. https://github.com/sunix/blog.sunix.org/issues/63 Qute was interpreting ${current.class.fqn} inside code blocks as a template expression, causing rendering failures with errors like: Key 'current' not found. (Note: Roq 2.1 now supports an alternative expression syntax using {# and {= prefixes, which avoids this issue entirely.) https://github.com/sunix/blog.sunix.org/issues/65 The GitHub workflow was uploading the GitHub Pages artifact twice. The quarkiverse/quarkus-roq@v1.1 action already uploads it, and the workflow tried to upload it again, causing a conflict. Each issue described one concrete problem. Each one was assigned to @copilot. And step by step, Copilot fixed them. At that point, everything still wasn't perfect. But the important thing was this: 👉 The site was now running on Roq. That was a big milestone. From there, I wasn't migrating anymore, I was improving. The Next Problems to Solve Once the migration was complete, a new checklist appeared. The site was running on Roq, but it wasn't fully production-ready yet. Here's what still needed to be addressed: Excerpts were no longer working Old URLs were broken and needed proper redirects Disqus comments were gone Tailwind was still being loaded via CDN instead of using a production build None of these issues were critical on their own. The site worked. The content was accessible. But together, they made the difference between: "It runs." and "It's clean, polished, and production-ready." Each of these problems deserved its own issue, and its own iteration. And that's exactly how I approached them. Bringing Back Excerpts (<!-- more -->) One thing that immediately stood out after the migration was the lack of excerpts. On my Jekyll blog, I was using the classic <!-- more --> marker to define a short preview of each post, displayed on the homepage. It's a small detail, but an important one: excerpts make the homepage more readable and give context before clicking into an article. After migrating to Quarkus Roq, that feature was simply gone. The posts were still there, but the homepage list only showed metadata: title, tags, date, and a "read more" button. No preview text anymore. After migrating to Roq, the homepage list only displays metadata (title, tags, date) and a "read more" button, the excerpt is missing. This is how it should be: meaningful preview text extracted from each article, giving context before clicking "read more." I noticed it right away during the migration PR, but I deliberately chose not to fix it there. The migration pull request was already quite large, and I prefer one PR per concern. Also, the site was usable without excerpts, so it wasn't blocking. I even documented the limitation directly in the PR comments: Note on article excerpts: Roq doesn't expose the content before <!-- more --> through the template API. The articles contain the marker in their Markdown, but there's no excerpt or content property available on the post object. The current layout only shows tags, title, date, and the read more button. At that point, it was clear that this wasn't just a template tweak. Turning It Into a Proper Issue Later on, once the migration had stabilized, I went back to my usual GitHub Issues Driven Development workflow. I created a dedicated issue: https://github.com/sunix/blog.sunix.org/issues/68 Title: Implement Excerpt capability In the issue description, I explained the problem and proposed a technical solution. To extract the content before <!-- more -->, we would need to: Create a Qute @TemplateExtension that adds an excerpt() method to DocumentPage Access the raw Markdown content Parse it and extract everything before the marker Convert it to HTML Strip the HTML tags and return clean preview text In short: this wasn't just templating, it required custom Java code. I even sketched the idea directly in the issue: @TemplateExtension public class ExcerptExtension { public static String excerpt(DocumentPage post) { // Read markdown file // Extract content before <!-- more --> // Convert to HTML // Strip tags return extractedText; } } And then I concluded the issue with a simple decision: Let's create this extension. And… It Worked 🎉 Copilot picked up the issue and implemented the whole solution in this PR: 👉 https://github.com/sunix/blog.sunix.org/pull/69 And just like that, excerpts were back. The homepage now displays meaningful previews again, just like it did with Jekyll, but this time implemented cleanly in Java as a proper extension. This was one of those moments where Roq really shined for me: No plugin hacks No fragile template tricks Just explicit, readable Java code Problem solved. Hooray 🚀 Update: Since Roq 2.1, content abstracts are now built in. You can use {=post.contentAbstract} in your templates to get a word-limited preview of any post, no custom extension needed. Tailwind in Production Once the site was running on Roq, I opened the browser console. And there it was: (index):64 cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation And indeed, I was still using: <script src="https://cdn.tailwindcss.com"></script> <script> tailwind.config = { darkMode: "class", theme: { extend: { fontFamily: { ... }, boxShadow: { ... } } } } </script> In this mode, Tailwind downloads a JavaScript file in the browser and generates the final CSS dynamically at runtime. That's perfectly fine for development. But not ideal for production. Why CDN Tailwind Is Not Ideal There are a few reasons: It relies on an external JavaScript resource (which can be problematic in restricted environments). The JS file is larger than the compiled CSS would be. CSS is generated dynamically in the browser on every page load. Slightly slower performance overall. For a proper production setup, Tailwind should: Scan your templates at build time. Generate only the CSS classes you actually use. Output a small, optimized static CSS file. That's the correct way to use Tailwind in production. Doing It Properly With Roq I had already solved this problem for the Paris 11 Tennis Club website. At first, I implemented a manual Maven exec command: 👉 https://github.com/tc11-fr/tc11.fr/pull/111 Later, I discovered that Quarkus Roq supports Web Bundler + Tailwind (and since Roq 2.1, Tailwind works with zero configuration via the default theme), so I switched to the proper integration: 👉 https://github.com/tc11-fr/tc11.fr/pull/120 So applying the same approach to my blog looked straightforward. I implemented it here: 👉 https://github.com/sunix/blog.sunix.org/pull/85 But… it wasn't working. This is what happened when switching to dark mode: the text color changed, but the background didn't, leaving grey text on a white page and making the article difficult to read. Dark Mode Was Broken After switching to the production build setup, the dark/light mode toggle stopped working properly. Copilot tried to fix it: 👉 https://github.com/sunix/blog.sunix.org/pull/93/commits/b74d4bd5fbe4053882d9f98805b8d72f1f9b4ed9 But the fix only addressed the button state, not the actual theme-switching logic. So I went into debugging mode. I compared the new version with an older working one and started investigating the differences. During a live Copilot session, I finally discovered the real issue: 👉 I wasn't using the correct Tailwind version. I provided some guidance, but Copilot did most of the work, and helped uncover that the project was using an outdated version of the Quarkus Web Bundler. The setup was supposed to work with Tailwind CSS 4, but my project was effectively using an older configuration via an outdated Quarkus Web Bundler plugin. Even worse, Copilot hadn't initially spotted that the project was using older versions of: Quarkus quarkus-web-bundler-tailwindcss Once I upgraded the relevant dependencies, everything fell back into place. The final fix was mainly: Upgrading Quarkus Upgrading the Tailwind Web Bundler extension Aligning everything with Tailwind CSS v4 Tip: Since Roq 2.1, you can simply run roq update to keep all your dependencies in sync and avoid this kind of version mismatch. After that, dark mode worked perfectly again, and Tailwind is now compiled at build time into a properly optimized CSS file. Tailwind is compiled at build time in both dev and production modes, so what you see in dev mode is the same optimized output you get in production. After upgrading the dependencies, dark mode finally behaves as expected, proper background, proper contrast, fully readable content. Minor but Important: Old URL Redirects Another issue appeared after the migration. Over the years, I had shared blog posts on social media using Jekyll-style URLs like: /articles/howto/2026/01/11/feeling-powerful-with-just-a-browser.html But the new Roq site uses cleaner URLs like: /posts/feeling-powerful-with-just-a-browser-working-around-a-broken-tennis-booking-system/ The result? Ugly 404 pages for old links. Not great. These links were already out there, on social media, in bookmarks, maybe even in other blog posts. Breaking them wasn’t acceptable. First Attempt: The Wrong Direction Once again, I created an issue: 👉 https://github.com/sunix/blog.sunix.org/issues/80 Copilot’s first solution was to generate static HTML redirect files using a separate Main class. Technically, it worked. But architecturally? I didn’t like it. It felt like stepping outside the spirit of Quarkus Roq. It introduced a custom mechanism that lived outside the framework instead of using the tools already provided by Roq. It solved the problem, but not in the right way. The Proper Way: Plugin Aliases While researching, I discovered the Roq plugin-aliases feature: 👉 https://iamroq.dev/docs/plugins/#plugin-aliases That looked much cleaner. So I commented directly in the pull request: @copilot Sorry, I don’t like the idea of going outside Quarkus Roq with an external Main class. Could we explore aliases instead? That was the right direction. The final implementation uses Roq’s alias mechanism properly: 👉 https://github.com/sunix/blog.sunix.org/pull/83/changes Each old article now defines its legacy paths as aliases. Yes, it required updating each article to declare its old URLs. But that’s actually what I wanted: explicit, controlled redirects, fully inside Roq. Clean. Maintainable. Aligned with the framework. And That’s It There were other small fixes and improvements along the way. But these were the most interesting ones: redesigning without a theme using AI for mockups and iteration GitHub Issues Driven Development previewing PRs with /preview migrating to Quarkus Roq implementing excerpts properly fixing Tailwind for production handling old URL redirects cleanly This whole journey wasn’t just about changing a blog engine. It was about: reducing friction owning the design simplifying the stack and building in small, controlled iterations AI has been a game changer in recent months. Things are moving very fast. I would never have done all of this without it. Sometimes you have ideas, and you know how something works in theory, but implementing it is slow and painful. For me, CSS is one of those areas. It’s not that I don’t understand it… it’s just time-consuming and frustrating. AI helped remove that friction. But in the end, this isn’t just something generated by a machine. I now have a design that truly fits my personality. I made the decisions. I iterated. I refined. AI was a powerful assistant, not the author. And honestly, I don’t think I would have gone this far without these tools. That’s where I am right now. If you’re considering moving to Quarkus Roq, or refreshing your blog design, I hope this gives you ideas, and maybe the confidence to try. You don’t need to be a designer. You don’t need to have weeks of free time. You just need a few good issues… and a couple of coffee breaks ☕🚀 Last but not least, if you liked this post, feel free to leave a ⭐ on the GitHub repository of my blog https://blog.sunix.org. It helps me know the content was useful to someone 😉 Happy coding. Happy blogging. ### [GFM Alert Blocks: Styled Callouts in Your Markdown](/posts/gfm-alert-blocks-styled-callouts-in-your-markdown/) Roq now supports GitHub Flavored Markdown (GFM) alert blocks (also known as admonition blocks) — the styled callouts you see on GitHub READMEs and issues, complete with icons and color themes: Note Useful information that users should know, even when skimming content. Tip Helpful advice for doing things better or more easily. Important Key information users need to achieve their goal. Warning Urgent info that needs immediate user attention to avoid problems. Caution Advises about risks or negative outcomes of certain actions. How It Works Alert blocks use a special blockquote syntax with a type identifier: > [!NOTE] > Your note content here. The five standard types are: Type Icon Color Use Case NOTE Info circle Blue General information TIP Light bulb Green Helpful suggestions IMPORTANT Verified badge Purple Critical information WARNING Alert triangle Orange Potential issues CAUTION Stop octagon Red Dangerous actions Icons are from GitHub Octicons (MIT license). Custom Alert Types You can configure custom alert types beyond the standard five. Add this to your application.properties: quarkus.qute.web.markdown.plugin.alerts.custom-types.INFO=Information quarkus.qute.web.markdown.plugin.alerts.custom-types.BUG=Known Bug quarkus.qute.web.markdown.plugin.alerts.custom-types.SECURITY=Security Notice Then use them in your markdown: > [!INFO] > This is a custom info alert. > [!BUG] > This is a known issue. > [!SECURITY] > This is a security notice. Custom alert types get basic styling (border, padding, rounded corners) but no icon or color by default. To add them, see the Styling section below. Custom alerts without custom CSS: Information This alert has basic styling but no icon or color. Known Bug Same here — add custom CSS to style it. Security Notice And this one too. Styling The roq-default theme includes full styling for the 5 standard GFM alert types: icons, colored borders, pastel backgrounds, and dark mode support. How SVG Icons Work Icons use the mask-image + background-color technique. The SVG defines only the shape (mask), and background-color: var(--alert-color) fills that shape with a color. This means one SVG works in any color — including dark mode. The SVGs are inlined as data URIs in the CSS using URL-encoded format: --alert-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D...%3E%3Cpath%20d%3D%22...%22/%3E%3C/svg%3E"); Icons are from GitHub Octicons (MIT license). For Custom Themes If you're using a custom theme, add alert styling to your CSS: .markdown-alert { padding: 1rem; margin: 1rem 0; border-radius: 0.5rem; border-left: 4px solid; } .markdown-alert-title { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; margin-bottom: 0.25rem; } .markdown-alert-title::before { content: ""; display: inline-block; width: 1rem; height: 1rem; flex-shrink: 0; background-color: var(--alert-color); mask-image: var(--alert-icon, none); mask-size: 100%; mask-repeat: no-repeat; mask-position: center; } /* Standard types */ .markdown-alert-note { --alert-color: #0969da; --alert-icon: url("data:image/svg+xml,..."); /* info-16 SVG as data URI */ border-color: #0969da; background: #0969da08; } .markdown-alert-note .markdown-alert-title { color: #0969da; } Adding Icons & Colors for Custom Types To style a custom alert type (e.g., INFO), add CSS with the --alert-icon and --alert-color variables: .markdown-alert-info { --alert-color: #0550ae; --alert-icon: url("data:image/svg+xml,..."); /* your SVG as data URI */ border-color: #0550ae; background: #0550ae08; } .markdown-alert-info .markdown-alert-title { color: #0550ae; } .markdown-alert-bug { --alert-color: #cf222e; --alert-icon: url("data:image/svg+xml,..."); /* bug SVG as data URI */ border-color: #cf222e; background: #cf222e08; } .markdown-alert-bug .markdown-alert-title { color: #cf222e; } .markdown-alert-security { --alert-color: #da3633; --alert-icon: url("data:image/svg+xml,..."); /* shield-16 SVG */ border-color: #da3633; background: #da363308; } .markdown-alert-security .markdown-alert-title { color: #da3633; } Which of these alert blocks will you use first? ### [Set It in Roq: The Editor that changes the game!](/posts/set-it-in-roq-the-editor-that-changes-the-game/) Roq started as a solid foundation for building modern apps and static sites. But now, it’s leveling up in a big way. With the introduction of a TipTap-powered Editor with Markdown support, Roq is no longer just an SSG tool, it’s stepping into CMS territory. Why This Is a Big Deal Until now, writers had to: Use an IDE or a text editor. Manually create new article files. Manually open the article preview. Use Markdown as code Now, with Roq’s built-in editor: Native integration: all integrated in Quarkus dev experience. Rich Text Editor with Markdown support: write your content in a Notion-like editor, render beautifully. Preview article: directly from the editor or using a new tab. This makes Roq feel less like a static site generator and more like a developer-friendly CMS, closer to the flexibility of WordPress but without the heaviness. Key Features Rich formatting: Bold, italic, headings, lists. Markdown support: Switch between rich text and Markdown seamlessly. Code editor with syntax highlighting: For HTML and AsciiDoc content. Media embedding: Images, links, and more. How to Try It The editor is natively integrated into Roq 2.1. roq create my-blog Start it roq 🚀 Hit m (like Manage) to Open The Roq Editor. ### [Roq 2.1 is here!](/posts/roq-2-1-is-here/) Hello fellow Roqers, Roq 2.1 is officially out, and it's packed with features that make content creation smoother, the developer experience richer, and the tooling more powerful than ever. This release is big enough that we're covering it as a series of posts. We already published posts about the built-in editor and GFM alert blocks, and this post gives you the full picture of everything else that landed. The highlights Here's what's new in Roq 2.1: Built-in Content Editor with rich text, Markdown support, and image upload GFM Alert Blocks (NOTE, TIP, WARNING, CAUTION, IMPORTANT) with icons and themed colors Standalone Roq CLI powered by Quarkus Picocli LLMs.txt generation for AI discoverability Custom error pages with source info and hints in dev mode Default theme with TailwindCSS and zero configuration Raclette link checker integration in roq-testing RSS content modes (full content or word-limited abstracts) Dynamic pages from data collections Qute alternative expression syntax support Simplified layout resolution with theme-layout support The Roq CLI Roq now ships as a standalone CLI tool, no Maven or Gradle knowledge required: roq create my-blog cd my-blog roq That's it. The CLI handles project creation, dev mode, static generation, plugin management, and updates. It's built as a Quarkus Picocli application and distributed via JBang. LLMs.txt Roq can auto-generate /llms.txt and /llms-full.txt following the llms.txt specification. AI systems like ChatGPT, Claude, and Perplexity use these files to understand your site's structure and content. Dynamic pages from data You can now generate pages dynamically from data collections, perfect for catalogs, team pages, or any content driven by structured data files. Developer experience Dev mode got a lot of love in this release: Custom error pages with source file location, detailed hints, and available alternatives when something goes wrong Centralized filesystem watcher for reliable live reload Simplified layout resolution with the new theme-layout key for explicit theme layout targeting What's coming next in this series Upcoming posts will dive deeper into: The Roq CLI LLMs.txt and AI discoverability Dynamic pages from data The new default theme and TailwindCSS integration Ready to try it? Check out the Getting Started guide, or if you're already on Roq, see how to update. Stay tuned and happy Roqing! ### [Roq 2.0 and Java Advent Calendar article](/posts/roq-2-0-and-java-advent-calendar-article/) Hello fellow Roqers, I’m thrilled to announce that Roq 2.0 is here 🚀—and it’s a big one! 🔥 Think of it like a new iPhone launch: most of the magic happens under the hood. Many changes aren’t immediately visible, but they’re packed with powerful developer features that make a huge difference. 🕵️ Added lightning filesystem watcher for live reload 📂 Allow web directory at the root of the Roq site 🧩 Simplified default app structure: supports web/app.js and web/app.scss (or web/app/app.js like before …​) ⚡️ TailwindCSS support without any config 💫 Directory support for data and allow iterating on nested data files using the directory name It might not look like much at first glance, but this release represents a long journey to build a solid foundation. That foundation now makes it possible to support plugins like TailwindCSS, Svelte, and Vue — true to the Quarkus spirit, with zero configuration required. I might write a blog post about Web Bundler 2.0, which makes all this possible. The native binding with architecture driven Maven/Gradle dependencies is pretty cool.. let me know in the comments if that would interest you. I’ve also spent time writing a tutorial to showcase all these new features. It’s published in the Java Advent Calendar alongside other cool Java content to explore this Christmas. Take care and happy coding! 🎄 ### [Major site migrations to Roq](/posts/major-site-migrations-to-roq/) Two prominent websites have recently completed their migration to Roq: wildfly.org and jbang.dev. These migrations mark a significant milestone in the adoption of Roq as a modern static site generator. WildFly.org on the roq The official WildFly project site, wildfly.org, has transitioned from its legacy setup on Jekyll. This move brings faster build times, simplified deployment, and better integration with modern Java tooling. WildFly is a powerful, modular, and lightweight Java application server that provides all the tools you need to build robust enterprise applications. https://github.com/wildfly/wildfly.org/ JBang.dev is awesome right? Similarly, jbang.dev, the home of the JBang scripting tool, has now adopted Roq for its site generation instead of Jekyll. JBang let Students, Educators and Professional Developers create, edit and run self-contained source-only Java programs with unprecedented ease. https://github.com/jbangdev/jbang.dev Roq’s Enhanced AsciiDoc Support Roq now offers robust support for AsciiDoc content. This includes: Header parsing: Roq can extract metadata from AsciiDoc headers, enabling dynamic routing and layout selection without FrontMatter header. Includes support: Authors can modularize content using AsciiDoc includes, making documentation more maintainable and reusable. These features make Roq an excellent choice for documentation-heavy sites and technical blogs. Read more about Roq with Asciidoc…​ More Sites Coming Soon! The momentum doesn’t stop here. Several other sites are currently in the pipeline to migrate to Roq, signaling growing confidence in its capabilities and developer experience. With its blazing-fast builds and flexible content handling, Roq is quickly becoming the go-to solution for modern static site generation in the Java ecosystem. Stay tuned for more announcements! ### [More diagram than you could have dreamed of.](/posts/more-diagram-than-you-could-have-dreamed-of/) As much as you love writing content in a textual format, you like to produce your diagram as code. But there are so many: PlantUML, Ditaa, Mermaid, BPMN and so on and so forth. Integrating all those formats would be a real pain. Hopefully you don’t have to, Kroki.io has already done it for you. A new plugin has been added to integrate its capability seamlessly to ROQ You can use it through an already deployed server or let the plugin make use of Quarkus dev services to do the job for you. 👉 Full documentation is available here, let’s diagram all the things! ### [🔎 Your users deserve searching capabilities!](/posts/your-users-deserve-searching-capabilities/) So your site is growing larger and larger and so it becomes harder and harder to find anything you wrote more than a few weeks ago. And that is frustrating. Words vanish, writing remains Yes…​ But if it remains buried deep in a pile of posts, it won’t be of any use to any one. It seems you need to add a search engine to your site. But…​ you choose static generation for a reason, right ? Economy of resources, matters to you. And so you don’t want to add a full-blown search engine like ElasticSearch or Solr. And guess what ? We couldn’t agree more with you 🤩. We think you are perfectly right, and that people should listen to you more. At least that’s what we do. 👂 So we did a bit of research and found out exactly what you need : Lunr.js, it’s a small, full-text search engine written in JavaScript. It runs in the browser based on a static generated json index and don’t need any other third party services ✨. Tadddaaah! We wrote a Lunr.js plugin for Roq.   👉 Full documentation is available here, don’t wait any longer, go check it out. ### [No pain updates with Roq](/posts/no-pain-updates-with-roq/) One of the most overlooked aspects when choosing a Static Site Generator (SSG) is how easy it is to keep your project up to date. Many developers have struggled with complex upgrade processes, dependency conflicts, and breaking changes when using traditional SSGs like Jekyll or Hugo. With Roq, upgrading is refreshingly simple. Updating Roq: A One-Command Upgrade Roq is built on Quarkus, which provides a streamlined upgrade process. To update Roq to the latest version, all you need to do is run: roq update This is equivalent to running quarkus update directly if you have the Quarkus CLI installed. For more details, check the advanced documentation and the migration guide. ### [Roq n Roll Your Tests 🎶](/posts/roq-n-roll-your-tests/) Hello folks, I'm excited to share something very cool! I've developed a way to: Test the full generation of your website. Use RestAssured to test the generated site (thanks to an already started static server). Step 1: Add the Dependency First, include the quarkus-roq-testing test dependency in your pom.xml. Step 2: Basic Test Example Once you've added the dependency, you can easily ensure all pages are generated without errors: @QuarkusTest @RoqAndRoll public class RoqSiteTest { @Test public void testGen() { // All pages will be generated/validated during test setup } } That's it! This basic test already verifies that your site generation is error-free. Step 3: Test the Generated Content To go even further, you can test the actual content of your generated site. The RestAssured port will automatically use the Roq static server. Here's how: @QuarkusTest @RoqAndRoll public class RoqSiteTest { @Test public void testIndex() { RestAssured.when().get("/") .then() .statusCode(200) .body(containsString( "Roq is a static site generator that makes it easy to build websites and blogs" )); } } Why I Love It ❤️ With just a few annotations and a bit of setup, you can effortlessly test both the generation process and the content of your site. It's powerful, elegant, and super simple to use. Give it a try and let me know how it works for you. Happy testing! 🚀 ### [Easily Generate a `sitemap.xml` for Your Site with Roq](/posts/easily-generate-a-sitemap-xml-for-your-site-with-roq/) Creating a sitemap.xml for your site has never been easier! With the Sitemap plugin, you can automatically generate a well-structured sitemap for search engines to crawl your pages efficiently. Installation To get started, install the plugin by running the following command: roq add plugin:sitemap Setting Up the Sitemap Next, create a new sitemap file in the content/sitemap.xml: <!-- Include your sitemap template --> {#include fm/sitemap.xml} And that's it! Your sitemap is now ready. Excluding Pages from the Sitemap If there are pages you don't want included in the sitemap, simply set the sitemap property to false in the FM of those pages. For example: --- title: "Hidden Page" sitemap: false --- Accessing Your Sitemap Once your site is up and running, you can view your sitemap by navigating to: http://localhost:8080/sitemap.xml Congratulations! You’ve successfully set up a sitemap.xml for your site. ### [Static attached files for posts and pages](/posts/static-attached-files-for-posts-and-pages/) This Christmas, I’m Roq-ing a cool new feature (inspired by Hugo 😅): it is possible to attach static files to posts and pages. They will be served relative to the page. 🎁🤩 I love it because it allows to put all the content related to one page or post in the same place. Bonus, images are displayed on previews since they are relative to the template. For example here is a sample pdf: link. Fun fact: @parisjug is already using this feature on their site (which is on Hugo 🤪)! The doc for this feature is here. ### [Already some happy users 🧑‍💻](/posts/already-some-happy-users/) Roq is a rookie and still needs to prove itself, but already, there are good signs ☀️ The very first to give Roq a shot was David—a talented developer from the Quarkus Team! No images on your blog, David? Maybe it’s because you don’t want them stealing the spotlight from your content, right? Don’t worry, I’ll create an Unsplash plugin just for you. It will automatically pick images based on your content, so you don’t have to lift a finger! David's was nice enough to share about switching from Jekyll to Roq (in his first Roq post:): https://word-bits.flurg.com/posts/it-s-alive/ (repo) I live near Marseille and know the owners of MarsJug. I decided to try switching their website to Roq. Their original site consisted of plain HTML pages with raw event details hardcoded, making updates and maintenance a nightmare. It took just a minute to convince them of the benefits of using Roq. https://marsjug.org/ (repo) Jotak, a Red Hat developer working on the NetObserv project, used to lean more towards Vert.x than Quarkus—something we debated quite a bit back in the day. It’s a shame he had to step away from Java development due to project constraints a few years ago. Still, he managed to convince his team (working with Go and React) to switch to Roq 🚀. https://netobserv.io (repo) Will you be the next to share your Roq's journey? Come on 😎 !!! ### [Do you want to publish a blog post series ?](/posts/do-you-want-to-publish-a-blog-post-series/) So you plan to do a series of blog posts about a given subject. This is as simple as adding a series attribute to the front matter of your posts. Step 1: Add the Series plugin in your dependencies file: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-series</artifactId> <version>${quarkus-roq.version}</version> </dependency> Step 2: Edit the layout for your posts, for example when using roq-default theme: templates/layouts/post-series.html --- theme-layout: post --- {#include partials/roq-series /} (1) {#insert /} (2) 1 This will add the series partial before the post content, if it’s declared. 2 This is the post content. And finally, use this layout and add the series attribute in the Front Matter of the posts you want to join. --- layout: series-post title: Assemble you blog post in a series description: Automatically series header for your posts tags: plugin, frontmatter, new-feature, series author: John Doe series: My series Title (1) --- 1 You should use the exact same title for all documents in the series. It will add the following at the head of your post: A bit like what you see at the very begining of this post. ### [Need a QR Code?](/posts/need-a-qr-code/) Need to add a scannable QR Code to your website? Whether it's for a restaurant menu, event ticket, or any other use case where you want to make your content easily accessible via mobile devices, the Roq QR Code plugin has you covered. Step 1: Add the QRCode plugin in your dependencies file: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-qrcode</artifactId> <version>...</version> </dependency> Step 2: Add the QRCode tag to your template with all the parameters you need: {#qrcode value="https://luigis.com/menu/" alt="Luigi's Menu" foreground="#000066" background="#FFFFFF" width=300 height=300 /} It will render a QR Code like this: ### [Roq with Blogs](/posts/roq-with-blogs/) Hello folks, First let me thanks the Roq contributors, they have been awesome and this has been so fun to create Roq! If you want to get started quickly: Click here to generate your Roq Starter App. or use the Roq CLI: roq create Then cd blog-with-roq roq If you have a bit of time, with this release, I think it's time for me to give you the full story 📖: It all started a while back when I helped my wife create her blog. After reviewing a few options, I decided to use Jekyll, as it was the easiest solution with GitHub Pages. Over time, I grew quite frustrated with the process: It was hard for my wife to install and start using. It was challenging to maintain and keep updated. Using Ruby didn’t feel great. Plugins were often outdated or unmaintained. Then my wife said: My wife: “But why don’t you use your famous Quarkus?” Me: “This is not the right tool to create a blog 😭” I think this was around the time Quarkus 1.0 was being released... ... 😴 Time passes ... 🗓️ Mar 23, 2022: quarkus-quinoa 🗓️ Feb 3, 2023: quarkus-web-bundler 🗓️ Early 2024: Quarkus web guide At this point, I thought back on what my wife had said... maybe it was time to reconsider? But Qute processes things at runtime, so it didn’t seem possible 😤 ... 😴 Time passes ... 🗓️ May 7, 2024: My idea was to generate static pages at runtime… because then all of Quarkus could become static without any changes 😍. 🗓️ May 17, 2024: quarkus-roq (generator part) At this point, I thought we (mostly) had everything in Quarkus to change my answer to my wife 🤓 For those who wonder, "Roq" was chosen because: static = rock, rock + quarkus = roq 🗓️ June 19, 2024: Roq Focus Group And now, thanks to the awesome team 🧑‍💻👩🏻‍💻! 🗓️ October 31, 2024: Roq 1.0 🎉🍾🥂 If you like the idea, support us, give us a star ⭐ or start contributing... ### [Write your blog posts in AsciiDoc](/posts/write-your-blog-posts-in-asciidoc/) Writing content is AsciiDoc format is an absolut no brainer. Roq provides a plugin to handle it transparently for you. To use it, you need to add the `quarkus-roq-plugin-asciidoc' to your project. Details You can do that using several ways : Manually pom.xml <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-asciidoc</artifactId> <version>${quarkus-roq.version}</version> </dependency> Using the Roq CLI roq add plugin:asciidoc Using Maven ./mvnw quarkus:add-extension -Dextensions="io.quarkiverse.roq:quarkus-roq-plugin-asciidoc" Using the Gradle ./gradlew addExtension --extensions="io.quarkiverse.roq:quarkus-roq-plugin-asciidoc" Once done, you can start writing your blog posts in AsciiDoc format. ### [RSS Feed of your blog posts](/posts/rss-feed-of-your-blog-posts/) Adding RSS is as easy as adding this tag to your <head> section: {#rss site /} Like this: <link rel="alternate" type="application/rss+xml" title="Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free." href="https://iamroq.dev/rss.xml"/> It will automatically utilize the Frontmatter data from all your blog posts to generate a valid Atom RSS feed link at rss.xml. Ensure you create an rss.xml file at the root of your site and include this single line of code: {#include fm/rss.html /} The Atom Syndication Format is an XML language used for web feeds. A web feed (also called ‘news feed’ or ‘RSS feed’) is a data format used for providing users with frequently updated content. Content distributors syndicate a web feed, thereby allowing users to subscribe a channel to it. A typical scenario of web-feed use might involve the following: a content provider publishes a feed link on its site which end users can register with an aggregator program (also called a feed reader or a newsreader) running on their own machines. <rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> <channel> <title><![CDATA[ Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. ]]></title> <description><![CDATA[ An Open Source static site generator (SSG) that makes it fun and easy to build websites and blogs. It's built with Java and Quarkus under the hood. ]]></description> <link>https://iamroq.dev/</link> <atom:link href="https://iamroq.dev/rss.xml" rel="self" type="application/rss+xml"/> <generator>Quarkus Roq</generator> <lastBuildDate>Thu, 23 Jul 2026 00:00:00 +0000</lastBuildDate> <item> <title><![CDATA[Comparing Roq with Hugo, Jekyll, and JBake: A Feature Breakdown]]></title> <link>https://iamroq.dev/posts/comparing-roq-with-hugo-jekyll-and-jbake-a-feature-breakdown/</link> <guid isPermaLink="false">https://iamroq.dev/posts/comparing-roq-with-hugo-jekyll-and-jbake-a-feature-breakdown/</guid> <pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate> <description><![CDATA[]]></description> <content:encoded><![CDATA[<p></p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/comparing-roq-with-hugo-jekyll-and-jbake-a-feature-breakdown/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Smarter Search Ranking]]></title> <link>https://iamroq.dev/posts/smarter-search-ranking/</link> <guid isPermaLink="false">https://iamroq.dev/posts/smarter-search-ranking/</guid> <pubDate>Thu, 16 Jul 2026 12:00:00 +0000</pubDate> <description><![CDATA[How we fixed search boost to let keyword relevance shine.]]></description> <content:encoded><![CDATA[<p>How we fixed search boost to let keyword relevance shine.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/smarter-search-ranking/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Add Comments to Your Blog with a Web Component (30min)]]></title> <link>https://iamroq.dev/posts/add-comments-web-component/</link> <guid isPermaLink="false">https://iamroq.dev/posts/add-comments-web-component/</guid> <pubDate>Sun, 05 Jul 2026 14:00:00 +0000</pubDate> <description><![CDATA[Step-by-step tutorial: build a Lit web component for comments backed by a Quarkus REST API on your Roq blog.]]></description> <content:encoded><![CDATA[<p>Step-by-step tutorial: build a Lit web component for comments backed by a Quarkus REST API on your Roq blog.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/add-comments-web-component/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Add Comments to Your Blog with Hybrid Mode (30min)]]></title> <link>https://iamroq.dev/posts/add-comments-hybrid/</link> <guid isPermaLink="false">https://iamroq.dev/posts/add-comments-hybrid/</guid> <pubDate>Sun, 05 Jul 2026 13:00:00 +0000</pubDate> <description><![CDATA[Step-by-step tutorial: add dynamic comments to your Roq blog using hybrid mode, Panache, and Qute templates.]]></description> <content:encoded><![CDATA[<p>Step-by-step tutorial: add dynamic comments to your Roq blog using hybrid mode, Panache, and Qute templates.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/add-comments-hybrid/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Create a Link-Tree with Roq (45min)]]></title> <link>https://iamroq.dev/posts/create-a-link-tree-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/create-a-link-tree-with-roq/</guid> <pubDate>Sun, 05 Jul 2026 12:00:00 +0000</pubDate> <description><![CDATA[Step-by-step tutorial: build a personal link-tree site from scratch with Roq.]]></description> <content:encoded><![CDATA[<p>Step-by-step tutorial: build a personal link-tree site from scratch with Roq.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/create-a-link-tree-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Create a Blog from Scratch with Roq (45min)]]></title> <link>https://iamroq.dev/posts/create-a-blog-from-scratch-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/create-a-blog-from-scratch-with-roq/</guid> <pubDate>Sun, 05 Jul 2026 11:00:00 +0000</pubDate> <description><![CDATA[Step-by-step tutorial: build a blog from scratch with Roq using the base theme. Learn layouts, collections, and Tailwind styling.]]></description> <content:encoded><![CDATA[<p>Step-by-step tutorial: build a blog from scratch with Roq using the base theme. Learn layouts, collections, and Tailwind styling.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/create-a-blog-from-scratch-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Create your own Blog with Roq (30min)]]></title> <link>https://iamroq.dev/posts/create-a-blog-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/create-a-blog-with-roq/</guid> <pubDate>Sun, 05 Jul 2026 10:00:00 +0000</pubDate> <description><![CDATA[Step-by-step tutorial: create and customize a blog with Roq using the default theme.]]></description> <content:encoded><![CDATA[<p>Step-by-step tutorial: create and customize a blog with Roq using the default theme.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/create-a-blog-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Collapsible Sections: Hide and Reveal Content in Your Posts]]></title> <link>https://iamroq.dev/posts/collapsible-sections-hide-and-reveal-content-in-your-posts/</link> <guid isPermaLink="false">https://iamroq.dev/posts/collapsible-sections-hide-and-reveal-content-in-your-posts/</guid> <pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate> <description><![CDATA[The Roq default theme now styles HTML collapsible sections out of the box, in both Markdown and AsciiDoc content. Perfect for tutorials with hints, FAQs, and long reference sections.]]></description> <content:encoded><![CDATA[<p>The Roq default theme now styles HTML collapsible sections out of the box, in both Markdown and AsciiDoc content. Perfect for tutorials with hints, FAQs, and long reference sections.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/collapsible-sections-hide-and-reveal-content-in-your-posts/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Generate Open Graph Images for Social Sharing with Roq]]></title> <link>https://iamroq.dev/posts/generate-open-graph-images-for-social-sharing-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/generate-open-graph-images-for-social-sharing-with-roq/</guid> <pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate> <description><![CDATA[Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically.]]></description> <content:encoded><![CDATA[<p>Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/generate-open-graph-images-for-social-sharing-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Generate first class citizen pages from your data]]></title> <link>https://iamroq.dev/posts/generate-first-class-citizen-pages-from-your-data/</link> <guid isPermaLink="false">https://iamroq.dev/posts/generate-first-class-citizen-pages-from-your-data/</guid> <pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate> <description><![CDATA[You can now generate pages dynamically from data collections, perfect for catalogs, team pages, or any content driven by structured data files.]]></description> <content:encoded><![CDATA[<p>You can now generate pages dynamically from data collections, perfect for catalogs, team pages, or any content driven by structured data files.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/generate-first-class-citizen-pages-from-your-data/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Devoured: My Healthy Instagram for Tech News]]></title> <link>https://iamroq.dev/posts/devoured-my-healthy-instagram-for-tech-news/</link> <guid isPermaLink="false">https://iamroq.dev/posts/devoured-my-healthy-instagram-for-tech-news/</guid> <pubDate>Wed, 20 May 2026 08:00:00 +0000</pubDate> <description><![CDATA[How I built a daily AI-curated tech digest with Roq, replacing doomscrolling with something actually useful.]]></description> <content:encoded><![CDATA[<p>How I built a daily AI-curated tech digest with Roq, replacing doomscrolling with something actually useful.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/devoured-my-healthy-instagram-for-tech-news/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq]]></title> <link>https://iamroq.dev/posts/how-ai-helped-me-rebuild-my-blog-and-move-from-jekyll-to-quarkus-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/how-ai-helped-me-rebuild-my-blog-and-move-from-jekyll-to-quarkus-roq/</guid> <pubDate>Wed, 06 May 2026 08:00:00 +0000</pubDate> <description><![CDATA[A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites.]]></description> <content:encoded><![CDATA[<p>A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/how-ai-helped-me-rebuild-my-blog-and-move-from-jekyll-to-quarkus-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[GFM Alert Blocks: Styled Callouts in Your Markdown]]></title> <link>https://iamroq.dev/posts/gfm-alert-blocks-styled-callouts-in-your-markdown/</link> <guid isPermaLink="false">https://iamroq.dev/posts/gfm-alert-blocks-styled-callouts-in-your-markdown/</guid> <pubDate>Mon, 04 May 2026 09:00:00 +0000</pubDate> <description><![CDATA[Roq supports GitHub Flavored Markdown alert blocks with icons and themed colors. Learn how to use NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks, and how to add custom alert types.]]></description> <content:encoded><![CDATA[<p>Roq supports GitHub Flavored Markdown alert blocks with icons and themed colors. Learn how to use NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks, and how to add custom alert types.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/gfm-alert-blocks-styled-callouts-in-your-markdown/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Set It in Roq: The Editor that changes the game!]]></title> <link>https://iamroq.dev/posts/set-it-in-roq-the-editor-that-changes-the-game/</link> <guid isPermaLink="false">https://iamroq.dev/posts/set-it-in-roq-the-editor-that-changes-the-game/</guid> <pubDate>Mon, 04 May 2026 08:00:00 +0000</pubDate> <description><![CDATA[Roq introduces a TipTap-powered editor with Markdown support, transforming it from a static site generator into a lightweight, developer-friendly CMS. Create, edit, and preview content seamlessly within the Quarkus dev experience.]]></description> <content:encoded><![CDATA[<p>Roq introduces a TipTap-powered editor with Markdown support, transforming it from a static site generator into a lightweight, developer-friendly CMS. Create, edit, and preview content seamlessly within the Quarkus dev experience.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/set-it-in-roq-the-editor-that-changes-the-game/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Roq 2.1 is here!]]></title> <link>https://iamroq.dev/posts/roq-2-1-is-here/</link> <guid isPermaLink="false">https://iamroq.dev/posts/roq-2-1-is-here/</guid> <pubDate>Fri, 01 May 2026 08:00:00 +0000</pubDate> <description><![CDATA[Roq 2.1 brings a standalone CLI, LLMs.txt generation, dynamic pages from data, custom error pages, and much more. This post kicks off a series covering all the new features.]]></description> <content:encoded><![CDATA[<p>Roq 2.1 brings a standalone CLI, LLMs.txt generation, dynamic pages from data, custom error pages, and much more. This post kicks off a series covering all the new features.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/roq-2-1-is-here/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Roq 2.0 and Java Advent Calendar article]]></title> <link>https://iamroq.dev/posts/roq-2-0-and-java-advent-calendar-article/</link> <guid isPermaLink="false">https://iamroq.dev/posts/roq-2-0-and-java-advent-calendar-article/</guid> <pubDate>Tue, 09 Dec 2025 00:00:00 +0000</pubDate> <description><![CDATA[An introduction to Roq 2.0, a Quarkus-inspired approach to static site generation in Java. Learn about its new foundation, plugin support, and live-reload feature through a practical tutorial.]]></description> <content:encoded><![CDATA[<p>An introduction to Roq 2.0, a Quarkus-inspired approach to static site generation in Java. Learn about its new foundation, plugin support, and live-reload feature through a practical tutorial.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/roq-2-0-and-java-advent-calendar-article/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Major site migrations to Roq]]></title> <link>https://iamroq.dev/posts/major-site-migrations-to-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/major-site-migrations-to-roq/</guid> <pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate> <description><![CDATA[✨ Two prominent websites have just migrated to Roq—any guesses who they might be?]]></description> <content:encoded><![CDATA[<p>✨ Two prominent websites have just migrated to Roq—any guesses who they might be?</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/major-site-migrations-to-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[More diagram than you could have dreamed of.]]></title> <link>https://iamroq.dev/posts/more-diagram-than-you-could-have-dreamed-of/</link> <guid isPermaLink="false">https://iamroq.dev/posts/more-diagram-than-you-could-have-dreamed-of/</guid> <pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate> <description><![CDATA[Leveraging Kroki.io to generate diagram from text]]></description> <content:encoded><![CDATA[<p>Leveraging Kroki.io to generate diagram from text</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/more-diagram-than-you-could-have-dreamed-of/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[🔎 Your users deserve searching capabilities!]]></title> <link>https://iamroq.dev/posts/your-users-deserve-searching-capabilities/</link> <guid isPermaLink="false">https://iamroq.dev/posts/your-users-deserve-searching-capabilities/</guid> <pubDate>Fri, 04 Apr 2025 00:00:00 +0000</pubDate> <description><![CDATA[No third party service needed 🚀]]></description> <content:encoded><![CDATA[<p>No third party service needed 🚀</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/your-users-deserve-searching-capabilities/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[No pain updates with Roq]]></title> <link>https://iamroq.dev/posts/no-pain-updates-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/no-pain-updates-with-roq/</guid> <pubDate>Mon, 24 Mar 2025 00:00:00 +0000</pubDate> <description><![CDATA[One of the most overlooked aspects when choosing a Static Site Generator (SSG) is how easy it is to keep your project up to date. Many developers have struggled with complex upgrade processes, dependency conflicts, and breaking changes when using traditional SSGs like Jekyll or Hugo.]]></description> <content:encoded><![CDATA[<p>One of the most overlooked aspects when choosing a Static Site Generator (SSG) is how easy it is to keep your project up to date. Many developers have struggled with complex upgrade processes, dependency conflicts, and breaking changes when using traditional SSGs like Jekyll or Hugo.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/no-pain-updates-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Roq n Roll Your Tests 🎶]]></title> <link>https://iamroq.dev/posts/roq-n-roll-your-tests/</link> <guid isPermaLink="false">https://iamroq.dev/posts/roq-n-roll-your-tests/</guid> <pubDate>Tue, 28 Jan 2025 00:00:00 +0000</pubDate> <description><![CDATA[Testing the actual Roq generation has never been this cool! 🎸]]></description> <content:encoded><![CDATA[<p>Testing the actual Roq generation has never been this cool! 🎸</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/roq-n-roll-your-tests/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Easily Generate a `sitemap.xml` for Your Site with Roq]]></title> <link>https://iamroq.dev/posts/easily-generate-a-sitemap-xml-for-your-site-with-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/easily-generate-a-sitemap-xml-for-your-site-with-roq/</guid> <pubDate>Wed, 08 Jan 2025 00:00:00 +0000</pubDate> <description><![CDATA[Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin.]]></description> <content:encoded><![CDATA[<p>Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/easily-generate-a-sitemap-xml-for-your-site-with-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Static attached files for posts and pages]]></title> <link>https://iamroq.dev/posts/static-attached-files-for-posts-and-pages/</link> <guid isPermaLink="false">https://iamroq.dev/posts/static-attached-files-for-posts-and-pages/</guid> <pubDate>Thu, 26 Dec 2024 00:00:00 +0000</pubDate> <description><![CDATA[This Christmas, I’m Roq-ing a cool new feature (inspired by Hugo 😅): it is possible to attach static files to posts and pages. They will be served relative to the page. 🎁🤩 ]]></description> <content:encoded><![CDATA[<p>This Christmas, I’m Roq-ing a cool new feature (inspired by Hugo 😅): it is possible to attach static files to posts and pages. They will be served relative to the page. 🎁🤩 </p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/static-attached-files-for-posts-and-pages/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Already some happy users 🧑‍💻]]></title> <link>https://iamroq.dev/posts/already-some-happy-users/</link> <guid isPermaLink="false">https://iamroq.dev/posts/already-some-happy-users/</guid> <pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate> <description><![CDATA[This is a good start, we already have a few happy users!]]></description> <content:encoded><![CDATA[<p>This is a good start, we already have a few happy users!</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/already-some-happy-users/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Do you want to publish a blog post series ?]]></title> <link>https://iamroq.dev/posts/do-you-want-to-publish-a-blog-post-series/</link> <guid isPermaLink="false">https://iamroq.dev/posts/do-you-want-to-publish-a-blog-post-series/</guid> <pubDate>Fri, 06 Dec 2024 07:00:00 +0000</pubDate> <description><![CDATA[Make your blog posts part of a series.]]></description> <content:encoded><![CDATA[<p>Make your blog posts part of a series.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/do-you-want-to-publish-a-blog-post-series/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Need a QR Code?]]></title> <link>https://iamroq.dev/posts/need-a-qr-code/</link> <guid isPermaLink="false">https://iamroq.dev/posts/need-a-qr-code/</guid> <pubDate>Thu, 14 Nov 2024 12:00:00 +0000</pubDate> <description><![CDATA[Add a QR Code to your Roq website.]]></description> <content:encoded><![CDATA[<p>Add a QR Code to your Roq website.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/need-a-qr-code/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Roq with Blogs]]></title> <link>https://iamroq.dev/posts/roq-with-blogs/</link> <guid isPermaLink="false">https://iamroq.dev/posts/roq-with-blogs/</guid> <pubDate>Thu, 31 Oct 2024 00:00:00 +0000</pubDate> <description><![CDATA[🚀 Roq 1.0 is ON! It is time to give it a shot and give us feedback 🚀]]></description> <content:encoded><![CDATA[<p>🚀 Roq 1.0 is ON! It is time to give it a shot and give us feedback 🚀</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/roq-with-blogs/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Write your blog posts in AsciiDoc]]></title> <link>https://iamroq.dev/posts/write-your-blog-posts-in-asciidoc/</link> <guid isPermaLink="false">https://iamroq.dev/posts/write-your-blog-posts-in-asciidoc/</guid> <pubDate>Tue, 22 Oct 2024 00:00:00 +0000</pubDate> <description><![CDATA[Automatically generate html from AsciiDoc content]]></description> <content:encoded><![CDATA[<p>Automatically generate html from AsciiDoc content</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/write-your-blog-posts-in-asciidoc/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[RSS Feed of your blog posts]]></title> <link>https://iamroq.dev/posts/rss-feed-of-your-blog-posts/</link> <guid isPermaLink="false">https://iamroq.dev/posts/rss-feed-of-your-blog-posts/</guid> <pubDate>Thu, 10 Oct 2024 00:00:00 +0000</pubDate> <description><![CDATA[Automatically generate an RSS feed of your blog links.]]></description> <content:encoded><![CDATA[<p>Automatically generate an RSS feed of your blog links.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/rss-feed-of-your-blog-posts/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[The second Roq plugin is for redirecting your page to a better place!]]></title> <link>https://iamroq.dev/posts/the-second-roq-plugin-is-for-redirecting-your-page-to-a-better-place/</link> <guid isPermaLink="false">https://iamroq.dev/posts/the-second-roq-plugin-is-for-redirecting-your-page-to-a-better-place/</guid> <pubDate>Wed, 09 Oct 2024 00:00:00 +0000</pubDate> <description><![CDATA[We introduced a way to declare aliases in FrontMatter. It is now easy create redirections to your blog posts!]]></description> <content:encoded><![CDATA[<p>We introduced a way to declare aliases in FrontMatter. It is now easy create redirections to your blog posts!</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/the-second-roq-plugin-is-for-redirecting-your-page-to-a-better-place/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[The first Roq plugin is for tagging (with pagination)]]></title> <link>https://iamroq.dev/posts/the-first-roq-plugin-is-for-tagging-with-pagination/</link> <guid isPermaLink="false">https://iamroq.dev/posts/the-first-roq-plugin-is-for-tagging-with-pagination/</guid> <pubDate>Tue, 08 Oct 2024 00:00:00 +0000</pubDate> <description><![CDATA[We introduced the first Roq plugin, it is for collection tagging & with pagination support!]]></description> <content:encoded><![CDATA[<p>We introduced the first Roq plugin, it is for collection tagging & with pagination support!</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/the-first-roq-plugin-is-for-tagging-with-pagination/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Out of the box awesome SEO]]></title> <link>https://iamroq.dev/posts/out-of-the-box-awesome-seo/</link> <guid isPermaLink="false">https://iamroq.dev/posts/out-of-the-box-awesome-seo/</guid> <pubDate>Mon, 23 Sep 2024 12:00:00 +0000</pubDate> <description><![CDATA[Learn how to implement SEO in Roq in a blink of an eye.]]></description> <content:encoded><![CDATA[<p>Learn how to implement SEO in Roq in a blink of an eye.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/out-of-the-box-awesome-seo/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Mastering Pagination in Roq]]></title> <link>https://iamroq.dev/posts/mastering-pagination-in-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/mastering-pagination-in-roq/</guid> <pubDate>Fri, 20 Sep 2024 12:00:00 +0000</pubDate> <description><![CDATA[Learn how to implement pagination in Roq to enhance your content navigation. This article walks through the process of adding pagination, configuring page size, and customizing links.]]></description> <content:encoded><![CDATA[<p>Learn how to implement pagination in Roq to enhance your content navigation. This article walks through the process of adding pagination, configuring page size, and customizing links.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/mastering-pagination-in-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[How to add syntax highlighting to your Roq site with Highlight.js]]></title> <link>https://iamroq.dev/posts/how-to-add-syntax-highlighting-to-your-roq-site-with-highlight-js/</link> <guid isPermaLink="false">https://iamroq.dev/posts/how-to-add-syntax-highlighting-to-your-roq-site-with-highlight-js/</guid> <pubDate>Fri, 20 Sep 2024 09:00:00 +0000</pubDate> <description><![CDATA[Learn how to integrate syntax highlighting into your Roq site using Highlight.js and the Quarkus web-bundler extension. This guide walks you through the simple steps to add it via the pom.xml, JavaScript, and SCSS files.]]></description> <content:encoded><![CDATA[<p>Learn how to integrate syntax highlighting into your Roq site using Highlight.js and the Quarkus web-bundler extension. This guide walks you through the simple steps to add it via the pom.xml, JavaScript, and SCSS files.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/how-to-add-syntax-highlighting-to-your-roq-site-with-highlight-js/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Easily manage Drafts and Future articles in Roq]]></title> <link>https://iamroq.dev/posts/easily-manage-drafts-and-future-articles-in-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/easily-manage-drafts-and-future-articles-in-roq/</guid> <pubDate>Thu, 19 Sep 2024 08:45:00 +0000</pubDate> <description><![CDATA[Roq SSG introduces a new feature that allows you to hide or show draft and future articles using simple Quarkus configurations. This update gives developers greater control over which content is visible, improving content management and workflow.]]></description> <content:encoded><![CDATA[<p>Roq SSG introduces a new feature that allows you to hide or show draft and future articles using simple Quarkus configurations. This update gives developers greater control over which content is visible, improving content management and workflow.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/easily-manage-drafts-and-future-articles-in-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Effortless URL Handling in Roq with Qute super-power]]></title> <link>https://iamroq.dev/posts/effortless-url-handling-in-roq-with-qute-super-power/</link> <guid isPermaLink="false">https://iamroq.dev/posts/effortless-url-handling-in-roq-with-qute-super-power/</guid> <pubDate>Mon, 16 Sep 2024 11:32:20 +0000</pubDate> <description><![CDATA[Effortlessly manage both relative and absolute URLs with our enhanced Qute-powered feature. Utilizing the RoqUrl class, you can easily join and resolve paths, ensuring clean and predictable URLs. This update simplifies URL handling, making your code more efficient and your content easier to navigate and share.]]></description> <content:encoded><![CDATA[<p>Effortlessly manage both relative and absolute URLs with our enhanced Qute-powered feature. Utilizing the RoqUrl class, you can easily join and resolve paths, ensuring clean and predictable URLs. This update simplifies URL handling, making your code more efficient and your content easier to navigate and share.</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/effortless-url-handling-in-roq-with-qute-super-power/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> <item> <title><![CDATA[Welcome to Roq!]]></title> <link>https://iamroq.dev/posts/welcome-to-roq/</link> <guid isPermaLink="false">https://iamroq.dev/posts/welcome-to-roq/</guid> <pubDate>Thu, 29 Aug 2024 11:32:20 +0000</pubDate> <description><![CDATA[This is the first article ever made with Quarkus Roq]]></description> <content:encoded><![CDATA[<p>This is the first article ever made with Quarkus Roq</p><div style="margin-top: 50px; font-style: italic;"><strong><a href="https://iamroq.dev/posts/welcome-to-roq/">Keep reading</a>.</strong></div><br /> <br />]]></content:encoded> </item> </channel> </rss> ### [The second Roq plugin is for redirecting your page to a better place!](/posts/the-second-roq-plugin-is-for-redirecting-your-page-to-a-better-place/) In the last post, we saw how easy it is to use Quarkus for static site generator (@ia3andy's was right!). I am excited to share that we now have a new plugin that allows you to set up redirects for your blog posts! For this post, I've created three aliases. aliases-very-cool aliases-4-ever aliases-again If you click on at least one alias, you will be redirected here again! And how did I work my magic to set this up? Step 1: Add the aliases plugin in your dependencies file: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-aliases</artifactId> <version>...</version> </dependency> Step 2: Add a new entry aliases: [name-of-aliases-here] in your FM data. In this blog post I used the following FM: ... aliases: [aliases-very-cool, aliases-4-ever, aliases-again] ... For more info check out the doc. ### [The first Roq plugin is for tagging (with pagination)](/posts/the-first-roq-plugin-is-for-tagging-with-pagination/) My mind is getting blown by how much Quarkus was made for Static Site Generation. I just implemented a new plugin to generate tag pages and that was soooo easy. To use it: <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-plugin-tagging</artifactId> <version>...</version> </dependency> and adding a new layouts/tag.html page or any layout with tagging: [name of collection] as FM data. For more info check out the doc. ### [Out of the box awesome SEO](/posts/out-of-the-box-awesome-seo/) Adding SEO is as easy as adding this tag to your <head> section: {#seo page site /} It will automatically use the Frontmatter data to fill the tags. Read the Roq documentation for more... Like this: <!-- SEO TITLE --> <title>Out of the box awesome SEO - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free.</title> <meta property="og:title" content="Out of the box awesome SEO" /> <meta name="twitter:title" content="Out of the box awesome SEO"> <meta property="og:site_name" content="Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free."> <!-- SEO DESCRIPTION --> <meta name="description" content="Learn how to implement SEO in Roq in a blink of an eye."> <meta property="og:description" content="Learn how to implement SEO in Roq in a blink of an eye." /> <meta name="twitter:description" content="Learn how to implement SEO in Roq in a blink of an eye."> <!-- SEO AUTHOR --> <meta property="article:author" content="ia3andy" /> <meta name="author" content="ia3andy" /> <!-- SEO URL --> <link rel="canonical" href="https://iamroq.dev/posts/out-of-the-box-awesome-seo/" /> <meta property="og:url" content="https://iamroq.dev/posts/out-of-the-box-awesome-seo/" /> <meta name="twitter:url" content="https://iamroq.dev/posts/out-of-the-box-awesome-seo/"> <!-- SEO TYPE --> <meta property="og:type" content="article" /> <meta property="article:published_time" content="2024-09-23T12:00Z[Etc/UTC]" /> <!-- SEO IMAGE --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:image:src" content="https://images.unsplash.com/photo-1562577309-2592ab84b1bc?q=80&w=1200&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <meta property="og:image" content="https://images.unsplash.com/photo-1562577309-2592ab84b1bc?q=80&w=1200&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" /> <!-- SEO COLLECTION PAGES --> <link rel="prev" href="https://iamroq.dev/posts/the-first-roq-plugin-is-for-tagging-with-pagination/" /> <link rel="next" href="https://iamroq.dev/posts/mastering-pagination-in-roq/" /> <!-- SEO LOCALE --> <meta property="og:locale" content="en" /> <!-- SEO GENERATOR --> <meta name="generator" content="Quarkus Roq v999-SNAPSHOT" /> ### [Mastering Pagination in Roq](/posts/mastering-pagination-in-roq/) Adding pagination to your Roq site is an easy way to improve content navigation. Let’s walk through how to implement pagination and customize its behavior in your site. Step 1: Basic Pagination Setup First, include the following in your frontmatter on the page which will iterate on the paginated collection: layout: main paginate: posts Next, in your template, loop through the paginated posts using: {#for post in site.collections.posts.paginated(page.paginator)} <article class="post">...</article> {/for} Step 2: Adding Pagination Controls To add pagination controls, add something like this to partials/pagination.html and include it in your page {#include partials/pagination.html/}: {#include fm/pagination.html} {#newer}<i class="fa fa-long-arrow-left" aria-hidden="true"></i>{/newer} {#older}<i class="fa fa-long-arrow-right" aria-hidden="true"></i>{/older} {/include} You can further customize your pagination by setting the page size and link format: paginate: size: 4 collection: posts link: posts/page-:page With these steps, you can create a flexible pagination system to improve your site’s navigation. ### [How to add syntax highlighting to your Roq site with Highlight.js](/posts/how-to-add-syntax-highlighting-to-your-roq-site-with-highlight-js/) Adding syntax highlighting to your Roq project has never been easier. Here’s a quick guide to help you integrate Highlight.js in your project with the help of the Quarkus web-bundler extension. Step 1: Add Highlight.js Dependency Next, add Highlight.js to your pom.xml like this: <dependency> <groupId>org.mvnpm</groupId> <artifactId>highlight.js</artifactId> <version>11.10.0</version> <scope>provided</scope> </dependency> This will make the Highlight.js library available to your project. Step 2: Initialize Highlight.js Roq is pre-configured with the Quarkus Web-Bundler to automatically bundle you Javascripts and Styles located in src/main/resource/web/app. The Roq default theme includes the {#bundle /} tag, if you are using your own templates, make sure it is present. Now, let’s configure Highlight.js. In your web/app/main.js, import the library and activate it: import hljs from 'highlight.js'; import 'highlight.js/scss/monokai.scss'; hljs.highlightAll(); And that's it! Now your code blocks will be beautifully highlighted, adding a more polished and professional look to your content. This process is quick and effective, making it easy to provide clear, readable syntax highlighting for your users. Happy coding! ### [Easily manage Drafts and Future articles in Roq](/posts/easily-manage-drafts-and-future-articles-in-roq/) Roq just made content management easier with a cool new feature that lets you control drafts and future articles directly in your configuration. No more messing around with hard-to-track content—now you can manage everything through the Quarkus config: roq -Dsite.draft -Dsite.future This is using frontmatter data in articles and pages draft: true and date: 2024-09-19 10:45:00 +0200 to take the decision. By default, both options are set to false, meaning that drafts and future pages will stay hidden until you’re ready to reveal them. All you need to do is update these configs when you're ready to publish. This simple feature adds flexibility and control, making your publishing process more streamlined. Happy content managing! ### [Effortless URL Handling in Roq with Qute super-power](/posts/effortless-url-handling-in-roq-with-qute-super-power/) Managing URLs is now very easy! With our updated Qute-powered feature, you can now manage relative and absolute URLs with more flexibility, thanks to new methods for joining paths and handling absolute URLs. Let’s explore some examples. How to Use It: Relative URL Example (toString prints the relative url): <a class="post-thumbnail" href="{=post.url}"> </a> Absolute URL Example: <a class="post-thumbnail" href="{=post.url.absolute}"> </a> ** Smart URL:** <meta name="twitter:image:src" content="{=page.image.absolute}" > There is a method in Page to retrieve the image url as a RoqUrl from the configured site images path. It is smart so that if the page image is external, it won't be affected. Under the Hood: The Power of RoqUrl At the core of this feature is the RoqUrl class that you can leverage from Qute, which makes joining and resolving URLs super easy. With this structure, joining paths is as simple as calling resolve(). This ensures your URLs are clean, predictable, and easy to manage—whether they’re relative or absolute. Wrapping Up: With Qute’s URL handling, you can now dynamically create and manage both relative and absolute URLs without any hassle. This new implementation will help keep your code clean while making it easier to navigate, link, and share content across your site. ### [Welcome to Roq!](/posts/welcome-to-roq/) Hello folks, A bunch of Quarkus contributors started this new initiative to allow Static Site Generation with Quarkus (similar to Hugo, Jekyll, Lume, ...). Quarkus already provides most of the pieces to create great web applications (https://quarkus.io/guides/web). And Roq adds the missing pieces: Roq Generator: allows to generate a static website out of any Quarkus application (it starts the app, fetch all the configured pages and assets, generate a static website and stop). Roq Data: allows to create json or yaml data file and consume them from your templates. It is also possible to map them to beans to get type-safe validation in bonus! Roq FrontMatter: allow to create pages and collections (posts, ...) using Markdown or Asciidoc with layouting. In fact, your static website content. What's missing? we now need to incrementally add the toolkit to ease the process of creating static content through Quarkus: SEO Image processing (quarkus-web-bundler/issues/42) Pagination (quarkus-roq/issues/65) Advanced routing (redirect, ...) To go further: Compat with tools like https://frontmatter.codes/ Compat with IDEs plugins Roq GitHub action Dev-UI integrated headless CMS (to edit md/asciidoc on the fs) With Roq you can develop the content using Quarkus dev-mode, and then generate (on CI) for Github Pages or similar when it's ready. Bonus, everything added will benefit any "non-static" Quarkus app and any static Quarkus app could also go back to being non static. This effort is now tracked using a "Focus Group" (temporary wording) project: https://github.com/orgs/quarkiverse/projects/6 This is a great opportunity to participate in a fun focus group and be involved with the Quarkus community, if anyone is interested in being a part of this, please reach out to me 🚀 There will be small, medium, bigger features to develop with any level of involvement. Participating could just be giving thoughts and discussing things.. Check out the Roq docs for more info on how to get the most out of Roq. File all bugs/feature requests at Roq’s GitHub repo. ## Pages ### [Markup Examples](/markups/) Markup Examples This section contains pages demonstrating the rendering of different markup languages in Roq. Available Examples AsciiDoc Example - AsciiDoc content types including headings, lists, tables, code blocks, admonitions, and more Markdown Example - Markdown content types including headings, lists, tables, code blocks, and more These pages are used for seeing theme styling and are excluded from search engines and sitemaps. ### [Oops! Roq is saying 404](/404.html) ### [About Roq](/about/) This tool is a testament to how extensible and powerful Quarkus is, offering a low-risk yet highly capable platform that will evolve as demand grows. Origins I wrote a blog post explaining how it all started. Credits Those are generated as a JSON by all-contributors, then we leverage roq-data to print them... slick 🏄! Thanks goes to these wonderful people: Andy Damevin author @ia3andy Matheus Cruz author @mcruzdev Melloware @melloware Max Rydahl Andersen @maxandersen Holly Cummins @holly-cummins Erik Jan de Wit @edewit Jérôme Tama author @jtama Rayza Luana @RayzaAnchayhua Martin Kouba @mkouba Foivos @zakkak Joel Takvorian @jotak Pablo Gutierrez @pablomxnl Pedro Hos @pedro-hos OKC JUG @okcjug Jason Lee @jasondlee João Nascimento @jotaNas janwesterkamp @janwesterkamp Clément de Tastes @CodeSimcoe Stéphane Philippart @philippart-s Patrik Duditš @pdudits Rolfe Dlugy-Hegwer @rolfedh Matheus André @matheusandre1 Sun S. D. Tan @sunix Matheus Oliveira @omatheusmesmo Dimitri Hautot @DimitriHautot Markus Eisele @myfear Andreas Fertsch-Röver @and-y ### [Create a Roq Project](/create/) Pick a name, choose your theme and plugins, then download or push to GitHub. [ { "kind" : "plugin", "name" : "TOC", "installName" : "toc", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-toc", "description" : "Generate a table of contents from page headings at build time, with no JavaScript required", "icon" : "fa-solid fa-list-ol", "tags" : [ "navigation", "seo" ] }, { "kind" : "plugin", "name" : "Tagging", "installName" : "tagging", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-tagging", "description" : "Auto-generate tag pages and filtered views for any content collection", "icon" : "fa-solid fa-tags", "tags" : [ "collections", "navigation" ] }, { "kind" : "plugin", "name" : "Sitemap", "installName" : "sitemap", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-sitemap", "description" : "Generate an XML sitemap automatically so search engines index every page", "icon" : "fa-solid fa-sitemap", "tags" : [ "seo" ] }, { "kind" : "plugin", "name" : "Series", "installName" : "series", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-series", "description" : "Organize posts into multi-part series with automatic navigation", "icon" : "fa-solid fa-layer-group", "tags" : [ "collections", "navigation" ] }, { "kind" : "plugin", "name" : "QR Code", "installName" : "qrcode", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-qrcode", "description" : "Embed auto-generated QR codes for any URL or custom text", "icon" : "fa-solid fa-qrcode", "tags" : [ "media" ] }, { "kind" : "plugin", "name" : "OG Card", "installName" : "og-card", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-og-card", "description" : "Generate 1200×630 social preview PNGs from Qute SVG templates", "icon" : "fa-solid fa-image", "tags" : [ "seo" ] }, { "kind" : "plugin", "name" : "Markdown", "installName" : "markdown", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-markdown", "description" : "Write content in Markdown with CommonMark support, included by default", "icon" : "fa-brands fa-markdown", "tags" : [ "content", "markup" ] }, { "kind" : "plugin", "name" : "Lunr Search", "installName" : "lunr", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-lunr", "description" : "Add instant full-text search to your site using Lunr.", "icon" : "fa-solid fa-magnifying-glass", "tags" : [ "search" ] }, { "kind" : "plugin", "name" : "Hybrid", "installName" : "hybrid", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-hybrid", "description" : "Build Quarkus applications with Roq static content, adding runtime page caching, future page scheduling, and cache management", "icon" : "fa-solid fa-bolt", "tags" : [ "performance", "caching", "dynamic" ] }, { "kind" : "plugin", "name" : "Faker", "installName" : "faker", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-faker", "description" : "Generate fake blog posts with realistic content for development and testing", "icon" : "fa-solid fa-wand-magic-sparkles", "tags" : [ "development", "testing" ] }, { "kind" : "plugin", "name" : "Diagram", "installName" : "diagram", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-diagram", "description" : "Render diagrams from code blocks using Kroki (Mermaid, PlantUML, and more)", "icon" : "fa-solid fa-diagram-project", "tags" : [ "content", "media" ] }, { "kind" : "plugin", "name" : "AsciiDoc", "installName" : "asciidoc", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-asciidoc", "description" : "Write content in AsciiDoc with a fast, pure-Java processor", "icon" : "fa-solid fa-file-lines", "tags" : [ "content", "markup" ] }, { "kind" : "plugin", "name" : "AsciiDoc JRuby", "installName" : "asciidoc-jruby", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-asciidoc-jruby", "description" : "Full AsciiDoctor via JRuby with support for all extensions and macros", "icon" : "fa-solid fa-file-lines", "tags" : [ "content", "markup" ] }, { "kind" : "plugin", "name" : "Aliases", "installName" : "aliases", "extensionId" : "io.quarkiverse.roq:quarkus-roq-plugin-aliases", "description" : "Set up URL redirects and short links to keep old URLs working", "icon" : "fa-solid fa-shuffle", "tags" : [ "navigation", "seo" ] }, { "kind" : "theme", "name" : "Resume Theme", "installName" : "resume", "extensionId" : "io.quarkiverse.roq:quarkus-roq-theme-resume", "description" : "Build a polished personal resume or CV from simple YAML data files", "icon" : "fa-solid fa-id-card", "tags" : [ "portfolio", "responsive", "tailwind" ] }, { "kind" : "theme", "name" : "Linktree Theme", "installName" : "linktree", "extensionId" : "io.quarkiverse.roq:quarkus-roq-theme-linktree", "description" : "Build a personal link-tree to share your links, social profiles, and QR codes", "icon" : "fa-solid fa-link", "tags" : [ "linktree", "links", "responsive", "tailwind", "qrcode" ] }, { "kind" : "theme", "name" : "Default Theme", "installName" : "default", "extensionId" : "io.quarkiverse.roq:quarkus-roq-theme-default", "description" : "The default Roq theme for blogs and sites, built with Tailwind CSS, featuring dark mode, responsive design, sidebar navigation, and social media links.", "icon" : "fa-solid fa-palette", "tags" : [ "blog", "responsive", "tailwind" ] }, { "kind" : "theme", "name" : "Base Theme", "installName" : "base", "extensionId" : null, "description" : "Minimal built-in theme with SEO, favicon, and Web Bundler. The ideal starting point to build a fully custom site from scratch.", "icon" : "fa-solid fa-cube", "tags" : [ "minimal", "starter" ] } ] Project Title Name (artifactId) Theme Base Theme Minimal built-in theme with SEO, favicon, and Web Bundler. The ideal starting point to build a fully custom site from scratch. Default Theme The default Roq theme for blogs and sites, built with Tailwind CSS, featuring dark mode, responsive design, sidebar navigation, and social media links. Linktree Theme Build a personal link-tree to share your links, social profiles, and QR codes Resume Theme Build a polished personal resume or CV from simple YAML data files Plugins Aliases AsciiDoc AsciiDoc JRuby Diagram Faker Hybrid Lunr Search Markdown OG Card QR Code Series Sitemap TOC Tagging Download ZIP Push to GitHub via code.quarkus.io ### [Roq Advanced Stuff](/docs/advanced/) If you find any issue or missing info, be awesome and edit this document to help others Roqers. Built-in features Roq and its themes include several built-in features for SEO, feeds, and discoverability. The base theme provides the essential tags (SEO, favicon, Web Bundler), while the default theme builds on top of it with a full layout, dark mode, sidebar, and more. If you are using either theme, most of these features are already enabled. Configure them through your site index frontmatter or by adding content files. For details on each feature, see the Base Theme, Favicon, SEO, Analytics, RSS, and LLMs.txt sections in the basics guide. Pagination Adding pagination to your Roq site is an easy way to improve content navigation. Let’s walk through how to implement pagination and customize its behavior in your site. Step 1: Iterate on the paginated collection First, include the following in your FrontMatter header on the page which will iterate on the paginated collection: paginate: posts Next, in your template, loop through the paginated posts using: {#for post in site.collections.posts.paginated(page.paginator)} (1) <article class="post"> ... </article> {/for} {#include partials/pagination.html/} 1 Calling .paginated(page.paginator) will resolve to the posts for the computed page. Step 2: Including Pagination Controls To add pagination controls, use the provided fm/pagination.html in your own partials/pagination.html: {#include fm/pagination.html} {#newer}<i class="fa fa-long-arrow-left" aria-hidden="true"></i>{/newer} {#older}<i class="fa fa-long-arrow-right" aria-hidden="true"></i>{/older} {/include} If you want to write your own controls, find inspiration in the FM sources fm/pagination.html. Just by doing so, Roq will generate a bunch of pages based on the pagination setting. For example with a pagination size of 4 and with 9 posts, you would get: index.html (posts 1 to 4) posts/page-2 (posts 5 to 8) posts/page-3 (post 9) the first page uses the declaring page link. You can further customize your pagination by setting the page size and link format: paginate: size: 4 collection: posts link: posts/page-:page With these steps, you can create a flexible pagination system to improve your site’s navigation. Themes Browse all available themes in the Plugins & Themes directory. Overriding theme In Roq, you can override theme partials or layouts. To override a theme partial, it is very simple, you just need to add the same template in your site. For example, adding your own templates/partials/roq-default/pagination.html will override the one from the default Roq theme To override a theme layout, you need to insert an extra layout layer. This allows you to override only specific sections of the theme layout, without duplicating the entire layout structure. Roq layouts leverage Qute include under the hood. It is possible to define insert sections that provide overridable default content. Example Let’s override the roq-default theme’s main layout so that our customizations apply everywhere it is used. templates/layouts/main.html --- theme-layout: main (1) --- {#insert /} (2) {#description} (3) Here I can override the description section {/} {#footer} <footer> And here the footer </footer> {/} 1 Inherits from the theme layout: theme-layout: main explicitly targets the theme’s main layout as a base. 2 Inheritance mechanism: {#insert /} ensures that this layout will inherit sections defined in the theme layout. 3 Override specific sections: You can override individual sections such as description and footer without affecting other parts of the layout. Now, everywhere layout: main is used (even in the theme), your override will be used. How it Works Internally Roq handles theme layouts in two layers: The original theme layouts are always kept under theme-layouts/…​. These are the base templates that overrides can extend for partial customization. Roq also produces corresponding site layouts for each theme layout. Site layouts are the versions that your pages actually reference. When Roq builds your site, it checks for overrides: If you provide an override: Roq uses your override as the site layout, while still keeping the theme’s original layout under theme-layouts/…​ for inheritance. If you don’t provide an override: Roq copies the theme layout as the site layout, so it can be used directly. This mechanism ensures you only need to override the parts you want to customize, everything else automatically falls back to the theme. Developing a theme To develop a theme, create a Maven module which will contain the theme layouts, partials, scripts and styles. . └── main ├── resources │ ├── application.properties │ └── templates │ ├── partials │ │ └── roq-default (1) │ │ ├── head.html │ │ ├── pagination.html │ │ ├── sidebar-about.html │ │ ├── sidebar-contact.html │ │ ├── sidebar-copyright.html │ │ └── sidebar-menu.html │ └── theme-layouts (2) │ └── roq-default │ ├── default.html │ ├── index.html │ ├── main.html │ ├── page.html │ ├── post.html │ └── tag.html └── web ├── roq.js ├── roq.scss 1 You can add partials for your theme, they need to be located in a directory with the theme name templates/partials/{theme-name}/. 2 Layouts need to be declared in theme-layouts/ using a directory with the theme name templates/theme-layouts/{theme-name}/. Same as for a site, scripts and styles can either be added to src/main/resources/META-INF/resources or bundled using Maven esbuild plugin: pom.xml <plugin> <groupId>io.mvnpm</groupId> <artifactId>esbuild-maven-plugin</artifactId> <version>0.0.2</version> <executions> <execution> <id>esbuild</id> <goals> <goal>esbuild</goal> </goals> </execution> </executions> <configuration> <entryPoint>roq.js</entryPoint> (1) </configuration> <dependencies> (2) <dependency> <groupId>org.mvnpm.at.fortawesome</groupId> <artifactId>fontawesome-free</artifactId> <version>6.6.0</version> </dependency> <dependency> <groupId>org.mvnpm.at.fontsource</groupId> <artifactId>pt-serif</artifactId> <version>5.1.0</version> </dependency> </dependencies> </plugin> 1 Add your esbuild entrypoint from src/main/resources/web 2 Add mvnpm or webjars dependencies This bundle will be available in /static/bundle/roq.js and /static/bundle/roq.css which can be used in your theme html <head> You need to create an application.properties: src/main/resources/application.properties site.theme=roq-default (1) 1 This allows site referencing the theme to default to this theme. Links & Urls The output location of pages and documents is determined by the FrontMatter link key. This link value can include placeholders, which will be dynamically replaced with relevant values for routing. Those links are also available in the Qute data to allow Creating links between your pages. Link placeholders Type of page Placeholder Description Example Output All :path The file path of the page, slugified (converted to a URL-friendly format) without the extension. If a slug is set in frontmatter, it replaces the filename portion while preserving the directory structure. my-page, search or docs/my-doc All :raw-path The raw file path of the page without the extension. My$, my car or été/2024 All :dir The directory portion of the page’s file path, slugified. For index files (directory pages), this is the grandparent directory (since the slug already captures the directory name). Empty for files at the content root. Use :dir[N] to skip the first N path segments — for example, :dir[2] drops the first two folders, giving a link of my-post/my-file for content in /content/posts/2025/my-post/my-file.md. posts, posts/v2/guides, or empty All :slug The slugified title of the page, derived from the title. Defaults to the slug property in data, if available or using the slugified title, falling back to the name. my-page-title All :Slug The case-preserving slugified title of the page, derived from the title. Defaults to the slug property in data, if available or using the slugified title, falling back to the name. My-Page-Title All :name The slugified name of the file (or directory if index). If the filename contains a date (e.g., '2025-08-21-My-Blog-Post.md'), the date portion will be stripped away. This behavior mimics that found in Jekyll builds, making any migrations simpler. my-blog-post All :Name The case-preserving slugified name of the file (or directory if index). If the filename contains a date (e.g., '2025-08-21-My-Blog-Post.md'), the date portion will be stripped away. This behavior mimics that found in Jekyll builds, making any migrations simpler. My-Blog-Post All :ext The file extension with the dot. Empty for all files with html output (md, asciidoc, html, …​). .json All :ext! Force the output file extension. .html, .json All :year The year of the page’s date or the current year if the date is not available. 2024 All :month The month (formatted as two digits) of the page’s date or the current month if the date is not available. 10 All :day The day (formatted as two digits) of the page’s date or the current day if the date is not available. 28 Document :collection Represents the collection to which the document belongs, such as a specific category or folder name. blog, articles, recipes Paginated :page Represents the current page. 1, 2 The slug derivation replaces all non-alphanumeric characters by - to make them url friendly. Default link value: for pages: /:path:ext (configurable via site.page-link). for documents in collections: /:collection/:slug/ (configurable via site.collections.<name>.link). for paginated page: /:collection/page:page/. Global link template configuration Instead of setting link in the frontmatter of every page, you can configure default link templates globally in application.properties: # Default link template for non-collection pages site.page-link=/:slug/ # Default link template for a specific collection site.collections.posts.link=/:collection/:year/:month/:name/ These global defaults apply to all pages or collection documents that don’t have an explicit link in their frontmatter or layout. A frontmatter link always takes precedence over the global configuration. You can also define link in a layout to affect all the pages using that layout. Creating links between your pages The pages links are automatically converted to urls by Roq, they are available in the site.url and the page.url variables. This makes creating links very easy: <a href="{=site.url}">Back to main page</a> or to get the next page url in a document: <a href="{=page.next.url}">{=page.next.title}</a> or when iterating on documents: {#for post in site.collections.posts} <a href="{=post.url}">{=post.title}</a> {/for} or also to manually retrieve a page url with site.page(sourcePath): <a href="{=site.page('foo.html').url}">{=site.page('foo.html').title}</a> By default, url will be rendered as the path from the site root. You can also get the full absolute url (i.e. from http(s)://) by using absolute on any url (e.g. {=site.url.absolute}). Manual linking Sometimes, you want to create a link for a page without holding the variable, in this case, you can use site.url(relativePath) which will be automatically resolved from the site root path. Alternative expression syntax Qute supports an alternative expression syntax where output expressions use {=expr} instead of {expr}. This makes templates safer when content contains curly braces (e.g. code samples, JSON) since only {=…​} and {#…​} are interpreted as Qute expressions, everything else is plain text. Standard Qute syntax is still the default, but this will change in a future version. New projects already have alt syntax enabled via the generated config. We recommend setting the config explicitly so your project is ready. All examples in this documentation use the alternative expression syntax. To enable it, add to your configuration: quarkus.qute.alt-expr-syntax=true With this enabled, templates use {=page.title} for expressions and {#for …​} / {#include …​} for sections (sections are unaffected). Regular {foo} is treated as plain text. Escaping pages content There are cases where you might not want your page content to be parsed by Qute, to avoid conflicts with the content. You have different options: Configure it globally via site.escaped-pages (globs are allowed): config/application.properties site.escaped-pages=posts/escaped**,my-page.html Set it in FrontMatter by adding escape: true in your page data (not working with layouts). Escape inline content by wrapping the section with {| and |}, or by manually escaping Qute expressions using \{. Setting the Root Path for your site (base-path) When the entire Roq site is under a root path such as mysite.io/foo/, configure quarkus.http.root-path in the Quarkus configuration: config/application.properties quarkus.http.root-path=/foo Environment variable: QUARKUS_HTTP_ROOT_PATH For GitHub Pages, this is already detected and handled by the Roq GitHub Action, no need to do anything. Data (advanced) For basic data usage (data files, directories, and template access), see the Data section in the basics guide. Type-safe mapping Use @DataMapping on a record to create a typed CDI bean from a data file: Mountain.java import io.quarkiverse.roq.data.runtime.annotations.DataMapping; @DataMapping("mountain") (1) public record Mountain(String name, Integer elevation) {} 1 The value must match the data filename (without extension). Then use it in templates: {=cdi:mountain.name}: {=cdi:mountain.elevation} Array data files For data files where the root element is a JSON/YAML array, use Type.ARRAY_FILE: @DataMapping(value = "mountains", type = DataMapping.Type.ARRAY_FILE) public record Mountains(List<Mountain> list) {} (1) 1 The record must have a single List<T> constructor parameter. {#for mountain in cdi:mountains.list} {=mountain.name}: {=mountain.elevation} {/for} Directory mapping For type-safe access to data directories, use ARRAY_DIR or OBJECT_DIR with @DataMapping: As a list @DataMapping(value = "heroes", type = DataMapping.Type.ARRAY_DIR) (1) public record HeroList(List<Hero> list) { public record Hero(String name, String city) {} } 1 The value must match the data directory name {#for hero in cdi:heroList.list} {=hero.name} from {=hero.city} {/for} As a map (filename as key) @DataMapping(value = "heroes", type = DataMapping.Type.OBJECT_DIR) public record HeroMap(Map<String, Hero> map) { public record Hero(String name, String city) {} } {=cdi:heroMap.map.batman.name} Type reference Type Source Constructor Description OBJECT_FILE Single file Direct fields Default. Maps a file to a typed object ARRAY_FILE Single file (array) List<T> Maps a root-level array file to a list ARRAY_DIR Directory List<T> Maps each file in a directory to a list item OBJECT_DIR Directory Map<String, T> Maps each file to a map entry (filename as key) Collections from data While plain data files let you display a list of items in an existing page, a data collection goes further: it generates a dedicated page for each entry in the data, rendered with a specified layout. This is useful when each item deserves its own URL (e.g. one page per event, per team member, or per link-tree). From a single data file (data/events.yml) - id: hello-lads name: "First event" description: "This is the first event" - id: roq-and-roll name: "SSG FTW" description: "Static site generation with Roq" site.collections.events.layout=page-event site.collections.events.from-data.id-key=id From a data directory (data/events/first.yml, data/events/second.yml) site.collections.events.layout=page-event site.collections.events.from-data.id-key=_key The id-key field specifies which data field to use as the page identifier (like a filename for content-based collections). For data directories, _key is the object key (filename without extension). The id-key value is slugified (e.g. Hello lads ! becomes hello-lads). By default, the data source name matches the collection id (e.g. the events collection looks for data/events.yml or data/events/). Use from-data.name to point to a different data source: site.collections.highlights.layout=page-highlight site.collections.highlights.from-data.id-key=id site.collections.highlights.from-data.name=events (1) 1 The highlights collection generates pages from the events data source. This lets multiple collections share the same data source with different layouts. The layout template accesses data fields via page.data: templates/layouts/page-event.html <h1>{=page.data.name}</h1> <p>{=page.data.description}</p> Data configuration Property Default Description quarkus.roq.data.dir data Location of data files relative to the Roq root directory quarkus.roq.data.enforce-bean false When true, only data files with a matching @DataMapping annotation produce beans quarkus.roq.data.log-data-beans false Log the list of registered data beans at INFO level during build (always available at DEBUG level) Debugging To debug errors, you may print debug information in the logs: roq -Dquarkus.log.category.\"io.quarkiverse.roq.frontmatter\".level=DEBUG Testing All templates will be validated at generation. Sometimes, for example on Pull-Request, you want to detect issues before actual generation. Roq provides a way to generate the full site during the test phase. First, include the quarkus-roq-testing test dependency in your pom.xml. pom.xml <dependency> <groupId>io.quarkiverse.roq</groupId> <artifactId>quarkus-roq-testing</artifactId> <version>2.1.9</version> <scope>test</scope> </dependency> Test Site Generation Once you’ve added the dependency, you can easily ensure all pages are generated without errors: src/test/java/RoqSiteTest.java @QuarkusTest @RoqAndRoll public class RoqSiteTest { @Test public void testGen() { // All pages will be generated/validated during test setup } } That’s it! This basic test already verifies that your site generation is error-free. You can also add checks on the actual generated content as it is served using a static file server: src/test/java/RoqSiteTest.java @QuarkusTest @RoqAndRoll public class RoqSiteTest { @Test public void testIndex() { RestAssured.when().get("/") .then() .statusCode(200) .body(containsString( "Ready to Roq my world!" )); } } The RestAssured port will automatically use the Roq static test server, running on port 8082 by default. The Roq test server port could be modified by an annotation parameter like this @RoqAndRoll(port=9090). Using standard Quarkus test It’s possible to use the standard Quarkus test support (Testing Your Application) to check the content, but then pages will be rendered dynamically on demand at runtime: src/test/java/QuteWebSiteTest.java @QuarkusTest public class QuteWebSiteTest { @Test public void testIndex() { RestAssured.when().get("/") .then() .statusCode(200) .body(containsString( "Ready to Roq my world!" )); } } In this case the RestAssured port will automatically use the Quarkus dynamic test server, running on port 8081 by default. Roq CLI The Roq CLI wraps common Quarkus commands with Roq-specific defaults. See the Getting Started guide for installation instructions. roq create Create a new Roq site: roq create my-site roq create my-site -x theme:base roq create my-site -x plugin:tagging,plugin:sitemap Option Description -x, --extension Extensions to add, comma-separated. Prefixes: theme:, plugin:, web: for Roq/Web Bundler extensions, or any Quarkus extension name (e.g. rest-jackson) or full GAV. -g, --group-id Maven group ID (default: io.acme) --no-code Skip example content from codestarts --gradle Use Gradle instead of Maven --roq-version Pin a specific Roq version roq start Start dev mode with live reload: roq start roq start -p 8081 Option Description -p, --port HTTP port (default: 8080, use -p without a value for a random port) Press w to open the browser, s to force a restart, m to open the editor. roq add Add extensions to an existing project: roq add plugin:tagging plugin:sitemap roq add theme:resume roq add web:tailwindcss roq add rest-jackson Extension prefixes: Prefix Description plugin:<name> Roq plugin (e.g. tagging, sitemap, aliases, series) theme:<name> Roq theme (e.g. default, resume, base) web:<name> Web Bundler extension (e.g. tailwindcss) <name> Any Quarkus extension (e.g. rest-jackson, hibernate-orm) <group:artifact> Full GAV coordinate Other commands roq generate — Generate the static site output. roq serve — Serve a previously generated static site directory. roq update — Update the Quarkus and Roq versions. See Updating Roq. Editor Preview feature, configuration and behavior may change in future releases. Roq ships a browser-based content editor in Quarkus Dev UI, available in dev mode. Create and edit pages, posts, and docs, including FrontMatter, with a live preview, no IDE required. Availability Included in the core quarkus-roq extension, no extra dependency needed. Dev mode only, never part of the generated site or a production build. Opening the editor Start dev mode (roq start or mvn quarkus:dev), then: Click the Roq Editor card in the Dev UI (/q/dev-ui). Press m in the dev console ("Manage content"). The editors Roq picks the right editor per file: Visual editor Block (WYSIWYG) editor for Markdown: slash commands, bubble menu, tables, images. Default for supported files. Simple editor Plain source editor with syntax highlighting, for AsciiDoc, HTML, and anything the visual editor can’t handle. Safe mode (default) opens Markdown files with Qute or HTML blocks in the simple editor, so existing content is never altered. Configure: # Always use the simple editor editor.visual-editor.enabled=false # Keep the visual editor even on files with Qute/HTML blocks (advanced) editor.visual-editor.safe=false FrontMatter, images, and tags A FrontMatter panel lets you edit title, date, layout, tags, and other keys alongside the content. An image picker manages page and site images; a tag manager handles tagged collections. AI content generation With the Quarkus Assistant configured, a prompt widget generates content straight into the current document. Steer tone and topic with a custom context: editor.ai.context=This is a tech blog about Quarkus. Write in a friendly, concise tone. Requires the Quarkus Assistant; otherwise the prompt widget is hidden. Default markup for new files New pages and docs default to Markdown. Change the default per content type: editor.page-markup=asciidoc editor.doc-markup=markdown Supported values: markdown, asciidoc, html. File naming conventions New file names are derived from a link-style pattern per collection. Default for posts and docs: :date-:slug~7 (date + first seven slug words). Placeholders: :date, :slug, :Slug, :name, :Name, :year, :month, :day; ~W truncates to W words. # Custom file name pattern for the "posts" collection editor.collections.posts.name=:year/:month/:slug~5 # Keep the file name in sync when the title or date changes (default: true) editor.collections.posts.sync-name=true Git sync (commit, push, pull) Commit, push, and pull content from the UI. Disabled by default; enable: editor.sync.enabled=true Then configure auto-sync and the default commit message: # Automatically pull from the remote on an interval (seconds) editor.sync.auto-sync.enabled=true editor.sync.auto-sync.interval-seconds=60 # Automatically commit + push content changes on an interval (seconds) editor.sync.auto-publish.enabled=true editor.sync.auto-publish.interval-seconds=300 # Default commit message editor.sync.commit-message.template=Update content via Roq Editor Push/pull use your existing SSH key. If it’s passphrase-protected and no SSH agent is running, provide the passphrase via the EDITOR_SYNC_SSH_PASSPHRASE environment variable. Never put an SSH passphrase in application.properties. Provide it via the EDITOR_SYNC_SSH_PASSPHRASE environment variable or another non-version-controlled source (e.g. .env). Editor configuration reference Configuration property fixed at build time - All other configuration properties are overridable at runtime Configuration property Type Default editor.page-markup Markup to use for new pages Environment variable: EDITOR_PAGE_MARKUP markdown, asciidoc, html markdown editor.doc-markup Markup to use for new docs Environment variable: EDITOR_DOC_MARKUP markdown, asciidoc, html markdown editor.visual-editor.enabled When true, use the visual editor on supported files (Markdown). When false, always use the simple editor Environment variable: EDITOR_VISUAL_EDITOR_ENABLED boolean true editor.visual-editor.safe Use simple editor if the file contains qute or html blocks without data-type="raw" to make sure we don’t break existing content Environment variable: EDITOR_VISUAL_EDITOR_SAFE boolean true editor.collections."collections-map".name File name pattern using link-style placeholders. Supported: :date, :slug, :Slug, :name, :Name, :year, :month, :day. Use ~W to truncate to W hyphen-separated words (e.g., :slug~7). Environment variable: EDITOR_COLLECTIONS__COLLECTIONS_MAP__NAME string :date-:slug~7 editor.collections."collections-map".sync-name If enabled, auto-sync file names when they match the convention and the title/date changes. Environment variable: EDITOR_COLLECTIONS__COLLECTIONS_MAP__SYNC_NAME boolean true editor.sync.enabled Enable Git sync feature (commit, push, pull via the Editor UI) Environment variable: EDITOR_SYNC_ENABLED boolean false editor.sync.ssh-passphrase Optional SSH passphrase used as a fallback when no SSH agent is available to unlock a passphrase-protected key for remote operations. Most users do not need this: JGit uses the system SSH agent (macOS Keychain, ssh-agent, Pageant) automatically. Set it only when no agent is running. For security, provide it via the EDITOR_SYNC_SSH_PASSPHRASE environment variable or a non-version-controlled config file (e.g. .env). Never commit it to application.properties. It is never sent to the browser. Environment variable: EDITOR_SYNC_SSH_PASSPHRASE string editor.sync.auto-sync.enabled Enable automatic sync (pull) from remote Environment variable: EDITOR_SYNC_AUTO_SYNC_ENABLED boolean false editor.sync.auto-sync.interval-seconds Auto-sync interval in seconds Environment variable: EDITOR_SYNC_AUTO_SYNC_INTERVAL_SECONDS int 60 editor.sync.auto-publish.enabled Enable automatic publish (commit + push) on content changes Environment variable: EDITOR_SYNC_AUTO_PUBLISH_ENABLED boolean false editor.sync.auto-publish.interval-seconds Auto-publish interval in seconds Environment variable: EDITOR_SYNC_AUTO_PUBLISH_INTERVAL_SECONDS int 300 editor.sync.commit-message.template Default commit message template Environment variable: EDITOR_SYNC_COMMIT_MESSAGE_TEMPLATE string Update content via Roq Editor editor.ai.context Custom context to include in every AI content generation request. Use this to set the tone, topic, or style for your blog. For example: "This is a tech blog about Quarkus. Write in a friendly, concise tone." Environment variable: EDITOR_AI_CONTEXT string Updating Roq Run the update command from your project directory: $ roq update This will update the Quarkus version and extensions (including Roq) and make sure they are compatible together. The releases and migration info can be found here. Site Configuration Site configuration is done in config/application.properties (or src/main/resources/application.properties): In a multi-module Maven project, use src/main/resources/application.properties instead. The config/application.properties location is resolved relative to the JVM working directory, which may not match the module directory when building from the reactor root. Configuration property fixed at build time - All other configuration properties are overridable at runtime Configuration property Type Default site.url the base hostname & protocol for your site, e.g. http://example.com Environment variable: SITE_URL string site.route-order The order of the route which handles the templates. <p> By default, the route is executed before the default routes (static resources, etc.). Environment variable: SITE_ROUTE_ORDER int 1100 site.ignored-files Add new ignored files to the default list. The ignored files (relative to the site directory). Only the content/, public/, and static/ directories are scanned. Environment variable: SITE_IGNORED_FILES list of string site.default-ignored-files The default ignored files (relative to the site directory) include: All files or directories starting with an underscore (_) These patterns are additional to the scanner’s own OS-level defaults (e.g. .DS_Store, Thumbs.db, *~, .class). Environment variable: SITE_DEFAULT_IGNORED_FILES list of string **/_**, _** site.escaped-pages Pages whose content should be escaped— i.e., included in Qute rendering but not parsed for Qute expressions. This is based on the page’s relative path from the content directory. This applies only to pages (not layouts or partials). Supports glob expressions. Environment variable: SITE_ESCAPED_PAGES list of string site.page-layout The layout to use for normal html pages if not specified in FM. When empty, the page will not use a layout when it doesn’t specify it in FM. Resolves local layout first, then theme layout as fallback. Environment variable: SITE_PAGE_LAYOUT string page site.content-dir The directory which contains content (pages and collections) in the Roq site directory. Environment variable: SITE_CONTENT_DIR string content site.static-dir The directory (dir name) which contains static files to be served (with 'static/' prefix). Environment variable: SITE_STATIC_DIR string static site.public-dir The directory which contains public static files to be served without processing (dir name) Environment variable: SITE_PUBLIC_DIR string public site.images-path The path containing static images (in the public directory) Environment variable: SITE_IMAGES_PATH string images/ site.generator When enabled it will select all FrontMatter pages in Roq Generator Environment variable: SITE_GENERATOR boolean true site.future Show future documents Environment variable: SITE_FUTURE boolean false site.theme The theme name. Used to resolve theme layouts when using theme-layout: in front matter. With a theme, layout: foo resolves local first, then theme layout as fallback. Environment variable: SITE_THEME string roq-base site.draft Show draft pages Environment variable: SITE_DRAFT boolean false site.draft-directory Directory name used to mark collection documents as draft when frontmatter does not define attribute draft. Frontmatter draft takes precedence over this directory-based fallback. Environment variable: SITE_DRAFT_DIRECTORY string drafts site.date-format Format for dates Environment variable: SITE_DATE_FORMAT string yyyy-MM-dd[ HH:mm][:ss][ Z] site.time-zone The default timezone Environment variable: SITE_TIME_ZONE string document timezone if provided or system timezone site.default-locale The default language to use when no language is specified in the frontmatter. This language will be used as a fallback for articles that don’t have a 'locale' property. Environment variable: SITE_DEFAULT_LOCALE string en site.slugify-files Indicates whether file names in the public directory and files attached to pages should be slugified (converted to a URL-friendly format). When enabled, file names will automatically be transformed into a URL-safe format. Additionally, page.file and site.file references can use the original file names, as they will also be slugified during the process. Environment variable: SITE_SLUGIFY_FILES boolean true site.collections."collections-map" If this collection is enabled Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP_ boolean true site.collections."collections-map".future Show future documents (overrides global future for this collection) Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__FUTURE boolean false site.collections."collections-map".hidden If true, the collection won’t be available on path but consumable as data. Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__HIDDEN boolean false site.collections."collections-map".layout The layout to use if not specified in FM data. When empty, the document will not use a layout when it doesn’t specify it in FM. Resolves local layout first, then theme layout as fallback. Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__LAYOUT string site.collections."collections-map".link Default link template for documents in this collection. Can be overridden per-page using the frontmatter link key. Supports placeholders: :collection, :slug, :name, :ext, etc. Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__LINK string /:collection/:slug/ site.collections."collections-map".from-data.id-key The data attribute to use as the page identifier. The value is slugified. Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__FROM_DATA_ID_KEY string required site.collections."collections-map".from-data.name The name of the data source (file or directory in data/). Defaults to the collection id. Environment variable: SITE_COLLECTIONS__COLLECTIONS_MAP__FROM_DATA_NAME string site.generated-templates-output-dir The directory where the generated templates should be created inside the output directory. Environment variable: SITE_GENERATED_TEMPLATES_OUTPUT_DIR string roq-templates site.path-prefix READ CAREFULLY: The root path of your site (e.g. /blog) should be set using quarkus.http.root-path. This path prefix should be relative to the Quarkus HTTP root path and is meant to be used only when the Roq site is served alongside a Quarkus application on a separate path. Environment variable: SITE_PATH_PREFIX string site.page-link Default link template for non-collection pages. Can be overridden per-page using the frontmatter link key. Supports placeholders: :path, :slug, :name, :ext, etc. Environment variable: SITE_PAGE_LINK string /:path:ext ### [Roq the basics](/docs/basics/) If you find any issue or missing info, be awesome and edit this document to help others Roqers. By default, your site files should be located in the project root directory (or in the Java resources dir: src/main/resources/). Directory Structure The default directory structure is: my-site/ ├── data/ (1) │ ├── menu.yml │ └── tags.yml │ ├── content/ (2) │ ├── posts/ (3) │ │ ├── 2024-10-14-roq-ssg/ │ │ │ ├── index.md │ │ │ └── image.jpg │ │ └── 2024-10-20-heart-roq.md │ │ │ ├── roq-page.md (4) │ └── index.html (5) │ ├── public/ (6) │ └── images/ │ └── logo.png │ ├── web/ (7) │ ├── app.js │ └── app.css │ ├── templates/ (8) │ ├── partials/ (9) │ │ ├── head.html │ │ └── pagination.html │ │ │ └── layouts/ (10) │ ├── base.html │ ├── page.html │ └── post.html ├── config/ │ └── application.properties (11) └── pom.xml (12) 1 Roq Data Files The data/ directory contains data files like menu.yml and tags.yml. These files can hold structured data used across the site. 2 Content Files The content/ directory is where all content of the site that will be generated as pages resides (.html, .md, .adoc*, .json, .xml, …​) 3 Collections The posts/ directory is the default collection in Roq, it is optional. It holds content files for blog posts or similar structured documents. You can configure multiples collections (recipes, events, …​). 4 Additional pages You may provide additional pages files like roq-page.md outside a collection. You can also use sub-directories which will be part of the resulting path. 5 Index File The index.html file is required and serves as the homepage. It provides site-wide data using FrontMatter. 6 Static Files The public/ directory holds static files such as images, PDFs, or other assets. These files are served as-is without processing. The default image directory is public/images/. 7 Web Bundler The web/ directory contains JavaScript and CSS source files bundled by the Quarkus Web Bundler (included by default with Roq). 8 Templates The templates/ directory is optional as templates can be provided by a theme. It contains Qute templates for partials and layouts. 9 Qute Partials The partials/ directory contains reusable Qute template fragments, such as head.html and pagination.html, which can be included in layouts. 10 Layouts The layouts/ directory defines the structure for pages and documents. For example: base.html is the main layout. page.html and post.html are specific layouts for pages and posts. 11 Configuration The config/application.properties file contains the site configuration such as collections, theme, and other settings. 12 Build files The build file such as a pom.xml is needed to configure the build. It contains dependencies for your site such as theme and plugins. Qute and FrontMatter All templates may use the awesome type-safe Qute template engine. Type-safety doesn’t make it more complex—it just means that using wrong variables will result in a build error. This prevents issues from leaking into production. Templates for layouts, documents, and pages may also declare a FrontMatter (FM) header, delimited by two ---. This header contains YAML data used to configure things like: Routing Data for templates Content generation Pagination For example, a page template blog-post.html might start with: --- title: "My First Blog Post" date: 2025-09-08 author: "Andy" layout: post tags: qute, roq, tutorial --- **Hello World** Content The content/ directory contains the site index page and all the Pages and Collections (such as blog posts). It may also contain attached static files (Page attached static files). Content templates can be written in html, json, yaml, yml, or xml or using Markup languages. We sometimes refer to pages in collections as documents, documents are just a special kind of pages. Prefer writing in your browser? Roq includes a block editor in Dev UI (dev mode) with live preview, FrontMatter editing, images, and Git publishing; see Editor. Site index Your site index page is required and should be located in content/index.html (you can also use a markup extension). content/index.html --- title: Hello Roqers (1) description: It is time to start Roqing 🎸! --- <h1>Hello fellow Roqers 🤘</h1> <p> With Roq, it is very easy to link to another <a href="{=site.url('/roq-page')}">page</a>. (2) </p> 1 The index.html also describe your site information through a FrontMatter header. 2 We use the {=site.url(path)} using Qute to manually resolve other pages urls. There are different ways to link your pages as explained in the Links & Urls section. Pages Any content template file without the _ prefix in the site content/ directory (and subdirectories) will be scanned as pages. Let’s create your first page and spice things up a bit by using Markdown (included by default with Roq). roq-bottom.md --- title: Roq Bottom description: When you hit Roq bottom, try Roq to climb back up! link: /climb-back-up (1) the-rope: You Roq! (2) --- # Roq Bottom If you thought you hit Roq Bottom, take this 🪢 because : __{=page.data.the-rope}!__ (3) 1 you can use link to give this page a custom link (by default it will use the file-name). 2 you can add other FM data. 3 FM data is available through page.data. By default, pages use the page layout and documents in collections use the post layout (from the theme if using one). You can override this with layout: in the FrontMatter. Markup languages You can use different markup languages in Roq content. Markdown Roq ships with Markdown support out of the box (powered by commonmark-java). To write pages or documents in Markdown, simply use the .md or .markdown file extension. This plugin also allows converting Qute data that contains Markdown into HTML using the mdToHtml template extension. content/page.html --- bar: | ## Hello This is using **Markdown** --- {=page.data.bar.mdToHtml} If you don’t need Markdown, you can disable it by excluding the Markdown plugin from the Roq extension in your pom.xml. See the Markdown markup test page for example usage of all supported Markdown features. AsciiDoc AsciiDoc is also fully supported in Roq via the AsciiDoc plugin. Once installed, to write pages or documents in AsciiDoc, simply use the .adoc or .asciidoc file extension. When using Qute expressions as link targets in AsciiDoc, use the explicit link: macro. AsciiDoc only auto-detects links for known URL schemes (http, https, …​), so {=page.url}[Click here] won’t render as a link. Use Click here instead. See the AsciiDoc markup test page for example usage of all supported AsciiDoc features. Collections Collections are a great way to group related content such as blog posts, recipes, member of a team or talks at a conference. Once created you can easily iterate and link to them. By default, Roq is configured with a posts collection using the content/posts directory. Let’s create our first post: content/posts/2024-10-14-roq-solid.md --- title: Roq bad puns description: Roq is very good for bad puns 🤭 tags: (1) - funny - ai img: 2024/10/roq-solid.jpg --- # {=page.title} (2) Here is a list of puns suggested by Chat GPT: 1. Roq and Rule – A play on "rock and roll," implying dominance or success. 2. Between a Roq and a Hard Place – Classic pun meaning stuck in a difficult situation. 3. Roq Solid – Something that is extremely reliable or stable. 4. You Roq! – A compliment, suggesting someone is awesome or does something well. 5. Roq Bottom – Referring to the lowest possible point, often used metaphorically. 6. Roq the Boat – To cause trouble or disturb the status quo. 7. Roq Star – A person who excels or stands out in their field. 8. Let's Roq – Slang for getting started or doing something exciting. 9. Roq On! – An enthusiastic way to say "keep going" or "stay awesome." 10. Roqy Road – Could be literal (the type of road) or metaphorical for a difficult journey. 11. Roq of Ages – A historical reference, often implying something long-standing and unchanging. 12. Roq the Cradle – Can be literal or a pun about nurturing or starting something new. 13. Roqy Relationship – A tumultuous or unstable relationship. 14. Heavy as a Roq – Something burdensome or difficult to manage. 15. Stone Cold Roq – Referring to something very cool or emotionless. 1 You can define tags (see Tagging plugin to create pages for tags). 2 You have shortcut on the page to access title and description. Ok, to dive a bit deeper, we could create a json listing all posts with some info: content/posts.json [ {#for post in site.collections.posts} (1) { "title": "{=post.title}", "url": "{=post.url.absolute}", (2) "image": "{=post.image.absolute}", (3) "date": "{=post.date}", (4) "read-time": "{=post.readTime}" (5) }{#if !post_isLast},{/if} {/for} ] 1 You can use site.collections.[collection id] to access the full list of documents (it is also possible to paginate). 2 post.url contains the post url (as a RoqUrl), absolute to get the absolute url. 3 post.image is smart and is already resolved to the image url (as a RoqUrl), absolute to get the absolute url. 4 post.date returns a ZonedDateTime and can be formatted the way you want. 5 post.readTime is a Qute template extension which compute the read time based on the post content. Draft and Future To create a draft page or document, you can use the frontmatter field draft: true. Drafts are hidden by default unless you set %dev.site.draft=true in your Quarkus configuration (the %dev makes it effective only in dev mode). You can also place draft documents (for collections) in a drafts/ (for example posts/drafts/) directory. Documents in that directory are treated as drafts only when their frontmatter does not define draft. If frontmatter explicitly sets draft, that value takes precedence. You may also start roq with the option: roq -Dsite.draft. By default, documents with a date in the future (FrontMatter data or file name) will not be visible unless you have %dev.site.future=true in your Quarkus configuration (the %dev makes it only available in dev-mode). You may also start roq with the option: roq -Dsite.future. It is possible to configure a collection to always show future documents: site.collections.events.future=true (1) 1 Always show future documents for the "events" collection. How to create custom collections? You can easily create your own collection, such as documentation, recipes, team members, or conference talks. To do this, simply create a new folder under the content directory. For example, if you’re adding docs, it would look like this: content/ ├── docs │ ├── 01-chap │ │ ├── image1.png │ │ └── index.adoc │ ├── 02-chap │ │ ├── image2.png │ │ ├── index.adoc └── posts └── 2025-01-02-my-first-blog └── index.md In this example, we have two collections: posts and docs. You need to define the new collection in the config/application.properties (or src/main/resources/application.properties) file. If you created your site with roq create, this file already exists with the posts collection configured: site.collections.docs.layout="page" (1) site.collections.docs.future=true (2) site.collections.posts.layout="post" 1 Here, we set the layout for docs pages to page; 2 Since the new collection is not a time-based collection, we need to set future as true to show all files. Since we’re adding a new collection, it’s also necessary to declare the existing posts collection to ensure it continues to function correctly. Now, we can access all the new collection docs data as follows: {#for doc in site.collections.docs} - [{=doc.title}]({=doc.url}) {/for} Since the new collection is also a normal page, we can use all variables described in the variable section. Using a theme If you created your site with roq create, you already have a theme installed. A theme provides layouts, styles, and scripts for your site. Pages automatically use layouts from the theme (local layouts take priority if they exist). The default theme comes with a full blog layout, dark mode, sidebar, and SEO support. The base theme provides a minimal HTML structure with just SEO, favicon, and Web Bundler, giving you full control over the design. Browse all available themes in the Plugins & Themes directory. For advanced usage (overriding, developing), refer to the Themes section. To add a theme to an existing project (adds the dependency only, without initial site files): roq add theme:default Templates (Layouts, Partials, and User Tags) Layouts, partials, and user tags are templates and must use one of the following extensions: html, xhtml, htm, json, yaml, yml, or xml. INFO: .md and .adoc files are not parsed as templates. Instead, you can use markup inside templates through Qute sections {#md} and {#adoc}, provided the corresponding plugins are available. Layouts For your site, you will have one or more kind of pages, this is what we call "layouts", located by default in templates/layouts/. For example: main: the base layout for all kind of pages page: the layout of normal pages post: the layout for blog posts recipe: the layout for recipes or whatever A layout may be specified in pages through the layout FrontMatter key (e.g., layout: page). By default (if the content of the file is not a full HTML page — i.e., it does not contain a <html> tag or <!DOCTYPE declaration), posts will use the post layout and normal pages will use the page layout. This can be configured through site configuration. Roq layouts are using the Qute include section under the hood to achieve template inheritance. For more details, see the Qute documentation on includes: Qute includes. Unlike partials, layouts can also define Frontmatter data, which is inherited along with the template structure. If you’re not using a theme, you can create your own templates (example templates). How to create a layout page If you’re using a theme, you can take advantage of its layouts, but you’re also free to add your own. Layouts are built on a powerful inheritance system, allowing flexibility and customization. When using a theme, you can create new layouts that extend the existing theme layouts or partially override them to better suit your needs. Learn more here: Overriding Theme. In this sample we will create a simple 3 steps inheritance layout. All below files should be located in the templates/layouts/ directory. To do so, you will first need a default.html file as followed: default.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{=page.title}</title> (1) </head> <body> {#insert /} (2) </body> </html> 1 Use the title page attribute that will be defined by the using page 2 Allows to insert arbitrary content from the page using the layout. Then you can create a main.html file as followed: main.html --- layout: default (1) title: And now for something completely different (2) --- <header> Head </header> {#insert /} <footer> Toes </footer> 1 Uses the default layout. 2 Defines a default title attribute value for all its children. Finally, you can create a content.html file as followed: content.html --- layout: main (1) --- {#include partials/image /} (2) knees 1 Uses the main layout. 2 Includes a partial template. Then it will be rendered as followed: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>And now for something completely different</title> </head> <body> <header> Head </header> <img src="https://supersimple.com/wp-content/uploads/head-shoulders-knees-and-toes-flashcards-726x1024.png" alt="Illustration" /> knees <footer> toes </footer> </body> </html> To summarize, each page will be rendered using their parent layout recursively. The inheritance system works for the page content as much as for the FrontMatter data. Partials You can split layouts into partial, reusable templates. Partials make it easier to maintain different sections of your layouts. By default, they are located in templates/partials/. For example: ➡️ templates/partials/pagination.html can be included using: {#include partials/pagination /} Tags Tags (User Tags) are custom reusable snippets of code, similar to small components. They help reduce duplication and let you create reusable sections. For example, create a tag in templates/partials/post-link.html: {@io.quarkiverse.roq.frontmatter.runtime.model.DocumentPage post} <div class="post-link"> <a href="{=post.url}"> <img src="{=post.image}" alt="{=post.title}" class="post-image"/> <h3>{=post.title}</h3> <p>{=post.description}</p> </a> </div> You can then use it in another template like this : {#post-link post=site.document('posts/2025-09-08-my-article.md')/} This would render a styled card linking to the given DocumentPage post. You can reference pages and documents by their source path. For example: site.document('posts/2025-09-08-my-article.md') site.page('about.md') To learn more, see the Qute User Tags guide. Built-in features Favicon Roq automatically discovers favicon files from your public/ directory. Place any of the following files and they will be included in the HTML head: favicon.svg (preferred, scalable) favicon.ico (legacy fallback) favicon.png (PNG fallback) apple-touch-icon.png (iOS devices) To override auto-discovery, set icon or favicon in your site index frontmatter: icon: my-custom-icon.svg SEO Roq includes built-in SEO support with meta tags, Open Graph, and Twitter cards. The {#seo page site /} tag is automatically included by the default theme. If you use a custom layout, add it to your HTML head: <head> {#seo page site /} </head> It will automatically generate <title>, <meta> author/description, Open Graph and Twitter card tags from the FrontMatter data. robots meta tag (per page) Set a robots value in a page’s (or document’s) FrontMatter to control how search engines index it. When the page is rendered, the {#seo page site /} tag will emit a corresponding <meta name="robots"> tag. --- title: "Internal Draft" robots: noindex --- Common values: noindex — exclude the page from search engine indexes (drafts, internal docs, staging content) nofollow — don’t follow links from this page noindex, nofollow — combine both The <meta name="robots"> tag is only rendered when the robots key is set on the page. Configure site-wide SEO defaults through the site index frontmatter: Key Description author Default author name for meta tags (can be overridden per page) lang Default language/locale (e.g. en, fr) twitter Twitter/X handle for Twitter cards (e.g. quarkusio) Facebook Open Graph facebook: app_id: "123456789" publisher: "https://www.facebook.com/yourpage" admins: "your-fb-admin-id" Webmaster Verifications webmasterVerifications: google: "your-google-verification-code" bing: "your-bing-verification-code" yandex: "your-yandex-verification-code" Available keys: google, bing, alexa, yandex, baidu, facebook. Analytics To add Google Analytics 4, configure it in the site index frontmatter (used by the default theme): analytics: ga4: XXXXXXXXXX If you use a custom layout, add the {#ga4 /} tag to your HTML head. RSS The {#rss site /} tag is automatically included by the base theme layout and all built-in themes (default, resume). If you use a custom layout that overrides {#head-meta}, add it to your HTML head: <head> {#rss site /} </head> Then create a content/rss.xml file with: {#include fm/rss.html /} Roq will generate a valid RSS feed from your blog posts FrontMatter data. By default, <content:encoded> contains the post description. Use the contentLimit parameter to include richer content: {#include fm/rss.html contentLimit=0 /} (1) {#include fm/rss.html contentLimit=150 /} (2) 1 Full rendered content 2 Content abstract limited to 150 words LLMs.txt Roq includes built-in support for generating /llms.txt and /llms-full.txt following the llms.txt specification. AI systems like ChatGPT, Claude, and Perplexity use these files to understand site structure and content. /llms.txt — A structured index containing the site title, summary, and a list of all pages with their titles and descriptions. /llms-full.txt — The same structure, with the full plain-text content of each page included. To enable llms.txt generation, create the following content files: content/llms.qute.txt: {#include fm/llms.html} content/llms-full.qute.txt: {#include fm/llms-full.html} To exclude a specific page, set llmstxt: false in its frontmatter. Loading Roq context in your AI assistant You can give your AI coding assistant full context about Roq by pointing it to https://iamroq.dev/llms.txt (or /llms-full.txt for complete content). This provides the project structure, quick start instructions, template syntax, and links to detailed skill files. Locale and date formatting Roq uses the site locale to format dates in templates. Set the default locale in your config/application.properties: site.default-locale=fr This affects all locale-aware date extensions such as shortDate, longDate, and dateStyle. For example, with site.default-locale=fr, {=page.date.longDate} renders "9 octobre 2024" instead of "October 9, 2024". You can also override the locale per page using frontmatter: --- locale: fr --- The locale is resolved in this order: page frontmatter locale, browser Accept-Language header (in dev mode), then site.default-locale (defaults to en). Date format style Date extensions use Java’s FormatStyle for locale-aware formatting. The available extensions on page.date are: Extension Style Example (en) shortDate MEDIUM Oct 9, 2024 longDate LONG October 9, 2024 style('short') SHORT 10/9/24 style('medium') MEDIUM Oct 9, 2024 style('long') LONG October 9, 2024 style('full') FULL Wednesday, October 9, 2024 The style extension also accepts a custom date pattern: {=page.date.style('yyyy, MMM dd')}. Roq does not yet support i18n collections (generating the same content in multiple languages with locale-specific URLs). The locale setting only affects date and number formatting in templates. Variables You can use Qute to access site and pages data. For this use the site and page variables: The site (javadoc) variable allow to access site global info from any page, document, layout or partial. Show attributes Variable Type Description Example site.url RoqUrl The Roq site URL http://example.com/my-roq-site/ site.data JsonObject The site FM data (declared in the index.html) {"title": "My Site", "description": "A description"} site.pages java.util.List<NormalPage> All the pages in this site (without the documents) [Page1, Page2, Page3] site.collections RoqCollections All the collections in this site (containing documents) {"collection1": Collection1, "collection2": Collection2} site.title String The site title My Site site.description String The site description A description site.image RoqUrl The cover image URL of the page with disk check http://example.com/static/images/site.png site.image(String relativePath) RoqUrl The image from the public images directory with disk check site.image(‘foo.jpg’) ⇒ http://example.com/images/foo.jpg site.file(String relativePath) RoqUrl The file from the public directory with disk check site.file(‘foo.pdf’) ⇒ http://example.com/foo.pdf site.url(String path, String…​ others) RoqUrl Shortcut for site.url.resolve(path) site.url("/about") ⇒ http://example.com/my-roq-site/about site.page(String sourcePath) Page Get a page or document page by source path (e.g. pages/first-page.html) site.page(‘foo.html’).url.absolute ⇒ http://example.com/the-foo-page The page (javadoc) variable is available in pages, documents, layouts, and partials. It contains the info for the page it is used from. Show attributes Variable Type Description Example page.url RoqUrl The URL to this page http://example.com/about page.source Origin The page source (file name, …​) page.data JsonObject The FM data of this page {"title": "About Us", "description": "This is the about us page."} page.paginator Paginator The paginator if any Paginator{currentPage=1, totalPages=5} page.collection String The collection id if this a document posts page.title String The title of the page (shortcut from FM) About Us page.description String The description of the page (shortcut from FM) This is the about us page. page.image RoqUrl The cover image URL of the page with disk check http://example.com/static/images/about.png page.image(String relativePath) RoqUrl The image from the attached files (for index pages) or from the public image directory with disk check (for other pages) page.image(‘foo.jpg’) ⇒ http://example.com/foo-page/foo.jpg page.file(String relativePath) RoqUrl The file from the attached files with disk check page.file(‘foo.pdf’) ⇒ http://example.com/foo-page/foo.pdf page.date ZonedDateTime The publication date of the page or null 2023-10-01T12:00:00Z Data Place JSON or YAML files in the data/ directory. Each file becomes a named CDI bean, accessible in any Qute template. Data is enough to display a list of items in an existing page (e.g. a list of team members in your about page). If you need Roq to generate a dedicated page for each item in the data (e.g. one page per team member), use a data collection instead. Supported extensions: .json, .yml, .yaml. Accessing data in templates data/foo.yml bar: Roq Access it with the cdi namespace: {=cdi:foo.bar} For structured data: data/authors.yml ia3andy: name: Andy url: https://github.com/ia3andy john: name: John Doe url: https://example.com {=cdi:authors.ia3andy.name} {#let author=cdi:authors.get(page.data.author)} <a href="{=author.url}">{=author.name}</a> {/let} Data directories A directory inside data/ is automatically grouped into a single bean, with each file as a key (filename without extension): data/heroes/ batman.yaml # { "name": "Batman", "city": "Gotham" } superman.yaml # { "name": "Superman", "city": "Metropolis" } {=cdi:heroes.batman.name} {=cdi:heroes.superman.city} For type-safe Java mappings, data collections, and configuration, see the Data section in the advanced guide. Template Extensions The Qute templating language supports a concept called template extension methods. These methods allow us to add functionality to types and expose it in our templates. Moving logic from the template to Java code gives us the ability to use a more robust language for more complex logic and makes the functionality more easily reusable, as well as keeping our templates clean: Qute provides built-in template extensions Roq has several built-in template extensions (javadoc): Usage in a Qute template Description text.numberOfWords Returns the number of words in the string text.wordLimit(limit) Returns the text limited to limit words, adds "…​" if truncated text.slugify Returns a slugified version of the text htmlContent.contentAbstract(limit) Returns the HTML content limited to limit words htmlContent.stripHtml Returns the text with all HTML tags removed page.readTime Returns the estimated reading time in minutes for the page content page.contentAbstract Returns the first 75 words of the page content page.contentAbstract(limit) Returns the page content limited to limit words list.randomise Returns the list in random order jsonArray.asJsonObjects Returns a list of JsonObject. All items must be JSON objects. collections.collection(key) Returns the collection for the given key posts.filter(key, value) Returns only documents matching the front-matter key/value posts.future Returns only documents dated in the future posts.past Returns only documents dated in the past posts.sortBy(key, reverse) Sorts documents by a front-matter key (string), optionally reversed posts.sortByDate(reverse) Sorts documents by date, optionally reversed field.asStrings Normalizes a front-matter field into a list of strings fileName.mimeType Returns the MIME type based on the file name extension You can provide your own template extensions by adding a class to your projects src/main/java directory and annotating either class or the public static method with @TemplateExtension: public class Extensions { @TemplateExtension public static String doSomething(String text) { // .... } } and then you can use that extension in your template: {=site.pageContent(page).doSomething} For more details, see the Quarkus and Qute documentation. Site static files Site static files are served as-is without any additional processing. By default, all files in public/ are scanned as static files. public/ ├── images/image.jpg (1) ├── scripts/script (2).js (2) └── presentation.pdf (3) 1 generated as on /images/image.jpg 2 generated as /scripts/script-2.js(slugified 👇) 3 generated as /presentation.pdf Site static files url can be accessed through site.file('presentation.pdf') or site.file('scripts/script (2).js') or just their relative paths. site.file(path) also checks that the file exists on disk and will adapt on site configuration (e.g. root path change). To improve SEO, all static files are slugified, Roq replaces non-URL-friendly characters with -. URL-friendly characters are alphanumeric, -, and _ (multiple dots are also tolerated for files). Using site.file and page.file variables automatically applies this replacement on returned url (same for images). To disable this behavior, set site.slugify-files=false in Roq’s configuration. Page attached static files Pages may have attached static files (image, pdf, slides, …​). For this, instead of creating a file page, create a directory with an index page: content/my-page/ ├── image.jpg (1) ├── slide.pdf (1) └── index.md (2) 1 Every non page files in the directory will be attached to the page. 2 Use an index.(html,md,…​) for the page content; this also works in collections. In that case, those attached files will be served under the same path as the page and can be accessed via a relative link: [slide](./slide.pdf) The resulting link for a page can be different from its directory name, attached files will be relative to the resulting link. This way it works both in IDEs preview and in the browser. Let’s imagine for a minute that the page link is https://my-site.org/awesome-page/, then the slide will be served on https://my-site.org/awesome-page/slide.pdf. You can use {=page.file("slide.pdf")} to resolve the file url and check that the file exists. This is also useful in other cases, for example from another page (e.g. {=site.page("my-page/index.md").file("slide.pdf")}) or if you want the absolute url (e.g. {=page.file("slide.pdf").absolute}): If you want to iterate over page files, they can be listed using {=page.files}. Images This section explains how to access images in your site or in a specific page. Site images Site‑level images live in public/images/ (e.g. my-site/public/images/image-1.png). The default public path is images/ and can be customized in the site configuration. Use site.image() to generate the correct URL: <img src="{=site.image('image-1.png')}" /> site.image(name) is equivalent to: <img src="{=site.file('images/' + name)}" /> Page images When a page is a directory (for example posts/surf/index.html), the method {=page.image(name)} checks if the image is attached to that page and returns its URL. For single‑file pages (posts/basketball.md), {=page.image(name)} behaves like site.image(name) and resolves from public/images/. Example structure: my-site/ ├── content/ │ └── posts/ │ ├── basketball-article.md (1) │ └── surf-article/ │ ├── cover.jpg │ ├── surf.jpg (2) │ └── index.html └── public/ └── images/ (3) ├── basketball-cover.png ├── basketball.png └── football.jpg 1 Non-directory pages → page.image() == site.image(). 2 Page-attached file → accessible via {=page.image('surf.jpg')}. 3 Site images → accessible everywhere via site.image(name). Usage example File: `surf-article/index.html` --- image: cover.jpg --- <h2>👍</h2> <img src="surf.jpg" /> <!-- 1 --> <img src="{=page.image()}" /> <!-- 2 --> <img src="{=page.image('surf.jpg')}" /> <!-- 3 --> <img src="{=site.image('basketball.jpg')}" /> <!-- 4 --> <img src="{=site.image('basketball.png').absolute}" /> <!-- 5 --> <h2>👎</h2> <img src="{=site.image('surf.jpg')}" /> <!-- 6 --> <img src="{=page.image('soccer.jpg')}" /> <!-- 6 --> <img src="{=page.image('basketball.jpg')}" /> <!-- 6 --> Page & Site cover image Page cover image is referenced in the page FM image data. some-page.md --- image: my-page.png --- {=page.image} The url can be accessed from this template (and its parent layouts) through {=page.image}. index.html --- image: my-site.png --- It can be accessed in any template through {=site.image}. Styles and Javascript Here are two options to consume scripts and styles: Add css and scripts in your site static directory, see Site static files section. Use the Quarkus Web Bundler to bundle your script and styles 👇. The Quarkus Web Bundler is included by default in Roq. To use bundling, add your scripts (js, ts) and styles (css, scss) in the web/ directory at the project root: my-site/ ├── web/ │ ├── app.js │ └── app.scss src/main/resources/web/app/ also works if you prefer the standard Java resources layout. The bundled files will be generated in /static/bundle/…​, you can see them in the target directory or the output of generation, but you don’t have to reference them manually. Instead, to include the generated bundle in your templates (the base theme and the others already includes it), specify the bundle user tag in the html>head tag: partials/head.html <head> ... {#bundle /} </head> It will be rendered with the relevant <script> and <style> tags to include your bundle. You may also consume and bundle npm dependencies among other cool things. For more info, read the Quarkus Web Bundler documentation. ### [Getting started](/docs/getting-started/) Create your first Roq site in seconds. 1. Install the Roq CLI Install it via JBang (installs JBang if needed): Linux/macOS Windows $ curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --force roq@quarkiverse/quarkus-roq ✓ roq installed > iex "& { $(iwr https://ps.jbang.dev) } app install --fresh --force roq@quarkiverse/quarkus-roq" ✓ roq installed 2. Create your site Use the web creator to configure your site and download it as a ZIP (or push directly to GitHub): Create a Roq site Or create from the CLI (you can change the name): Create $ roq create my-site Creating Roq site: my-site ✓ Roq site created in ./my-site This creates a site with the default theme (blog layout, dark mode, sidebar, SEO). Add plugins and pick a different theme with -x (e.g. -x theme:resume, -x plugin:tagging). To start from scratch, use the base theme: -x theme:base. Browse all available themes and plugins. 3. Start dev mode Dev $ cd my-site $ roq start Listening on http://localhost:8080 ✓ Live-reload enabled Open localhost Roq the basics → ### [Migrating to Roq](/docs/migrating/) 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. Already using Roq and looking to update to a newer version? See the updating guide and the migration guide. 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: Project scaffold — roq create, application.properties, and directory structure Layouts and templates — Liquid/Jinja/Go templates to Qute templates Content files — Markdown or AsciiDoc with front matter adjustments 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. Recommended phased approach 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. Sanitize Before starting to convert, it’s a good idea to tidy up any inconsistencies, warnings or use of deprecated capabilities in your site. Here are some things to look out for. Eliminate forward-lookups in SCSS Older versions of Jekyll use the RubySass or LibSass libraries, both deprecated. These Sass implementations allow variables in .scss files to be references before they are defined. Jekyll’s RubySass and LibSass uses a multi-pass compilation approach. The first pass scans entire file and collects all variable definitions. The second pass resolves variable references. However, the Sass specification requires variables to be defined before they are used. This is enforced by Dart Sass, which is used by both Roq and newer versions of Jekyll. Choose a single date format in frontmatter Roq allows you to specify the date format used for posts, but only one. If your site uses a number of different date formats that can’t be described by the same date format string, reduce how many are used. Remember that [] can be used for optional parts of the format string, for example: site.date-format=yyyy-MM-dd['T'HH:mm:ss][X] This gives some flexibility, as long as the date formats aren’t totally inconsistent. 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 When migrating from Jekyll or Hugo, consider which features in your original site require plugins in Roq. For example, if your Jekyll site uses the jekyll-sitemap plugin, add plugin:sitemap to your Roq project. If you use tags for categorization, add plugin:tagging. Check the available plugins with roq list plugins or browse the Plugins & Themes directory to find equivalents for your site’s features. 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 files (see File and directory mappings). site.slugify-files Set to false to preserve original filenames in URLs instead of slugifying them. File and directory mappings Jekyll and Roq use different directory conventions. The table below shows where each category of files lives after migration. Some directories have a configurable location — where a config property is listed, you can point Roq at the original Jekyll path instead of moving files. Category Jekyll Roq default Config property Blog posts _posts/ content/posts/ site.content-dir (default: content) Custom collection _<name>/ content/<name>/ site.content-dir Root pages .md, .adoc in project root content/ site.content-dir Layouts _layouts/ templates/layouts/ Includes / partials _includes/ templates/partials/ Data files _data/ data/ quarkus.roq.data.dir (default: data) Sass / SCSS partials _sass/ web/ CSS entry points assets/css/ web/ Static assets (images, fonts) assets/ public/ site.public-dir (default: public) Site configuration _config.yml config/application.properties + data/siteConfig.yml Ruby plugins _plugins/ Removed (replaced by Roq plugins or custom Java code in src/main/java/) When moving files, use git mv instead of cp to preserve file history. If a target directory already exists, git mv moves the source directory into it — remove the target first if it was created during testing. Jekyll site properties (title, description, custom keys from _config.yml) are split between application.properties (for Roq-specific settings like site.url) and data/siteConfig.yml (for everything else, accessed as {=cdi:siteConfig.myProp} in templates). 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. Variable and object mappings Beyond syntax, the template object model changes. Custom front matter fields, site properties, pagination, and loop metadata all have different access patterns in Roq. Category Jekyll (Liquid) Roq (Qute) Custom front matter page.myField page.data.myField Page path page.path page.sourcePath Page tags page.tags page.data.tags.asStrings Content (layouts) {{ content }} {#insert /} Content (partials) {{ content }} {=page.content} Include parameters include.param param (direct access) Site posts site.posts site.collections.get('posts') Site tags site.tags site.collections.get('posts').tagsCount (requires tagging plugin) Site data files site.data.books.items cdi:books.items Site config properties site.myProp cdi:siteConfig.myProp (via data/siteConfig.yml) Site base URL site.baseurl Removed (Roq URLs are site-relative) Site URL (as string) site.url site.url.root.url (Roq’s site.url is a RoqUrl object) Page URL (comparisons) page.url == '/' page.url.path == '/' (RoqUrl is not a String) Build time site.time now Paginator posts paginator.posts site.collections.get('posts').paginated(page.paginator) Total pages paginator.total_pages page.paginator.total Next page paginator.next_page_path page.paginator.next Previous page paginator.previous_page_path page.paginator.previous Loop index (1-based) forloop.index item_count (named after the loop variable) Loop index (0-based) forloop.index0 item_index First iteration forloop.first item_count == 1 Last iteration forloop.last !item_hasNext Filter mappings Liquid filters become Qute method calls. Common conversions: Liquid filter Qute equivalent | upcase .toUpperCase | downcase .toLowerCase | capitalize .capitalize | strip_html .stripHtml | size .size | first .first | last .last | sort .sort or .sort('property') | reverse .reverse | uniq .distinct | strip .trim() | join .join | default: value ?: value | append: str .concat(str) | prepend: str str.concat(expr) | replace: "a", "b" .replace("a", "b") | split: "," str:split(expr, ",") | where: "key", "val" .where("key", "val") | where_exp: "v", expr list:whereExp(coll, "v", expr) | group_by: prop .groupBy(prop) | map: prop .map(prop) | truncatewords: 50 .wordLimit(50) | date: "%B %d, %Y" .format('MMMM dd, yyyy') | xml_escape .escapeHtml | url_encode .urlEncode | slugify .slugify | markdownify .markdownify | relative_url Removed (or prepends /) | absolute_url Removed 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: Convert the main layout first (usually default.html) Convert each include/partial file separately 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/ link: /about/ (or removed if it matches the file path; see Convert site configuration) 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 Roq’s default data directory is data/, but you can point it at Jekyll’s _data/ instead to avoid moving files (see File and directory mappings). Each YAML or JSON file in the data directory 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 plugins: [jekyll-sitemap] roq add plugin:sitemap collections: site.collections.<name>.layout=<layout> exclude: [vendor, node_modules] site.ignored-files=vendor/**,node_modules/** Permalink and link mappings Permalink handling depends on whether the permalink is in _config.yml (collection-level) or in a content file’s front matter (per-page). Per-page front matter: Jekyll front matter Roq front matter Notes permalink: /about/ link: /about/ If the permalink matches the file’s natural path, it is removed as redundant permalink: /old/path/ aliases: [/old/path/] When the old URL should redirect to the new file-path-based URL (requires aliases plugin) Collection-level permalink patterns (from _config.yml defaults): Jekyll permalink placeholder Roq link placeholder :path :dir[1]/:name :title :name :categories :collection For example, a Jekyll collection permalink /:categories/:title/ becomes site.collections.<name>.link=/:collection/:name/ in application.properties. Properties set by the migration tool If you use the automated roq-it-jekyll migration script, these properties are set automatically. If you are migrating manually, consider adding them to config/application.properties: Property Purpose quarkus.qute.alt-expr-syntax=true Uses {=expr} for output so plain {...} is not parsed (safe for code samples) quarkus.qute.strict-rendering=false Allows missing template variables without failing the build quarkus.qute.property-not-found-strategy=NOOP Missing properties render as empty string (matches Liquid’s silent null behavior) site.date-format=yyyy-MM-dd['T’HH:mm:ss][X] Flexible date parsing for Jekyll’s mixed date formats site.escaped-pages=posts/** Wraps content in these paths with Qute escape markers (protects curly braces in code samples) quarkus.asciidoc.attributes."!showtitle"=true Prevents duplicate titles (Jekyll layouts render page.title as <h1> separately) quarkus.web-bundler.bundling.external=/assets/* Tells the CSS bundler not to resolve Jekyll-style /assets/…​ URL references 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 (see File and directory mappings for configurable directory locations): _data/ (if quarkus.roq.data.dir=_data is configured) What’s next Read the Roq the basics for the full feature set Browse the Plugins & Themes directory Check the Roq blog source for a complete working example If you develop migration scripts or improved prompts, consider contributing them back to the project (see issue #780) ### [Publishing a Roq Site](/docs/publishing/) If you find any issue or missing info, be awesome and edit this document to help others Roqers. Generating your Roq site This command: roq generate 🚀 The site will be generated in target/roq, use roq serve to serve it. Without the Roq CLI using Maven: QUARKUS_ROQ_GENERATOR_BATCH=true ./mvnw -B package quarkus:run Roq GitHub Action Roq provides a GitHub action to publish to GitHub pages or other services. To GitHub Pages The deploy workflow file .github/workflows/deploy.yml is already included when you create a project with roq create (view source). If you don’t have it, create it from the source link. Then enable GitHub Pages in your repository: Go to Settings > Pages Under Build and deployment, select GitHub Actions as the source Push to main and the workflow will run automatically. After a minute or two, your site will be live! The workflow also runs daily to publish any scheduled content (posts with a future date). To other services .github/workflows/deploy-other.yml ## Deploy to another service for your Quarkus Roq site. name: Roq Site Deploy other on: push: branches: [ main ] # Switch to the branch which should be deployed to GitHub Pages workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Generate Roq Site uses: quarkiverse/quarkus-roq@v1.1 with: github-pages: false - name: Publishing blog uses: actions/upload-artifact@v4 with: name: site path: target/roq retention-days: 3 This will create a GitHub artifact named site that you can download from another job (or another workflow). For example, the PR Preview workflow of Roq publishes to Surge. Gitlab CI Add this file at the root of your Gitlab repository .gitlab-ci.yml stages: - build - deploy build_roq: # Look for appropriate maven docker images in https://hub.docker.com/_/maven/tags image: "maven:3.9.9-eclipse-temurin-23-alpine" stage: build # Generate the static site on merge request events and on the main branch script: - QUARKUS_ROQ_GENERATOR_BATCH=true mvn -B -q package quarkus:run rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' artifacts: reports: junit: target/surefire-reports/*.xml paths: - target/roq - target/surefire-reports deploy_roq: image: alpine pages: true stage: deploy # For main branch take the artifacts from `build_roq` and deploy them. needs: - build_roq script: - cp -R target/roq public - echo "Quarkus Roq static site deployed to Gitlab Pages at $CI_PAGES_URL" rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' artifacts: paths: - public If everything goes well the pipeline will deploy, the url of the deployment is found via these options: Console output of deploy_roq job. Clicking Deploy ⇒ Pages on the project sidebar Navigating to the url https://gitlab.example.com/user-or-organization/projectpath/project/pages Other CIs Using the command above should be easy to configure on any CI. if you created a configuration for a given CI which could help others, please share it here (or create an issue) 🙏 ### [Roq Release Notes](/docs/releases/) If you find any issue or missing info, be awesome and edit this document to help others Roqers. We aim to keep the API stable, but since Roq is still young, breaking changes may happen. The good news: almost all breakage will be caught during build or site generation tests. Minimum Quarkus version required: Quarkus ≥ 3.33.1. Want to follow Roq’s progress and update your project safely? You’re in the right place. Roq 2.1 ✏️ Built-in Block Editor: a powerful block editor right in your browser during dev mode, with live preview, front matter editing, and image management. No IDE needed to write your next post! 🎨 Simplified layout resolution: layout: page now resolves local layouts first, then falls back to the theme. No more :theme/ prefix needed 🎨 Default theme rewritten with TailwindCSS instead of SCSS for better maintainability and modern styling 🌙 Built-in dark mode with automatic system preference detection 🎨 Color palette system: three customizable palettes (accent, pop, neutral) via CSS variables 🖼️ Favicon auto-discovery: automatically detects favicon.svg, favicon.ico, favicon.png, apple-touch-icon.png in public/ 🛒 Marketplace: new collection type for discovering plugins, themes, and web extensions 🤖 llms.txt: automatic generation of llms.txt for AI-friendly site indexing 🖥️ CLI: new roq add command for adding plugins, themes, and web extensions 🔧 Internal refactoring: the frontmatter pipeline is now split into clear numbered steps (Step0-Step6) for better maintainability 🔧 Migrated to ProjectScanner API and StringPaths library for file scanning ⚠️ page.date now returns null for normal pages without a date. Collection documents (posts) still default to the current date. 🔧 Common RoqException: all Roq exceptions (roq-frontmatter, roq-data) now extend a common io.quarkiverse.roq.exception.RoqException in roq-common, providing structured error pages with title, detail, hint, and source info across all modules → Guide for migrating to 2.1 Roq 2.0 🕵️ Added lightning filesystem watcher for live reload 📂 Allow web directory at the root of the Roq site 🧩 Simplified default app structure: supports web/app.js and web/app.scss (or web/app/app.js like before …​) ⚡️ TailwindCSS support without any config 💫 Directory support for data and allow iterating on nested data files using the directory name → Guide for migrating to 2.0 Roq 1.8 Sorry for the breaking releases back to back but this includes a refactor to allow safely including files from the whole site directory when using AsciidocJ (requested by a user). This mostly change internal api, but can eventually break really specific usage. → Guide for migrating to 1.8 Roq 1.7 The Asciidoc support was already available, but with this new release, we made it a Roq top level citizen: Support for Asciidoc headers to control the Roq data Includes Roq page and site attributes (urls, …​) xref are working out of the box for structured content such as docs Fine grained Asciidoc attributes (config, layout, page) Harmonization between Ruby and Java implementation Dynamic TOC support 👉 The Roq Asciidoc plugin doc A few weeks ago, we added support for search as a plugin to Roq. I wasn’t fully happy with the style and the fact that it was targetting the full page instead of the nearest fragment for the actual keyword. I spend a bit of time on this and came up with a new way of indexing the content which slice the content based on fragments. Currently, it supports both Asciidoc and Markdown output. Give it a try, it is enabled on this site. If you want this for your site: 👉 The Roq Search plugin doc → Guide for migrating to 1.7 Migration guide Make sure you update using Quarkus CLI Make sure you implemented generation tests to 2.1 Applies to Before After Action All users io.quarkiverse.roq.util.PathUtils io.quarkiverse.tools.stringpaths.StringPaths ⚠ Update import and adapt usage All users layout: :theme/foo layout: foo or theme-layout: foo ⚠ Update layout references in front matter All users Layouts in templates/layouts/{theme-name}/ templates/layouts/ ⚠ Move custom layouts All users page.date returns current date for all pages Returns null for non-collection pages ⚠ Wrap with {#if page.date}…​{/if} Theme users index.html layout for blog listing New blog.html layout (with pagination support); index.html now delegates to it ⚠ If using layout: blog without pagination (paginate: false), add paginate: posts; if you have a custom blog.html override, use {#if page.paginator and page.paginator.isFirst} instead of {#if page.paginator.isFirst} Theme users SCSS (app.scss) CSS with TailwindCSS ⚠ Migrate custom styles Theme users {#author-card …​} (kebab-case) {#roq/authorCard …​} (namespaced camelCase) ⚠ Update tag references Plugin authors RoqFrontMatterScanProcessor Step-based build items ⚠ Update plugin code Plugin authors deployment.scan.\* deployment.items.scan.* (same for data, publish) ⚠ Update imports All users Generated templates in target/roq-templates/full/ Now in target/roq-templates/content/ (#861) ℹ Update scripts or workflows referencing this path Plugin authors TemplateSource.generatedQuteContentTemplateId(), RoqFrontMatterRawPageBuildItem.generatedContentTemplate() Removed, page content now extracted via {#fragment RoqPageContent} (#861) ⚠ Update plugin code Plugin authors PAGINATE_KEY in scan package RoqFrontMatterKeys (runtime) ⚠ Update imports Plugin authors io.quarkiverse.roq.frontmatter.runtime.exception.RoqException io.quarkiverse.roq.exception.RoqException (in roq-common) ⚠ Update imports Plugin authors RoqException.Builder.source(TemplateSource) RoqException.Builder.sourceInfo(new RoqSourceInfo(…​)) ⚠ Update builder calls Plugin authors roq-data exceptions extend RuntimeException / UncheckedIOException All extend RoqException with builder pattern ⚠ Update exception construction Theme users Custom SCSS/CSS overrides Theme CSS completely rewritten ⚠ Review your overrides — some may be unnecessary now, others may need updating Theme users Custom color variables Three customizable color palettes: accent, pop, neutral ℹ See Color Palettes Theme users Custom CSS classes for prose not-prose class to exclude from prose styling ℹ Good to know Theme users No dark mode Built-in with dark: variants ℹ Good to know to 2.0 Applies to Before After Action All users Nested data mapped with _ (e.g. dir_bar) Mapped with / (e.g. dir/bar) ⚠ Update data references to 1.8 Applies to Before After Action All users page.info() page.source() and page.source().template() ⚠ Update page info calls Plugin authors Previous BuildItems API Restructured BuildItems ⚠ Update plugin code to 1.7 Applies to Before After Action AsciiDoc users Qute parsing enabled by default Disabled by default ⚠ Use :qute: per page or quarkus.asciidoc.qute=false AsciiDoc users quarkus.asciidoctorj quarkus.asciidoc ⚠ Rename config AsciiDoc users quarkus.asciidoctorj.templates-dir Removed (TOC handled by script) ⚠ Remove config Search users Previous search result DOM Updated DOM structure ⚠ Verify custom search styles All users site.ignored-files replaces defaults Now extends site.default-ignored-files ⚠ Check ignore config ### [Roq Events](/events/) 26 May 2026 Static You Can Maintain. Static With a Live CMS… What Is the Missing Roq? Andy Damevin talks about Quarkus Roq at JNation 2026. 09 Dec 2025 Roq 2.0 Roq 2.0 release featuring enhanced capabilities and improved developer experience. Learn more 09 Jul 2025 Roq: Create your static site with superpowers – fun, powerful, and easy to maintain! Andy Damevin talks about Quarkus Roq at Riviera Dev. 11 Nov 2024 Quarkus Insight - What is Quarkus Roq? Andy Damevin, Matheus Cruz and Melloware join us to discuss Quarkus Roq, which includes tooling for generating a static website with Quarkus. Learn more 31 Oct 2024 Roq 1.0 Roq 1.0 is out, it's time to Roq with blogs! Learn more 20 Oct 2024 Roq 1.0 Beta You can start building your site or blog with Roq. More features will come to cover all the needs you can expect from an awesome SSG! ### [Plugins & Themes](/marketplace/) ### [AsciiDoc Markup Example](/markups/asciidoc/) View source on GitHub This is a paragraph under heading 1. It contains strong text, emphasized text, and inline code. Heading 2 This is a paragraph under heading 2 with a link to example.com. Heading 3 This is a paragraph under heading 3. Heading 4 This is a paragraph under heading 4. Heading 5 This is a paragraph under heading 5. Heading 6 This is a paragraph under heading 6. Blockquotes This is a blockquote. It can span multiple lines. It can even have multiple paragraphs. Strong and Emphasis This paragraph contains bold/strong text, italic/emphasized text, and bold and italic text. Code Inline Code This paragraph contains inline code within regular text. Code Blocks public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } function greet(name) { return `Hello, ${name}!`; } console.log(greet("World")); Code Blocks with Callouts public class Example { public static void main(String[] args) { String message = "Hello"; (1) System.out.println(message); (2) } } 1 Initialize the message variable 2 Print the message to console server: port: 8080 (1) host: localhost (2) database: url: jdbc:postgresql://localhost:5432/mydb (3) username: admin 1 Configure server port 2 Set server host 3 Database connection URL Lists Unordered Lists First item Second item Third item Nested item 1 Nested item 2 Fourth item Ordered Lists First step Second step Third step Nested step 1 Nested step 2 Fourth step Tables Header 1 Header 2 Header 3 Cell 1 Cell 2 Cell 3 Cell 4 Cell 5 Cell 6 Cell 7 Cell 8 Cell 9 Cell 10 Cell 11 Cell 12 Table with Alignment Left Aligned Center Aligned Right Aligned Left Center Right Left Center Right Horizontal Rule Paragraphs This is a standard paragraph with regular text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This is another paragraph. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Links Here is an inline link and here is a link with title. Mixed Content Here’s a paragraph with strong text, emphasized text, inline code, and a link all in one. And here’s a blockquote that contains strong text, emphasized text, and inline code as well. Complex List First item with strong text Second item with inline code Third item with a link Nested unordered item Another nested item with emphasis Fourth item Code in Other Elements Code in Headings: inline-code This heading has inline code: variable = value Code in Lists Item with inline code Item with code block: def hello(): print("Hello") Code in Blockquotes This blockquote contains inline code and shows how code is styled within quotes. AsciiDoc-Specific Elements Admonition Blocks This is a note admonition. It provides additional information. This is a tip admonition. It offers helpful advice. This is an important admonition. Pay attention to this. This is a warning admonition. Be careful about this. This is a caution admonition. Proceed with care. Verse Block This is a verse block. It preserves line breaks and formatting exactly as written. — Attribution Source Example Block This is an example block. It’s used to highlight examples or demonstrate concepts. It can contain multiple paragraphs and other elements. Sidebar Block Optional Title This is a sidebar. It contains supplementary information that’s related but not essential to the main content. Definition Lists Term 1 Definition 1 Term 2 Definition 2 Term 3 Definition 3 With additional paragraph. Collapsible Blocks Click to expand This is hidden content inside a collapsible block. It supports bold, italic, and code. Show result This is a result block with a distinct background style. Checklist Checked item Another checked item Unchecked item Another unchecked item Special List Styles Circle item 1 Circle item 2 Square item 1 Square item 2 Code Listing with Language fn main() { println!("Hello, world!"); } Complex Table Name Description Status Feature 1 This is a longer description of feature 1 ✓ Feature 2 This is a longer description of feature 2 ✓ Feature 3 This is a longer description of feature 3 Pending ### [Markdown Markup Test](/markups/markdown/) View source on GitHub Heading 1 This is a paragraph under heading 1. It contains strong text, emphasized text, and inline code. Heading 2 This is a paragraph under heading 2 with a link to example.com. Heading 3 This is a paragraph under heading 3. Heading 4 This is a paragraph under heading 4. Heading 5 This is a paragraph under heading 5. Heading 6 This is a paragraph under heading 6. Blockquotes This is a blockquote. It can span multiple lines. It can even have multiple paragraphs. Strong and Emphasis This paragraph contains bold/strong text, italic/emphasized text, and bold and italic text. Code Inline Code This paragraph contains inline code within regular text. Code Blocks public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } function greet(name) { return `Hello, ${name}!`; } console.log(greet("World")); Lists Unordered Lists First item Second item Third item Nested item 1 Nested item 2 Fourth item Ordered Lists First step Second step Third step Nested step 1 Nested step 2 Fourth step Tables Header 1 Header 2 Header 3 Cell 1 Cell 2 Cell 3 Cell 4 Cell 5 Cell 6 Cell 7 Cell 8 Cell 9 Cell 10 Cell 11 Cell 12 Table with Alignment Left Aligned Center Aligned Right Aligned Left Center Right Left Center Right Horizontal Rule Paragraphs This is a standard paragraph with regular text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This is another paragraph. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Links Here is an inline link and here is a link with title. Mixed Content Here's a paragraph with strong text, emphasized text, inline code, and a link all in one. And here's a blockquote that contains strong text, emphasized text, and inline code as well. Complex List First item with strong text Second item with inline code Third item with a link Nested unordered item Another nested item with emphasis Fourth item Code in Other Elements Code in Headings: inline-code This heading has inline code: variable = value Code in Lists Item with inline code Item with code block: def hello(): print("Hello") Code in Blockquotes This blockquote contains inline code and shows how code is styled within quotes. Collapsible Sections Click to expand This is hidden content inside a collapsible section. It supports bold, italic, and code. With a code block public class Hello { public static void main(String[] args) { System.out.println("Hello!"); } } ### [🎸 The Roqers Hall Of Fame](/roqers/) const giscusScript = document.createElement('script'); giscusScript.src = 'https://giscus.app/client.js'; giscusScript.setAttribute('data-repo', 'quarkiverse/quarkus-roq'); giscusScript.setAttribute('data-repo-id', 'R_kgDOL4WdMA'); giscusScript.setAttribute('data-category', 'Comments'); giscusScript.setAttribute('data-category-id', 'DIC_kwDOL4WdMM4CjtXB'); giscusScript.setAttribute('data-mapping', 'pathname'); giscusScript.setAttribute('data-strict', '0'); giscusScript.setAttribute('data-reactions-enabled', '1'); giscusScript.setAttribute('data-emit-metadata', '0'); giscusScript.setAttribute('data-input-position', 'bottom'); giscusScript.setAttribute('data-theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light'); giscusScript.setAttribute('data-lang', 'en'); giscusScript.setAttribute('crossorigin', 'anonymous'); giscusScript.async = true; document.currentScript.parentNode.insertBefore(giscusScript, document.currentScript.nextSibling); ### [Blog](/blog/) ### [Blog](/posts/page2/) ### [Blog](/posts/page3/) ### [Blog](/posts/page4/) ### [#blogging](/posts/tag/blogging/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #blogging - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × blogging Comparing Roq with Hugo, Jekyll, and JBake: A Feature Breakdown Here’s a feature comparison with some popular SSGs to highlight how Roq stacks up. Feature Roq Hugo Jekyll JBake Build Perf Fast Extremely fast (written in Go) Slower due to Ruby and plugins Slower, runs on Java with Freemarker/Groovy templates Dev Perf Instant hot reload with Quarkus dev-mode Fast rebuilds Slow rebuilds on large sites Manual rebuild required Templating Qute (simple & readable) Go templates (powerful but complex) Liquid (easy but limited... Jul 23, 2026 — 3 minute(s) read How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read Roq 2.0 and Java Advent Calendar article An introduction to Roq 2.0, a Quarkus-inspired approach to static site generation in Java. Learn about its new foundation, plugin support, and live-reload feature through a practical tutorial. Dec 9, 2025 — 2 minute(s) read Major site migrations to Roq ✨ Two prominent websites have just migrated to Roq—any guesses who they might be? Aug 26, 2025 — 2 minute(s) read Roq with Blogs 🚀 Roq 1.0 is ON! It is time to give it a shot and give us feedback 🚀 Oct 31, 2024 — 2 minute(s) read Page 1 of 2 ### [#blogging](/posts/tag/blogging/page2/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #blogging - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × blogging Welcome to Roq! This is the first article ever made with Quarkus Roq Aug 29, 2024 — 2 minute(s) read Page 2 of 2 ### [#github-copilot](/posts/tag/github-copilot/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #github-copilot - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × github-copilot How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read ### [#release](/posts/tag/release/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #release - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × release Roq 2.1 is here! Roq 2.1 brings a standalone CLI, LLMs.txt generation, dynamic pages from data, custom error pages, and much more. This post kicks off a series covering all the new features. May 1, 2026 — 2 minute(s) read ### [#ai](/posts/tag/ai/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #ai - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × ai How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read ### [#happy-users](/posts/tag/happy-users/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #happy-users - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × happy-users Already some happy users 🧑‍💻 This is a good start, we already have a few happy users! Dec 10, 2024 — 2 minute(s) read ### [#jekyll](/posts/tag/jekyll/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #jekyll - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × jekyll How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read ### [#frontmatter](/posts/tag/frontmatter/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #frontmatter - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × frontmatter Easily Generate a `sitemap.xml` for Your Site with Roq Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin. Jan 8, 2025 — 1 minute(s) read ### [#improvement](/posts/tag/improvement/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #improvement - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × improvement Smarter Search Ranking How we fixed search boost to let keyword relevance shine. Jul 16, 2026 — 2 minute(s) read ### [#new-feature](/posts/tag/new-feature/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #new-feature - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × new-feature Collapsible Sections: Hide and Reveal Content in Your Posts The Roq default theme now styles HTML collapsible sections out of the box, in both Markdown and AsciiDoc content. Perfect for tutorials with hints, FAQs, and long reference sections. Jul 1, 2026 — 2 minute(s) read Generate Open Graph Images for Social Sharing with Roq Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically. Jun 25, 2026 — 3 minute(s) read Set It in Roq: The Editor that changes the game! Roq introduces a TipTap-powered editor with Markdown support, transforming it from a static site generator into a lightweight, developer-friendly CMS. Create, edit, and preview content seamlessly within the Quarkus dev experience. May 4, 2026 — 2 minute(s) read More diagram than you could have dreamed of. Leveraging Kroki.io to generate diagram from text Jun 11, 2025 — 1 minute(s) read 🔎 Your users deserve searching capabilities! No third party service needed 🚀 Apr 4, 2025 — 2 minute(s) read Page 1 of 4 ### [#new-feature](/posts/tag/new-feature/page2/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #new-feature - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × new-feature Easily Generate a `sitemap.xml` for Your Site with Roq Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin. Jan 8, 2025 — 1 minute(s) read Static attached files for posts and pages This Christmas, I’m Roq-ing a cool new feature (inspired by Hugo 😅): it is possible to attach static files to posts and pages. They will be served relative to the page. 🎁🤩 Dec 26, 2024 — 1 minute(s) read Do you want to publish a blog post series ? Make your blog posts part of a series. Dec 6, 2024 — 2 minute(s) read Need a QR Code? Add a QR Code to your Roq website. Nov 14, 2024 — 1 minute(s) read Write your blog posts in AsciiDoc Automatically generate html from AsciiDoc content Oct 22, 2024 — 1 minute(s) read Page 2 of 4 ### [#new-feature](/posts/tag/new-feature/page3/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #new-feature - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × new-feature RSS Feed of your blog posts Automatically generate an RSS feed of your blog links. Oct 10, 2024 — 37 minute(s) read The second Roq plugin is for redirecting your page to a better place! We introduced a way to declare aliases in FrontMatter. It is now easy create redirections to your blog posts! Oct 9, 2024 — 1 minute(s) read The first Roq plugin is for tagging (with pagination) We introduced the first Roq plugin, it is for collection tagging & with pagination support! Oct 8, 2024 — 1 minute(s) read Out of the box awesome SEO Learn how to implement SEO in Roq in a blink of an eye. Sep 23, 2024 — 3 minute(s) read Easily manage Drafts and Future articles in Roq Roq SSG introduces a new feature that allows you to hide or show draft and future articles using simple Quarkus configurations. This update gives developers greater control over which content is visible, improving content management and workflow. Sep 19, 2024 — 1 minute(s) read Page 3 of 4 ### [#new-feature](/posts/tag/new-feature/page4/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #new-feature - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × new-feature Effortless URL Handling in Roq with Qute super-power Effortlessly manage both relative and absolute URLs with our enhanced Qute-powered feature. Utilizing the RoqUrl class, you can easily join and resolve paths, ensuring clean and predictable URLs. This update simplifies URL handling, making your code more efficient and your content easier to navigate and share. Sep 16, 2024 — 2 minute(s) read Page 4 of 4 ### [#gfm](/posts/tag/gfm/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #gfm - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × gfm GFM Alert Blocks: Styled Callouts in Your Markdown Roq supports GitHub Flavored Markdown alert blocks with icons and themed colors. Learn how to use NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks, and how to add custom alert types. May 4, 2026 — 4 minute(s) read ### [#features](/posts/tag/features/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #features - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × features GFM Alert Blocks: Styled Callouts in Your Markdown Roq supports GitHub Flavored Markdown alert blocks with icons and themed colors. Learn how to use NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks, and how to add custom alert types. May 4, 2026 — 4 minute(s) read ### [#plugin](/posts/tag/plugin/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #plugin - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × plugin Smarter Search Ranking How we fixed search boost to let keyword relevance shine. Jul 16, 2026 — 2 minute(s) read Generate Open Graph Images for Social Sharing with Roq Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically. Jun 25, 2026 — 3 minute(s) read More diagram than you could have dreamed of. Leveraging Kroki.io to generate diagram from text Jun 11, 2025 — 1 minute(s) read 🔎 Your users deserve searching capabilities! No third party service needed 🚀 Apr 4, 2025 — 2 minute(s) read Easily Generate a `sitemap.xml` for Your Site with Roq Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin. Jan 8, 2025 — 1 minute(s) read Page 1 of 3 ### [#plugin](/posts/tag/plugin/page2/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #plugin - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × plugin Do you want to publish a blog post series ? Make your blog posts part of a series. Dec 6, 2024 — 2 minute(s) read Need a QR Code? Add a QR Code to your Roq website. Nov 14, 2024 — 1 minute(s) read Write your blog posts in AsciiDoc Automatically generate html from AsciiDoc content Oct 22, 2024 — 1 minute(s) read RSS Feed of your blog posts Automatically generate an RSS feed of your blog links. Oct 10, 2024 — 37 minute(s) read The second Roq plugin is for redirecting your page to a better place! We introduced a way to declare aliases in FrontMatter. It is now easy create redirections to your blog posts! Oct 9, 2024 — 1 minute(s) read Page 2 of 3 ### [#plugin](/posts/tag/plugin/page3/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #plugin - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × plugin The first Roq plugin is for tagging (with pagination) We introduced the first Roq plugin, it is for collection tagging & with pagination support! Oct 8, 2024 — 1 minute(s) read Page 3 of 3 ### [#styling](/posts/tag/styling/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #styling - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × styling How to add syntax highlighting to your Roq site with Highlight.js Learn how to integrate syntax highlighting into your Roq site using Highlight.js and the Quarkus web-bundler extension. This guide walks you through the simple steps to add it via the pom.xml, JavaScript, and SCSS files. Sep 20, 2024 — 2 minute(s) read ### [#design](/posts/tag/design/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #design - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × design How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read ### [#migration](/posts/tag/migration/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #migration - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × migration How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read ### [#markdown](/posts/tag/markdown/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #markdown - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × markdown GFM Alert Blocks: Styled Callouts in Your Markdown Roq supports GitHub Flavored Markdown alert blocks with icons and themed colors. Learn how to use NOTE, TIP, IMPORTANT, WARNING, and CAUTION blocks, and how to add custom alert types. May 4, 2026 — 4 minute(s) read ### [#tutorial](/posts/tag/tutorial/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #tutorial - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × tutorial Add Comments to Your Blog with a Web Component (30min) Step-by-step tutorial: build a Lit web component for comments backed by a Quarkus REST API on your Roq blog. Jul 5, 2026 — 13 minute(s) read Add Comments to Your Blog with Hybrid Mode (30min) Step-by-step tutorial: add dynamic comments to your Roq blog using hybrid mode, Panache, and Qute templates. Jul 5, 2026 — 12 minute(s) read Create a Link-Tree with Roq (45min) Step-by-step tutorial: build a personal link-tree site from scratch with Roq. Jul 5, 2026 — 22 minute(s) read Create a Blog from Scratch with Roq (45min) Step-by-step tutorial: build a blog from scratch with Roq using the base theme. Learn layouts, collections, and Tailwind styling. Jul 5, 2026 — 19 minute(s) read Create your own Blog with Roq (30min) Step-by-step tutorial: create and customize a blog with Roq using the default theme. Jul 5, 2026 — 15 minute(s) read Page 1 of 2 ### [#tutorial](/posts/tag/tutorial/page2/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #tutorial - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × tutorial Devoured: My Healthy Instagram for Tech News How I built a daily AI-curated tech digest with Roq, replacing doomscrolling with something actually useful. May 20, 2026 — 4 minute(s) read No pain updates with Roq One of the most overlooked aspects when choosing a Static Site Generator (SSG) is how easy it is to keep your project up to date. Many developers have struggled with complex upgrade processes, dependency conflicts, and breaking changes when using traditional SSGs like Jekyll or Hugo. Mar 24, 2025 — 1 minute(s) read Mastering Pagination in Roq Learn how to implement pagination in Roq to enhance your content navigation. This article walks through the process of adding pagination, configuring page size, and customizing links. Sep 20, 2024 — 1 minute(s) read How to add syntax highlighting to your Roq site with Highlight.js Learn how to integrate syntax highlighting into your Roq site using Highlight.js and the Quarkus web-bundler extension. This guide walks you through the simple steps to add it via the pom.xml, JavaScript, and SCSS files. Sep 20, 2024 — 2 minute(s) read Page 2 of 2 ### [#cool-stuff](/posts/tag/cool-stuff/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #cool-stuff - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × cool-stuff Collapsible Sections: Hide and Reveal Content in Your Posts The Roq default theme now styles HTML collapsible sections out of the box, in both Markdown and AsciiDoc content. Perfect for tutorials with hints, FAQs, and long reference sections. Jul 1, 2026 — 2 minute(s) read Generate Open Graph Images for Social Sharing with Roq Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically. Jun 25, 2026 — 3 minute(s) read Generate first class citizen pages from your data You can now generate pages dynamically from data collections, perfect for catalogs, team pages, or any content driven by structured data files. May 22, 2026 — 2 minute(s) read Devoured: My Healthy Instagram for Tech News How I built a daily AI-curated tech digest with Roq, replacing doomscrolling with something actually useful. May 20, 2026 — 4 minute(s) read Set It in Roq: The Editor that changes the game! Roq introduces a TipTap-powered editor with Markdown support, transforming it from a static site generator into a lightweight, developer-friendly CMS. Create, edit, and preview content seamlessly within the Quarkus dev experience. May 4, 2026 — 2 minute(s) read Page 1 of 4 ### [#cool-stuff](/posts/tag/cool-stuff/page2/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #cool-stuff - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × cool-stuff Roq 2.1 is here! Roq 2.1 brings a standalone CLI, LLMs.txt generation, dynamic pages from data, custom error pages, and much more. This post kicks off a series covering all the new features. May 1, 2026 — 2 minute(s) read Roq 2.0 and Java Advent Calendar article An introduction to Roq 2.0, a Quarkus-inspired approach to static site generation in Java. Learn about its new foundation, plugin support, and live-reload feature through a practical tutorial. Dec 9, 2025 — 2 minute(s) read Major site migrations to Roq ✨ Two prominent websites have just migrated to Roq—any guesses who they might be? Aug 26, 2025 — 2 minute(s) read Roq n Roll Your Tests 🎶 Testing the actual Roq generation has never been this cool! 🎸 Jan 28, 2025 — 2 minute(s) read Easily Generate a `sitemap.xml` for Your Site with Roq Learn how to quickly set up and customize a sitemap.xml for your site using the Roq plugin. Jan 8, 2025 — 1 minute(s) read Page 2 of 4 ### [#cool-stuff](/posts/tag/cool-stuff/page3/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #cool-stuff - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × cool-stuff Static attached files for posts and pages This Christmas, I’m Roq-ing a cool new feature (inspired by Hugo 😅): it is possible to attach static files to posts and pages. They will be served relative to the page. 🎁🤩 Dec 26, 2024 — 1 minute(s) read Do you want to publish a blog post series ? Make your blog posts part of a series. Dec 6, 2024 — 2 minute(s) read Need a QR Code? Add a QR Code to your Roq website. Nov 14, 2024 — 1 minute(s) read The second Roq plugin is for redirecting your page to a better place! We introduced a way to declare aliases in FrontMatter. It is now easy create redirections to your blog posts! Oct 9, 2024 — 1 minute(s) read The first Roq plugin is for tagging (with pagination) We introduced the first Roq plugin, it is for collection tagging & with pagination support! Oct 8, 2024 — 1 minute(s) read Page 3 of 4 ### [#cool-stuff](/posts/tag/cool-stuff/page4/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #cool-stuff - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × cool-stuff Out of the box awesome SEO Learn how to implement SEO in Roq in a blink of an eye. Sep 23, 2024 — 3 minute(s) read How to add syntax highlighting to your Roq site with Highlight.js Learn how to integrate syntax highlighting into your Roq site using Highlight.js and the Quarkus web-bundler extension. This guide walks you through the simple steps to add it via the pom.xml, JavaScript, and SCSS files. Sep 20, 2024 — 2 minute(s) read Effortless URL Handling in Roq with Qute super-power Effortlessly manage both relative and absolute URLs with our enhanced Qute-powered feature. Utilizing the RoqUrl class, you can easily join and resolve paths, ensuring clean and predictable URLs. This update simplifies URL handling, making your code more efficient and your content easier to navigate and share. Sep 16, 2024 — 2 minute(s) read Page 4 of 4 ### [#seo](/posts/tag/seo/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #seo - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × seo Generate Open Graph Images for Social Sharing with Roq Create 1200×630 PNG social preview cards from Qute SVG templates and inject og:image metadata automatically. Jun 25, 2026 — 3 minute(s) read Out of the box awesome SEO Learn how to implement SEO in Roq in a blink of an eye. Sep 23, 2024 — 3 minute(s) read ### [#guide](/posts/tag/guide/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #guide - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × guide Generate first class citizen pages from your data You can now generate pages dynamically from data collections, perfect for catalogs, team pages, or any content driven by structured data files. May 22, 2026 — 2 minute(s) read ### [#quarkus-roq](/posts/tag/quarkus-roq/) const savedTheme = localStorage.getItem('darkMode'); const isDark = savedTheme !== null ? savedTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { document.documentElement.classList.add('dark'); } #quarkus-roq - Hello, world! I'm Roq — a funny little SSG (Static Site Generator) with a Java soul and Quarkus energy — Open Source and Free. setupSearch({ url: '/search-index.json' }); I am ROQ Java Static Site Generator Create a Roq site Search... ⌘K MENU Home Blog Events About Roqers Doc Getting Started Roq the Basics Tutorials Plugins & Themes Publishing Advanced stuff Migrating LLMs.txt GitHub 2026 © ROQ × quarkus-roq How AI Helped Me Rebuild My Blog and Move from Jekyll to Quarkus Roq A comprehensive journey of rebuilding a personal blog with the help of AI, moving from Jekyll to Quarkus Roq, exploring GitHub Issues Driven Development, and discovering how modern AI tools can transform the way we build and maintain websites. May 6, 2026 — 26 minute(s) read