HUSKY aka WOOF
Permite a los compradores acotar un catálogo grande por categoría, atributo, precio, stock y texto, con páginas de filtro que Google puede indexar.
Llevo un producto desde la primera especificación hasta la cola de soporte años después. 12 plugins en wordpress.org, un SaaS que convierte una tienda WooCommerce en un bot de Telegram, una extensión de Chrome y apps de Android.
const realmag777 = { role: ["Senior WordPress & WooCommerce Developer", "Plugins & Products Architect"], inIT: 2006, ownProductsSince: 2012, // first plugin on wordpress.org activeInstalls: 167_000, // sum of wordpress.org counters stack: ["PHP", "WordPress", "WooCommerce", "MySQL", "JavaScript", "Node.js", "HTML5", "CSS3", "Docker", "MCP"],}; export default realmag777;
Proyectos
Cada página de proyecto enumera los problemas resueltos dentro del producto, agrupados por área. Los recuentos de instalaciones y las valoraciones proceden de wordpress.org, donde puedes leer lo que escriben los usuarios: Reseñas de HUSKY, Reseñas de FOX, Reseñas de BEAR.
Permite a los compradores acotar un catálogo grande por categoría, atributo, precio, stock y texto, con páginas de filtro que Google puede indexar.
Muestra los precios en la moneda del propio visitante y puede cobrar en ella, para que los clientes del extranjero sepan lo que pagan.
Permite al personal editar miles de productos como en una hoja de cálculo, con cada cambio reversible, y permite que los asistentes de IA lo hagan con seguridad.
Muestra el catálogo como tablas de productos ordenables y filtrables, donde los clientes añaden artículos al carrito directamente desde cada fila.
Permite al personal del sitio editar, filtrar, exportar y revertir miles de entradas, páginas y contenido personalizado en una sola pantalla similar a una hoja de cálculo.
Los visitantes buscan anuncios, entradas o productos por varias propiedades a la vez, como precio, año y categoría.
Muestra los precios escritos en el contenido del sitio en la moneda de cada visitante, sin necesidad de una tienda WooCommerce.
Muestra entradas o productos como tablas ordenables y con búsqueda, que los visitantes pueden filtrar y explorar en cualquier dispositivo.
Adjunta un conjunto de imágenes a cualquier entrada y muéstralas como galerías o sliders en cualquier parte del sitio.
Desplegables paso a paso, como marca, modelo y generación, guían a los visitantes por un catálogo grande hasta la página o el producto adecuados.
Una tienda WooCommerce funcionando como bot de Telegram: los clientes exploraban, llenaban el carrito y pagaban sin salir del chat.
Una extensión de Chrome que escribe lo que dices en cualquier campo de texto de cualquier sitio, con limpieza opcional mediante IA.
Un cronómetro de Android sin conexión con tiempos de vuelta, que sigue contando correctamente incluso con la app en segundo plano.
Un lector que muestra un texto palabra a palabra al ritmo que elijas y recuerda dónde lo dejaste.
Empresas como burbujas en un gráfico de cuadrantes que se mueven cuando cambian los pesos del modelo.
Recogida de eventos, agregación e informes con gráficos dentro del administrador de WordPress.
Un comprador se suscribe a una consulta de filtro y recibe un correo cuando aparecen nuevos productos coincidentes.
Un PDF con estilo con todos los coches del conjunto de resultados actual de un catálogo de vehículos.
Qué se resolvió · 14 productos
Elige un producto. Cada pestaña es un área de su trabajo y empieza con lo que aporta al propietario, en palabras sencillas. Debajo está la parte de ingeniería para tu equipo técnico: el problema y cómo se resolvió, extraído del código fuente. Abre una fila para ver los detalles.
qué se resolvió · elige un proyecto
Qué aportaLas páginas filtradas obtienen direcciones limpias y estables que los buscadores pueden indexar. El propietario decide qué páginas filtradas aparecen en Google y con qué título.
Interceptar el análisis de la petición, quitar el segmento del filtro, dejar que WordPress resuelva el resto y reconstruir el estado del filtro a partir del segmento con una codificación fija y reglas de índice por profundidad.
Un enlace de filtro es la URL del archivo más el slug del filtro (swoof por defecto) y un segmento por cada filtro: /product-category/shoes/swoof/color-red-and-blue/size-xl/. El plugin no registra reglas de reescritura. url_process() se ejecuta en do_parse_request en el front end y termina de inmediato salvo que get_search_request() encuentre /<slug>/ en la ruta. Toma el segmento del filtro hasta /page/, ? o #, lo elimina y compara el resto (/product-category/shoes/ o /shop/page/2) con $wp_rewrite->wp_rewrite_rules() usando código copiado de WP::parse_request(): reglas de página detalladas (verbose), variables de consulta públicas y privadas, tipos de contenido visibles, taxonomías no consultables, wp_resolve_numeric_slug_conflicts() y el filtro request. A continuación llama a query_posts(), handle_404() y register_globals() (la acción parse_request antes de WordPress 6.0) y devuelve false, de modo que el núcleo no vuelve a analizar la URL.
parse_url_query() divide el segmento por '/' y compara cada parte con el mapa de nombres de get_all_items(): slugs de atributos sin pa_, slugs de taxonomías traducidos por WPML, price y las palabras fijas instock, onsale, featured, backorder_not_in. -and- y -or- pasan a ser comas, -to- un separador de rango y + un espacio; price-10-to-50 pasa a ser min_price y max_price. add_request() combina el resultado mediante el filtro woof_get_request_data, de modo que get_request_data() devuelve el mismo array que para una cadena de consulta ?swoof=1&pa_color=red. En la consulta principal, parse_query() establece entonces post_type product, marca la consulta como archivo de tipo de contenido y añade las meta queries, y tax_query() en woocommerce_product_query_tax_query sustituye las entradas de WooCommerce para las mismas taxonomías por la tax query del plugin.
Como el resto se enruta con las reglas existentes, la base de la tienda, la base de categorías de producto y la paginación de archivos siguen funcionando sin reglas nuevas: /page/N/ se corta antes de localizar el segmento del filtro y permanece en la ruta enrutada. parse_query() desactiva is_tax en las páginas filtradas, por lo que la categoría actual se recupera del último segmento de la URL limpia (get_term_from_cleared_url()). Cuando la página de la tienda es también la página de inicio, la corrección de redirect_canonical que se describe más abajo evita que WooCommerce redirija la URL del filtro a /.
Para una URL que coincide con una regla SEO, meta_head() imprime la canónica: la URL del filtro sin la cadena de consulta si el estado es indexable, con las canónicas de Yoast y SEOPress desactivadas; en caso contrario, la URL cortada en el slug del filtro, que es el archivo simple. Con Yoast SEO, sitemap_index() en wpseo_sitemap_index añade las URL que un administrador enumera en un textarea de ajustes, una por línea; la versión premium las escapa con esc_url_raw() y esc_xml().
woof_get_submit_link() en url_parser.js ordena los filtros activos por clave y escribe cada uno como <name>-<values>, con ',' como -and-, un separador de rango como -to- y los espacios como +. El precio pasa a ser price-<min>-to-<max>; en stock, en oferta, destacado y backorder pasan a ser palabras fijas. parse_url_query() invierte la correspondencia.
Gracias a la ordenación, elegir primero el color y después la talla da la misma ruta que hacerlo al revés. Los valores dentro de un mismo filtro se escriben en el orden en que se seleccionaron.
Los nombres de URL omiten el prefijo pa_, así que un atributo y una clave meta pueden producir el mismo nombre, y una sola entrada del mapa ocultaría a la otra. resolve_seo_key_conflict() asigna el nombre corto a la taxonomía registrada con independencia del orden de los filtros en los ajustes, de modo que las URL existentes siguen siendo válidas, y deriva un nombre para el otro filtro a partir de su clave real, añadiendo un sufijo numérico si hace falta. Los slugs de taxonomías traducidos por WPML se resuelven antes de este paso.
get_request_data() se llama desde cada consulta de recuento, y con las URL SEO cada llamada pasa por woof_get_request_data hasta parse_url_query(). Con Dynamic Recount y Hide Empty (premium), una sola página puede hacer más de mil de esas llamadas, por lo que el resultado del análisis se guarda en caché por URL.
La caché se activa solo después de wp_loaded, de modo que una llamada hecha antes de que existan las taxonomías y los ajustes no pueda fijar un mapa de nombres incompleto durante el resto de la petición.
WOOF_SEO cuenta los filtros activos. Por encima de index_deep (2 por defecto), la página recibe el ajuste 'Disable page indexing' (sí por defecto, es decir, noindex,nofollow mediante wp_robots) y no se comprueban las reglas. En ese valor o por debajo, una regla coincidente hace que la página sea indexable. Las reglas son patrones de URL con marcadores {any} compilados a expresiones regulares, cada una con título, meta descripción, H1 y texto SEO que pueden incluir los nombres de los términos seleccionados, y se guardan por idioma. Las etiquetas canónicas solo se imprimen para las URL que coinciden con una regla.
En la versión premium, preg_match se ejecuta bajo un manejador de errores temporal, de modo que una regla cuya URL no es una expresión válida se omite y se lista en un aviso de administración, y una regla vacía también se omite. La edición gratuita usa solo las dos primeras reglas.
Cuando la página de la tienda es también la página de inicio estática, WooCommerce 11.0.0 la establece como objeto consultado en los archivos de productos. redirect_canonical() trata entonces /swoof/... como una copia no canónica de la página de inicio y redirige a /, perdiendo todos los filtros. fix_front_page_shop_canonical() devuelve false solo cuando la página de inicio es la página de la tienda, la ruta contiene el slug del filtro y el destino de la redirección es la URL de inicio, de modo que la normalización de barras y slugs sigue funcionando en el resto de casos.
Qué aportaCada opción del filtro muestra cuántos productos devolverá, así que los compradores evitan callejones sin salida. Los recuentos se guardan en caché, por lo que la tienda sigue siendo rápida en catálogos grandes.
Los recuentos salen de una consulta reducida que solo devuelve IDs y reproduce la semántica del filtro. Los resultados se guardan en caché por petición, en una tabla propia y en transients, y el formulario puede diferirse hasta que el visitante lo pida.
dynamic_count() reconstruye la tax_query para cada término. Los términos ya seleccionados no reciben recuento. En radio y select (simple), el término sustituye al valor seleccionado de su taxonomía; en checkbox y multiselect (múltiple), se añade a él. Los términos de la página de categoría actual y de los de la taxonomía indicada en el atributo taxonomies del shortcode se combinan siempre con AND, de modo que los recuentos en una página de categoría se mantienen dentro de esa categoría.
La lógica por taxonomía (OR, AND, NOT IN) definida en los ajustes, los rangos de precio y valoración, las condiciones meta de las extensiones, el idioma de WPML y la visibilidad del catálogo se aplican igual que en la consulta de productos. Las extensiones declaran su comportamiento de recuento mediante html_type_dynamic_recount_behavior, de modo que los tipos color, imagen y etiqueta reutilizan este código.
Ocultar las opciones sin resultados se basa en estos recuentos: el término se cuenta y se omite cuando el recuento es 0, por lo que no puede ser más barato que mostrar los recuentos. Este interruptor es exclusivo de la edición premium; la edición gratuita lo fuerza a desactivado en las vistas.
WP_QueryWOOFCounterIn extiende WP_Query y sobrescribe set_found_posts(), setup_postdata(), the_post() y have_posts(). La consulta de recuento se ejecuta con fields=ids y nopaging, de modo que MySQL devuelve una lista de IDs, WordPress no construye objetos de entrada y no lanza una segunda consulta FOUND_ROWS. El recuento es la longitud de la lista.
Los filtros posts_clauses de popularidad y valoración de WooCommerce se eliminan antes de la consulta de recuento, porque el orden no importa para un recuento y esos filtros añaden joins al SQL.
Con 'cache count data' activado, WP_QueryWoofCounter calcula el hash del JSON de los argumentos finales de la consulta, que ya incluyen las partes de idioma y visibilidad, y busca el hash en {prefix}woof_query_cache, una tabla de dos columnas con el hash como clave primaria. Las escrituras usan INSERT ... ON DUPLICATE KEY UPDATE, de modo que dos visualizaciones de página que escriben la misma clave a la vez no fallan. Un sitio con caché de objetos persistente puede mover los recuentos a wp_cache_* mediante el filtro woof_use_wp_cache.
No hay una forma barata de averiguar qué recuentos en caché afecta un producto nuevo o editado, así que un evento programado vacía (TRUNCATE) toda la tabla en el periodo configurado. Si el usuario de MySQL carece del privilegio CREATE, la página de ajustes imprime el SQL para ejecutarlo a mano y mantiene la opción desactivada.
get_filtered_price() construye un único SELECT MIN/MAX sobre _price unido a la tax query y la meta query actuales. El mismo rango se pedía varias veces por página (mín. y máx. para cada filtro de precio), así que el resultado se memoriza en un array estático indexado por el md5 del SQL. Con 'Optimize price filter' activado, los resultados también van a un transient durante un día, y cualquier woocommerce_update_product lo elimina. El SQL pasa por woof_get_filtered_price_query, de modo que un sitio puede añadir sugerencias de índice (index hints).
WOOF_HELPER::get_terms() carga todos los términos de una taxonomía con get_categories() y arma el árbol padre-hijo en PHP. El resultado se memoriza por petición y, con 'Cache terms' activado, se guarda en un transient cuya clave incluye la taxonomía, hide_empty, los indicadores de profundidad y el idioma actual de WPML o Polylang, de modo que cada idioma tiene su propio árbol en caché.
La limpieza recorre wp_options en busca de _transient_woof_terms_cache_% y elimina cada transient, ya que WordPress no tiene borrado por prefijo para los transients.
Con start_filtering_btn=1 el servidor solo devuelve un botón, de modo que no se ejecuta ninguna consulta de términos al cargar la página. Al hacer clic, front.js envía woof_draw_products con el shortcode de productos establecido en 'woof_nothing': el servidor renderiza el formulario del filtro y ningún producto, se sustituye la zona de redibujado y se reinicializan los widgets. El botón se omite cuando ya hay una búsqueda activa, de modo que una página filtrada sigue mostrando su selección.
Ocultar el formulario con CSS o reglas display no ahorra nada, porque los recuentos se calculan en el servidor antes de enviar el HTML. En los hilos sobre rendimiento del foro de soporte hubo que insistir con frecuencia en este punto.
Qué aportaEl filtrado sigue funcionando con el tema propio de la tienda, las páginas de categoría, los productos variables, los impuestos y las divisas, de modo que los compradores nunca ven artículos en tallas agotadas.
Engancharse tarde a parse_query y a la consulta de productos de WooCommerce, restaurar el estado de archivo que esperan las plantillas y tratar de forma explícita cada diferencia del modelo de datos por versión o por tabla.
Cuando el slug de búsqueda está en la petición, parse_query() establece post_type=product e is_post_type_archive, y desactiva is_tax, is_tag, is_home, is_single e is_search. WooCommerce y el tema renderizan entonces el bucle de la tienda para cualquier URL filtrada. Las extensiones añaden sus condiciones meta mediante assemble_query_params(), y los operadores por taxonomía proceden del filtro woof_main_query_tax_relations.
Los mismos hooks no deben tocar las peticiones REST ni de Store API. Los manejadores terminan pronto en las URL /wp-json/ y parse_query se desengancha en rest_api_init, de modo que los temas de bloques y los clientes headless obtienen los resultados propios de WooCommerce.
Una URL de filtro ya no es el archivo del término, así que get_queried_object() deja de devolver la categoría. Cuando se renderiza el archivo, set_really_current_term() guarda el término mediante WOOF_STORAGE, que el plugin ejecuta en modo transient: un transient por visitante, indexado por un hash de la IP del visitante y conservado durante un día. El JS también añade really_curr_tax=<term_id>-<taxonomy> al enlace del filtro, y get_really_current_term() recurre a él cuando no hay nada guardado. El término se combina entonces con AND en la consulta de productos, en las consultas de recuento y en la búsqueda de texto. Las peticiones AJAX leen el término guardado pero nunca lo sobrescriben.
WooCommerce 3.0 movió las banderas de visibilidad, destacado y sin stock de postmeta a la taxonomía product_visibility. product_visibility_not_in() convierte las banderas necesarias en cláusulas term_taxonomy_id NOT IN. generate_visibility_keys() añade outofstock cuando la tienda oculta los artículos sin stock; cuando la opción 'listen catalog visibility' está activada, añade además exclude-from-search siempre y exclude-from-catalog fuera de una búsqueda. Las tiendas más antiguas reciben en su lugar una condición meta _visibility.
Las mismas reglas se aplican a la consulta principal, la consulta del shortcode, las consultas de recuento, la búsqueda de texto y los datos del modo turbo. Si una de ellas difiere, los recuentos y los resultados no coinciden.
Un producto variable cuenta como en stock si alguna de sus variaciones lo está, así que una consulta simple de 'en stock + talla M' devuelve una camisa cuya variación M está agotada. Cuando el filtro de en stock se combina con filtros de atributos, posts_where() reúne los términos pa_* seleccionados, incluido el término que se está contando durante el recuento, y ejecuta dos consultas agrupadas sobre wc_product_attributes_lookup: padres con una variación coincidente sin stock y padres con una variación coincidente en stock. La diferencia se añade como ID NOT IN.
Un modo anterior, seleccionable en los ajustes del filtro de en stock, consulta las entradas product_variation por meta de atributo y _stock_status en lugar de la tabla de búsqueda. Es más lento y, por defecto, excluye a todos los padres que tienen una variación coincidente agotada; los padres que también tienen una coincidencia en stock se conservan solo cuando el filtro woof_exclude_existing_variations devuelve true.
_price se almacena en la divisa base de la tienda y, en muchas tiendas, sin impuestos. Si el visitante usa una divisa de WPML (WCML), assemble_price_params() convierte los límites de nuevo a la divisa base. Si los precios se muestran con impuestos pero se almacenan sin ellos, el impuesto incluido de la tarifa estándar se resta de ambos límites. Para el extremo superior del slider, get_max_price() busca el producto con el precio máximo y pide a WooCommerce su precio con impuestos incluidos.
Con FOX (WOOCS), los valores del slider que se muestran se convierten mediante woocs_exchange_value mientras que la consulta mantiene cifras en la divisa base.
woof_draw_products() reconstruye $_GET a partir del enlace que habría abierto el visitante, renderiza [woof_products] y [woof] con ese estado y devuelve como JSON los productos, el formulario y fragmentos adicionales (número de resultados, ordenación, paginación). front.js sustituye cada fragmento, restituye los fragmentos que la respuesta no incluía, reinicializa los widgets y llama a history.pushState para que la URL pueda compartirse. onpopstate recarga la página cuando la cadena de consulta ha cambiado. Una bandera de bloqueo descarta los clics mientras hay una petición en curso.
El bucle de la tienda se envuelve enganchándose a woocommerce_before_shop_loop y woocommerce_after_shop_loop. Los temas que no disparan ambos hooks rompen el modo AJAX, lo que explica toda una familia de hilos de soporte con síntomas distintos y una sola causa.
Qué aportaEn los catálogos grandes, el filtrado se ejecuta en el navegador del comprador en lugar de en el servidor. Los resultados aparecen rápido y el servidor soporta menos carga en las horas de más tráfico.
Precalcular un índice JSON plano de productos y variaciones, cargarlo en el navegador y ejecutar el filtro como SQL sobre él con AlaSQL. El servidor solo renderiza los IDs coincidentes. Incluido en ambas ediciones.
get_data_by_id() escribe el ID, el título, el SKU, el autor, el padre, el estado de stock, los slugs de términos de cada taxonomía filtrable (los atributos de variación para las variaciones), los campos meta configurados y una lista de pares de precio normal/precio de oferta. Un producto variable guarda un par por cada hijo, de modo que el navegador puede calcular su rango de precios con la misma regla que usa WooCommerce: el precio de oferta solo cuenta cuando es menor que el precio normal.
Los precios se guardan con impuestos cuando la tienda muestra precios con impuestos incluidos, y FOX se cambia a la divisa por defecto antes de leerlos, de modo que el índice queda en una sola divisa.
create_data_search_files() recibe un desplazamiento (offset), procesa 10 IDs de productos y variaciones con las reglas de visibilidad aplicadas, los fusiona en el archivo JSON y devuelve el siguiente desplazamiento; el script de administración itera hasta recibir -1. WPML se cambia al idioma por defecto y cada producto se escribe en el archivo de cada idioma mediante su ID de traducción. check_id_file() añade el ID del blog en multisitio para que los sitios no compartan archivo.
El límite por defecto es de 15,000 registros, ajustable con el filtro woof_turbo_mode_products_limit.
PN_WP_CRON_WOOF_TURBO_MODE guarda el estado del evento (lista completa de IDs, posición, próxima ejecución) en una opción y se ejecuta en init. Cada llamada a process() gestiona una porción de 10 IDs, y una limitación de 5 segundos almacenada en woof_cron_limit_do evita que una ráfaga de visualizaciones de página ejecute porciones una tras otra. Las porciones se escriben en data_<id>_temp_<lang>.json; cuando la lista se agota, do_recreate_file() mueve el archivo temporal sobre el activo, y los visitantes conservan el índice anterior hasta que el nuevo está completo.
El coste de este diseño es la obsolescencia: los productos añadidos después de la última reconstrucción son invisibles para el filtro, y la reconstrucción depende del tráfico del sitio igual que WP-Cron.
turbo_mode.js convierte los valores actuales en una cadena WHERE: pertenencia a taxonomías mediante indexOf sobre los arrays de términos, comparaciones meta, LIKE para SKU y título, BETWEEN para sliders y fechas. Funciones personalizadas de AlaSQL calculan el precio mínimo y máximo de un producto a partir de su lista de variaciones y aplican la visibilidad del catálogo. Para los productos variables, el filtro de en stock selecciona primero los padres de variaciones sin stock que coinciden con los demás filtros. El recuento se ejecuta sobre la misma tabla en memoria.
Los IDs resultantes se envían a woof_draw_products como turbo_mode_ids, y el servidor renderiza una consulta post__in en lugar de los joins del filtro.
El JS reconstruye la lógica del filtro a partir de woof_settings, así que necesita en la página la mayor parte de esa opción. get_public_settings() elimina el bloque de estadísticas, que contiene las credenciales de la base de datos MySQL externa, y todas las cron_secret_key, y pasa el resto por un filtro para eliminar más datos. En la versión premium, el endpoint de reconstrucción exige un nonce y manage_woocommerce.
Qué aportaEl propietario ve qué filtran y buscan realmente los compradores, sin ralentizar la tienda ni llenar su base de datos con un registro que crece sin parar.
Escribir una pequeña fila en bruto por petición en una tabla de preparación (staging) y mantener las estadísticas a largo plazo en un servidor MySQL independiente que la extensión requiere.
La extensión de estadísticas se engancha a woof_get_request_data, que se ejecuta muchas veces por página; una bandera estática limita el registro a la primera llamada, y las llamadas AJAX distintas del renderizado de productos se ignoran. Solo se conservan las claves de filtro elegidas en los ajustes, la petición se recorta a request_max_deep claves (5 por defecto) conservando el rango de precio, y se calcula su hash junto con la IP y la fecha.
Solo se inserta una fila si ese hash es nuevo y la IP tiene menos de user_max_requests filas (10 por defecto) en la tabla de staging.
La extensión necesita su propio servidor MySQL, definido en las opciones. init_pdo() se conecta a él con PDO, ya que $wpdb no puede mantener una segunda conexión con otras credenciales. place_statdata_into_db() crea una tabla por mes con el nombre <year>_<month> e inserta las filas con sentencias preparadas.
Los informes consultan las tablas mensuales por rango de fechas, así que la base de datos de la tienda no almacena las estadísticas a largo plazo.
Qué aportaLos compradores encuentran productos por SKU, atributo o palabras en las descripciones de las variaciones. Los clientes registrados pueden guardar una búsqueda y recibir un aviso cuando lleguen nuevos productos que coincidan.
Construir la cláusula WHERE a partir de fragmentos preparados por palabra, con joins añadidos solo cuando hacen falta, resolver las variaciones a sus padres en una consulta aparte, acotar lo que puede pedir el endpoint público y volver a ejecutar las búsquedas guardadas a partir del SQL capturado en el servidor.
create_where_query() divide la frase en palabras. Para cada palabra combina con OR los campos de entrada seleccionados y las claves meta indicadas (SKU es simplemente _sku), más una coincidencia de nombre de término con la frase completa, y los grupos de palabras se combinan con AND. Un '-' inicial convierte una palabra en NOT LIKE unido con AND. Los fragmentos pasan por $wpdb->prepare().
Los joins de términos y de postmeta se añaden en posts_join solo cuando esas opciones están activadas, y posts_groupby colapsa las filas duplicadas que crean los joins.
Las variaciones son entradas independientes, y la consulta del padre no puede ver _variation_description. Cuando la opción está activada, una consulta encuentra las entradas product_variation cuya descripción contiene todas las palabras, una segunda las asigna a post_parent, y la cláusula principal pasa a ser (condiciones de palabras OR ID IN (padres)). La lista de padres puede guardarse en la caché de objetos por lista de palabras.
ajax_search() está abierto a invitados y pagina la consulta en SQL con max_posts como tamaño de página, de modo que solo se carga una página de coincidencias. En la versión premium, el max_posts de la petición se limita entre 1 y el mayor valor entre el ajuste del administrador y un 100 filtrable. Las sugerencias respetan el estado actual del filtro, la página de categoría y la restricción de en oferta.
El filtro de texto construye su propia consulta de búsqueda, así que un plugin de búsqueda independiente no puede decidir el conjunto de resultados en la misma petición; los hilos de soporte citan Ivory Search, Relevanssi y Advanced Woo Search entre esos conflictos. get_all_ids() aplica primero el filtro woof_text_search_ids. Cuando un callback devuelve un array, por ejemplo IDs de SearchWP, ese array es el conjunto de coincidencias y se omite el SQL integrado. Conectar un motor concreto requiere código a medida.
La vista de Products Messenger guarda $wp_query->request, el SQL que WordPress generó para los resultados filtrados, en el almacenamiento por visitante del plugin bajo el ID de usuario. Al suscribirse, lo cual requiere un usuario con sesión iniciada, el servidor lo vuelve a leer de ese almacenamiento en lugar de la petición, elimina LIMIT, comprueba que es un único SELECT sin palabras clave DML ni DDL, lo ejecuta una vez para registrar una línea base de IDs y guarda la suscripción en user meta.
La tarea programada vuelve a ejecutar cada consulta guardada y envía por correo solo los IDs que no están en la línea base, y luego amplía la línea base. Una suscripción termina por número de mensajes o por fecha. El user meta solo se actualiza cuando wp_mail() tiene éxito, de modo que un envío fallido se reintenta en la siguiente ejecución.
Qué aportaLos visitantes anónimos no pueden usar el filtro para ejecutar código ni acceder a datos ocultos, y los ajustes guardados no se pierden ni se sobrescriben al hacer cambios.
Tratar todo valor de la petición como datos: permitir solo shortcodes y plantillas conocidos, sanear los ajustes de forma recursiva según la ruta de la clave, exigir capacidades para todo lo que almacene código y combinar los ajustes en lugar de sobrescribirlos.
[woof_products custom_tpl=...] se devuelve legítimamente en las llamadas AJAX, así que el valor no puede ni aceptarse sin más ni descartarse. sanitize_custom_tpl() conserva solo letras, dígitos, '_', '-' y '/', lo que elimina los puntos y las secuencias de recorrido de directorios. Un nombre renderizado a partir de contenido real de la página se añade a una opción de lista de permitidos (se conservan los últimos 20); durante AJAX solo se aceptan los nombres que ya están en esa lista.
Las rutas de inclusión construidas a partir de nombres de tipos de vista pasan por safe_view_slug(), que rechaza todo lo que no sea un slug simple.
woof_draw_products() recibe de la página el texto de los shortcodes de productos y de formulario para que la salida AJAX coincida con la llamada original. check_shortcode() construye get_shortcode_regex() solo para las etiquetas del plugin ([woof_products] o [woof], más los alias del shortcode de productos del plugin) y conserva la primera coincidencia; todo lo demás de la cadena se descarta antes de do_shortcode().
sanitize_array() recorre el árbol de ajustes, guarda cada valor bajo su clave saneada y clasifica cada campo por nombre o por su ruta en el árbol: el texto de las reglas SEO como HTML mediante wp_kses_post, las descripciones como textarea, las claves usadas en rutas de inclusión como slugs estrictos y todo lo demás como texto plano. Las reglas por ruta son necesarias porque las claves situadas en lo profundo del árbol, como text o description, son demasiado genéricas para enumerarlas globalmente.
El ajuste 'JavaScript after AJAX' solo pueden cambiarlo los usuarios con unfiltered_html.
El renderizado AJAX reconstruye $_GET a partir del enlace de página enviado por el navegador, y la respuesta inyecta woof_redraw_elements en la página como HTML, por lo que un enlace manipulado no debe poder aportar esa clave. En la versión premium, la clave se elimina del enlace analizado. Los fragmentos legítimos (título SEO y H1, número de resultados, ordenación) los añaden filtros del lado del servidor, y cada fragmento pasa por wp_kses antes de devolverse.
La página de ajustes lista solo los términos del idioma actual de WPML o Polylang, así que un guardado envía colores solo para esos términos. woof_save_options() combina las entradas almacenadas de color, color_img e images_term_* por debajo de los valores enviados, de modo que los demás idiomas conservan los suyos.
Un término traducido sin color propio usa el de su original en el idioma por defecto, que encuentra get_original_language_term(). Lee el slug directamente de wp_terms, porque con el ajuste de ID de WPML get_term() podría devolver de nuevo el término traducido.
Qué aportaLos problemas de una tienda en producción se encuentran en su causa real, incluso cuando es el tema, el servidor, una caché u otro plugin.
Reproducir en una instalación limpia o en una copia del sitio, descartar una capa cada vez y corregir donde está la causa: en el plugin para todos, o con un cambio específico en ese sitio.
Incluso un parámetro sin sentido como ?dwdadd=1adw rompía la página, lo que demostró que el fallo estaba en el tratamiento de la cadena de consulta y no en el filtro. La configuración de Nginx insertaba un ? suelto después de php y descartaba todos los parámetros, así que el filtro nunca recibía sus argumentos.
La respuesta llegaba, pero JSON.parse fallaba por un carácter de escape incorrecto, lo que significaba que el CDN Mapping de LiteSpeed estaba reescribiendo URL dentro de ella. Desactivar CDN Mapping restableció el filtrado.
El sitio cargaba jQuery slim, que no tiene módulo AJAX, de modo que la petición de resultados filtrados nunca se enviaba mientras la URL sí cambiaba en el cliente; desactivar todos los demás plugins no cambió nada. La versión slim venía con un núcleo de WordPress modificado por el instalador automático del hosting; restaurar jQuery completo y reinstalar el núcleo lo solucionó.
woof_slider_meta_query_type para poder fijar el cast SQL a DECIMAL por clave metaQuien lo reportó reconstruyó el campo como un campo personalizado normal y el fallo persistió, así que ACF no era la causa; soporte lo reprodujo. La meta query comparaba los valores sin cast a DECIMAL, y el plugin recibió el hook woof_slider_meta_query_type, que recibe $type y $key.
pageshow que restablece el bloqueo de envío y el popup al restaurar desde bfcache, publicado en actualizacionesLas páginas restauradas desde la caché de atrás/adelante mantenían establecido woof_submit_link_locked y el popup de carga abierto. El listener comprueba event.persisted o el tipo de navegación 2 y restablece ambos; el cambio pasó al plugin.
Anillo interior: stack principal · anillo exterior: herramientas y plataformas
Sobre mí · stack
Diseño la arquitectura y el modelo de datos, y después escribo el código, lo publico y lo mantengo. Ninguna capa queda en manos de alguien a quien no pueda controlar.
Trabajo a medida
Trabajo a medida para clientes, normalmente personas que empezaron con uno de mis plugins. Los nombres y los sitios se mantienen privados.
Un gran directorio de proveedores B2B creado sobre WordPress.
El mismo portal de WordPress.
Empezó como trabajo a medida para el propietario de una tienda y después se publicó como la extensión Products Messenger para HUSKY y MDTF.
Un catálogo de vehículos creado con MDTF, complemento a medida para un cliente en Alemania.
Integrado, no añadido
Los propietarios ven las cifras en el mismo administrador donde configuran el plugin.
Cronología
Solo mis propios productos. Las fechas de lanzamiento son las fechas en que cada plugin se añadió a wordpress.org.
Antes de escribir
Describe la tarea en el formulario de abajo o por correo electrónico a realmag777 [at] gmail.com: qué hay que crear o cambiar y para cuándo. Si tienes un sitio de pruebas (staging), menciónalo: ahí se hace y se comprueba el trabajo. Respondo en un plazo de dos días laborables con preguntas o con una estimación de tiempo y coste. El trabajo empieza cuando acordamos el alcance.
Una tarea descrita con claridad recibe un presupuesto cerrado para el alcance acordado. El trabajo abierto, como encontrar la causa de un problema en un sitio en producción o el soporte continuo, se acuerda por horas.
Hora central europea (UTC+1, UTC+2 en verano). Escribo y hablo en inglés, ucraniano y español. La mayor parte del trabajo se hace por correo electrónico y mensajes, así que la diferencia horaria rara vez importa.
Sí. Los nombres y los sitios de los clientes nunca se muestran aquí, y firmo un NDA cuando el proyecto lo requiere.
No. Construyo funcionalidades sobre mis plugins o sobre cualquier otro, trabajo con código escrito por otros desarrolladores y diseño proyectos nuevos desde cero. Con el código de otra persona, primero lo leo y te cuento lo que he encontrado antes de cambiar nada.
Solo. La arquitectura, el código, la publicación y el soporte son cosa mía, así que hablas con la persona que construye el producto y nada se pierde entre personas.
Sí, también bajo la marca de la agencia. Escribe quién eres y qué necesitas.
Según acuerdo. Cuéntame el proyecto y el volumen que esperas, y lo fijamos antes de empezar el trabajo.
En euros.
Un sitio de pruebas (staging), una copia del sitio en producción, con acceso de administrador y a los archivos, además de una copia de la base de datos cuando la tarea trata sobre datos. Si no hay staging, lo preparo yo. Los cambios llegan al sitio en producción solo después de funcionar en staging.
Tuyo, una vez pagado el trabajo.
Los errores del trabajo entregado se corrigen. Los cambios posteriores y el soporte se pueden acordar por separado. Mis propios plugins llevan más de diez años con soporte, así que el mantenimiento a largo plazo es lo habitual, no una excepción.
Contacto
Escribe qué hay que crear o cambiar y para cuándo. Respondo en un plazo de dos días laborables.
realmag777 [at] gmail.com linkedin.com/in/realmag777