diff --git a/Dioxus.toml b/Dioxus.toml
index da58640..5ce9802 100644
--- a/Dioxus.toml
+++ b/Dioxus.toml
@@ -12,7 +12,7 @@ reload_html = true
 watch_path = ["src", "assets"]
 
 [web.resource]
-style = ["/assets/style/main.css"]
+style = []
 script = []
 
 [web.resource.dev]
diff --git a/assets/admin/style/main.css b/assets/admin/style/main.css
new file mode 100644
index 0000000..6041100
--- /dev/null
+++ b/assets/admin/style/main.css
@@ -0,0 +1,15 @@
+@import url(https://rsms.me/inter/inter.css);
+
+:root {
+	background-color: #312337;
+	color: #fff6fe;
+
+	font-family: Inter, sans-serif;
+	/* fix for Chrome */
+	font-feature-settings: 'liga' 1, 'calt' 1;
+}
+
+a,
+a:visited {
+	color: #f0cf96;
+}
\ No newline at end of file
diff --git a/assets/style/main.css b/assets/renderer/style/main.css
similarity index 100%
rename from assets/style/main.css
rename to assets/renderer/style/main.css
diff --git a/src/inbound/renderer/admin_ui/mod.rs b/src/inbound/renderer/admin_ui/mod.rs
index 8b6997d..bb4b1e3 100644
--- a/src/inbound/renderer/admin_ui/mod.rs
+++ b/src/inbound/renderer/admin_ui/mod.rs
@@ -2,12 +2,16 @@ use dioxus::prelude::*;
 
 mod server;
 
-#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+#[derive(Clone, Routable, Debug, PartialEq)]
 #[rustfmt::skip]
 enum Route {
     #[layout(AdminLayout)]
     #[route("/")]
     Home {},
+
+    #[nest("/site")]
+        #[route("/:domain")]
+        Site { domain: String },
 }
 
 pub fn App() -> Element {
@@ -16,24 +20,33 @@ pub fn App() -> Element {
     }
 }
 
+#[component]
 fn Home() -> Element {
     rsx! {
-        h2 { "Hello!" }
         SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "Loading sites..." }, SiteList {} }
     }
 }
 
+#[component]
 fn SiteList() -> Element {
     let sites = use_server_future(server::site_list)?.suspend()?;
 
     rsx! {
         h3 { "Sites" }
+        button { "New site" }
         match &*sites.read() {
             Ok(sites) => {
                 rsx! {
                     ul {
                         for site in sites {
-                            li { "{site.domain} - {site.title}" }
+                            li {
+                                Link {
+                                    to: Route::Site {
+                                        domain: site.domain.clone(),
+                                    },
+                                    "{site.domain} - {site.title}"
+                                }
+                            }
                         }
                     }
                 }
@@ -47,9 +60,35 @@ fn SiteList() -> Element {
     }
 }
 
+#[component]
+fn Site(domain: String) -> Element {
+    let site = use_server_future(move || server::get_site(domain.clone()))?.suspend()?;
+
+    let info = match &*site.read() {
+        Ok(site) => site.clone(),
+        Err(err) => {
+            return rsx! {
+                h1 { "Error: {err}" }
+            }
+        }
+    };
+
+    rsx! {
+        h1 { "{info.title}" }
+    }
+}
+
+static CSS: Asset = asset!("/assets/admin/style/main.css");
+
+#[component]
 fn AdminLayout() -> Element {
     rsx! {
-        h1 { "Admin UI" }
+        document::Stylesheet { href: CSS }
+        header { "data-test-id": "admin-ui",
+            div {
+                Link { to: Route::Home {}, "Administration panel" }
+            }
+        }
         SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "..." },
             main { Outlet::<Route> {} }
         }
@@ -69,7 +108,7 @@ mod tests {
     fn site_list_shows_sites() {
         let mut app = VirtualDom::new(|| {
             rsx! {
-                SiteList {}
+                Router::<Route> {}
             }
         });
 
diff --git a/src/inbound/renderer/admin_ui/server.rs b/src/inbound/renderer/admin_ui/server.rs
index 10739ce..d064e76 100644
--- a/src/inbound/renderer/admin_ui/server.rs
+++ b/src/inbound/renderer/admin_ui/server.rs
@@ -18,3 +18,17 @@ pub async fn site_list() -> Result<Vec<SiteInfo>, ServerFnError> {
         })
         .collect())
 }
+
+#[server]
+pub async fn get_site(domain: String) -> Result<SiteInfo, ServerFnError> {
+    use crate::outbound::services::site::SiteServiceProvider;
+    let FromContext(SiteServiceProvider { service }) = extract().await?;
+
+    let site = service.get_site(&domain).await?;
+
+    Ok(SiteInfo {
+        domain: site.domain.clone(),
+        title: site.title.clone(),
+        pages: vec![],
+    })
+}
diff --git a/src/inbound/renderer/mod.rs b/src/inbound/renderer/mod.rs
index ec4f213..b21a052 100644
--- a/src/inbound/renderer/mod.rs
+++ b/src/inbound/renderer/mod.rs
@@ -15,7 +15,7 @@ mod server;
 #[cfg(test)]
 mod testing;
 
-#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+#[derive(Clone, Routable, Debug, PartialEq)]
 #[rustfmt::skip]
 enum Route {
     #[layout(SiteLayout)]
@@ -31,6 +31,7 @@ enum Route {
     PageNotFound { route: Vec<String> },
 }
 
+#[component]
 pub fn App() -> Element {
     // Check for special redirects
     let site_res = use_server_future(server::get_site_info)?.suspend()?;
@@ -79,12 +80,15 @@ fn PageLink(page: String, title: String, current: Route) -> Element {
     }
 }
 
+static CSS: Asset = asset!("/assets/renderer/style/main.css");
+
 #[component]
 fn SiteLayout() -> Element {
     let site = meta::site();
     let route = use_route::<Route>();
 
     rsx! {
+        document::Stylesheet { href: CSS }
         header { class: "site-header",
             nav { class: "page-list",
                 ul {
@@ -228,6 +232,20 @@ mod tests {
                 })
             })
         });
+        mock_service.expect_get_page().times(1).returning(|_, _| {
+            Box::pin(async {
+                Ok(entities::site::Page {
+                    info: entities::site::PageInfo {
+                        title: "Test page".to_string(),
+                        name: "/".to_string(),
+                        order: 0,
+                    },
+                    content: PageContent::Single {
+                        content: entities::site::Post { blocks: vec![] },
+                    },
+                })
+            })
+        });
 
         server_context().insert(AppConfig {
             admin_host: "admin.local".to_string(),
@@ -337,6 +355,6 @@ mod tests {
         app.rebuild_in_place();
         let elem_str = dioxus::ssr::render(&app);
         println!("elem_str: {elem_str}");
-        assert!(elem_str.contains("Admin UI"));
+        assert!(elem_str.contains("admin-ui"));
     }
 }