diff --git a/Cargo.lock b/Cargo.lock
index 76e82bc..31df059 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -571,7 +571,7 @@ dependencies = [
  "pin-project",
  "serde",
  "server_fn",
- "thiserror",
+ "thiserror 1.0.69",
  "tokio",
  "tokio-stream",
  "tokio-util",
@@ -676,7 +676,7 @@ dependencies = [
  "http",
  "lru",
  "rustc-hash",
- "thiserror",
+ "thiserror 1.0.69",
  "tracing",
 ]
 
@@ -718,7 +718,7 @@ dependencies = [
  "serde",
  "serde_json",
  "slab",
- "thiserror",
+ "thiserror 1.0.69",
  "tokio",
  "tokio-stream",
  "tokio-util",
@@ -1079,7 +1079,7 @@ dependencies = [
  "pin-project",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 1.0.69",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
@@ -1510,6 +1510,7 @@ dependencies = [
  "dioxus-cli-config",
  "dioxus-logger",
  "dotenvy",
+ "thiserror 2.0.11",
  "tokio",
 ]
 
@@ -2046,7 +2047,7 @@ checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c"
 dependencies = [
  "percent-encoding",
  "serde",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2096,7 +2097,7 @@ dependencies = [
  "serde_json",
  "serde_qs",
  "server_fn_macro_default",
- "thiserror",
+ "thiserror 1.0.69",
  "tower 0.4.13",
  "tower-layer",
  "url",
@@ -2293,7 +2294,16 @@ version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl 2.0.11",
 ]
 
 [[package]]
@@ -2307,6 +2317,17 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.1.8"
@@ -2557,7 +2578,7 @@ dependencies = [
  "log",
  "rand",
  "sha1",
- "thiserror",
+ "thiserror 1.0.69",
  "utf-8",
 ]
 
@@ -2575,7 +2596,7 @@ dependencies = [
  "log",
  "rand",
  "sha1",
- "thiserror",
+ "thiserror 1.0.69",
  "utf-8",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 3304734..7b673dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ dioxus = { version = "0.6", features = ["fullstack"] }
 dioxus-cli-config = "0.6"
 dioxus-logger = "0.6"
 dotenvy = { version = "0.15", optional = true }
+thiserror = "2.0.11"
 tokio = { version = "1", features = ["full"], optional = true }
 
 [features]
diff --git a/src/domain/entities/site.rs b/src/domain/entities/site.rs
index 2ee7b18..a6b7188 100644
--- a/src/domain/entities/site.rs
+++ b/src/domain/entities/site.rs
@@ -12,3 +12,19 @@ pub struct SiteInfo {
     pub info: SiteMetadata,
     pub pages: Vec<PageInfo>,
 }
+pub struct Page {
+    pub info: PageInfo,
+    pub content: PageContent,
+}
+
+pub enum PageContent {
+    Single { content: Post },
+}
+
+pub struct Post {
+    pub blocks: Vec<Block>,
+}
+
+pub enum Block {
+    Text { text: String },
+}
diff --git a/src/outbound/repository/adapters/static_data.rs b/src/outbound/repository/adapters/static_data.rs
index 523d5a6..33556ac 100644
--- a/src/outbound/repository/adapters/static_data.rs
+++ b/src/outbound/repository/adapters/static_data.rs
@@ -1,20 +1,20 @@
 use crate::{
-    domain::entities::site::{PageInfo, SiteMetadata},
-    outbound::repository::site::SiteRepository,
+    domain::entities::site::{Block, Page, PageContent, PageInfo, Post, SiteMetadata},
+    outbound::repository::site::{Error, Result, SiteRepository},
 };
 
 pub struct StaticData {}
 
 impl SiteRepository for StaticData {
-    async fn get_site_by_domain(&self, domain: &str) -> SiteMetadata {
-        SiteMetadata {
+    async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata> {
+        Ok(SiteMetadata {
             domain: domain.to_string(),
             title: "Test site".to_string(),
-        }
+        })
     }
 
-    async fn get_pages_for_site(&self, _: &str) -> Vec<PageInfo> {
-        vec![
+    async fn get_pages_for_site(&self, _: &str) -> Result<Vec<PageInfo>> {
+        Ok(vec![
             PageInfo {
                 title: "Home".to_string(),
                 name: "/".to_string(),
@@ -23,6 +23,38 @@ impl SiteRepository for StaticData {
                 title: "Cool page".to_string(),
                 name: "cool".to_string(),
             },
-        ]
+        ])
+    }
+
+    async fn get_page(&self, _: &str, name: &str) -> Result<Page> {
+        match name {
+            "/" => Ok(Page {
+                info: PageInfo {
+                    title: "Home".to_string(),
+                    name: name.to_string(),
+                },
+                content: PageContent::Single {
+                    content: Post {
+                        blocks: vec![Block::Text {
+                            text: "Hello, world!".to_string(),
+                        }],
+                    },
+                },
+            }),
+            "cool" => Ok(Page {
+                info: PageInfo {
+                    title: "Cool page".to_string(),
+                    name: name.to_string(),
+                },
+                content: PageContent::Single {
+                    content: Post {
+                        blocks: vec![Block::Text {
+                            text: "Hello, world!".to_string(),
+                        }],
+                    },
+                },
+            }),
+            _ => Err(Error::NotFound),
+        }
     }
 }
diff --git a/src/outbound/repository/site.rs b/src/outbound/repository/site.rs
index 38408b7..201eba7 100644
--- a/src/outbound/repository/site.rs
+++ b/src/outbound/repository/site.rs
@@ -1,6 +1,20 @@
-use crate::domain::entities::site::{PageInfo, SiteMetadata};
+use thiserror::Error;
+
+use crate::domain::entities::site::{Page, PageInfo, SiteMetadata};
 
 pub trait SiteRepository {
-    async fn get_site_by_domain(&self, domain: &str) -> SiteMetadata;
-    async fn get_pages_for_site(&self, domain: &str) -> Vec<PageInfo>;
+    async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata>;
+    async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>>;
+    async fn get_page(&self, domain: &str, name: &str) -> Result<Page>;
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("resource not found")]
+    NotFound,
+
+    #[error("the server encountered an error: {0}")]
+    ServerError(String),
 }
diff --git a/src/outbound/services/site.rs b/src/outbound/services/site.rs
index e928e77..bae1112 100644
--- a/src/outbound/services/site.rs
+++ b/src/outbound/services/site.rs
@@ -1,4 +1,9 @@
-use crate::{domain::entities::site::SiteInfo, outbound::repository::site::SiteRepository};
+use thiserror::Error;
+
+use crate::{
+    domain::entities::site::{Page, SiteInfo},
+    outbound::repository::{self, site::SiteRepository},
+};
 
 pub struct SiteService<SiteRepo: SiteRepository> {
     site_repository: SiteRepo,
@@ -9,24 +14,80 @@ impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
         Self { site_repository }
     }
 
-    pub async fn get_site(&self, domain: &str) -> SiteInfo {
-        let info = self.site_repository.get_site_by_domain(domain).await;
-        let pages = self.site_repository.get_pages_for_site(&info.domain).await;
-        SiteInfo { info, pages }
+    pub async fn get_site(&self, domain: &str) -> Result<SiteInfo> {
+        let info = self
+            .site_repository
+            .get_site_by_domain(domain)
+            .await
+            .map_err(|e| match e {
+                repository::site::Error::NotFound => Error::NotFound,
+                _ => Error::RepositoryError(e),
+            })?;
+        let pages = self
+            .site_repository
+            .get_pages_for_site(&info.domain)
+            .await?;
+
+        Ok(SiteInfo { info, pages })
     }
+
+    pub async fn get_page(&self, domain: &str, name: &str) -> Result<Page> {
+        let info = self
+            .site_repository
+            .get_site_by_domain(domain)
+            .await
+            .map_err(|e| match e {
+                repository::site::Error::NotFound => Error::NotFound,
+                _ => Error::RepositoryError(e),
+            })?;
+        let page = self.site_repository.get_page(&info.domain, name).await?;
+
+        Ok(page)
+    }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("resource not found")]
+    NotFound,
+
+    #[error("the server encountered an error: {0}")]
+    RepositoryError(#[from] repository::site::Error),
 }
 
 #[cfg(test)]
 mod tests {
     use super::SiteService;
-    use crate::outbound::repository::adapters::static_data::StaticData;
+    use crate::{
+        domain::entities::site::{Block, PageContent},
+        outbound::repository::adapters::static_data::StaticData,
+    };
 
     #[tokio::test]
     async fn gets_site_info() {
         let service = SiteService::new(StaticData {});
-        let info = service.get_site("example.com").await;
+        let info = service.get_site("example.com").await.unwrap();
         assert_eq!(info.info.domain, "example.com");
         assert_eq!(info.info.title, "Test site");
         assert_eq!(info.pages.len(), 2);
     }
+
+    #[tokio::test]
+    async fn gets_page_data() {
+        let service = SiteService::new(StaticData {});
+        let page = service.get_page("example.com", "/").await.unwrap();
+
+        assert_eq!(page.info.title, "Home");
+        assert_eq!(page.info.name, "/");
+
+        // page content must be a single text block
+        let PageContent::Single {
+            content: page_content,
+        } = page.content;
+        assert_eq!(page_content.blocks.len(), 1);
+        let Block::Text { text } = &page_content.blocks[0];
+        assert_eq!(text, "Hello, world!");
+    }
 }