Contents
Overview
This tutorial explains, step by step and in full detail, how to build a favorites (bookmark / wishlist) system for WordPress that works for guest visitors using browser cookies. The system will:
- Let guests (not signed in) add/remove posts to a favorites list.
- Store favorites in a cookie on the client (so it persists across pages and browser restarts until the cookie expires).
- Render favorite state server-side (so buttons show the correct state before JavaScript runs).
- Provide AJAX endpoints to do actions if needed and to optionally merge guest favorites into a user account on login.
- Cover security, cookie size limits, encoding formats, synchronization, server-side rendering, and sample code you can drop into your theme or plugin.
Prerequisites
- WordPress (any modern version).
- Basic knowledge of PHP, JavaScript, and WordPress hooks (enqueueing scripts, AJAX actions, template functions).
- Ability to add code to your theme functions.php or preferably a custom plugin.
High-level design
We will store an array of favorited post IDs in a cookie. The cookie will only contain the post IDs (as a compact string) to reduce size. JavaScript will update the cookie when the user toggles favorites. The server will read the cookie via _COOKIE and output the correct button state on page render. We will also add optional AJAX endpoints and a login hook to merge guest favorites into a user meta favorites list.
Important considerations
- Cookie size limit: Cookies are typically limited to about 4 KB per cookie. Store only IDs, not whole objects. If users favorite many posts, consider a fallback or limit the number of stored IDs.
- Cookie security: Cookies set by JavaScript cannot be HttpOnly. Do not store sensitive data. Ensure server-side sanitization of any incoming cookie data.
- Cookie encoding: Use a compact format, e.g. pipe-separated or comma-separated list of numeric IDs, and sanitize them as integers on the server.
- SameSite and Secure flags: Set SameSite and Secure attributes appropriately.
- Nonces CSRF: For AJAX endpoints you should use nonces to mitigate CSRF even for guests (you can generate a nonce on page render and check it in requests).
Cookie format and usage recommendations
- Cookie name: Favorites cookie, e.g. my_favorites (prefix with theme/plugin slug to avoid collisions).
- Value format: Compact string of integers separated by pipe , example: 124598 (no whitespace).
- Max items: Estimate how many IDs fit under ~4KB. Each ID can be up to several digits allow maybe 300-500 IDs in the best case but real-world safe limit is lower. Consider enforcing a configured max items (e.g., 200).
- Expiry: Choose expiry days (e.g., 365 days). Use explicit expires or max-age.
- Path: Use / so it is available site-wide.
- Secure SameSite: If your site uses HTTPS set Secure, and set SameSite=Lax or Strict per needs.
Server-side rendering (PHP)
To ensure the favorite button shows the right state before JavaScript runs, read the cookie from _COOKIE on the server and output an is-favorited class when rendering templates. Well provide helper functions that you can use in theme templates or use a shortcode to render a favorites button or list.
Core PHP helper functions
Save this code in a plugin or functions.php. It defines cookie name, parsing, rendering helpers, and a shortcode for a favorites button and a favorites list.
0 ) { ids[] = id } } ids = array_values(array_unique(ids)) if ( count(ids) > MY_FAV_MAX_ITEMS ) { ids = array_slice(ids, 0, MY_FAV_MAX_ITEMS) } return ids } / Check if a post is favorited according to cookie. @param int post_id @return bool / function my_fav_is_favorited_cookie(post_id) { ids = my_fav_get_from_cookie() return in_array((int)post_id, ids, true) } / Output a favorites button (use in template). It prints a button element with data-post-id and proper class. / function my_fav_render_button(post_id = null) { if ( ! post_id ) { post_id = get_the_ID() } is = my_fav_is_favorited_cookie(post_id) class = is ? my-fav-btn is-favorited : my-fav-btn // Provide attributes for JS identification (data-post-id) echo } / Shortcode [my_favorites_list] to show a list of favorited posts (guest cookie-based). / function my_fav_shortcode_list(atts) { ids = my_fav_get_from_cookie() if ( empty(ids) ) { returnNo favorites yet.
} // Keep order as stored query = new WP_Query(array( post_type => post, // adjust if you store other post types post__in => ids, orderby => post__in, posts_per_page => -1, )) ob_start() echo ltul class=my-fav-listgt while (query->have_posts()) { query->the_post() echo ltligtlta href= . esc_url(get_permalink()) . gt . esc_html(get_the_title()) . lt/agtlt/ligt } echo lt/ulgt wp_reset_postdata() return ob_get_clean() } add_shortcode(my_favorites_list, my_fav_shortcode_list) ?>
Notes about server-side code
- Use my_fav_render_button() in your loop or templates where you want the favorites button to appear.
- Always sanitize cookie contents. We only use positive integers for IDs.
- Server-side rendering ensures the correct initial UI state and is important for SEO or users who have JS disabled.
We provide a minimal, robust JavaScript snippet that reads and writes the cookie, toggles ui, and can optionally call an AJAX endpoint if you want server-side processing. All cookie operations are done on the client.
/ my-favorites.js Minimal vanilla JS for favorites using cookies. - Reads cookie MY_FAV_COOKIE - Uses pipe separated format - Updates cookie with expiration days / (function () { var COOKIE_NAME = my_favorites var COOKIE_EXPIRE_DAYS = 365 var MAX_ITEMS = 300 // keep in sync with PHP function readCookie() { var nameEQ = COOKIE_NAME = var ca = document.cookie.split() for (var i = 0 i < ca.length i ) { var c = ca[i].trim() if (c.indexOf(nameEQ) === 0) { var v = decodeURIComponent(c.substring(nameEQ.length)) if (!v) return [] // support both pipe and comma var parts = v.split(/[,] /) var ids = [] for (var j = 0 j < parts.length j ) { var p = parts[j].trim() if (p === ) continue var id = parseInt(p, 10) if (!isNaN(id) id > 0) { ids.push(id) } } // unique var uniq = Array.from(new Set(ids)) if (uniq.length > MAX_ITEMS) { uniq = uniq.slice(0, MAX_ITEMS) } return uniq } } return [] } function writeCookie(ids) { // ensure only ints and unique var clean = ids.map(function (n) { return parseInt(n, 10) }) .filter(function (n) { return !isNaN(n) n > 0 }) clean = Array.from(new Set(clean)) if (clean.length > MAX_ITEMS) { clean = clean.slice(0, MAX_ITEMS) } var value = clean.join() var expires = if (COOKIE_EXPIRE_DAYS) { var date = new Date() date.setTime(date.getTime() (COOKIE_EXPIRE_DAYS 24 60 60 1000)) expires = expires= date.toUTCString() } var secure = (location.protocol === https:) ? Secure : var sameSite = SameSite=Lax document.cookie = COOKIE_NAME = encodeURIComponent(value) path=/ expires secure sameSite } function toggleFavorite(postId) { var ids = readCookie() var idx = ids.indexOf(postId) var added = false if (idx === -1) { ids.push(postId) added = true } else { ids.splice(idx, 1) added = false } writeCookie(ids) return added } function updateButtonState(btn, isFavorited) { if (isFavorited) { btn.classList.add(is-favorited) btn.setAttribute(aria-pressed, true) var icon = btn.querySelector(.fav-icon) if (icon) icon.textContent = ♥ var label = btn.querySelector(.fav-label) if (label) label.textContent = Favorited } else { btn.classList.remove(is-favorited) btn.setAttribute(aria-pressed, false) var icon2 = btn.querySelector(.fav-icon) if (icon2) icon2.textContent = ♡ var label2 = btn.querySelector(.fav-label) if (label2) label2.textContent = Add to favorites } } function attachButtons() { var buttons = document.querySelectorAll(.my-fav-btn[data-post-id]) var current = readCookie() buttons.forEach(function (btn) { var pid = parseInt(btn.getAttribute(data-post-id), 10) if (isNaN(pid) pid <= 0) return var isFav = current.indexOf(pid) !== -1 updateButtonState(btn, isFav) // prevent adding multiple listeners btn.removeEventListener(click, btn._myFavHandler function () {}) var handler = function (e) { e.preventDefault() var added = toggleFavorite(pid) updateButtonState(btn, added) // Optional: fire a custom event for other scripts to react to var event = new CustomEvent(myFavToggled, { detail: { postId: pid, added: added } }) document.dispatchEvent(event) // Optional: call AJAX to let the server know (e.g. sync or tracking) // sendAjaxUpdate(pid, added) } btn._myFavHandler = handler btn.addEventListener(click, handler) }) } // Optional AJAX function template (requires localized ajaxUrl nonce) function sendAjaxUpdate(postId, added) { if (typeof myFavAjax === undefined) return var data = new FormData() data.append(action, my_fav_toggle) data.append(post_id, postId) data.append(added, added ? 1 : 0) if (myFavAjax.nonce) data.append(nonce, myFavAjax.nonce) fetch(myFavAjax.ajax_url, { method: POST, credentials: same-origin, body: data }).then(function (res) { return res.json() }) .then(function (json) { // handle response if needed }).catch(function (err) { console.error(Fav AJAX error, err) }) } // Initialize on DOM ready if (document.readyState === loading) { document.addEventListener(DOMContentLoaded, attachButtons) } else { attachButtons() } // Public for debugging window.myFav = { readCookie: readCookie, writeCookie: writeCookie, toggleFavorite: toggleFavorite } })()
How to enqueue the script in WordPress
Enqueue the JS file and localize any variables (cookie name, nonce, ajax URL). Below is example code.
admin_url(admin-ajax.php), nonce => nonce, cookie_name => MY_FAV_COOKIE, )) } add_action(wp_enqueue_scripts, my_fav_enqueue_scripts) ?>
AJAX endpoint (optional)
You can optionally provide a server-side AJAX action to receive favorite add/remove notifications, or to support server-side features. For guests, register the nopriv action. Use check_ajax_referer to validate the nonce.
Invalid post_id), 400) } // For guests, you might simply echo OK since cookie is client-side. // For logged-in users you might also store a copy in user meta: if ( is_user_logged_in() ) { user_id = get_current_user_id() meta_key = my_favorites_user favorites = get_user_meta(user_id, meta_key, true) if ( !is_array(favorites) ) favorites = array() if (added) { if ( !in_array(post_id, favorites, true) ) { favorites[] = post_id } } else { favorites = array_filter(favorites, function(v) use (post_id) { return intval(v) !== intval(post_id) }) } update_user_meta(user_id, meta_key, array_values(favorites)) } wp_send_json_success(array(post_id => post_id, added => added)) } add_action(wp_ajax_my_fav_toggle, my_fav_ajax_toggle) add_action(wp_ajax_nopriv_my_fav_toggle, my_fav_ajax_toggle) ?>
Sync guest favorites to a user on login
When a guest logs in, you may want to merge cookie favorites into the users account favorites and then clear the cookie. Hook into wp_login.
ID, meta_key, true) if ( !is_array(saved) ) saved = array() // Merge unique merged = array_values(array_unique(array_merge(saved, cookie_ids))) if ( count(merged) > MY_FAV_MAX_ITEMS ) { merged = array_slice(merged, 0, MY_FAV_MAX_ITEMS) } update_user_meta(user->ID, meta_key, merged) // Clear cookie by setting it to expire in the past (client-side cookie) // Because PHP setcookie must run before output, use setcookie only if headers not sent. if ( ! headers_sent() ) { setcookie(MY_FAV_COOKIE, , time() - 3600, /) } else { // As a fallback, print an inline script to remove the cookie after login redirect. add_action(wp_footer, function() { echo ltscriptgtdocument.cookie = . MY_FAV_COOKIE . = path=/ expires=Thu, 01 Jan 1970 00:00:00 GMTlt/scriptgt }, 9999) } } add_action(wp_login, my_fav_merge_on_login, 10, 2) ?>
Shortcode to list favorites
We already showed [my_favorites_list]. Use it on a page to allow users to view their guest favorites. The server reads the cookie and renders the list.
Styling (basic)
You can style the button using CSS. Keep styles accessible (use aria-pressed). Below is an example.
/ my-favorites.css / .my-fav-btn { display: inline-flex align-items: center gap: 0.5rem padding: 0.35rem 0.6rem border: 1px solid #ddd background: #fff cursor: pointer border-radius: 4px } .my-fav-btn .fav-icon { font-size: 1.1rem color: #bdbdbd } .my-fav-btn.is-favorited { border-color: #ff6b6b background: #fff5f6 } .my-fav-btn.is-favorited .fav-icon { color: #ff4b4b }
Template placement
Insert the button where appropriate, e.g. in single.php inside the loop:
Edge cases troubleshooting
-
Cookies blocked or cleared:
If the visitor blocks cookies or clears them, favorites are lost. Document this limitation in UI if needed.
-
Cookie size overflow:
Enforce a max number of items. When client tries to add beyond the limit, reject and optionally show a message.
-
HTTP vs HTTPS:
If your site runs both protocols, be careful with Secure flag. Prefer setting Secure only when using HTTPS.
-
Multiple devices:
Cookies are device/browser specific. To have favorites across devices, require login and sync to user meta.
-
Nonces for guests:
wp_create_nonce outputs a nonce value you can use for guest AJAX security check_ajax_referer will validate it. This does not prevent all attacks but raises the bar.
-
Server-side renders different from JS:
Ensure PHP and JS use the same parsing rules for cookie values (both use integer IDs and the same separator).
Testing checklist
- Verify that favorites are added/removed in the cookie (check Application → Cookies in browser devtools).
- Reload pages and verify server-rendered buttons display favorited state based on cookie.
- Log in as a guest then register/login to see if merging works (cookie merges to user meta).
- Test with many items near the cookie size limit to see behavior.
- Test on both HTTP and HTTPS and with SameSite behavior for cross-site navigation if relevant.
Advanced ideas and extensions
- LocalStorage fallback: If you need more space, you can store a larger favorites list in localStorage, and still keep a small cookie for server-side rendering. The cookie could mirror only the first N IDs for server rendering.
- Compression: Store IDs in a compact base36 format or use a short hash map, but add complexity. Only recommended if you must store many items.
- Server-side API: Create a REST API route to manage favorites for guests, the API can rely on cookie values and merge behavior for logged-in users.
- Pagination lazy loading: If listing favorites from cookie results in many entries, paginate or limit initial load.
- Analytics: Track favorite events via your analytics system by firing events in the JS toggle handler.
Security sanitization summary
- Never trust cookie input. Always cast to int and validate post IDs on server-side.
- Escape any output used in HTML attributes (use esc_attr/esc_html in WordPress).
- Do not store any secret or PII in cookies that are accessible to JavaScript.
- Use nonces for AJAX endpoints use HTTPS and Secure cookie flag if site uses HTTPS.
Full-file placement suggestions
- Place PHP helper functions in a simple plugin file or in functions.php (prefer plugin for portability).
- Enqueue the JS and CSS through wp_enqueue_scripts include the JS file path that matches your theme/plugin layout.
- Place markup in templates by calling my_fav_render_button() or use the provided header/footer hooks.
Conclusion
This tutorial provided a complete cookie-based favorites approach for guest users: server-side rendering helpers, a compact and safe cookie format, JavaScript for toggling and maintaining the cookie, optional AJAX server endpoints, merging on login, and a shortcode to list favorites. Follow the sanitation, cookie size, and security notes to ensure reliable behavior across browsers and devices.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |