Serve Next-Gen (.webp) images with Wordpress, no plugins - manual guide

I hope you already know what next-get image formats are why they are they beneficial for SEO. In this article i will try to put small guide on how to make/convert your current Wordpress site images to webp, manually, no plugins involved.

To read more about webp image format: link

Main goal here for us is to understand better how Wordpress works on deeper level and how is the database structured.

Although this method is completely possible and works, it requires more serious technical knowledge and is not for everyone.

WARNING

This method is for research and educational purposes.

Its prone to error and not suitable for production websites.

WP database is changed in the process!

Be extra careful and decide if you want to take this path for your self!

Requirements:

  • Access to the database
  • Access to Wordpress files (/wp-content/uploads)
  • Access to Nginx / Apache configs (optional)

Let's go.

# 1. First of all - make backup of the whole /wp-content folder and the database!

# 2. Convert original images with a tool of choice.

Choose the best way that serves to you and convert the images in [wp-root]/wp-content/uploads folder. Me personaly, i made simple bash script to do it. If you interested, you can check it below:

The script

Download and save locally the precompiled webp binaries from: https://developers.google.com/speed/webp/docs/precompiled (opens new window)

In the code, you see my download is located at Downloads/Software

Usage:

./the_script -l /path_to_images_folder -e jpg|png|gif
#!/bin/bash

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'

location=''
extension='jpg'

while [[ $# -gt 0 ]]; do
  case $1 in
    -l|--location)
      location=$2
      shift
      shift
      ;;
    -e|--extension)
      extension=$2
      shift
      shift
      ;;
    -*|--*)
      echo "Unknown option $1"
      exit 1
      ;;
  esac
done

IFS=$'\n'

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/Downloads/Software/libwebp-1.3.2-linux-x86-64/lib

cwebp=$HOME/Downloads/Software/libwebp-1.3.2-linux-x86-64/bin/cwebp
options='-alpha_q 0 -m 6 -qrange 35 60 -sns 100 -quiet'

if [ "$extension" == "gif" ] ; then
    cwebp=$HOME/Downloads/Software/libwebp-1.3.2-linux-x86-64/bin/gif2webp
    options='-metadata all -quiet'
fi

touch originals ; echo '' > originals
touch optimized ; echo '' > optimized

files=`find "$location" -iname "*."$extension""`

original_count=`echo "$files" | wc -l`

printf "Files count: ${original_count} \n"

optimized_count=0

printf "Location size is: ${RED}$(du -sh "$location")${NC} \n"

for file in $files
do
    echo ["$file"] `du -b "$file" | cut -f1` >> originals
    
    filename=$(basename -- "$file")
    dirname="${file/$filename/}"
    extension="${file##*.}"
    newfile="$dirname${filename%.*}.webp"

    eval "$cwebp $options '${file}' -o '${newfile}'" 

    echo ["$newfile"] `du -b "${newfile}" | cut -f1` >> optimized

    # Dont delete original files
    # rm -f "${file}"

    optimized_count=$(( optimized_count + 1 ))

    printf "Done: ${optimized_count} \r"
done

printf "Optimized: ${optimized_count} \n"

original_size=0

while read line 
do
    line_size=`echo $line | sed -n "s/^.* \([0-9]*\)$/\1/p"`
    original_size=$(( original_size + line_size ))
done<originals

printf "Total size ORIGINALS: ${RED}$original_size${NC} \n"

optimized_size=0

while read line 
do
    line_size=`echo $line | sed -n "s/^.* \([0-9]*\)$/\1/p"`
    optimized_size=$(( optimized_size + line_size ))
done<optimized

printf "Total size OPTIMIZED: ${YELLOW}${optimized_size}${NC}\n"

printf "New Location size is: ${GREEN}$(du -sh "$location")${NC}\n"

printf "${GREEN}You saved $(( original_size - optimized_size )) bytes${NC}\n"

Feel yourself free to use or change the code as you wish.

After the images are converted to webp and are present in wp/wp-content/uploads folder its time to work on DB.

# 3. Check DB->wp_posts, where your images are located as entities. They are still with their old extension in this table.

Ex:

ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count

'56', '1', '2023-10-30 08:33:53', '2023-10-30 08:33:53', '', 'Image1.jpg', '', 'inherit', 'open', 'closed', '', 'image1-jpg', '', '', '2023-10-30 08:33:53', '2023-10-30 08:33:53', '', '26', 'https://example.com/wp-content/uploads/2023/10/Image1.jpg', '0', 'attachment', 'image/jpeg', '0'

# 4. Update all image entities:

We will have to update the following columns: post_title, post_name, guid, post_mime_type and we want them as:

ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count

'56', '1', '2023-10-30 08:33:53', '2023-10-30 08:33:53', '', 'Image1.webp', '', 'inherit', 'open', 'closed', '', 'image1-webp', '', '', '2023-10-30 08:33:53', '2023-10-30 08:33:53', '', '26', 'https://example.com/wp-content/uploads/2023/10/Image1.webp', '0', 'attachment', 'image/webp', '0'

The DB query is fairly simple and ugly, but will do the job.

Example Sql query for jpeg to webp(run similar queries for png to webp and gif to webp):

UPDATE wp_posts
SET 
	post_title = concat(left(post_title,length(post_title) -3), 'webp')
	post_name = concat(left(post_name,length(post_name) -3), 'webp')
	guid = concat(left(guid,length(guid) -3), 'webp')
	post_mime_type = REPLACE(post_mime_type, 'jpeg', 'webp')
WHERE
post_parent > 0 AND
post_type = 'attachment' AND
post_title LIKE '%jpg' AND
post_name LIKE '%jpg' AND
guid LIKE '%jpg' AND
post_mime_type = 'image/jpeg';

# 5. Also, update wp_postmeta

Each attachment row in wp_posts, have association in wp_postmeta, using wp_postmeta->post_id column. You can query postmeta, using post_id:

SELECT * FROM wp_postmeta WHERE post_id = 56

You will likely have 3 rows for every attachment.

meta_key
_wp_attached_file
_wp_attachment_metadata
_wp_attachment_source

We need to change those too.

# 5.1 _wp_attached_file and _wc_attachment_source

_wp_attached_file becomes:

2023/10/Image1.webp

_wc_attachment_source becomes:
https://example.com/wp-content/uploads/2023/10/Image1.webp

# 5.2 _wp_attachment_metadata

_wp_attachment_metadata is a PHP serialized array. We cannot just replace all "jpg" => "webp" and all "image/jpeg" => "image/webp". We will have to also change the length of the new strings. Example:

s:18:"2023/10/Image1.jpg"

means that this value is 18 characters long. And if we change this link to webp, it must be:

s:19:"2023/10/Image1.webp"

Best way is somehow to unserialize the array, change the values and serialize again. Then, DB save.

For this purpose I used this:

https://github.com/steelbrain/php-serialize#readme

So, the procedure in my head is (i will use Node.js)

-> Connect to DB

-> Get all attachment id's from wp_posts (``` WHERE post_type = 'attachment' ```)

---> For each id

-----> Get wp_postmeta rows (``` WHERE post_id = ID ```)

-----> Update _wp_attached_file (replace jpg to webp)

-----> Update _wc_attachment_source (replace jpg to webp)

-----> Use php-serialize package to convert _wp_attachment_metadata to JSON object

-----> JSON.serialize the result from the above step

-----> replace all jpg and jpeg to webp

-----> Use php-serialize package .unserialize the replaced string

-----> Save to DB

One more time:

This method is for research and educational purposes.

Its prone to error and not suitable for production websites.