I needed a quick way to view all my WordPress posts without logging into the admin dashboard. Sometimes you just want a fast, no-nonsense list that you can sort and browse through. So I built a standalone PHP script that sits in my WordPress root directory and does exactly that.
This tool displays posts in a clean HTML table with sorting options, category filtering, and pagination. You can filter by category using a dropdown, sort by date or title, and page through results. Perfect for when you need quick access to your content without the overhead of the full WordPress admin.
The Complete Script
Create a file called list-posts.php or something similar in your WordPress root directory (the same folder that contains wp-config.php):
<?php
/**
* Simple WordPress Post Lister
* Place this file in your WordPress root directory
*/
// Configuration
$posts_per_page = 25;
// Load WordPress
require_once( 'wp-load.php' );
// Get current page, sort parameters, and category
$current_page = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
$orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( $_GET['orderby'] ) : 'date';
$order = isset( $_GET['order'] ) ? sanitize_text_field( $_GET['order'] ) : 'DESC';
$selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : '';
// Build query arguments
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'paged' => $current_page,
'orderby' => $orderby,
'order' => $order
);
// Add category filter if specified
if ( ! empty( $selected_category ) ) {
$args['category_name'] = $selected_category;
}
// Run the query
$query = new WP_Query( $args );
$total_posts = $query->found_posts;
$total_pages = $query->max_num_pages;
// Get all categories
$categories = get_categories( array(
'orderby' => 'name',
'order' => 'ASC',
'hide_empty' => true
) );
// Helper function to build sort URLs
function get_sort_url( $new_orderby, $current_orderby, $current_order, $category ) {
$new_order = 'DESC';
if ( $new_orderby === $current_orderby ) {
$new_order = ( $current_order === 'DESC' ) ? 'ASC' : 'DESC';
}
$url = '?orderby=' . $new_orderby . '&order=' . $new_order;
if ( ! empty( $category ) ) {
$url .= '&category=' . $category;
}
return $url;
}
// Helper function to build pagination URLs
function get_page_url( $page, $orderby, $order, $category ) {
$url = '?orderby=' . $orderby . '&order=' . $order . '&paged=' . $page;
if ( ! empty( $category ) ) {
$url .= '&category=' . $category;
}
return $url;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WordPress Post List</title>
<style>
body {
font-family: "Segoe UI", Roboto, sans-serif;
max-width: 1200px;
margin: 40px auto;
padding: 0 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba( 0, 0, 0, 0.1 );
}
h1 {
margin-top: 0;
color: #333;
}
.filter-bar {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.filter-bar label {
font-weight: 600;
color: #333;
}
.filter-bar select {
padding: 8px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
font-size: 14px;
min-width: 200px;
}
.info {
margin-bottom: 20px;
color: #666;
font-size: 14px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th {
background: #f8f9fa;
padding: 12px;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
th a {
color: #333;
text-decoration: none;
}
th a:hover {
color: #0066cc;
}
td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
}
tr:hover {
background: #f8f9fa;
}
.post-link {
color: #0066cc;
text-decoration: none;
}
.post-link:hover {
text-decoration: underline;
}
.pagination {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 30px;
}
.pagination a,
.pagination span {
padding: 8px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
text-decoration: none;
color: #333;
}
.pagination a:hover {
background: #f8f9fa;
}
.pagination .current {
background: #0066cc;
color: white;
border-color: #0066cc;
}
.sort-indicator {
font-size: 12px;
margin-left: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>WordPress Post List</h1>
<div class="filter-bar">
<label for="category-select">Category:</label>
<select id="category-select" onchange="window.location.href=this.value;">
<option value="?orderby=<?php echo $orderby; ?>&order=<?php echo $order; ?>">All Categories</option>
<?php foreach ( $categories as $category ) : ?>
<option value="?orderby=<?php echo $orderby; ?>&order=<?php echo $order; ?>&category=<?php echo $category->slug; ?>"
<?php selected( $selected_category, $category->slug ); ?>>
<?php echo esc_html( $category->name ); ?> (<?php echo $category->count; ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="info">
Showing <?php echo $total_posts; ?> posts
<?php if ( ! empty( $selected_category ) ) : ?>
<?php
$cat_obj = get_category_by_slug( $selected_category );
if ( $cat_obj ) :
?>
in category: <strong><?php echo esc_html( $cat_obj->name ); ?></strong>
<?php endif; ?>
<?php endif; ?>
</div>
<table>
<thead>
<tr>
<th style="width: 70%;">
<a href="<?php echo get_sort_url( 'title', $orderby, $order, $selected_category ); ?>">
Title
<?php if ( $orderby === 'title' ) : ?>
<span class="sort-indicator"><?php echo ( $order === 'DESC' ) ? '↓' : '↑'; ?></span>
<?php endif; ?>
</a>
</th>
<th style="width: 30%;">
<a href="<?php echo get_sort_url( 'date', $orderby, $order, $selected_category ); ?>">
Date
<?php if ( $orderby === 'date' ) : ?>
<span class="sort-indicator"><?php echo ( $order === 'DESC' ) ? '↓' : '↑'; ?></span>
<?php endif; ?>
</a>
</th>
</tr>
</thead>
<tbody>
<?php if ( $query->have_posts() ) : ?>
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<tr>
<td>
<a href="<?php echo get_permalink(); ?>" class="post-link" target="_blank">
<?php echo get_the_title(); ?>
</a>
</td>
<td>
<?php echo get_the_date( 'F j, Y' ); ?>
</td>
</tr>
<?php endwhile; ?>
<?php else : ?>
<tr>
<td colspan="2">No posts found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php if ( $total_pages > 1 ) : ?>
<div class="pagination">
<?php if ( $current_page > 1 ) : ?>
<a href="<?php echo get_page_url( $current_page - 1, $orderby, $order, $selected_category ); ?>">← Previous</a>
<?php endif; ?>
<?php for ( $i = 1; $i <= $total_pages; $i++ ) : ?>
<?php if ( $i === $current_page ) : ?>
<span class="current"><?php echo $i; ?></span>
<?php else : ?>
<a href="<?php echo get_page_url( $i, $orderby, $order, $selected_category ); ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ( $current_page < $total_pages ) : ?>
<a href="<?php echo get_page_url( $current_page + 1, $orderby, $order, $selected_category ); ?>">Next →</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</div>
</body>
</html>
How It Works
The script starts by loading WordPress with require_once( 'wp-load.php' ). This gives us access to all WordPress functions without needing to be inside the admin dashboard or a theme file.
Configuration
At the top, I set one configuration variable:
$posts_per_page = 25;
The $posts_per_page variable controls pagination. Set it to whatever number works for you.
Query Parameters
The script accepts URL parameters for sorting, pagination, and category filtering:
$current_page = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1; $orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( $_GET['orderby'] ) : 'date'; $order = isset( $_GET['order'] ) ? sanitize_text_field( $_GET['order'] ) : 'DESC'; $selected_category = isset( $_GET['category'] ) ? sanitize_text_field( $_GET['category'] ) : '';
I use sanitize_text_field() to clean the input and max( 1, intval() ) to make sure the page number is always at least 1.
Getting Categories
To populate the dropdown, I fetch all categories that have at least one post:
$categories = get_categories( array(
'orderby' => 'name',
'order' => 'ASC',
'hide_empty' => true
) );The hide_empty parameter makes sure empty categories don’t show up in the list. Categories are sorted alphabetically by name.
Building the Query
WordPress’s WP_Query class handles all the heavy lifting:
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'paged' => $current_page,
'orderby' => $orderby,
'order' => $order
);If a category is selected from the dropdown, it gets added to the query arguments automatically.
Helper Functions
I created two helper functions to keep the template code clean. The get_sort_url() function builds URLs for the sortable column headers. When you click a column that’s already sorted, it reverses the sort direction. It also preserves the selected category:
function get_sort_url( $new_orderby, $current_orderby, $current_order, $category ) {
$new_order = 'DESC';
if ( $new_orderby === $current_orderby ) {
$new_order = ( $current_order === 'DESC' ) ? 'ASC' : 'DESC';
}
$url = '?orderby=' . $new_orderby . '&order=' . $new_order;
if ( ! empty( $category ) ) {
$url .= '&category=' . $category;
}
return $url;
}The get_page_url() function builds pagination links while preserving both the current sort settings and selected category.
The Category Dropdown
The filter bar sits at the top of the page with a clean dropdown selector:
<select id="category-select" onchange="window.location.href=this.value;">
<option value="?orderby=<?php echo $orderby; ?>&order=<?php echo $order; ?>">All Categories</option>
<?php foreach ( $categories as $category ) : ?>
<option value="?orderby=<?php echo $orderby; ?>&order=<?php echo $order; ?>&category=<?php echo $category->slug; ?>"
<?php selected( $selected_category, $category->slug ); ?>>
<?php echo esc_html( $category->name ); ?> (<?php echo $category->count; ?>)
</option>
<?php endforeach; ?>
</select>Each category option shows the category name and post count. The selected() function makes sure the current category stays selected when you sort or paginate. When you change the category, the page reloads with the new filter applied while keeping your current sort order.
The Display
The HTML table shows post titles as clickable links that open in a new tab. Column headers are clickable for sorting, with small arrows indicating the current sort direction.
Pagination appears at the bottom if there’s more than one page of results. It shows Previous/Next buttons plus numbered page links. All pagination links preserve your category filter and sort order.
Using the Script
Upload list-posts.php to your WordPress root directory and access it at https://yoursite.com/list-posts.php.
The page loads with all posts sorted by date, newest first. Use the category dropdown to filter posts. Click the column headers to change the sort order. The date column toggles between newest and oldest. The title column toggles between A-Z and Z-A.
When you filter by category, the info line shows which category you’re viewing. The dropdown stays on your selected category as you sort and page through results.
Security Notes
This script doesn’t include authentication, so anyone who finds the URL can view your post list. For a production site, I would add a simple password check at the top:
$password = 'your-secure-password';
if ( ! isset( $_GET['pw'] ) || $_GET['pw'] !== $password ) {
die( 'Access denied' );
}Then access it with https://yoursite.com/list-posts.php?pw=your-secure-password.
Or better yet, protect it with .htaccess authentication or only use it on development sites.
Customization Ideas
You can easily extend this script. Here are some ideas:
- Add more columns like author, word count, or comment count
- Include post excerpts in an expandable row
- Add a search box to filter by title
- Show draft posts alongside published ones
- Export the list to CSV
- Add checkboxes for bulk actions
- Filter by multiple categories at once
- Add date range filtering
The basic structure is flexible enough to add whatever you need.
That’s it! A simple, fast way to view, filter, and sort your WordPress posts without touching the admin dashboard.





















