PRIVACIDAD

Tus preferencias.

Rechazar no limita el acceso a la web. Cerrar sin guardar conserva tu decisión anterior.

Técnicas · siempre activas

Guardamos únicamente tu elección durante un máximo de 12 meses en este navegador.

Consultar proveedores, duración y transferencias

Servidores

Redirecciones .htaccess: guía con ejemplos probados en Apache

Redirect o RewriteRule, flags, https y www, bucles y rendimiento: ejemplos probados en Apache con el resultado de curl, y qué hay en el .htaccess de mi web.

¿Quieres aplicarlo a tu web?Hablemos
ResumenCómo probé los ejemplos
Puntos clave
  • Redirect y RedirectMatch (mod_alias) sirven para el uno a uno; RewriteRule (mod_rewrite) hace falta cuando la decisión depende del host, de https o de la query string.
  • En .htaccess el patrón de RewriteRule se compara con la ruta sin barra inicial: ^/vieja$ no coincide nunca, ^vieja$ sí.
  • El orden cuenta: lo específico va antes que lo general, y las redirecciones antes del catch-all de WordPress o de tu front controller.
  • Probé los ejemplos con un Apache 2.4.58 real y reproduje tres bucles típicos: [L] que reprocesa, Redirect con prefijo y reglas de www contradictorias.
  • .htaccess se lee en cada petición: 200 reglas en un .htaccess bajaron mi prueba de unas 19.600 a unas 4.400 peticiones por segundo; las mismas reglas en la configuración del servidor, no.
  • Si no tienes acceso a la configuración, .htaccess es la vía; si lo tienes, el VirtualHost es mejor.

Casi todas las guías de redirecciones en .htaccess son una lista de snippets para copiar. El problema aparece después: pegas el bloque y la web devuelve un 500, o redirige dos veces, o no hace nada porque el servidor ignora el archivo. Y no sabes cuál de las tres cosas pasa porque casi nadie enseña la respuesta real de cada regla.

Aquí cada ejemplo lo ejecuté contra un Apache de verdad y pongo lo que devolvió curl. Cubre la elección entre Redirect y RewriteRule, los flags, https y www, barra final, query strings, 410, el orden de las reglas, los bucles, el coste de leer .htaccess y cómo depurarlo. Al final, lo que hay (y lo que no hay) en el .htaccess de esta web.

Dos tarjetas comparan mod_alias (Redirect y RedirectMatch: prefijo, 410 sin destino, solo ruta) con mod_rewrite (RewriteRule y RewriteCond: condiciones de host, https y query, flags).
Los dos módulos que redirigen en Apache. La diferencia práctica es qué información puede mirar cada regla antes de decidir.

Cómo probé los ejemplos

Arranqué un Apache 2.4.58 (Ubuntu) con su propia configuración, AllowOverride All en el DocumentRoot, mod_rewrite, mod_alias, mod_headers y mod_ssl cargados, y un certificado autofirmado para probar http a https de verdad. Los dominios son ficticios (ejemplo.test, viejo.test, nuevo.test) y los resuelvo con curl --connect-to, sin tocar las reglas ni el puerto. Cada comprobación es de este estilo:

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://ejemplo.test/ruta/pag?x=1
curl -sIL http://ejemplo.test/ruta/pag      # -L sigue los saltos
curl -s -o /dev/null -w '%{num_redirects}\n' -L http://ejemplo.test/ruta/pag

Lo que no probé: otras versiones de Apache, hostings concretos ni el efecto en Google; lo que cito de Google viene de su documentación.

Qué necesita el servidor para leer tus reglas

Antes de escribir una sola redirección, el servidor debe permitir .htaccess en ese directorio, tener cargados los módulos y dejar activos los permisos adecuados. Según lo que falle, el síntoma cambia, y conviene reconocerlo.

Configuración del directorioQué pasó con mi .htaccessDónde se ve
AllowOverride NoneIgnorado sin avisar: la URL vieja dio 404En ningún sitio
AllowOverride AuthConfig500 con una línea RedirectRedirect not allowed here en el error.log
Options -FollowSymLinks con AllowOverride All403 con una RewriteRuleAH00670: «Options FollowSymLinks and SymLinksIfOwnerMatch are both off…»
AllowOverride None + AllowOverrideList Redirect RedirectMatch RewriteEngine RewriteRule RewriteCondRedirect y RewriteRule funcionan; un Header da 500Header not allowed here

La documentación de Apache lo resume así: el valor por defecto de AllowOverride es None y, con ese valor, los .htaccess se ignoran por completo. La última fila es el ajuste fino que menciona la propia documentación, y me parece el más sensato en un hosting donde solo quieres permitir redirecciones. Las cabeceras como X-Robots-Tag necesitan mod_headers y que el directorio permita Header; las traté en el artículo sobre la cabecera X-Robots-Tag.

Y una limitación obvia: Nginx no lee .htaccess. Si tu hosting usa Nginx delante o en exclusiva, estas reglas no hacen nada y la configuración va en otro sitio.

Redirect y RedirectMatch: lo simple, bien hecho

Para mover una URL a otra, Redirect es lo más legible. Si no pones código, el estado es 302 (la documentación de mod_alias dice que sin argumento de estado la redirección es temporal). Las palabras clave son permanent (301), temp (302), seeother (303) y gone (410), y acepta un número como 308. Este fue el bloque que probé:

Redirect 301 /pagina-vieja /pagina-nueva/
Redirect permanent /servicios-antiguos/ /servicios/
RedirectMatch 301 ^/blog/([0-9]{4})/(.+)$ /articulos/$2
Redirect gone /promo-2019
RedirectMatch 410 ^/tienda/descatalogado/
Redirect /oferta /ofertas/actual/
Redirect 308 /legal /aviso-legal/
PeticiónRespuesta
/pagina-vieja301 a /pagina-nueva/
/pagina-vieja?utm_source=x301 a /pagina-nueva/?utm_source=x (la query se conserva)
/pagina-vieja/sub/pagina301 a /pagina-nueva//sub/pagina (doble barra)
/pagina-viejaX404: solo coinciden segmentos completos
/blog/2024/mi-post301 a /articulos/mi-post
/promo-2019 y /tienda/descatalogado/x410
/oferta (sin código)302
/legal (código 308)308

Dos cosas que no salen en los tutoriales. La primera: Redirect trabaja por prefijo y añade el resto de la ruta al destino. Por eso la tercera fila da //: el destino acababa en barra y el resto empezaba con otra. Si vas a mover una carpeta entera con Redirect, escribe origen y destino con el mismo criterio de barras. La segunda: RedirectMatch no está anclado. Sin ^ y $ coincide con cualquier parte de la ruta.

Google trata 301 y 308 como señal fuerte hacia el destino y 302 y 307 como señal débil; el artículo sobre qué tipo de redirección elegir lo desarrolla.

RewriteRule y sus flags

RewriteRule hace lo mismo con más control, y por eso tiene más formas de fallar. La sintaxis es RewriteRule patrón destino [flags], y estos son los flags que uso, con lo que devolvieron:

Tabla de flags de RewriteRule con su efecto y el resultado de curl: R da 302, R=301 da 301, L y END, QSA añade la query, QSD la descarta, NC ignora mayúsculas, NE evita escapar la almohadilla y G devuelve 410.
Los flags de RewriteRule que uso a diario y lo que devolvió cada uno contra el Apache de prueba.
RewriteEngine On
RewriteRule ^pagina-vieja/?$ /pagina-nueva/ [R=301,L]
RewriteRule ^sin-codigo$ /destino/ [R,L]
RewriteRule ^Mayusculas$ /minusculas/ [NC,R=301,L]
RewriteRule ^buscar$ /resultados/?origen=viejo [R=301,L]
RewriteRule ^buscar-qsa$ /resultados/?origen=viejo [QSA,R=301,L]
RewriteRule ^ancla/(.+)$ /guia/#$1 [NE,R=301,L]
RewriteRule ^ancla-sin-ne/(.+)$ /guia/#$1 [R=301,L]
RewriteRule ^descatalogado - [G,NC]
RewriteRule ^carpeta-vieja/(.*)$ /carpeta-nueva/$1 [R=301,L]
  • R sin código es 302. Con R=301 es permanente. Un código fuera del rango 3xx, como R=410, descarta el destino y devuelve ese estado; lo comprobé y dio 410, pero la documentación recomienda G o F para esos casos.
  • /buscar?q=zapatos acabó en /resultados/?origen=viejo: cuando el destino lleva su propia query, la original se pierde. Con QSA quedó /resultados/?origen=viejo&q=zapatos.
  • NE importa con anclas: sin él, #seccion-2 salió como %23seccion-2 en la cabecera Location.

L da problemas en .htaccess: lo veremos en los bucles.

El patrón no lleva barra inicial

Este es el error que más veces he visto en reglas copiadas de un VirtualHost a un .htaccess. En contexto de directorio, Apache quita del patrón la ruta del directorio, y lo que queda nunca empieza por barra. La documentación de mod_rewrite lo dice así: un patrón que empieza por ^/ no coincide nunca en este contexto.

Flujo de una petición GET /vieja: Apache quita el prefijo del directorio y la regla ve solo vieja. Tres casos: ^vieja$ en .htaccess da 301, ^/vieja$ en .htaccess da 404 y ^/vieja$ en VirtualHost da 301.
La misma regla con y sin barra inicial, en .htaccess y en un VirtualHost. Solo la del medio falla.
# .htaccess
RewriteRule ^vieja$ /nueva [R=301,L]     # 301
RewriteRule ^/vieja$ /nueva [R=301,L]    # 404, no coincide nunca

# VirtualHost (config del servidor)
RewriteRule ^/vieja$ /nueva [R=301,L]    # 301

Si necesitas comparar con la ruta completa dentro de un .htaccess, usa %{REQUEST_URI} en un RewriteCond. Ese es el motivo de que casi todos los bloques de abajo usen ^ a secas como patrón y decidan con condiciones.

Redirigir según la query string

El patrón de RewriteRule solo ve la ruta. Para actuar sobre ?id=42 hace falta un RewriteCond sobre %{QUERY_STRING}, y el flag QSD para que la query vieja no se arrastre al destino:

RewriteEngine On
RewriteCond %{QUERY_STRING} (?:^|&)id=([0-9]+)(?:&|$)
RewriteRule ^producto\.php$ /producto/%1/ [R=301,L,QSD]
RewriteCond %{QUERY_STRING} ^p=([0-9]+)$
RewriteRule ^$ /articulo-%1/ [R=301,L,QSD]
RewriteRule ^sin-parametros$ /destino/? [R=301,L]
PeticiónRespuesta
/producto.php?id=42301 a /producto/42/
/producto.php?color=rojo&id=42301 a /producto/42/ (el parámetro color se pierde)
/?p=7&x=1200: ^p=([0-9]+)$ no admite más parámetros
/sin-parametros?a=1&b=2301 a /destino/ (la ? final descarta la query)

Sin QSD ni ? final, la primera regla devolvió /producto/42/?id=42&color=rojo: la query vieja se pega al destino y se acaba indexando una URL con parámetros que ya no pintan nada. Decide qué parámetros se conservan y cuáles no antes de lanzar la regla, no después.

https, www, barra final e index

Son las cuatro normalizaciones que más se piden, y la mayoría de los tutoriales las resuelve con una regla por cada una. Cada regla extra es un salto extra. Con dos reglas separadas (una para https y otra para www), curl -L contó 2 saltos desde http://ejemplo.test/ruta/pag. Con una sola regla y dos condiciones, 1 salto.

Dos carriles: con dos reglas, http://ejemplo.test pasa por https://ejemplo.test y luego a https://www.ejemplo.test en dos 301; con una regla y dos condiciones va directo en un solo 301.
La misma petición con dos reglas separadas (dos saltos) y con una regla con dos condiciones (un salto).
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.ejemplo\.test$ [NC]
RewriteRule ^ https://www.ejemplo.test%{REQUEST_URI} [R=301,L]

Resultado, con el servidor escuchando también en TLS:

PeticiónRespuesta
http://ejemplo.test/ruta/pag?x=1&y=2301 a https://www.ejemplo.test/ruta/pag?x=1&y=2
https://ejemplo.test/ruta/pag301 a https://www.ejemplo.test/ruta/pag
https://www.ejemplo.test/200

Tres matices que descubrí probando. Uno: no pongas NE en esta regla. Con NE, una petición a /a%20b salió como /a b (espacio sin escapar) en Location; sin NE, Apache la devolvió como /a%20b. Dos: el equivalente de www a no-www es la misma regla con ^www\.(.+)$ y https://%1%{REQUEST_URI}, y también dio 301 con la query intacta. Tres: si el servidor está detrás de un proxy o CDN que termina el TLS, %{HTTPS} vale off siempre y tendrás un bucle; usa la cabecera que envíe el proxy. Con RewriteCond %{HTTP:X-Forwarded-Proto} !https, la petición con X-Forwarded-Proto: https dio 200 y la que no la llevaba, 301.

Qué versión del host dejar como canónica (con o sin www, http o https) es una decisión que va más allá del servidor; la desarrollo en el artículo sobre las versiones de la web con y sin www y http.

Barra final e index

RewriteEngine On
# index.html / index.php -> carpeta (mirando la petición original, no la interna)
RewriteCond %{THE_REQUEST} ^[A-Z]+\s/(.*/)?index\.(?:php|html)[\s?]
RewriteRule ^ /%1 [R=301,L]
# quitar la barra final salvo en carpetas reales
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]
PeticiónRespuesta
/index.html?x=1301 a /?x=1
/carpeta/index.html?x=1301 a /carpeta/?x=1
/carpeta301 a /carpeta/ (lo añade mod_dir, no mi regla)
/ofertas/ (es un archivo)301 a /ofertas

THE_REQUEST contiene la línea de petición original del cliente y no cambia con las reescrituras internas, así que la regla solo actúa cuando el visitante pidió index.html de verdad. Reconozco un límite: la versión simple, RewriteRule ^(.*/)?index\.html$ /$1 [R=301,L], también funcionó en mi prueba y no dio bucle; uso la condición por precaución, porque con un front controller que reescribe a index.php la petición interna puede volver a coincidir, y eso no lo he reproducido. Para el caso contrario, añadir la barra (RewriteCond %{REQUEST_FILENAME} !-f y RewriteRule ^(.*[^/])$ /$1/ [R=301,L]), respondió 301 a /nueva/ y conservó la query.

Dominio antiguo a dominio nuevo

Un cambio de dominio conserva la ruta y la query y cambia el host. Una regla con condición sobre el host basta para el mapeo URL a URL cuando las rutas no cambian:

RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?viejo\.test$ [NC]
RewriteRule ^(.*)$ https://www.nuevo.test/$1 [R=301,L]

http://viejo.test/ruta/pag?x=1 dio 301 a https://www.nuevo.test/ruta/pag?x=1, http://www.viejo.test/ y http://viejo.test/ dieron 301 a la raíz del nuevo, y www.nuevo.test respondió 200. La regla no mapea rutas que cambian ni resuelve las URLs que desaparecen. Ese trabajo (el mapa de redirecciones URL a URL, la preproducción y el seguimiento posterior) es el núcleo de una migración SEO bien planificada, y es donde se gana o se pierde el tráfico.

410 para contenido que no vuelve

Si una URL se retira sin sustituto, no la mandes a la home. Hay dos formas de devolver 410 y las dos dieron el mismo resultado: Redirect gone /promo-2019 (sin destino, porque con estados fuera del rango 3xx el URL debe omitirse) y RewriteRule ^descatalogado - [G,NC]. La respuesta lleva la página de error por defecto de Apache, con título «410 Gone».

¿Es mejor 410 que 404? Google, en su documentación sobre errores HTTP, dice que trata todos los 4xx igual (salvo el 429): una URL indexada que devuelve un 4xx se elimina del índice, y no señala que el 410 sea más rápido. Elijo 410 porque comunica la intención, no porque haya visto que Google reaccione antes. El resto de estados y qué hace cada uno con el rastreo los repaso en el artículo sobre los códigos de respuesta del servidor.

El orden de las reglas

Las reglas de mod_rewrite se ejecutan en el orden en que aparecen, y con [L] la primera que coincide gana. Reproduje dos errores de orden con las mismas dos reglas intercambiadas:

Dos tarjetas: poner la regla general antes que la específica manda /blog/oferta a /articulos/oferta; la específica primero la manda a /ofertas/. Y una redirección después del catch-all devuelve 200 en vez de 301.
Dos fallos de orden reales: lo general antes de lo específico, y la redirección detrás del catch-all.
# Mal: la general se come a la específica
RewriteRule ^blog/(.*)$ /articulos/$1 [R=301,L]
RewriteRule ^blog/oferta$ /ofertas/ [R=301,L]     # /blog/oferta -> /articulos/oferta

# Bien
RewriteRule ^blog/oferta$ /ofertas/ [R=301,L]     # /blog/oferta -> /ofertas/
RewriteRule ^blog/(.*)$ /articulos/$1 [R=301,L]

El segundo caso es el de WordPress y de cualquier web con front controller. Con el bloque típico (si no existe el archivo ni la carpeta, sirve index), una redirección escrita después nunca se evalúa: /viejo-post/ respondió 200 con la página de fallback. Escrita antes, 301 a /nuevo-post/. En WordPress, escribe tus reglas encima del bloque marcado como BEGIN WordPress, porque WordPress regenera lo que hay dentro (esto último no lo probé aquí).

Las cadenas se forman igual: con a a /b y b a /c, pedir /a dio 2 saltos; con a a /c, 1.

Redirect y RewriteRule en el mismo archivo

Para la misma URL puse RewriteRule ^conflicto$ /por-rewrite y Redirect 301 /conflicto /por-alias, en un orden y en el otro. En mi Apache 2.4.58 ganó siempre RewriteRule. No lo he contrastado en otras versiones. En la práctica: no pongas la misma URL en los dos sistemas; elige uno por archivo.

Bucles típicos y cómo salir

Reproduje tres, y los tres los he visto en webs reales.

Tres tarjetas con un bucle cada una: L que reprocesa en .htaccess y acaba en 500 con AH00124, Redirect con prefijo que añade /articulos/ sin fin, y reglas de www contradictorias que dan seis saltos.
Tres bucles reproducidos en el Apache de prueba, con su arreglo.

L en .htaccess reprocesa la petición

RewriteRule ^(.*)$ /app/$1 [L]

Respuesta: 500, y en el error.log, «AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error». En .htaccess, tras reescribir, Apache vuelve a pasar la petición por las reglas; L solo detiene esa pasada. La documentación recomienda END (desde Apache 2.3.9), que detiene todo el proceso, o una condición. Las dos funcionaron: RewriteRule ^(.*)$ /app/$1 [END] dio 200, y RewriteCond %{REQUEST_URI} !^/app/ delante de la regla con [L], también.

Redirect cuyo destino está dentro del origen

Redirect 301 /blog /blog/articulos/

Como Redirect coincide por prefijo, el destino también empieza por /blog y vuelve a coincidir. curl -L siguió /blog/articulos//articulos/articulos/… hasta cortarlo yo en 4 saltos. Arreglo: RedirectMatch 301 ^/blog/?$ /blog/articulos/, que dio un único salto y un 200.

Reglas de www que se contradicen

Una regla del panel del hosting que añade www y otra, heredada, que lo quita: cada una devuelve 301 a la otra. Con curl -L --max-redirs 6 acabó en 6 saltos y con el error 47 de curl (demasiadas redirecciones). Los navegadores acaban mostrando un error de demasiadas redirecciones, y Google, que según su documentación sigue hasta 10 saltos, no llega al contenido. La solución es una sola regla canónica, como la del apartado anterior, y borrar la otra.

Rendimiento: por qué .htaccess no es el mejor sitio

La documentación de Apache es clara: si tienes acceso al archivo de configuración principal, pon allí las reglas, también las de mod_rewrite, porque se cargan una vez al arrancar y no en cada petición. Con AllowOverride activado, Apache busca .htaccess en cada directorio de la ruta, existan o no, y lo lee cada vez que se pide un documento.

Gráfico de barras de peticiones por segundo: sin .htaccess 22.888, .htaccess vacío 19.854, 200 reglas en .htaccess 4.412 y 200 reglas en el Directory 19.599.
Peticiones por segundo con ab (20.000 peticiones, 4 simultáneas, keep-alive), mediana de tres rondas en mi contenedor. Orientativo.

Medí el coste en mi contenedor, con un archivo estático en una ruta de varios niveles. Sin .htaccess y con AllowOverride None: unas 22.900 peticiones por segundo. Con un .htaccess vacío y AllowOverride All: unas 19.900. Con 200 RewriteRule que no coinciden en .htaccess: unas 4.400. Las mismas 200 reglas dentro de un <Directory> de la configuración: unas 19.600. No es un benchmark de tu servidor: te dice la proporción, no el valor. Con pocas reglas el efecto es pequeño; con cientos, el coste de leerlas pasa a dominar la petición de un archivo estático.

Para las redirecciones de verdad masivas hay otra limitación: RewriteMap no se puede declarar en .htaccess, solo en la configuración del servidor. Si tienes miles de URLs que mapear, eso ya no es un trabajo de .htaccess.

La misma lógica en un VirtualHost la probé en un puerto aparte y funcionó con la barra inicial en el patrón: RewriteRule ^/vieja$ /nueva [R=301,L] dio 301, y Redirect permanent /legado /moderno también. El artículo sobre las redirecciones en la configuración de Apache desarrolla ese camino.

Cómo depurar y verificar

Mi orden, de lo más barato a lo más ruidoso:

Cuatro tarjetas numeradas: curl -I para estado y Location, error.log para errores de sintaxis, rewrite:trace3 en la configuración y apachectl -t, que no revisa los .htaccess.
Cuatro herramientas de depuración y la limitación de cada una.
  1. curl -I (y -L). Estado y Location de cada salto, y el contador %{num_redirects} para detectar cadenas. Prueba con R (302) y cambia a R=301 al final: Apache recomienda 302 mientras pruebas porque los navegadores cachean las permanentes.
  2. El error.log. Un .htaccess con un error de sintaxis devuelve 500 en todas las URLs del directorio y deja la causa aquí: con RewriteRul escrito mal salió «Invalid command 'RewriteRul', perhaps misspelled…»; con RewriteRule ^a$ sin destino, «RewriteRule: bad argument line '^a$'».
  3. Traza de mod_rewrite. LogLevel alert rewrite:trace3 en la configuración del servidor. Si lo pones en .htaccess, da 500 con «LogLevel not allowed here». La documentación avisa de que por encima de trace2 el servidor se ralentiza mucho: actívalo para el caso y quítalo.
  4. apachectl configtest. No revisa los .htaccess: me devolvió «Syntax OK» con un .htaccess roto que provocaba 500. No es una garantía de que las reglas estén bien.

Esto es lo que dejó la traza de una petición a http://ejemplo.test/ruta/pag?x=1 (recortadas las partes comunes de cada línea):

[perdir /srv/ht-redir/doc/] strip per-dir prefix: /srv/ht-redir/doc/ruta/pag -> ruta/pag
[perdir /srv/ht-redir/doc/] applying pattern '^' to uri 'ruta/pag'
[perdir /srv/ht-redir/doc/] rewrite 'ruta/pag' -> 'https://www.ejemplo.test/ruta/pag'
[perdir /srv/ht-redir/doc/] copying x=1 to query string for redirect
[perdir /srv/ht-redir/doc/] redirect to https://www.ejemplo.test/ruta/pag?x=1 [REDIRECT/301]

La línea strip per-dir prefix es la que explica por qué el patrón no lleva barra inicial. Con trace3 no vi las líneas de cada RewriteCond; no he probado niveles más altos.

Auditar una lista de URLs

Después de cualquier cambio, comprueba la lista de URLs antiguas que importan (las de Search Console, las del sitemap viejo, las que reciben enlaces). Con un archivo urls.txt:

while read -r u; do
  curl -s -o /dev/null -L -w "%{http_code} saltos=%{num_redirects} %{url_effective}\n" "$u"
done < urls.txt

Busca tres cosas: todo acaba en 200 (o 410 si lo decidiste), nada tiene más de un salto y nada termina en una URL con parámetros que no tocaban.

Cómo lo tengo en mi web

El .htaccess de cristofercruz.net tiene 33 líneas y ninguna redirección. Lo que contiene:

  • DirectoryIndex index.php y Options -Indexes.
  • ErrorDocument 404 /404.php y ErrorDocument 403 /404.php; el README indica que 404.php devuelve estado 404 con noindex.
  • Con mod_headers: X-Content-Type-Options, Referrer-Policy, X-Frame-Options y las reglas de Cache-Control (siete días para estáticos, un año con immutable para los que llevan hash de 12 caracteres). La lógica de esa política la cuento en el artículo sobre la caché y el SEO.
  • mod_deflate para HTML, CSS, JS, JSON, XML y SVG.
  • Un FilesMatch con Require all denied para documentación interna y archivos de vista previa, y otro para .git y DESPLIEGUE.md.
  • RewriteEngine On con dos reglas [F,L] que prohíben tools/ y .git/.

No hay redirección de http a https ni de www a la versión sin www. El README lo deja anotado en «Pendiente de verificar en el hosting: redirecciones https/no-www (no se han tocado por depender del servidor)». La URL canónica que declara el sitio en config.php es https://cristofercruz.net, sin www. No he comprobado desde aquí qué responde el servidor de producción; puede que el hosting ya lo resuelva, y es lo primero que miraría con curl -I.

Si hubiera que hacerlo en este .htaccess, pondría el bloque de una sola regla justo después de RewriteEngine On y antes de las reglas que prohíben carpetas. Lo probé sobre una copia del archivo real con la versión sin www:

RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^cristofercruz\.net$ [NC]
RewriteRule ^ https://cristofercruz.net%{REQUEST_URI} [R=301,L]

http://cristofercruz.net/robots.txt?x=1 dio 301 a https://cristofercruz.net/robots.txt?x=1; http://www.cristofercruz.net/tools/a.txt dio 301 a https://cristofercruz.net/tools/a.txt (con el dominio de prueba, la regla de tools/ siguió dando 403 tras el salto); https://cristofercruz.net/robots.txt, 200. Un efecto secundario que conviene saber: esa condición de host redirige también cualquier otro nombre, y en mi prueba una petición a 127.0.0.1 acabó en 301 hacia producción. Si tienes un entorno de desarrollo o pruebas en otro host, añade una condición para excluirlo. Y en las carpetas, /blog sin barra da dos saltos (el de la canónica y el de mod_dir añadiendo la barra), así que las URLs del blog llevan barra final.

Antes de tocar un .htaccess de producción, guarda una copia y prueba con R (302) y curl. Un error de sintaxis en un .htaccess devuelve 500 en todas las URLs del directorio, y un navegador que ya guardó una 301 equivocada la sigue aplicando aunque corrijas la regla.

Auditoría SEO

Ver auditoría SEO

Preguntas frecuentes

¿Redirect o RewriteRule?

Redirect (o RedirectMatch) cuando la regla solo depende de la ruta: mover una página o una carpeta. RewriteRule cuando depende del host, de https o de la query string, o cuando necesitas flags como QSD o NE. Mezclarlos para la misma URL no es buena idea: en mi prueba ganó RewriteRule en cualquier orden.

¿Por qué mi .htaccess no hace nada?

Lo más probable es que el directorio tenga AllowOverride None, que ignora el archivo sin dar error, o que el servidor sea Nginx, que no lo lee. Prueba a escribir una línea inválida: si la web no devuelve 500, Apache no está leyendo ese archivo.

¿Cuánto tarda Google en reflejar una 301?

No está documentado. Google explica que una redirección permanente indica que el destino debe ser el canónico y que, en un cambio de dominio, las URLs viejas pueden seguir apareciendo durante un tiempo, pero no da plazos. No cuentes con un número.

¿Cuántas redirecciones aguanta un .htaccess?

No hay un límite documentado, pero cada regla se evalúa en cada petición y el archivo se vuelve a leer cada vez. En mi prueba, 200 reglas bajaron el rendimiento de unas 19.600 a unas 4.400 peticiones por segundo. Con cientos o miles de URLs, es mejor la configuración del servidor, con RewriteMap.

¿Uso 301 o 302 mientras pruebo?

302 (R sin código). La documentación de Apache lo recomienda porque los navegadores cachean las redirecciones permanentes, y una regla mal escrita se queda pegada aunque la corrijas. Cuando el resultado sea el correcto, cambia a R=301.

¿410 o 404 para una página eliminada?

Para Google son equivalentes: su documentación dice que todos los 4xx, salvo el 429, se tratan igual y que una URL indexada que devuelve un 4xx se elimina del índice. Uso 410 cuando la retirada es definitiva y 404 cuando no estoy seguro.

Referencias y fuentes

Documentación consultada para este artículo.

  1. Apache HTTP Server Tutorial: .htaccess files Apache HTTP Server
  2. Apache Module mod_alias Apache HTTP Server
  3. Apache Module mod_rewrite Apache HTTP Server
  4. Apache mod_rewrite: RewriteRule Flags Apache HTTP Server
  5. Apache mod_rewrite: Rewriting in .htaccess Apache HTTP Server
  6. Apache mod_rewrite: When not to use mod_rewrite Apache HTTP Server
  7. Redirects and Google Search Google Search Central
  8. HTTP status codes, and network and DNS errors Google Search Central

Sigue leyendo

Servidores

· 14 min

Redirecciones SEO: tipos 301, 302, 307 y 308 según Google

Tipos de redirecciones, qué documenta Google de cada una, cadenas, 410 frente a 301 y cómo las trato y audito en mi propia web.

Leer el artículo: Redirecciones SEO: tipos 301, 302, 307 y 308 según Google
Servidores

· 13 min

Redirecciones en Apache: configuración central y VirtualHost

Redirect, RedirectMatch, RewriteRule y RewriteMap en el VirtualHost de Apache, con cada ejemplo probado en un servidor real y cuatro fallos que reproduje.

Leer el artículo: Redirecciones en Apache: configuración central y VirtualHost
Servidores

· 15 min

Versiones de una web: www, sin www, http y https en SEO

Por qué http, https, www y sin www son URLs distintas para Google, cómo elegir una y redirigir el resto con un 301 probado en Nginx y Apache.

Leer el artículo: Versiones de una web: www, sin www, http y https en SEO

Tu próximo paso empieza con una conversación.

Cuéntame qué necesitas conseguir con tu web.

Hablemos de tu proyecto