diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Composer
+/vendor/
+composer.lock
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# keruald/report
+
+Allow to build a report and output it.
+
+## Report skeleton
+
+### Introduction
+
+A report is a collection of sections, a general title, and some metadata.
+
+A section is a collection of entries, and a title. They can also be thought as the chapters of a book.
+
+An entry is text and a title.
+
+That gives the following hierarchy:
+
+```
+Report                  title   (sections)     properties
+  ReportSection         title   (entries)
+    ReportEntry         title   text
+```
+
+A full example can be found in the  `tests/WithSampleReport.php` file.
+
+### Simplified report
+
+You can build a simplified version using only the ReportSection class:
+
+```php
+use Keruald\Reporting\ReportSection;
+
+$report = new ReportSection("A simple report about historical geometric problems");
+$report->push("Issue 1", "Can we square a circle?");
+$report->push("Issue 2", "Can we divise an angle by 3?");
+$report->push("Issue 2", "Can we double a cube?");
+
+print_r($report);
+```
+
+## Output
+
+The library provides HTML and Markdown output.
+
+Examples of such output can be found in the `tests/data` folder.
+
+Those output classes aren't mandatory to use to present the results:
+the report data structure can be easily walked with foreach loops
+to manipulate it.
+
+## Uses
+
+The **keruald/healthcheck** library uses this reporting library to generate
+a site health check, and present the results to help to remediate to
+the issues detected.
diff --git a/composer.json b/composer.json
new file mode 100644
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,15 @@
+{
+    "name": "keruald/report",
+    "description": "Build a report with sections and entries. Markdown, PHP objects and HTML outputs.",
+    "type": "library",
+    "require-dev": {
+        "phpunit/phpunit": "^9.5"
+    },
+    "license": "BSD-2-Clause",
+    "autoload": {
+        "psr-4": {
+            "Keruald\\Reporting\\": "src/",
+            "Keruald\\Reporting\\Tests\\": "tests/"
+        }
+    }
+}
diff --git a/src/Report.php b/src/Report.php
new file mode 100644
--- /dev/null
+++ b/src/Report.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Keruald\Reporting;
+
+class Report {
+
+    public function __construct (
+        public string $title,
+
+        /**
+         * @var ReportSection[]
+         */
+        public array $sections = [],
+
+        /**
+         * @var array<string, mixed>
+         */
+        public array $properties = [],
+    ) { }
+
+    public function push (ReportSection $section) : void {
+        $this->sections[] = $section;
+    }
+
+    public function pushIfNotEmpty (ReportSection $report) : void {
+        if (!$report->isEmpty()) {
+            $this->push($report);
+        }
+    }
+
+}
diff --git a/src/ReportEntry.php b/src/ReportEntry.php
new file mode 100644
--- /dev/null
+++ b/src/ReportEntry.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Keruald\Reporting;
+
+class ReportEntry {
+
+    public function __construct (
+        public string $title,
+        public string $text,
+    ) { }
+
+}
diff --git a/src/ReportSection.php b/src/ReportSection.php
new file mode 100644
--- /dev/null
+++ b/src/ReportSection.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Keruald\Reporting;
+
+class ReportSection {
+
+    public function __construct (
+        public string $title,
+
+        /**
+         * @var ReportEntry[]
+         */
+        public array $entries = [],
+    ) { }
+
+    public function push (string $title, string $text) : void {
+        $this->entries[] = new ReportEntry($title, $text);
+    }
+
+    public function isEmpty () : bool {
+        return count($this->entries) === 0;
+    }
+
+}