Eleventy-based static site generator that creates a FAQ website for ORC WG's CRA Hub at cra.orcwg.org. The site consumes FAQ content from an external GitHub repository and generates a categorized FAQ website.
- Node.js
- npm
- poppler (optional): only needed to regenerate the Official CRA Guidance page and to run its end-to-end tests, which are skipped without it. Install it with
brew install poppleron macOS orapt-get install poppler-utilson Debian/Ubuntu.
git clone https://github.com/orcwg/cra.orcwg.org.git
cd cra.orcwg.org
npm installnpm run serve- Start development server with live reload (no cache update)npm run watch- Watch for file changes and rebuild (no cache update)npm run build- Build the production site (no cache update)npm test- Run the unit tests (Node's built-in test runner, tests intest/)npm run update-cache- Update external content cachenpm run update-cache -- branch-name- Update the external content cache to use a different branch of theorcwg/cra-hubrepositiory. Great for testing the build with unmerged pull requests.npm run extract-guidance- Regenerate the Official CRA Guidance page (src/official-guidance.html), its API document (src/_data/official-guidance.json, served at/api/v0/documents/official-cra-guidance.json) and its figures (src/assets/images/official-guidance/) from the Commission's guidance PDF insrc/_data/, verifying the extracted text against the PDF (requires poppler). The generated files are committed and shouldn't be edited by hand.npm run extract-guidance -- --check- Confirm the committed Official CRA Guidance page matches the PDF, without writing anything (requires poppler).
This is an Eleventy site that acts as a content processor and renderer for external FAQ content rather than managing content locally. The site uses a semantic HTML-first architecture with minimal component classes and CSS-only interactions.
- Semantic HTML Elements: Uses native HTML5 elements (
<details>,<summary>,<aside>,<article>) with minimal class names - CSS-Only Interactions: Accordions and animations use native browser features without JavaScript
- Component Independence: Components are self-contained and don't rely on parent context
- Modern CSS Features: Leverages
color-mix(),:has(),:empty, grid template rows, and CSS Custom Properties - Accessibility First: Native HTML semantics provide built-in keyboard navigation and screen reader support
The site uses a hybrid approach combining semantic HTML selectors with light component classes:
Semantic Elements:
<header class="site-header">- Site header with flexbox layout<nav class="site-nav">- Navigation menu<main>- Main content area<footer>- Site footer with grid layout<aside>- Warnings and disclaimers (styled automatically)<blockquote>- Minimal italic quotes
Component Classes:
.section-card- White boxes for FAQ content and list sections.section-intro- Light gray introductory text boxes.card-grid- Responsive card grids (with.half-widthvariant).link-list- FAQ link lists with hover states.faq-accordion-item- Native<details>with CSS animations
Utility Classes:
.badge- Status indicators with semantic color variants.info,.success,.warning,.danger,.important- Semantic colors usingcolor-mix()
- External Content - FAQ content is maintained in the
orcwg/cra-hubrepository - Cache Update - The
update-cache.shscript clones/updates external content into_cache/faq/ - Data Processing -
src/_data/data.jsorchestrates the complete data pipeline:- Text Processing: Markdown to plain text conversion for titles and summaries
- File Operations: Parsing markdown files with frontmatter
- FAQ Processing: Extract questions, answers, and metadata from FAQ markdown files
- Guidance Request Processing: Parse pending guidance request documents
- FAQ List Processing: Load and normalize README.yml files from FAQ subdirectories
- Authors Processing: Load and merge AUTHORS.md from FAQ repository and CONTRIBUTORS.md from website repository
- Cross-referencing: Cross-reference FAQs with guidance requests and lists
- Permalink Generation: URLs computed once in data layer, not reconstructed in templates
- Template Rendering - Nunjucks templates consume processed data using semantic HTML components
- Site Generation - Final site is output to
_site/for deployment
- FAQs: Questions and answers with status tracking (
draft,approved,pending guidance) - Guidance Requests: Items awaiting EU Commission clarification
The data processing pipeline in src/_data/data.js is organized into modular sections:
-
Utility Functions
- Text processing: Convert markdown to plain text for page titles
- File operations: Parse markdown files with frontmatter using gray-matter
-
Content Processors
- FAQs: Extract questions from
#headings, answers from subsequent content, status badges, and GitHub edit links - Guidance Requests: Parse pending guidance documents, extract titles and "Guidance Needed" sections
- FAQ Lists: Load README.yml files from FAQ subdirectories, normalize FAQ references
- Authors: Load and process AUTHORS.md from FAQ repository and CONTRIBUTORS.md from website repository, extract name lists for acknowledgements
- FAQs: Extract questions from
-
Relationship Building
- Link FAQs to their related guidance requests via
guidance-idfrontmatter field - Bidirectionally connect lists with their referenced FAQs
- Each FAQ knows which lists include it; each list knows its FAQs
- Link FAQs to their related guidance requests via
- README.yml files in FAQ subdirectories define FAQ collections
- Each README.yml file creates a new list page at
/lists/{category}/ - Lists reference FAQs by filename (short form) or category/filename (long form)
- FAQ references are automatically normalized to category/filename format
- Lists maintain bidirectional links with FAQs for navigation
- Lists are displayed using native HTML
<details>accordions for compact navigation
Besides the community FAQs from orcwg/cra-hub, the site integrates official EU content:
- European Commission "FAQs on the Cyber Resilience Act" (PDF) - parsed by
src/_data/parse-official-faqs.jsfrom a Markdown conversion of the PDF stored insrc/_data/, and published at/faq/official/. Section metadata (slugs, icons, descriptions) lives inofficial-faqs-config-lists.json, per-FAQ metadata (related issues) inofficial-faqs-config-faqs.json. - European Commission "Cyber Resilience Act - Questions and Answers" (web page) - fetched at build time from the Commission's press corner API by
fetchAndAddECFaqsinsrc/_data/data.js, parsed bysrc/_data/utils/ec-qanda-parser.js, and exposed as thecra-basicsdynamic list. - ENISA "Single Reporting Platform (SRP) - Frequently Asked Questions" (web page) - fetched at build time from ENISA's FAQ page by
fetchAndAddSrpFaqsinsrc/_data/data.js, parsed bysrc/_data/utils/srp-faq-parser.js, and exposed as thesrpdynamic list. The<dl class="ckeditor-accordion">question/answer pairs and the "Updated: ..." date are extracted from the page. Only questions ENISA marks "[UPDATED]" get that update date (the marker itself is removed from the question). The page doesn't expose its publication date, so it is set bySRP_FAQ_PUBLISHEDindata.js, based on press coverage of the page's launch. FAQ ids follow ENISA's question numbers (srp/faq_<n>), and references to CRA articles ("Art. 14(7)"), other SRP FAQs ("FAQ 21") or sections of the Commission's FAQ ("subsection 5.1") are linked bylinkSrpCrossReferencesin the same module.
Answers from both web pages are converted from HTML to Markdown (src/_data/utils/html-to-markdown.js, using turndown) before link resolution. The link resolver inserts Markdown links, which markdown-it would otherwise leave unrendered inside raw HTML blocks. The link resolver never inserts a link inside the text of an existing link.
Both pages are fetched by fetchWithFallback (src/_data/utils/fetch-with-fallback.js), which keeps a snapshot of the last page that was fetched and parsed successfully in _snapshots/ (not committed). When a page cannot be fetched within 30 seconds or its structure changes, the build falls back to that snapshot, logs a warning and, in GitHub Actions, adds a warning annotation and a job summary entry so that the problem doesn't go unnoticed. Without a snapshot, the build fails, so that a broken site is never deployed. Set OFFICIAL_FAQ_FETCH=strict to disable the fallback (e.g. to check that a parser still matches the live page). The parsers are covered by unit tests using excerpts of both pages (test/fixtures/); when a page structure changes, update the fixture along with the parser.
The system automatically generates special lists based on FAQ properties. These are configured in the DYNAMIC_LISTS array in src/_data/data.js and are automatically added to the Topics page.
Available Dynamic Lists:
new- FAQs created within the last 30 days (sorted newest first)recently-updated- FAQs updated within the last 14 days (sorted by update date)cra-basics- the European Commission's "Cyber Resilience Act - Questions and Answers" (fetched at build time)srp- ENISA's Single Reporting Platform FAQs (fetched at build time)unlisted- FAQs not yet assigned to any curated list
Configuration Properties:
id- Unique identifier for the listtitle,icon,description- Display metadataemptyMsg- Message shown when the list has no itemsinclusionFilter- Function determining which FAQs to includesortChildren- Optional function to sort FAQs in the listhideInAllFaqsFilter- Function controlling visibility in "All FAQs" viewhideInTopicsFilter- Function controlling visibility in topics viewinsertAt- Position in root list ('top','bottom'or'end')
How Dynamic Lists Work:
Dynamic lists are generated at build time by the createAndInsertDynamicLists() function:
- Each dynamic list is created from its configuration
- FAQs matching the
inclusionFilterare added as children - Bidirectional parent-child relationships are established
- Children are sorted using
sortChildrenif configured - Metadata (timestamps, visibility) is calculated from children
- Lists are inserted into the root list based on
insertAt:'top'lists appear at the beginning (in array order)'bottom'lists appear at the end (in array order)'end'lists appear after all other lists, including the official FAQs (in array order)
Dynamic lists are fully automatic and cannot be manually referenced in YAML files.
The site uses two reusable FAQ components with minimal markup:
-
faq-list-simple.njk- Simple link lists using semantic<ul class="link-list">- Displays FAQ questions as clickable links
- Shows status badges when present
- Used in main FAQ list page and list indexes
-
faq-accordion.njk- Native<details>accordions with CSS-only animations- Uses HTML5
<details>and<summary>elements - Exclusive accordion behavior via
name="faq-accordion"attribute - Smooth open/close animations using CSS grid template rows
- No JavaScript required for accordion functionality
- Used in lists for inline FAQ browsing
- Uses HTML5
Components follow a consistent data pattern:
{% set componentData = {
title: "Section Title",
description: "Optional description",
icon: "๐",
faqs: arrayOfFaqObjects
} %}
{% include "components/faq-accordion.njk" %}- Global site settings in
src/_data/site.json - Navigation menu configuration (used in header, homepage cards, and footer); entries with
hideFromNav: trueare shown on the homepage only - List ordering controlled via
listOrderarray - Footer content structured as sections with titles and lists
- Repository:
orcwg/cra-hub - Purpose: Contains all FAQ content and guidance requests in markdown format
- Update: Automatically pulled during builds via
update-cache.sh
- Static Site Generator: Eleventy 3.x
- Template Engine: Nunjucks
- Markdown Processing: markdown-it with GitHub alerts plugin
- Diagram Support: Mermaid.js (for rendering code blocks with
language-mermaid) - CSS Architecture:
- Semantic HTML-first approach with minimal utility classes
- CSS Custom Properties for theming
- Modern CSS features:
color-mix(),:has(),:empty, grid template rows - CSS-only accordions using native
<details>elements
- JavaScript: Minimal - only used for Mermaid diagram initialization (
src/assets/js/site.js)
- Style Guide: Visit
/style-guide/to preview all components, design patterns, and semantic HTML structures used throughout the site. The style guide is excluded from collections but available during development.
.section-card- White content boxes with shadow and border.section-intro- Light gray intro/description sections.card-grid- Responsive grid layout for navigation cards.card- Individual cards with hover effects and animated arrows.link-list- Styled FAQ link lists with badge support.faq-accordion-item- Native<details>elements with CSS animations- Semantic color utilities:
.info,.success,.warning,.danger,.important
The site is deployed to GitHub Pages at cra.orcwg.org with automatic updates triggered by changes to the content repository.
This repository (cra.orcwg.org) acts as the website generator, while orcwg/cra-hub is the content source. The two repositories work together through an automated workflow:
flowchart TD
A[orcwg/cra-hub<br/>Content Repository]
B[Push to main]
C[orcwg/cra.orcwg.org<br/>Website Repository]
D[Push to main]
E[Build and Deploy<br/>GitHub Actions]
F[cra.orcwg.org<br/>Live Website]
G[Daily schedule<br/>04:23 UTC]
A -->|Update content source| B
B -->|Webhook trigger| E
C -->|Update website generator| D
D -->|Direct trigger| E
G -->|Cron trigger| E
E -->|Deploy to Pages| F
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#ffe1e1
style F fill:#e8f5e9
The website automatically rebuilds and deploys through three triggers:
- Content updates - When content is pushed to
maininorcwg/cra-hub - Website changes - When code is pushed to
mainin this repository - Daily schedule - Every day at 04:23 UTC, to pick up changes to the official FAQ pages fetched at build time
When content is pushed to the main branch of orcwg/cra-hub, a GitHub Action workflow sends a repository_dispatch webhook to trigger a rebuild of this website.
How it works:
- The workflow listens for pushes to the
mainbranch incra-hub - It sends a
repository_dispatchevent with typecra-hub-updateto this repository via the GitHub API using a GitHub token (WEBSITE_DISPATCH_TOKENsecret stored incra-hub).
Note
The WEBSITE_DISPATCH_TOKEN must be renewed annually and requires the permissions listed below.
- Repository access: Only
orcwg/cra.orcwg.org - Contents: Read & Write (required for repository_dispatch)
- Metadata: Read (auto-selected)
When changes are pushed to the main branch of this repository (template updates, styling changes, configuration), the deployment workflow runs directly without needing a webhook.
The European Commission's "Questions and Answers" and ENISA's SRP FAQs are fetched from their web pages at build time, so the deployment workflow also runs on a daily schedule (04:23 UTC). Scheduled runs are resistant to problems with those pages: the last successfully parsed copy of each page is kept in the GitHub Actions cache between runs (actions/cache/restore and actions/cache/save around the build step) and used as a fallback, as described in Official FAQs. A run that had to fall back shows a warning annotation and a job summary entry. If a page is unavailable and there is no cached snapshot yet, the build fails and the previous deployment stays online.
Note
GitHub only runs scheduled workflows from the default branch, and automatically disables them in repositories with no activity for 60 days (a notification email is sent; the workflow can be re-enabled from the Actions tab). Cache entries are evicted after 7 days without access, which the daily run prevents.
All triggers execute the same deployment workflow:
- Pulls latest content from
cra-hubviaupdate-cache.sh - Processes FAQ data through the Eleventy data pipeline
- Generates static HTML pages
- Publishes to GitHub Pages at
https://cra.orcwg.org
For manual deployments or testing, the build process:
- Runs
update-cache.shto clone/updatecra-hubinto_cache/ - Processes FAQ data from cloned repository via
src/_data/data.js - Generates static HTML pages using Eleventy
- Outputs complete site to
_site/directory
When developing locally:
# Start development server (updates cache and serves with live reload)
npm run serve
# Build production site (updates cache and builds)
npm run build
# Manually update content cache only
npm run update-cacheThe update-cache.sh script clones or pulls the latest cra-hub content into _cache/
This project is licensed under the terms of the Apache License Version 2.0.
SPDX-License-Identifier: Apache-2.0