Skip to content

Instantly share code, notes, and snippets.

@ccbayer
Last active February 16, 2025 23:05
Show Gist options
  • Select an option

  • Save ccbayer/4f049262311d44a4265f2e45c62255e3 to your computer and use it in GitHub Desktop.

Select an option

Save ccbayer/4f049262311d44a4265f2e45c62255e3 to your computer and use it in GitHub Desktop.
WordPress Alt Image Importer / Exporter
document.addEventListener('DOMContentLoaded', () => {
const exportButton = document.querySelector('#export-media-csv');
if (exportButton) {
exportButton.addEventListener('click', (e) => {
e.preventDefault();
const url = `${ajaxurl}?action=export_media_csv`;
// if URL is returned, generate a hidden button that JS clicks to download the CSV
if (url) {
const link = document.createElement('a');
link.href = url;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
console.error('CSV not generated');
}
});
}
});
<?php
/**
* Alt Text tools for the theme.
*
* This file is for processing / batch uploading alt text for images
*
* @link https://developer.wordpress.org/themes/basics/template-tags/
* @package YourTheme
*/
namespace YourThemne\AltTools;
/**
* Handle a CSV file upload and update alt text for media items in WordPress using WP_Filesystem.
*
* The CSV file must have the following headings:
* ID, Title, _wp_attachment_image_alt, _wp_attached_file, URL
*/
add_action(
'admin_menu',
function () {
add_submenu_page(
'tools.php', // Parent menu slug
'Update Media Alt Text', // Page title
'Update Media Alt Text', // Menu title
'manage_options', // Capability
'update-media-alt-text', // Menu slug
__NAMESPACE__ . '\render_update_media_alt_text_page' // Callback function
);
}
);
/**
* Renders the markup for the Admin page. Includes a form and buttons to process the
* upload, and export existing media library.
*/
function render_update_media_alt_text_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
if ( ! isset( $_POST['update_alt_nonce'] ) || ! wp_verify_nonce( $_POST['update_alt_nonce'], 'update_media_alt_text' ) ) {
wp_die( 'Unauthorized request' );
}
if ( isset( $_FILES['csv_file']['error'] ) && UPLOAD_ERR_OK === $_FILES['csv_file']['error'] ) {
if ( isset( $_FILES['csv_file']['tmp_name'] ) ) {
$file_path = $_FILES['csv_file']['tmp_name'];
$updated_items = process_csv_and_update_alt_text( $file_path );
} else {
echo '<div class="notice notice-warning"><p>No file was found / uploaded.</p></div>';
return;
}
if ( ! empty( $updated_items ) ) {
echo '<div class="notice notice-success"><p>The following media items were updated:</p>';
echo '<table class="widefat fixed">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>New Alt Text</th>
<th>Media Link</th>
</tr>
</thead>
<tbody>';
foreach ( $updated_items as $item ) {
echo '<tr>
<td>' . esc_html( $item['ID'] ) . '</td>
<td>' . esc_html( $item['Title'] ) . '</td>
<td>' . esc_html( $item['AltText'] ) . '</td>
<td><a href="' . esc_url( $item['MediaLink'] ) . '" target="_blank">View Media</a></td>
</tr>';
}
echo '</tbody></table></div>';
} else {
echo '<div class="notice notice-warning"><p>No media items were updated.</p></div>';
}
} else {
echo '<div class="notice notice-error"><p>There was an error uploading the file.</p></div>';
}
}
?>
<div class="wrap">
<h1>Update Media Alt Text</h1>
<p>
Upload a CSV file to update the alt text for media items in your library.
The file must include the following columns: <code>ID</code>, <code>Title</code>, <code>_wp_attachment_image_alt</code>, <code>_wp_attached_file</code> and <code>URL</code>.
<p>Alt text will only be updated if the media ID and file name match and if the current alt text is empty. Ensure your file is correctly formatted before uploading.</p>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'update_media_alt_text', 'update_alt_nonce' ); ?>
<label for="csv_file">Upload CSV File:</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv">
<button type="submit" class="button button-primary">Upload and Update</button>
</form>
<hr/>
<h2>Export Existing Media</h2>
<p>Click the button below to download a CSV file containing all media items in your library.<p>
<p>This file will include columns for <code>ID</code>, <code>Title</code>, <code>_wp_attachment_image_alt</code>, <code>_wp_attached_file</code>, and <code>URL</code>. You can populate the _wp_attachment_image_alt column with the desired alt text and re-upload the file to update media items.
<form method="post">
<input type="hidden" name="export_csv" value="1">
<button id="export-media-csv" class="button">Export Media as CSV</button>
</form>
</div>
<?php
}
/**
* Reads the uploaded CSV, and processes each row
* if the row matches a media item (ID / Name) **and** it has no alt,
* the script will copy the alt field from the CSV into the media items alt text field.
*
* @param string $file_path - path to the uploaded file
*/
function process_csv_and_update_alt_text( $file_path ) {
global $wp_filesystem;
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
// Read the file contents
$file_contents = $wp_filesystem->get_contents( $file_path );
if ( false === $file_contents ) {
return []; // If file can't be read, return empty array
}
// Split file contents into rows; if rows are empty stop processing
$rows = explode( "\n", $file_contents );
if ( empty( $rows ) ) {
return [];
}
// Extract the header row; If header is invalid, stop processing
$header = str_getcsv( array_shift( $rows ) );
if ( ! validate_csv_header( $header ) ) {
return [];
}
$updated_items = [];
// Process each row while skipping empty rows
foreach ( $rows as $row ) {
if ( empty( trim( $row ) ) ) {
continue;
}
$data = str_getcsv( $row );
// Ensure the row has the same number of elements as the header
if ( count( $data ) !== count( $header ) ) {
continue;
}
$row_data = array_combine( $header, $data );
// Check if the media item exists
$attachment_id = intval( $row_data['ID'] );
$new_alt_text = sanitize_text_field( $row_data['_wp_attachment_image_alt'] );
$attached_file = sanitize_text_field( $row_data['_wp_attached_file'] );
// If it does, skip if alt text is empty
if ( empty( $new_alt_text ) ) {
continue;
}
$current_file = get_post_meta( $attachment_id, '_wp_attached_file', true );
$current_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
// Only update alt text if the ID and file match and the alt text is empty
if ( $attached_file === $current_file && empty( $current_alt ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $new_alt_text );
$updated_items[] = [
'ID' => $attachment_id,
'Title' => get_the_title( $attachment_id ),
'AltText' => $new_alt_text,
'MediaLink' => get_edit_post_link( $attachment_id ),
];
}
}
return $updated_items;
}
/**
* Validates the format of the CSV, expecting these headers:
* 'ID', 'Title', '_wp_attachment_image_alt', '_wp_attached_file', 'URL'
*
* @param array $header - the header of the CSV
* @return bool true if the header matches, false otherwise
*/
function validate_csv_header( $header ) {
$required_columns = [ 'ID', 'Title', '_wp_attachment_image_alt', '_wp_attached_file', 'URL' ];
return ! array_diff( $required_columns, $header );
}
/**
* AJAX handler to export media as CSV
*/
function export_media_csv() {
// Check user permissions
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Unauthorized', 403 );
}
// Generate CSV content
$args = [
'post_type' => 'attachment',
'post_status' => 'inherit',
'posts_per_page' => -1, // phpcs:ignore WordPress.WP.PostsPerPageNoUnlimited.posts_per_page_posts_per_page
];
$attachments = get_posts( $args );
if ( empty( $attachments ) ) {
wp_die( 'No media items found.', 404 );
}
// Start output buffering
ob_start();
// Output CSV headers
echo "ID,Title,_wp_attachment_image_alt,_wp_attached_file,URL\n";
foreach ( $attachments as $attachment ) {
$id = $attachment->ID;
$title = str_replace( ',', ' ', $attachment->post_title );
$alt_text = str_replace( ',', ' ', get_post_meta( $id, '_wp_attachment_image_alt', true ) );
$file = str_replace( ',', ' ', get_post_meta( $id, '_wp_attached_file', true ) );
$url = wp_get_attachment_url( $id );
echo "{$id},{$title},{$alt_text},{$file},{$url}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
// Send the CSV to the browser
header( 'Content-Type: text/csv' );
header( 'Content-Disposition: attachment; filename="media-export.csv"' );
header( 'Content-Length: ' . ob_get_length() );
// Flush output buffer
ob_end_flush();
exit;
}
add_action( 'wp_ajax_export_media_csv', __NAMESPACE__ . '\export_media_csv' );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment