diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2659611
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+composer.lock
diff --git a/README.md b/README.md
index 4125f53..dc05439 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,31 @@
-# TDD Dublin demo module
+# TDD Example Drupal 8 Blog Module
 
-A demo module to accompany my [TDD Test Driven Drupal][0] talk at DrupalCamp
+A demo module to accompany my [TDD - Test Driven Drupal][0] talk, originally for DrupalCamp
 Dublin 2017.
 
+In order to see my workflow of writing comments first, converting them into
+failing tests, and then writing the implementation code to make them pass, you
+can see the [list of previous commits][1] and see each step taken, as well as
+[the tags][2] that identify the commits when each failing test is added and
+then subsequently passes.
+
 ## Acceptance Criteria
 
 This module will be used to demonstrate how to take a test-driven approach to
 develop a module to the following acceptance criteria:
 
 - As a site visitor
-- I want to see a list of all published pages at `/pages`
-- Ordered alphabetically by title
+- I want to see a list of all published blog posts at `/blog`
+- Ordered by post date, with the newest posts first
+
+## Installation
+
+Within your Drupal 8 site:
+
+```bash
+cd modules
+git clone git@github.com:opdavies/drupal-module-tdd-blog.git tdd_blog
+```
 
 ## Running the Tests
 
@@ -18,23 +33,23 @@ These tests are functional tests based on the `BrowserTestBase` class so need
 to be executed with PHPUnit (which is required in core's `composer.json` file).
 The path to your `vendor` directory may be different depending on your setup.
 
-Because of autoloading, you will need to be inside Drupal's `core` subdirectory
-when running the tests for them to execute successfully.
+Because of autoloading, you will either need to be inside Drupal's `core` subdirectory
+, or add `-c core` to the PHPUnit command when running the tests for them to execute successfully.
 
 This also assumes that the module is within a `modules/custom` directory and
-named `tdd_dublin` as per the repository name.
+named `tdd_blog` as per the repository name.
 
 ```
-cd core
-
-../vendor/bin/phpunit ../modules/custom/tdd_dublin
+vendor/bin/phpunit -c core modules/custom/tdd_blog
 ```
 
 You can use PHPUnit's `--filter` option to specify a single test method to run,
 rather than all of the tests within the module. For example:
 
 ```
-../vendor/bin/phpunit ../modules/custom/tdd_dublin --filter=testOnlyPublishedPagesAreShown
+vendor/bin/phpunit -c core modules/custom/tdd_blog --filter=testOnlyPublishedPagesAreShown
 ```
 
 [0]: https://www.oliverdavies.uk/talks/tdd-test-driven-drupal
+[1]: https://github.com/opdavies/drupal-module-tdd-blog/commits/HEAD
+[2]: https://github.com/opdavies/drupal-module-tdd-blog/tags
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..aa7e974
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,4 @@
+{
+  "name": "drupal/tdd_blog",
+  "type": "drupal-custom-module"
+}
diff --git a/config/install/node.type.article.yml b/config/install/node.type.article.yml
new file mode 100644
index 0000000..1fd439c
--- /dev/null
+++ b/config/install/node.type.article.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies: {  }
+name: Article
+type: article
+description: 'Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.'
+help: ''
+new_revision: true
+preview_mode: 1
+display_submitted: true
diff --git a/config/install/views.view.blog.yml b/config/install/views.view.blog.yml
new file mode 100644
index 0000000..783846f
--- /dev/null
+++ b/config/install/views.view.blog.yml
@@ -0,0 +1,228 @@
+langcode: en
+status: true
+dependencies:
+  config:
+  - node.type.article
+  module:
+  - node
+  - user
+id: blog
+label: Blog
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access content'
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: ‹‹
+            next: ››
+      style:
+        type: default
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          uses_fields: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          entity_type: node
+          entity_field: title
+          label: ''
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      filters:
+        status:
+          value: '1'
+          table: node_field_data
+          field: status
+          plugin_id: boolean
+          entity_type: node
+          entity_field: status
+          id: status
+          expose:
+            operator: ''
+          group: 1
+        type:
+          id: type
+          table: node_field_data
+          field: type
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value:
+            article: article
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: node
+          entity_field: type
+          plugin_id: bundle
+      sorts:
+        created:
+          id: created
+          table: node_field_data
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: ASC
+          exposed: false
+          expose:
+            label: ''
+          granularity: second
+          entity_type: node
+          entity_field: created
+          plugin_id: date
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+      - 'languages:language_content'
+      - 'languages:language_interface'
+      - url.query_args
+      - 'user.node_grants:view'
+      - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: blog
+    cache_metadata:
+      max-age: -1
+      contexts:
+      - 'languages:language_content'
+      - 'languages:language_interface'
+      - url.query_args
+      - 'user.node_grants:view'
+      - user.permissions
+      tags: {  }
diff --git a/tdd_dublin.info.yml b/tdd_blog.info.yml
similarity index 53%
rename from tdd_dublin.info.yml
rename to tdd_blog.info.yml
index 4698633..1cdb6c0 100644
--- a/tdd_dublin.info.yml
+++ b/tdd_blog.info.yml
@@ -1,4 +1,9 @@
-name: 'TDD Dublin'
+name: 'TDD Blog'
 description: 'A demo module for DrupalCamp Dublin to show test driven module development.'
 core: 8.x
+core_version_requirement: ^8 || ^9
 type: module
+
+dependencies:
+  - drupal:node
+  - drupal:views
diff --git a/tests/src/Functional/PageListTest.php b/tests/src/Functional/PageListTest.php
index 9c1d9e6..6205de1 100644
--- a/tests/src/Functional/PageListTest.php
+++ b/tests/src/Functional/PageListTest.php
@@ -1,24 +1,20 @@
 <?php
 
-namespace Drupal\Tests\tdd_dublin\Functional;
+namespace Drupal\Tests\tdd_blog\Functional;
 
 use Drupal\Tests\BrowserTestBase;
+use Symfony\Component\HttpFoundation\Response;
 
 class PageListTest extends BrowserTestBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  protected static $modules = ['tdd_dublin'];
+  protected static $modules = ['tdd_blog'];
 
-  /**
-   * Test that the pages listing page exists and is accessible.
-   */
-  public function testListingPageExists() {
-    // Go to /pages and check that it is accessible by checking the status
-    // code.
-    $this->drupalGet('pages');
-    $this->assertSession()->statusCodeEquals(200);
+  protected $defaultTheme = 'stark';
+
+  public function testBlogPageExists() {
+    $this->drupalGet('blog');
+
+    $this->assertSession()->statusCodeEquals(Response::HTTP_OK);
   }
 
 }
diff --git a/tests/src/Kernel/PageListTest.php b/tests/src/Kernel/PageListTest.php
new file mode 100644
index 0000000..2e5cb48
--- /dev/null
+++ b/tests/src/Kernel/PageListTest.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Tests\tdd_blog\Kernel;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\views\ResultRow;
+
+/**
+ * @group tdd_blog
+ */
+class PageListTest extends EntityKernelTestBase {
+
+  use NodeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'tdd_blog',
+    'views',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+
+    $this->installConfig(['filter', 'tdd_blog']);
+  }
+
+  /**
+   * Ensure that only the correct nodes are returned.
+   *
+   * Ensure that only published pages are returned by the view. Unpublished
+   * pages or content of different types should not be shown.
+   */
+  public function testOnlyPublishedArticlesAreShown() {
+    // This is a published article, so it should be visible.
+    $this->createNode(['type' => 'page', 'status' => TRUE]);
+
+    // This is a page, so it should not be visible.
+    $this->createNode(['type' => 'article']);
+
+    // This article is not published, so it should not be visible.
+    $this->createNode(['type' => 'article', 'status' => FALSE]);
+
+    // Rather than testing the rendered HTML, we are going to load the view
+    // results programmatically and run assertions against the data it returns.
+    // This makes it easier to test certain scenarios, and ensures that the
+    // test is future-proofed and won't fail at a later date due to a change in
+    // the presentation code.
+    $nids = $this->getViewResults();
+
+    // Only node 1 matches the criteria of being a published page, so only that
+    // node ID should be being returned from the view. assertEquals() can be
+    // used to compare the expected result to what is being returned.
+    $this->assertEquals([2], $nids);
+  }
+
+  /**
+   * Ensure that the results are ordered by title.
+   */
+  public function testArticlesAreOrderedByDate() {
+    $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 day'))->getTimestamp()]);
+    $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 month'))->getTimestamp()]);
+    $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+3 days'))->getTimestamp()]);
+    $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 hour'))->getTimestamp()]);
+
+    // Get the result data from the view.
+    $nids = $this->getViewResults();
+
+    // Compare the expected order based on the titles defined above to the
+    // ordered results from the view.
+    $this->assertEquals([4, 1, 3, 2], $nids);
+  }
+
+  /**
+   * Load the view and get the results.
+   *
+   * @param string $view
+   *   (optional) The name of the view. Defaults to 'blog'.
+   *
+   * @return array
+   *   An array of returned entity IDs.
+   */
+  private function getViewResults($view = 'blog') {
+    return array_map(function (ResultRow $result) {
+      return $result->_entity->id();
+    }, views_get_view_result($view));
+  }
+
+}