diff --git a/Api/Data/PostInterface.php b/Api/Data/PostInterface.php
new file mode 100644
index 0000000..fa6ef6f
--- /dev/null
+++ b/Api/Data/PostInterface.php
@@ -0,0 +1,54 @@
+forwardFactory->create();
+ return $forward->setController('post')->forward('list');
+ }
+}
diff --git a/Controller/Post/Detail.php b/Controller/Post/Detail.php
new file mode 100644
index 0000000..ca47c16
--- /dev/null
+++ b/Controller/Post/Detail.php
@@ -0,0 +1,27 @@
+eventManager->dispatch('rubenromao_blog_post_detail_view', [
+ 'request' => $this->request,
+ ]);
+
+ return $this->pageFactory->create();
+ }
+}
diff --git a/Controller/Post/ListAction.php b/Controller/Post/ListAction.php
new file mode 100644
index 0000000..395fe93
--- /dev/null
+++ b/Controller/Post/ListAction.php
@@ -0,0 +1,19 @@
+pageFactory->create();
+ }
+}
diff --git a/Model/Post.php b/Model/Post.php
new file mode 100644
index 0000000..c4cd7c5
--- /dev/null
+++ b/Model/Post.php
@@ -0,0 +1,39 @@
+_init(ResourceModel\Post::class);
+ }
+
+ public function getTitle()
+ {
+ return $this->getData(self::TITLE);
+ }
+
+ public function setTitle($title)
+ {
+ return $this->setData(self::TITLE, $title);
+ }
+
+ public function getContent()
+ {
+ return $this->getData(self::CONTENT);
+ }
+
+ public function setContent($content)
+ {
+ return $this->setData(self::CONTENT, $content);
+ }
+
+ public function getCreatedAt()
+ {
+ return $this->getData(self::CREATED_AT);
+ }
+}
diff --git a/Model/PostRepository.php b/Model/PostRepository.php
new file mode 100644
index 0000000..215d130
--- /dev/null
+++ b/Model/PostRepository.php
@@ -0,0 +1,55 @@
+postFactory->create();
+ $this->postResourceModel->load($post, $id);
+
+ if (!$post->getId()) {
+ throw new NoSuchEntityException(__('The blog post with "%1" ID doesn\'t exist.', $id));
+ }
+
+ return $post;
+ }
+
+ public function save(PostInterface $post): PostInterface
+ {
+ try {
+ $this->postResourceModel->save($post);
+ } catch (\Exception $exception) {
+ throw new CouldNotSaveException(__($exception->getMessage()));
+ }
+
+ return $post;
+ }
+
+ public function deleteById(int $id): bool
+ {
+ $post = $this->getById($id);
+
+ try {
+ $this->postResourceModel->delete($post);
+ } catch (\Exception $exception) {
+ throw new CouldNotDeleteException(__($exception->getMessage()));
+ }
+
+ return true;
+ }
+}
diff --git a/Model/ResourceModel/Post.php b/Model/ResourceModel/Post.php
new file mode 100644
index 0000000..7db6c69
--- /dev/null
+++ b/Model/ResourceModel/Post.php
@@ -0,0 +1,16 @@
+_init(self::MAIN_TABLE, self::ID_FIELD_NAME);
+ }
+}
diff --git a/Model/ResourceModel/Post/Collection.php b/Model/ResourceModel/Post/Collection.php
new file mode 100644
index 0000000..16fd8cd
--- /dev/null
+++ b/Model/ResourceModel/Post/Collection.php
@@ -0,0 +1,15 @@
+_init(Post::class, PostResourceModel::class);
+ }
+}
diff --git a/Observer/LogPostDetailView.php b/Observer/LogPostDetailView.php
new file mode 100644
index 0000000..135df31
--- /dev/null
+++ b/Observer/LogPostDetailView.php
@@ -0,0 +1,22 @@
+getData('request');
+ $this->logger->info('blog post detail viewed', [
+ 'params' => $request->getParams(),
+ ]);
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..32f4083
--- /dev/null
+++ b/README.md
@@ -0,0 +1,82 @@
+# Rubenromao_Blog module
+
+Custom Magento v2.4.6-p3 Blog module to cover Magento 2 concepts and design patterns.
+
+It covers the app structure, how routing & controllers work, how to extend core code,
+dependency injection & interfaces, different design patterns and usages, ways to modify the page layout, and understand data management.
+
+The module is not intended to be used in production.
+It is a sample module to be used as a reference for Magento 2 development.
+
+The module is based on the [Magento 2.4.6-p3](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html) version.
+
+It provides the following functionality:
+Custom database table to store blog posts.
+Custom model to manage blog posts.
+Custom Web API endpoints to create, update, delete, and get blog posts.
+Custom admin grid to manage blog posts.
+Custom admin form to create and edit blog posts.
+Custom frontend page to create a blog post.
+Custom frontend page to edit a blog post.
+Custom frontend page to delete a blog post.
+Custom frontend page to display blog posts.
+
+## Installation details
+
+To install use composer or copy files manually.
+It is recommended to install the module in a development environment first.
+The
+
+### Install using composer
+
+```
+composer require rubenromao/blog
+```
+
+Run the following command to enable the module:
+
+```
+bin/magento module:enable Rubenromao_Blog
+```
+
+You must run the following commands after the module installation using magento-cli
+
+ ```
+ bin/magento setup:upgrade
+ bin/magento setup:di:compile
+ bin/magento setup:static-content:deploy -f (optional if you are in developer mode)
+ bin/magento cache:flush
+ ```
+
+For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html).
+
+## Extensibility
+
+The Rubenromao_Blog module contains extensibility points that you can interact with.
+Web API, Service contracts, plugins, events, and observers enable you to extend and customize the Magento application.
+You can interact with the following extension points:
+
+Extension developers can interact with the Rubenromao_Blog module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Rubenromao_Blog module.
+
+### Layouts
+
+The module introduces layout handles in the `view/frontend/layout` directory.
+You can extend these layouts in your custom modules and themes.
+
+For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html).
+
+### UI components
+
+You can extend product and category updates using the UI components located in the `view/adminhtml/ui_component` directory.
+Or you can extend the UI components located in the `view/base/ui_component` directory.
+
+For information about a UI component in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html).
+
+## Additional information
+
+The Rubenromao_Blog module creates a new database table `rubenromao_blog_post` during the installation process.
+This table stores blog posts.
+
+For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html).
diff --git a/Setup/Patch/Data/PopulateBlogPosts.php b/Setup/Patch/Data/PopulateBlogPosts.php
new file mode 100644
index 0000000..6741d31
--- /dev/null
+++ b/Setup/Patch/Data/PopulateBlogPosts.php
@@ -0,0 +1,42 @@
+moduleDataSetup->startSetup();
+
+ $post = $this->postFactory->create();
+ $post->setData([
+ 'title' => 'An awesome post',
+ 'content' => 'This is totally awesome!',
+ ]);
+ $this->postRepository->save($post);
+
+ $this->moduleDataSetup->endSetup();
+ }
+}
diff --git a/Setup/Patch/Data/PopulateBlogPosts1.php b/Setup/Patch/Data/PopulateBlogPosts1.php
new file mode 100644
index 0000000..a7c1884
--- /dev/null
+++ b/Setup/Patch/Data/PopulateBlogPosts1.php
@@ -0,0 +1,52 @@
+moduleDataSetup->startSetup();
+
+ $posts = [
+ [
+ 'title' => 'Today is sunny',
+ 'content' => 'The weather has been great all week.',
+ ],
+ [
+ 'title' => 'My movie review',
+ 'content' => 'I give this movie 5 out of 5 stars!',
+ ],
+ ];
+
+ foreach ($posts as $postData) {
+ $post = $this->postFactory->create();
+ $post->setData($postData);
+ $this->postRepository->save($post);
+ }
+
+ $this->moduleDataSetup->endSetup();
+ }
+}
diff --git a/ViewModel/Post.php b/ViewModel/Post.php
new file mode 100644
index 0000000..72c0f96
--- /dev/null
+++ b/ViewModel/Post.php
@@ -0,0 +1,34 @@
+collection->getItems();
+ }
+
+ public function getCount(): int
+ {
+ return $this->collection->count();
+ }
+
+ public function getDetail(): PostInterface
+ {
+ $id = (int) $this->request->getParam('id');
+ return $this->postRepository->getById($id);
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..80f394c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,17 @@
+{
+ "name": "rubenromao/m2-module-blog",
+ "version": "1.0.0",
+ "description": "Custom Blog module that covers most of the M2 concepts and design patterns.",
+ "type": "magento2-module",
+ "require": {
+ "magento/framework": "*"
+ },
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Rubenromao\\Blog\\": ""
+ }
+ }
+}
diff --git a/etc/db_schema.xml b/etc/db_schema.xml
new file mode 100644
index 0000000..3229f7e
--- /dev/null
+++ b/etc/db_schema.xml
@@ -0,0 +1,14 @@
+
+
+
+
= __('Created at: %1', $post->getCreatedAt()) ?>
+= $escaper->escapeHtml($post->getContent(), ['em', 'p', 'strong']) ?>
+= __('Post count: %1', $postVm->getCount()) ?>
+