When a client needs to display a large data table on their WordPress site — think product specs, pricing grids, or reference data — manually building the HTML in the block editor quickly becomes impractical. PHP’s native fgetcsv() function lets you read a CSV file row by row and output clean, properly escaped HTML, with no plugin required.
Problem: A client needs to display tabular data from a CSV file on a WordPress page — re-entering it manually in the block editor is impractical, and the data is updated periodically.
Solution: Build a shortcode that reads the CSV file from the uploads directory with fgetcsv(), treats the first row as column headers, and renders a semantic <table> with <thead> and <tbody>. Pass the filename as a shortcode attribute so different pages can load different files.
The function below reads a CSV file from the WordPress uploads directory, treats the first row as column headers, renders any cell that is a valid URL as a hyperlink, and outputs a Bootstrap-compatible <table>:
<?php
/**
* Convert a CSV file to an HTML table.
*
* @param string $file_url Absolute URL to the CSV file (must be on the same server).
* @param string $link_title Link text used when a cell value is a URL.
* @param string $link_class Optional CSS class(es) for generated links.
*/
function convert_csv_to_html( $file_url, $link_title, $link_class = '' ) {
$file_url = esc_url( $file_url, [ 'http', 'https' ] );
$file_path = $_SERVER['DOCUMENT_ROOT'] . wp_make_link_relative( $file_url );
if ( ! file_exists( $file_path ) ) {
return;
}
$handle = fopen( $file_path, 'r' );
if ( $handle === false ) {
return;
}
// Sanitise the class attribute once
$class_attr = '';
if ( ! empty( $link_class ) && preg_match( '/^[_a-z0-9 -]+$/i', $link_class ) ) {
$class_attr = ' class="' . esc_attr( $link_class ) . '"';
}
$row_index = 0;
echo '<div class="table-responsive"><table class="table">';
while ( ( $row = fgetcsv( $handle, 1000 ) ) !== false ) {
echo '<tr>';
foreach ( $row as $cell ) {
if ( $row_index === 0 ) {
// First row: column headers
echo '<th>' . esc_html( $cell ) . '</th>';
} else {
if ( filter_var( $cell, FILTER_VALIDATE_URL ) !== false ) {
echo '<td><a href="' . esc_url( $cell ) . '"' . $class_attr . '>'
. esc_html( $link_title ) . '</a></td>';
} else {
echo '<td>' . esc_html( $cell ) . '</td>';
}
}
}
echo '</tr>';
$row_index++;
}
fclose( $handle );
echo '</table></div>';
}
Usage — call the function from within a page template, passing the URL to a CSV file already uploaded to the Media Library:
<?php
$csv_url = 'https://example.com/wp-content/uploads/2019/10/products.csv';
convert_csv_to_html( $csv_url, 'View PDF', 'btn btn-sm btn-outline-primary' );
NOTE: The function reads the file path via $_SERVER['DOCUMENT_ROOT'], so the CSV must be hosted on the same server. For externally hosted CSV files, use wp_remote_get() to fetch the content and write it to a temporary file before passing it to fgetcsv(). Also ensure the CSV is UTF-8 encoded to avoid character encoding issues in the rendered table.