diff --git a/assets/style.css b/assets/style.css
index 95b436f4dc02d815bb8adfe6637e9e97ace954ac..6591942e96a5052a57029a60bb9f50b55eee0434 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -60,6 +60,43 @@
   margin-block: 0.5rem;
 }
 
+.repository-toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 0.5rem 1rem;
+  margin-block: 1.5rem;
+  padding: 0.75rem 1rem;
+  border: 1px solid GrayText;
+  border-radius: 0.25rem;
+}
+
+.repository-summary-layout {
+  display: grid;
+  grid-template-columns: minmax(0, 2fr) minmax(14rem, 1fr);
+  gap: 2rem;
+  align-items: start;
+}
+
+.repository-summary-main > section,
+.repository-sidebar > section {
+  margin-bottom: 2rem;
+}
+
+.repository-sidebar > section {
+  padding: 1rem;
+  border: 1px solid GrayText;
+  border-radius: 0.25rem;
+}
+
+.repository-sidebar h2 {
+  margin-top: 0;
+}
+
+.repository-file-list th:first-child {
+  width: 100%;
+}
+
 .site-header nav,
 .repository-header nav {
   display: flex;
@@ -208,7 +245,8 @@
 }
 
 @media (max-width: 44rem) {
-  .two-column {
+  .two-column,
+  .repository-summary-layout {
     grid-template-columns: 1fr;
   }
 }
diff --git a/src/http/public.rs b/src/http/public.rs
index 8b99179356acfa0353813a722cdc845596c30902..55bf5641ed98354e9dd2846f7091d64209de1784 100644
--- a/src/http/public.rs
+++ b/src/http/public.rs
@@ -436,20 +436,25 @@
                     .iter()
                     .find(|reference| reference.name == default_branch.as_bytes())
                     .map(|reference| reference.target);
-                let (history, readme) = match head {
+                let (history, readme, entries) = match head {
                     Some(head) => (
                         service.history(head, &cancellation)?,
                         service.readme(head, &cancellation)?,
+                        service.tree(head, &[], &cancellation)?,
                     ),
-                    None => (Vec::new(), None),
+                    None => (Vec::new(), None, Vec::new()),
                 };
                 Ok(RepositoryPage::summary(
                     record,
-                    description,
-                    clone_urls,
-                    head,
-                    history,
-                    readme.map(|readme| (readme.path, readme.blob.data)),
+                    RepositorySummary {
+                        description,
+                        clone_urls,
+                        default_branch,
+                        head,
+                        history,
+                        readme: readme.map(|readme| (readme.path, readme.blob.data)),
+                        entries,
+                    },
                 ))
             },
         )
@@ -1381,6 +1386,7 @@
     encoded_path: String,
     http_clone_url: String,
     ssh_clone_url: String,
+    default_branch: String,
     has_head: bool,
     has_readme: bool,
     readme_path: String,
@@ -1408,6 +1414,16 @@
     next_page: usize,
 }
 
+struct RepositorySummary {
+    description: String,
+    clone_urls: (String, String),
+    default_branch: String,
+    head: Option<ObjectId>,
+    history: Vec<CommitInfo>,
+    readme: Option<(Vec<u8>, Vec<u8>)>,
+    entries: Vec<TreeEntryInfo>,
+}
+
 impl RepositoryPage {
     fn base(record: RepositoryRecord, page_kind: &'static str, title: String) -> Self {
         Self {
@@ -1425,6 +1441,7 @@
             encoded_path: String::new(),
             http_clone_url: String::new(),
             ssh_clone_url: String::new(),
+            default_branch: String::new(),
             has_head: false,
             has_readme: false,
             readme_path: String::new(),
@@ -1453,27 +1470,33 @@
         }
     }
 
-    fn summary(
-        record: RepositoryRecord,
-        description: String,
-        clone_urls: (String, String),
-        head: Option<ObjectId>,
-        history: Vec<CommitInfo>,
-        readme: Option<(Vec<u8>, Vec<u8>)>,
-    ) -> Self {
+    fn summary(record: RepositoryRecord, summary: RepositorySummary) -> Self {
         let title = format!("{}/{}", record.owner, record.slug);
         let mut page = Self::base(record, "summary", title);
-        page.description = description;
-        page.http_clone_url = clone_urls.0;
-        page.ssh_clone_url = clone_urls.1;
-        page.has_head = head.is_some();
-        page.commit_id = head.map(|id| id.to_string()).unwrap_or_default();
-        page.history = history
+        page.description = summary.description;
+        page.http_clone_url = summary.clone_urls.0;
+        page.ssh_clone_url = summary.clone_urls.1;
+        page.default_branch = summary
+            .default_branch
+            .strip_prefix("refs/heads/")
+            .unwrap_or(&summary.default_branch)
+            .to_owned();
+        page.has_head = summary.head.is_some();
+        page.commit_id = summary.head.map(|id| id.to_string()).unwrap_or_default();
+        page.history = summary
+            .history
             .into_iter()
             .take(MAX_SUMMARY_COMMITS)
             .map(CommitView::from)
             .collect();
-        if let Some((path, data)) = readme {
+        if let Some(head) = summary.head {
+            page.entries = summary
+                .entries
+                .into_iter()
+                .map(|entry| TreeView::new(head, &[], entry))
+                .collect();
+        }
+        if let Some((path, data)) = summary.readme {
             page.has_readme = true;
             page.readme_path = display_bytes(&path);
             if let Ok(content) = std::str::from_utf8(&data)
diff --git a/templates/repository.html b/templates/repository.html
index 6e9095acc96575006b0b6aaa986274fdbadbfaeb..525b73c4631bf37ef71394b193dec09b11cbcf63 100644
--- a/templates/repository.html
+++ b/templates/repository.html
@@ -6,23 +6,37 @@
 {% include "repository-nav.html" %}
   </header>
 
-{% if has_head %}
+{% if has_head && page_kind != "summary" %}
   <p><a href="/{{ owner }}/{{ repository }}/tree/{{ commit_id }}">Browse tree</a> · <a href="/{{ owner }}/{{ repository }}/commit/{{ commit_id }}">Show commit</a> · <a href="/{{ owner }}/{{ repository }}/archive/{{ commit_id }}.tar">Download archive</a></p>
 {% endif %}
 
 {% if page_kind == "summary" %}
-{% if description != "" %}
-  <p class="repository-description">{{ description }}</p>
-{% endif %}
-  <section aria-labelledby="clone-heading">
-    <h2 id="clone-heading">Clone</h2>
-    <dl>
-      <dt>HTTP</dt><dd><code>{{ http_clone_url }}</code></dd>
-      <dt>SSH</dt><dd><code>{{ ssh_clone_url }}</code></dd>
-      <dt>Created</dt><dd><time datetime="{{ created_at }}">{{ created_at|human_time }}</time></dd>
-    </dl>
-  </section>
 {% if has_head %}
+  <nav class="repository-toolbar" aria-label="Repository files">
+    <strong>{{ default_branch }}</strong>
+    <a href="/{{ owner }}/{{ repository }}/tree/{{ commit_id }}">Browse files</a>
+    <a href="/{{ owner }}/{{ repository }}/commit/{{ commit_id }}"><code title="{{ commit_id }}">{{ commit_id|short_id }}</code></a>
+    <a href="/{{ owner }}/{{ repository }}/archive/{{ commit_id }}.tar">Download archive</a>
+  </nav>
+{% endif %}
+  <div class="repository-summary-layout">
+  <div class="repository-summary-main">
+{% if has_head %}
+  <section aria-labelledby="files-heading">
+    <h2 id="files-heading">Files</h2>
+    <div class="table-scroll"><table class="repository-file-list">
+      <thead><tr><th scope="col">Name</th><th scope="col">Type</th><th scope="col">Object</th></tr></thead>
+      <tbody>
+{% for item in entries %}
+        <tr>
+          <th scope="row"><a href="/{{ owner }}/{{ repository }}/{{ item.href }}">{{ item.name }}</a></th>
+          <td>{{ item.kind }}</td>
+          <td><code title="{{ item.id }}">{{ item.id|short_id }}</code></td>
+        </tr>
+{% endfor %}
+      </tbody>
+    </table></div>
+  </section>
   <section aria-labelledby="history-heading">
     <h2 id="history-heading">Recent commits</h2>
     <ol class="commit-list">
@@ -45,6 +59,26 @@
 {% else %}
   <p>This repository has no commits.</p>
 {% endif %}
+  </div>
+  <aside class="repository-sidebar" aria-label="Repository information">
+  <section aria-labelledby="about-heading">
+    <h2 id="about-heading">About</h2>
+{% if description != "" %}
+    <p class="repository-description">{{ description }}</p>
+{% else %}
+    <p>No description is available.</p>
+{% endif %}
+    <p>Created <time datetime="{{ created_at }}">{{ created_at|human_time }}</time></p>
+  </section>
+  <section aria-labelledby="clone-heading">
+    <h2 id="clone-heading">Clone</h2>
+    <dl>
+      <dt>HTTP</dt><dd><code>{{ http_clone_url }}</code></dd>
+      <dt>SSH</dt><dd><code>{{ ssh_clone_url }}</code></dd>
+    </dl>
+  </section>
+  </aside>
+  </div>
 {% endif %}
 
 {% if page_kind == "commits" %}
diff --git a/tests/public_routes.rs b/tests/public_routes.rs
index 519c6052128aa09524fc6b45326110707346b008..2205df793b20c5d729eff857c7dcc3043756b5aa 100644
--- a/tests/public_routes.rs
+++ b/tests/public_routes.rs
@@ -115,6 +115,16 @@
         assert!(summary_text.contains("https://tit.example/alice/example"));
         assert!(summary_text.contains("ssh://tit.example:2222/alice/example"));
         assert!(summary_text.contains(&fixture.head));
+        assert!(summary_text.contains("<div class=\"repository-summary-layout\">"));
+        assert!(summary_text.contains("<strong>main</strong>"));
+        assert!(summary_text.contains(&format!(
+            "href=\"/alice/example/blob/{}/README.md\"",
+            fixture.head
+        )));
+        assert!(summary_text.contains(&format!(
+            "href=\"/alice/example/tree/{}/nested\"",
+            fixture.head
+        )));
         assert!(summary_text.contains("README.md"));
         assert!(summary_text.contains("<h1>tit fixture</h1>"));
         assert!(summary_text.contains("<strong>safe</strong>"));
