diff --git a/composer.json b/composer.json
index 3e854fd..aba8b2e 100644
--- a/composer.json
+++ b/composer.json
@@ -109,6 +109,7 @@
     "require-dev": {
         "drupal/core-dev": "^10",
         "fenetikm/autoload-drupal": "dev-autoload-tests",
-        "phpspec/prophecy-phpunit": "^2"
+        "phpspec/prophecy-phpunit": "^2",
+        "phpstan/phpstan-strict-rules": "^1.5"
     }
 }
diff --git a/composer.lock b/composer.lock
index 66ff841..6abbdd8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b7efed8e16e9b99d9aba5afbdb499669",
+    "content-hash": "2d3f22a75cc98e2996a5d6b9f3934faf",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -8218,6 +8218,55 @@
             },
             "time": "2023-05-26T11:05:59+00:00"
         },
+        {
+            "name": "phpstan/phpstan-strict-rules",
+            "version": "1.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpstan/phpstan-strict-rules.git",
+                "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b21c03d4f6f3a446e4311155f4be9d65048218e6",
+                "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "phpstan/phpstan": "^1.10"
+            },
+            "require-dev": {
+                "nikic/php-parser": "^4.13.0",
+                "php-parallel-lint/php-parallel-lint": "^1.2",
+                "phpstan/phpstan-deprecation-rules": "^1.1",
+                "phpstan/phpstan-phpunit": "^1.0",
+                "phpunit/phpunit": "^9.5"
+            },
+            "type": "phpstan-extension",
+            "extra": {
+                "phpstan": {
+                    "includes": [
+                        "rules.neon"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PHPStan\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Extra strict and opinionated rules for PHPStan",
+            "support": {
+                "issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
+                "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.1"
+            },
+            "time": "2023-03-29T14:47:40+00:00"
+        },
         {
             "name": "phpunit/php-code-coverage",
             "version": "9.2.27",
diff --git a/web/modules/custom/example/tests/src/Functional/AdminPageTest.php b/web/modules/custom/example/tests/src/Functional/AdminPageTest.php
new file mode 100644
index 0000000..e5253b6
--- /dev/null
+++ b/web/modules/custom/example/tests/src/Functional/AdminPageTest.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\example\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\HttpFoundation\Response;
+
+final class AdminPageTest extends BrowserTestBase {
+
+  public $defaultTheme = 'stark';
+
+  /** @test */
+  public function the_admin_page_is_not_accessible_to_anonymous_users(): void {
+    $this->drupalGet(path: '/admin');
+
+    $assert = $this->assertSession();
+
+    $assert->statusCodeEquals(Response::HTTP_FORBIDDEN);
+  }
+
+  /** @test */
+  public function the_admin_page_is_accessible_by_admin_users(): void {
+    $adminUser = $this->createUser(
+      permissions: [
+        'access administration pages',
+      ],
+    );
+
+    $this->drupalLogin(account: $adminUser);
+
+    $this->drupalGet(path: '/admin');
+
+    $assert = $this->assertSession();
+
+    $assert->statusCodeEquals(Response::HTTP_OK);
+  }
+
+}
diff --git a/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php b/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php
deleted file mode 100644
index 5ecb1b8..0000000
--- a/web/modules/custom/example/tests/src/Functional/ExamplePageTest.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Drupal\Tests\example\Functional;
-
-use Drupal\Tests\BrowserTestBase;
-use Symfony\Component\HttpFoundation\Response;
-
-final class ExamplePageTest extends BrowserTestBase {
-
-  public $defaultTheme = 'stark';
-
-  protected static $modules = [
-    // Core.
-    'node',
-
-    // Custom.
-    "example"
-  ];
-
-  /** @test */
-  public function should_load_the_example_page_for_anonymous_users(): void {
-    // Arrange.
-
-    // Act.
-    $this->drupalGet('/@opdavies/drupal-module-template');
-
-    // Assert.
-    $this->assertSession()->statusCodeEquals(Response::HTTP_OK);
-  }
-
-}
diff --git a/web/modules/custom/example/tests/src/Functional/FrontPageTest.php b/web/modules/custom/example/tests/src/Functional/FrontPageTest.php
new file mode 100644
index 0000000..24ae108
--- /dev/null
+++ b/web/modules/custom/example/tests/src/Functional/FrontPageTest.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\example\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\HttpFoundation\Response;
+
+final class FrontPageTest extends BrowserTestBase {
+
+  public $defaultTheme = 'stark';
+
+  protected static $modules = ['node', 'views'];
+
+  /** @test */
+  public function the_front_page_loads_for_anonymous_users(): void {
+    $this->config('system.site')
+      ->set('page.front', '/node')
+      ->save(TRUE);
+
+    $this->drupalGet('<front>');
+
+    $assert = $this->assertSession();
+
+    $assert->statusCodeEquals(Response::HTTP_OK);
+    $assert->pageTextContains('No front page content has been created yet.');
+  }
+
+}
diff --git a/web/modules/custom/my_module/my_module.info.yml b/web/modules/custom/my_module/my_module.info.yml
new file mode 100644
index 0000000..60e27aa
--- /dev/null
+++ b/web/modules/custom/my_module/my_module.info.yml
@@ -0,0 +1,3 @@
+name: My Module
+type: module
+core_version_requirement: ^10
diff --git a/web/modules/custom/my_module/my_module.routing.yml b/web/modules/custom/my_module/my_module.routing.yml
new file mode 100644
index 0000000..9683277
--- /dev/null
+++ b/web/modules/custom/my_module/my_module.routing.yml
@@ -0,0 +1,7 @@
+blog.page:
+  path: /blog
+  defaults:
+    _controller: Drupal\my_module\Controller\BlogPageController
+    _title: Blog
+  requirements:
+    _permission: access content
diff --git a/web/modules/custom/my_module/my_module.services.yml b/web/modules/custom/my_module/my_module.services.yml
new file mode 100644
index 0000000..a97d210
--- /dev/null
+++ b/web/modules/custom/my_module/my_module.services.yml
@@ -0,0 +1,6 @@
+services:
+  Drupal\my_module\Controller\BlogPageController:
+    autowire: true
+
+  Drupal\my_module\Repository\ArticleRepository:
+    autowire: true
diff --git a/web/modules/custom/my_module/src/Controller/BlogPageController.php b/web/modules/custom/my_module/src/Controller/BlogPageController.php
new file mode 100644
index 0000000..59b6599
--- /dev/null
+++ b/web/modules/custom/my_module/src/Controller/BlogPageController.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\my_module\Controller;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\EntityViewBuilderInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\my_module\Repository\ArticleRepository;
+
+final class BlogPageController {
+
+  use StringTranslationTrait;
+
+  private EntityViewBuilderInterface $nodeViewBuilder;
+
+  public function __construct(
+    private RendererInterface $renderer,
+    EntityTypeManagerInterface $entityTypeManager,
+    private ArticleRepository $articleRepository,
+  ) {
+    $this->nodeViewBuilder = $entityTypeManager->getViewBuilder(entity_type_id: 'node');
+  }
+
+  /**
+   * @return array<string, mixed>
+   */
+  public function __invoke(): array {
+    $articles = $this->articleRepository->getAll();
+
+    if ($articles === []) {
+      return ['#markup' => $this->t('Welcome to my blog!')];
+    }
+
+    $build = [];
+
+    foreach ($articles as $article) {
+      $build[] = $this->nodeViewBuilder->view(
+        entity: $article,
+        view_mode: 'teaser',
+      );
+    }
+
+    return [
+      '#markup' => $this->renderer->render($build),
+    ];
+  }
+
+}
diff --git a/web/modules/custom/my_module/src/Repository/ArticleRepository.php b/web/modules/custom/my_module/src/Repository/ArticleRepository.php
new file mode 100644
index 0000000..4cacbb0
--- /dev/null
+++ b/web/modules/custom/my_module/src/Repository/ArticleRepository.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\my_module\Repository;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\node\NodeInterface;
+
+final class ArticleRepository {
+
+  private EntityStorageInterface $nodeStorage;
+
+  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+    $this->nodeStorage = $entityTypeManager->getStorage(entity_type_id: 'node');
+  }
+
+  /**
+   * @return array<int, NodeInterface>
+   */
+  public function getAll(): array {
+    /** @var array<int, NodeInterface> */
+    $articles = $this->nodeStorage->loadByProperties([
+      'status' => NodeInterface::PUBLISHED,
+      'type' => 'article',
+    ]);
+
+    // Sort the articles by their created time.
+    uasort($articles, function (NodeInterface $a, NodeInterface $b): int {
+      return $a->getCreatedTime() < $b->getCreatedTime() ? 1 : -1;
+    });
+
+    return $articles;
+  }
+
+}
diff --git a/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php
new file mode 100644
index 0000000..f1c9764
--- /dev/null
+++ b/web/modules/custom/my_module/tests/src/Functional/BlogPageTest.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\my_module\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\HttpFoundation\Response;
+
+final class BlogPageTest extends BrowserTestBase {
+
+  public $defaultTheme = 'stark';
+
+  protected static $modules = [
+    // Core.
+    'node',
+
+    // Custom.
+    'my_module',
+  ];
+
+  /** @test */
+  public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text(): void {
+    $this->drupalGet('/blog');
+
+    $assert = $this->assertSession();
+
+    $assert->statusCodeEquals(Response::HTTP_OK);
+    $assert->responseContains('<h1>Blog</h1>');
+    $assert->pageTextContains('Welcome to my blog!');
+  }
+
+  /** @test */
+  public function it_shows_articles(): void {
+    $this->createContentType(['type' => 'article']);
+
+    $this->createNode([
+      'title' => 'This is a test article',
+      'type' => 'article',
+    ]);
+
+    $this->drupalGet('/blog');
+
+    $assert = $this->assertSession();
+
+    $assert->statusCodeEquals(Response::HTTP_OK);
+    $assert->pageTextContains('This is a test article');
+  }
+
+}
diff --git a/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php b/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php
new file mode 100644
index 0000000..91799e3
--- /dev/null
+++ b/web/modules/custom/my_module/tests/src/Kernel/Repository/ArticleRepositoryTest.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\my_module\Kernel\Repository;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\my_module\Repository\ArticleRepository;
+use Drupal\node\Entity\Node;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+final class ArticleRepositoryTest extends EntityKernelTestBase {
+
+  use NodeCreationTrait;
+
+  protected $strictConfigSchema = FALSE;
+
+  public static $modules = [
+    'node',
+    'my_module',
+  ];
+
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installSchema(module: 'node', tables: ['node_access']);
+
+    $this->installConfig([
+      'filter',
+    ]);
+  }
+
+  /** @test */
+  public function nodes_that_are_not_articles_are_not_returned() {
+    $this->createNode(['type' => 'article'])->save();
+    $this->createNode(['type' => 'page'])->save();
+    $this->createNode(['type' => 'article'])->save();
+    $this->createNode(['type' => 'page'])->save();
+    $this->createNode(['type' => 'article'])->save();
+
+    $this->assertCount(5, Node::loadMultiple());
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $articles = $repository->getAll();
+
+    $this->assertCount(3, $articles);
+  }
+
+  /** @test */
+  public function only_published_articles_are_returned() {
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+    $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $articles = $repository->getAll();
+
+    $this->assertCount(3, $articles);
+  }
+
+  /** @test */
+  public function nodes_are_ordered_by_date_and_returned_newest_first() {
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-2 days'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 week'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 hour'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 year'))->getTimestamp(),
+    ]);
+
+    $this->createNode([
+      'type' => 'article',
+      'created' => (new DrupalDateTime('-1 month'))->getTimestamp(),
+    ]);
+
+    $repository = $this->container->get(ArticleRepository::class);
+    $nodes = $repository->getAll();
+    $nodeIds = array_keys($nodes);
+
+    $this->assertSame([3, 1, 2, 5, 4], $nodeIds);
+  }
+
+}