Real State Headless
CMS headless inmobiliario en WP puro (sin ACF ni terceros): CPTs, campos dinámicos, API REST con keys/OpenAPI, leads, instalador demo y backup.
by sn4p.dev · github.com/yosnap/real-state-headless · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/yosnap/real-state-headless/archive/refs/heads/main.zipPlugin WordPress "Zero-Dependencies" para CMS inmobiliario headless: sin ACF ni librerías de terceros, PHP 8.0+ orientado a objetos con namespaces RealStateHeadless\*, y admin en Vanilla JS ESM (wp_enqueue_script_module, WordPress 6.5+) sin build step. WordPress actúa como motor de datos headless, exponiendo todo vía una API REST propia (inmo/v1) para consumo desde un frontend externo (Astro, React, etc.).
Requisitos
- WordPress 6.5+ (usa
wp_enqueue_script_module, disponible desde 6.5) - PHP 8.0+
Instalación
- Copiar la carpeta
real-state-headlessawp-content/plugins/. - Activar el plugin desde el panel de administración de WordPress.
- El menú "Real State" agrupa todas las pantallas del plugin: Estado, Inmuebles/Agentes/Reseñas/Leads, Campos personalizados, Instalador Demo, API Keys y Backup.
Arquitectura
real-state-headless/
├── real-state-headless.php # bootstrap + header plugin
├── uninstall.php # borrado condicional (opt-in)
├── src/Core|PostTypes|Taxonomies|Fields|Admin|Agents|Leads|Api|Installer|Backup/
├── assets/admin/{js,css}/ # ES modules nativos, sin bundler
├── templates/ # formulario de leads + panel de agente (frontend)
└── README.md
Uso rápido
- Cargar contenido: creá inmuebles/agentes/reseñas manualmente desde wp-admin, o usá Instalador Demo (menú "Real State") para poblar datos de ejemplo (con imágenes de Unsplash si configurás una API key, o placeholders si no).
- Generar una API key: menú "Real State → API Keys" — elegí nivel
testolivey si la key puede ver ubicación precisa (lat/lng). La key completa se muestra una sola vez; guardala. - Consumir la API: mandá el header
X-API-Keycon cada request ainmo/v1(opcional para los endpoints de solo lectura — sin key, la respuesta omite campos sensibles y aplica un límite de tráfico más bajo). - Recibir consultas: insertá el shortcode
[rsh_lead_form property_id="123"]en cualquier página/entrada para mostrar el formulario de contacto de un inmueble. - Panel de agente: un usuario con el rol "Agente Inmobiliario" puede ver sus inmuebles y leads asignados con el shortcode
[rsh_agent_panel], sin acceder a wp-admin.
Referencia de la API (inmo/v1)
Base URL: https://tu-sitio.com/wp-json/inmo/v1
Autenticación: header X-API-Key: <tu key> (opcional en los endpoints de lectura). Sin key, el tráfico se limita a 60 req/min por IP y se ocultan campos sensibles (rsh_lat/rsh_lng). Con key válida, la cuota es horaria según el nivel (test: 100/h, live: 1000/h) y se agregan headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset a cada respuesta. Se recomienda HTTPS siempre que se use una API key.
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /properties |
Búsqueda de inmuebles con filtros: type, operation, location (slugs de taxonomía), min_price, max_price, min_m2, bedrooms, bathrooms, page, per_page (máx. 100, cap duro server-side). |
| GET | /properties/{id} |
Detalle de un inmueble publicado. |
| GET | /agents |
Listado de agentes (perfiles públicos). |
| GET | /agents/{id} |
Detalle de un agente. |
| GET | /reviews |
Listado de reseñas moderadas (post_status=publish únicamente). |
| GET | /openapi.json |
Especificación OpenAPI 3.x generada desde las rutas y campos reales (incluye campos dinámicos del Fields Builder). |
| GET | /docs |
Visor HTML/JS propio, 100% offline, que renderiza /openapi.json (ver nota abajo). |
Ejemplo (fetch desde React/Astro):
const res = await fetch('https://tu-sitio.com/wp-json/inmo/v1/properties?type=piso&operation=venta&max_price=250000', {
headers: { 'X-API-Key': 'rsh_live_...' },
});
const { items, total, page, per_page } = await res.json();
Nota sobre /docs: el plan original preveía vendorizar el paquete oficial swagger-ui-dist. Este entorno de desarrollo no tuvo acceso a red/npm para descargarlo, así que /docs sirve un visor HTML/JS propio, liviano y sin dependencias, que consume el /openapi.json real. Si se desea el Swagger UI oficial, assets/vendor/swagger-ui/ queda como punto de extensión: reemplazar Api\SwaggerUi::render_html() para servir esos archivos en su lugar.
Campos de un inmueble (meta)
Predefinidos (prefijo rsh_): precio, m2, habitaciones, banos, certificado_energetico (A-G), lat/lng (ocultos por defecto vía show_in_rest=false y por Api\FieldFilter — requieren permiso reveal_precise_location en la API key), galeria (IDs de adjuntos).
Campos dinámicos: se agregan desde "Real State → Campos personalizados" (tipos: texto, número, selección, checkbox) y aparecen automáticamente en el meta box del inmueble y en la respuesta de la API.
Hooks de extensión
Contrato público congelado a partir de v1.0.0 — el naming (rsh_*) y las firmas no cambian sin un bump mayor.
| Hook | Tipo | Cuándo dispara | Firma |
|---|---|---|---|
rsh_before_lead_saved |
action | Justo antes de insertar un lead enviado desde [rsh_lead_form]. Un callback puede rechazar el envío (ej. spam) respondiendo wp_send_json_error() + exit él mismo. |
do_action( 'rsh_before_lead_saved', array $lead ) — $lead = ['name','email','phone','message','property_id'] |
rsh_after_lead_saved |
action | Después de guardar el lead e intentar el email de notificación (chequeá el meta rsh_mail_sent para saber si se entregó). |
do_action( 'rsh_after_lead_saved', int $lead_id, int[] $agent_ids ) |
rsh_property_fields |
filter | Antes de aplicar las reglas de visibilidad de Api\FieldFilter, sobre el set de campos crudo de un inmueble en la respuesta de inmo/v1. |
apply_filters( 'rsh_property_fields', array $meta, int $post_id ) |
rsh_api_field_permissions |
filter | Último filtro sobre los campos de un inmueble antes de responder, después del default-deny de lat/lng. $key es null en requests anónimos. |
apply_filters( 'rsh_api_field_permissions', array $meta, ?array $key ) |
rsh_export_data |
filter | Sobre el dataset completo de un backup, justo antes de escribirlo a disco. Nunca agregar secretos acá — el archivo resultante es descargable por cualquier usuario manage_options y puede compartirse/restaurarse en otro sitio. |
apply_filters( 'rsh_export_data', array $dataset ) |
Ejemplo — rechazar un lead con un dominio de email bloqueado:
add_action( 'rsh_before_lead_saved', function ( array $lead ) {
if ( str_ends_with( $lead['email'], '@spamdomain.test' ) ) {
// wp_send_json_error() already calls wp_die(), which halts execution here.
wp_send_json_error( [ 'message' => 'No podemos procesar tu consulta.' ], 400 );
}
} );
Ejemplo — agregar un campo calculado a la respuesta de /properties:
add_filter( 'rsh_property_fields', function ( array $meta, int $post_id ) {
$meta['rsh_precio_por_m2'] = ( (float) $meta['rsh_m2'] > 0 )
? round( (float) $meta['rsh_precio'] / (float) $meta['rsh_m2'], 2 )
: null;
return $meta;
}, 10, 2 );
Seguridad
- Nonce +
current_user_can()en cada acción de admin/AJAX; sanitización tipada en cada guardado de meta (Fields\FieldSanitizer). X-API-Keynunca se guarda en claro — solo su hash SHA-256; la key completa se muestra una única vez al generarla.- Rate-limiting completo (cuota por key + límite por IP para tráfico anónimo) con headers
X-RateLimit-*y429al exceder. - Campos sensibles (
lat/lngprecisos) con default-deny tanto en la API propia como en la REST API nativa de WordPress. - Leads (datos personales) nunca se exponen en ninguna REST API; CPT restringido a
manage_options. - Backups: formato JSON únicamente (sin parser XML), nunca exportan
wp_rsh_api_keysni ningún secreto, se descargan solo víaadmin-ajaxautenticado (nunca una URL pública), y los archivos temporales se purgan automáticamente cada hora. - Se recomienda servir el sitio siempre por HTTPS cuando se usan API keys.
Desinstalación
Por defecto, desinstalar el plugin no borra ningún dato (solo limpia 2 opciones internas de bajo nivel). Si en "Real State → Estado" activás la casilla "Borrar todos los datos del plugin al desinstalarlo", uninstall.php elimina inmuebles/agentes/reseñas/leads (+ sus adjuntos), términos de taxonomía, opciones del plugin y la tabla wp_rsh_api_keys. Las cuentas de usuario de WordPress (incluidos los agentes) nunca se borran, aunque hayan sido creadas por el Instalador Demo.
Roadmap
| Fase | Descripción | Versión |
|---|---|---|
| 1 | Fundación del plugin | v0.1.0 |
| 2 | Arquitectura de datos + Fields Builder | v0.2.0 |
| 3 | Agentes, Leads y comunicación | v0.3.0 |
| 4 | API Headless (keys, OpenAPI, búsqueda+caché) | v0.4.0 |
| 5 | Instalador de datos demo | v0.5.0 |
| 6 | Backup / Restore | v0.6.0 |
| 7 | QA, Seguridad y Cierre | v1.0.0 |
Licencia
Free para uso no comercial.