diff --git a/lib/core/posts.ex b/lib/core/posts.ex
index 7594db5..832fd0a 100644
--- a/lib/core/posts.ex
+++ b/lib/core/posts.ex
@@ -37,6 +37,12 @@ defmodule Core.Posts do
     |> Core.Repo.all()
   end
 
+  def list_posts(kind, params \\ %{}) do
+    Post.Query.base()
+    |> Post.Query.where_kind(kind)
+    |> Flop.validate_and_run(params, for: Schema.Post)
+  end
+
   def list_published_posts(kind, params \\ %{}) do
     Post.Query.base()
     |> Post.Query.published()
diff --git a/lib/web/components/core_components.ex b/lib/web/components/core_components.ex
index 52e8986..8d1b43a 100644
--- a/lib/web/components/core_components.ex
+++ b/lib/web/components/core_components.ex
@@ -154,46 +154,6 @@ defmodule Web.CoreComponents do
     Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}"))
   end
 
-  attr :id, :string, required: true
-  attr :posts, :any, required: true
-
-  slot :inner_block, required: true
-
-  def post_list(assigns) do
-    ~H"""
-    <ol id={@id} phx-update={if is_struct(@posts, Phoenix.LiveView.LiveStream), do: "phx-update"}>
-      <li
-        :for={{dom_id, item} <- normalize_posts(@posts)}
-        id={dom_id}
-        class="flex flex-row justify-between"
-      >
-        <span>{render_slot(@inner_block, item)}</span>
-        <span>
-          <%= if item.deleted_at do %>
-            deleted
-          <% else %>
-            <%= if item.published_at do %>
-              <%= case item.kind do %>
-                <% :blog -> %>
-                  <.timex value={item.published_at} format="{YYYY}-{0M}-{0D}" />
-                <% :status -> %>
-                  <.timex value={item.published_at} format="{relative}" formatter={:relative} />
-              <% end %>
-            <% else %>
-              draft
-            <% end %>
-          <% end %>
-        </span>
-      </li>
-    </ol>
-    """
-  end
-
-  defp normalize_posts(%Phoenix.LiveView.LiveStream{} = stream), do: stream
-
-  defp normalize_posts(posts) when is_list(posts),
-    do: Enum.with_index(posts, &{"#{&1.kind}-#{&2}", &1})
-
   @doc """
   Renders markdown content as HTML.
 
@@ -251,4 +211,37 @@ defmodule Web.CoreComponents do
     </div>
     """
   end
+
+  attr :id, :string, required: true
+  attr :stream, Phoenix.LiveView.LiveStream, required: true
+  attr :class, :string, default: nil
+  attr :rest, :global
+
+  slot :col do
+    attr :label, :string
+    attr :class, :string
+  end
+
+  def table(assigns) do
+    ~H"""
+    <table id={@id} class={["border-collapse", @class]} {@rest}>
+      <thead>
+        <tr>
+          <%= for col <- @col do %>
+            <th class="border p-2">{col[:label]}</th>
+          <% end %>
+        </tr>
+      </thead>
+      <tbody id={"#{@id}-stream"} phx-update="stream">
+        <%= for {dom_id, item} <- @stream do %>
+          <tr id={dom_id}>
+            <%= for col <- @col do %>
+              <td class={["border p-2", col[:class]]}>{render_slot(col, item)}</td>
+            <% end %>
+          </tr>
+        <% end %>
+      </tbody>
+    </table>
+    """
+  end
 end
diff --git a/lib/web/components/layouts/app.html.heex b/lib/web/components/layouts/app.html.heex
index 94f066a..0330348 100644
--- a/lib/web/components/layouts/app.html.heex
+++ b/lib/web/components/layouts/app.html.heex
@@ -8,7 +8,7 @@
     <section :if={not is_nil(@current_user)} class="ml-2">
       <nav>
         <ul class="flex flex-row gap-x-2">
-          <li><.link navigate={~p"/admin"}>admin</.link></li>
+          <li><.link navigate={~p"/admin/writing"}>admin</.link></li>
         </ul>
       </nav>
     </section>
diff --git a/lib/web/controllers/blog_html.ex b/lib/web/controllers/blog_html.ex
index 00a01d3..eb220ee 100644
--- a/lib/web/controllers/blog_html.ex
+++ b/lib/web/controllers/blog_html.ex
@@ -2,10 +2,4 @@ defmodule Web.BlogHTML do
   use Web, :html
 
   embed_templates "blog_html/*"
-
-  def blog_path(%Schema.Post{} = blog) do
-    if date = Core.Posts.publish_date(blog) do
-      ~p"/blog/#{date.year}/#{date.month}/#{date.day}/#{blog.slug}"
-    end
-  end
 end
diff --git a/lib/web/controllers/blog_html/index.html.heex b/lib/web/controllers/blog_html/index.html.heex
index 4c06abb..75e183b 100644
--- a/lib/web/controllers/blog_html/index.html.heex
+++ b/lib/web/controllers/blog_html/index.html.heex
@@ -11,7 +11,7 @@
               <li>
                 <article class="h-entry">
                   <.link
-                    navigate={blog_path(blog)}
+                    navigate={Web.Paths.public_blog_path(blog)}
                     class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
                   >
                     <h3 class="p-name u-url">{blog.title}</h3>
diff --git a/lib/web/controllers/page_html/home.html.heex b/lib/web/controllers/page_html/home.html.heex
index 7d31bfa..76dd544 100644
--- a/lib/web/controllers/page_html/home.html.heex
+++ b/lib/web/controllers/page_html/home.html.heex
@@ -13,7 +13,7 @@
         <li>
           <article class="h-entry">
             <.link
-              navigate={Web.BlogHTML.blog_path(blog)}
+              navigate={Web.Paths.public_blog_path(blog)}
               class="flex justify-between items-center hover:bg-gray-50 -mx-2 px-2 py-1 rounded"
             >
               <h3 class="p-name u-url">{blog.title}</h3>
diff --git a/lib/web/live/admin_dashboard_live.ex b/lib/web/live/admin_dashboard_live.ex
index 8c20527..707cb35 100644
--- a/lib/web/live/admin_dashboard_live.ex
+++ b/lib/web/live/admin_dashboard_live.ex
@@ -2,42 +2,64 @@ defmodule Web.AdminDashboardLive do
   use Web, :live_view
 
   def mount(_params, _session, socket) do
-    statuses = Core.Posts.get_all_recent_statuses()
-    blogs = Core.Posts.get_all_recent_blogs()
-
-    socket =
-      socket
-      |> stream(:statuses, statuses)
-      |> stream(:blogs, blogs)
-
     {:ok, socket}
   end
 
-  def render(assigns) do
+  def handle_params(params, _uri, socket) do
+    kind = socket.assigns.live_action
+
+    {:ok, {posts, meta}} = Core.Posts.list_posts(kind, params)
+
+    socket =
+      socket
+      |> assign(
+        kind: kind,
+        meta: meta
+      )
+      |> stream(:posts, posts, reset: true)
+
+    {:noreply, socket}
+  end
+
+  attr :post, Schema.Post, required: true
+
+  defp post_status(%{post: %{published_at: nil, deleted_at: nil}} = assigns) do
     ~H"""
-    <div class="flex flex-col gap-y-4">
-      <h1 class="font-bold text-2xl">dashboard</h1>
+    draft
+    """
+  end
 
-      <section>
-        <header class="flex flex-row justify-between">
-          <h2 class="font-bold text-xl">recent statuses</h2>
-          <.link navigate={~p"/admin/posts/new?kind=status"}>new status</.link>
-        </header>
-        <.post_list :let={status} id="recent-statuses" posts={@streams.statuses}>
-          <.link navigate={~p"/admin/posts/#{status}"}>{status.body}</.link>
-        </.post_list>
-      </section>
+  defp post_status(%{post: %{published_at: _, deleted_at: nil}} = assigns) do
+    ~H"""
+    published
+    """
+  end
 
-      <section>
-        <header class="flex flex-row justify-between">
-          <h2 class="font-bold text-xl">recent blogs</h2>
-          <.link navigate={~p"/admin/posts/new?kind=blog"}>new blog</.link>
-        </header>
-        <.post_list :let={blog} id="recent-blogs" posts={@streams.blogs}>
-          <.link navigate={~p"/admin/posts/#{blog}"}>{blog.title}</.link>
-        </.post_list>
-      </section>
+  defp post_status(assigns) do
+    ~H"""
+    deleted
+    """
+  end
+
+  attr :post, Schema.Post, required: true
+
+  defp post_actions(assigns) do
+    ~H"""
+    <div class="flex flex-row gap-x-1">
+      <.link navigate={~p"/admin/posts/#{@post}"}>edit</.link>
+      <.link
+        :if={@post.published_at && is_nil(@post.deleted_at)}
+        navigate={Web.Paths.public_post_path(@post)}
+      >
+        view
+      </.link>
     </div>
     """
   end
+
+  defp build_path(:blog, meta),
+    do: Flop.Phoenix.build_path(~p"/admin/writing", meta, for: Schema.Post)
+
+  defp build_path(:status, meta),
+    do: Flop.Phoenix.build_path(~p"/admin/microblog", meta, for: Schema.Post)
 end
diff --git a/lib/web/live/admin_dashboard_live.html.heex b/lib/web/live/admin_dashboard_live.html.heex
new file mode 100644
index 0000000..e07c2df
--- /dev/null
+++ b/lib/web/live/admin_dashboard_live.html.heex
@@ -0,0 +1,59 @@
+<div class="flex flex-col py-4 px-6">
+  <header class="mb-4">
+    <nav>
+      <ul class="flex flex-row gap-x-4">
+        <li>
+          <.link class={[@kind == :blog && "underline"]} patch={~p"/admin/writing"}>
+            writing
+          </.link>
+        </li>
+        <li>
+          <.link class={[@kind == :status && "underline"]} patch={~p"/admin/microblog"}>
+            microblog
+          </.link>
+        </li>
+      </ul>
+    </nav>
+  </header>
+
+  <main class="flex flex-col">
+    <.link class="mb-4" navigate={~p"/admin/posts/new?kind=#{@kind}"}>new {@kind}</.link>
+    <%= case @kind do %>
+      <% :blog -> %>
+        <.table id="blog-posts" stream={@streams.posts}>
+          <:col :let={blog} label="title">{blog.title}</:col>
+          <:col :let={blog} label="status">
+            <.post_status post={blog} />
+          </:col>
+          <:col :let={blog}>
+            <.post_actions post={blog} />
+          </:col>
+        </.table>
+      <% :status -> %>
+        <.table id="status-posts" stream={@streams.posts}>
+          <:col :let={status} label="content">
+            {status.body}
+          </:col>
+          <:col :let={status} label="status">
+            <.post_status post={status} />
+          </:col>
+          <:col :let={status}>
+            <.post_actions post={status} />
+          </:col>
+        </.table>
+    <% end %>
+    <footer class="flex flex-row justify-between mt-2">
+      <%= if @meta.has_previous_page? do %>
+        <.link patch={build_path(@kind, Flop.to_previous_cursor(@meta))}>prev</.link>
+      <% else %>
+        <div />
+      <% end %>
+
+      <%= if @meta.has_next_page? do %>
+        <.link patch={build_path(@kind, Flop.to_next_cursor(@meta))}>next</.link>
+      <% else %>
+        <div />
+      <% end %>
+    </footer>
+  </main>
+</div>
diff --git a/lib/web/live/admin_post_live.ex b/lib/web/live/admin_post_live.ex
index 41dcc1c..7970e80 100644
--- a/lib/web/live/admin_post_live.ex
+++ b/lib/web/live/admin_post_live.ex
@@ -116,44 +116,6 @@ defmodule Web.AdminPostLive do
     {:noreply, socket}
   end
 
-  def render(assigns) do
-    ~H"""
-    <main>
-      <header>
-        <h1>{page_title(@post, @live_action)}</h1>
-      </header>
-
-      <.form for={@form} phx-change="validate" phx-submit="save">
-        <.input :if={@post.kind == :blog} type="text" field={@form[:title]} />
-        <.input
-          :if={@post.kind == :blog}
-          type="text"
-          field={@form[:slug]}
-          disabled={not update_slug?(@post)}
-        />
-        <.input type="textarea" field={@form[:body]} />
-
-        <.button type="submit">save</.button>
-      </.form>
-
-      <%= if @live_action == :edit do %>
-        <div>
-          <%= if @post.published_at do %>
-            <.button phx-click="unpublish">unpublish</.button>
-          <% else %>
-            <.button phx-click="publish">publish</.button>
-          <% end %>
-          <%= if @post.deleted_at do %>
-            <.button phx-click="undelete">undelete</.button>
-          <% else %>
-            <.button phx-click="delete">delete</.button>
-          <% end %>
-        </div>
-      <% end %>
-    </main>
-    """
-  end
-
   defp page_title(%Schema.Post{kind: :blog}, :new), do: "new blog"
   defp page_title(%Schema.Post{kind: :status}, :new), do: "new status"
   defp page_title(%Schema.Post{kind: :blog}, :edit), do: "edit blog"
diff --git a/lib/web/live/admin_post_live.html.heex b/lib/web/live/admin_post_live.html.heex
new file mode 100644
index 0000000..0fcf196
--- /dev/null
+++ b/lib/web/live/admin_post_live.html.heex
@@ -0,0 +1,33 @@
+<main>
+  <header>
+    <h1>{page_title(@post, @live_action)}</h1>
+  </header>
+
+  <.form for={@form} phx-change="validate" phx-submit="save">
+    <.input :if={@post.kind == :blog} type="text" field={@form[:title]} />
+    <.input
+      :if={@post.kind == :blog}
+      type="text"
+      field={@form[:slug]}
+      disabled={not update_slug?(@post)}
+    />
+    <.input type="textarea" field={@form[:body]} />
+
+    <.button type="submit">save</.button>
+  </.form>
+
+  <%= if @live_action == :edit do %>
+    <div>
+      <%= if @post.published_at do %>
+        <.button phx-click="unpublish">unpublish</.button>
+      <% else %>
+        <.button phx-click="publish">publish</.button>
+      <% end %>
+      <%= if @post.deleted_at do %>
+        <.button phx-click="undelete">undelete</.button>
+      <% else %>
+        <.button phx-click="delete">delete</.button>
+      <% end %>
+    </div>
+  <% end %>
+</main>
diff --git a/lib/web/paths.ex b/lib/web/paths.ex
new file mode 100644
index 0000000..4d3112a
--- /dev/null
+++ b/lib/web/paths.ex
@@ -0,0 +1,16 @@
+defmodule Web.Paths do
+  use Web, :html
+
+  def public_post_path(%Schema.Post{kind: :status} = status), do: public_status_path(status)
+  def public_post_path(%Schema.Post{kind: :blog} = blog), do: public_blog_path(blog)
+
+  def public_status_path(%Schema.Post{kind: :status} = status) do
+    ~p"/status/#{status}"
+  end
+
+  def public_blog_path(%Schema.Post{kind: :blog} = blog) do
+    if date = Core.Posts.publish_date(blog) do
+      ~p"/blog/#{date.year}/#{date.month}/#{date.day}/#{blog.slug}"
+    end
+  end
+end
diff --git a/lib/web/router.ex b/lib/web/router.ex
index 69bb5bc..d418ae0 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -31,7 +31,8 @@ defmodule Web.Router do
     live_session :require_authenticated_user, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
       live "/users/settings", UserSettingsLive, :edit
 
-      live "/", AdminDashboardLive
+      live "/writing", AdminDashboardLive, :blog
+      live "/microblog", AdminDashboardLive, :status
 
       live "/posts/new", AdminPostLive, :new
       live "/posts/:post_id", AdminPostLive, :edit