- Si controlas la configuración central de Apache, las redirecciones van en el
<VirtualHost>, no en.htaccess: la documentación oficial lo recomienda y se cargan una vez al arrancar, no en cada petición. - Para mover una URL o un grupo de URLs,
RedirectyRedirectMatch(mod_alias).RewriteRulequeda para lo que dependa de condiciones o de tablas. - El paso de HTTP a HTTPS y la unificación de www se resuelven con un VirtualHost por puerto y un
Redirect permanent, sin una sola condición. - Con miles de URLs,
RewriteMapen formatodbmevita un archivo de reglas inmanejable. Solo se puede declarar en la configuración central. - Probé cada ejemplo en un Apache 2.4.58 real y encontré cuatro fallos que no salen en los tutoriales: barra doble,
index.htmlatrapado, cadena de dos saltos y vhost por defecto. - El orden de comprobación es
apachectl configtest,gracefulycurl -I; el log derewrite:trace3queda para cuando algo no cuadra.
Casi todos los tutoriales de redirecciones en Apache acaban en un bloque de .htaccess con RewriteRule, porque es lo que funciona en un hosting compartido. Si administras tu propio servidor, un VPS o la configuración de un cliente, ese es el sitio equivocado: la propia documentación de Apache pide mover esas reglas a la configuración central.
Este artículo trata solo esa configuración central: httpd.conf, sites-available y el VirtualHost. Cada ejemplo lo he ejecutado contra un Apache 2.4.58 propio y lo que ves como resultado es lo que devolvió curl. Si trabajas con .htaccess, tienes la guía de redirecciones en .htaccess.

Por qué la configuración central y no .htaccess
La documentación oficial de Apache sobre .htaccess es tajante: si tienes acceso al archivo de configuración principal, pon ahí todo, incluidas las reglas de mod_rewrite. Da dos razones, rendimiento y seguridad. Sobre el rendimiento dice que, con AllowOverride activado, httpd busca archivos .htaccess en cada directorio «whether or not you actually even use them», y que el archivo se carga en cada petición. Las directivas de la configuración principal se cargan una vez al arrancar.
No he medido la diferencia de milisegundos y no voy a inventarla: depende del número de directorios, del sistema de archivos y de la caché del sistema operativo. Lo que sí he comprobado es el otro efecto, el que más sorprende en una auditoría. Con AllowOverride None, un .htaccess con un Redirect se ignora sin avisar; con AllowOverride FileInfo en ese directorio, funciona.
| Prueba (mismo .htaccess) | Resultado |
|---|---|
AllowOverride None | 404: el Redirect del .htaccess no se lee |
AllowOverride FileInfo en ese directorio | 301 a /nuevo/ |
Hay un motivo legítimo para seguir con .htaccess: no tener acceso a la configuración. La página «When not to use mod_rewrite» de Apache lo admite expresamente para quien solo dispone de ese archivo.
Redirect y RedirectMatch: mod_alias
La documentación de Apache dice que la redirección simple de una URL, o de una clase de URLs, debe hacerse con Redirect y RedirectMatch en lugar de RewriteRule. Redirect compara por prefijo de segmentos completos y añade al destino el resto de la ruta y la query string. RedirectMatch usa una expresión regular y permite reutilizar los grupos capturados.
<VirtualHost *:443>
ServerName www.ejemplo.test
Redirect permanent "/pagina-vieja" "/nuevo/"
RedirectMatch permanent "^/blog/([0-9]{4})/([0-9]{2})/(.+)$" "/blog/$3"
</VirtualHost>
| Petición | Respuesta |
|---|---|
/pagina-vieja | 301 → /nuevo/ |
/pagina-viejo y /pagina-vieja2 | 404: no coinciden como segmento completo |
/blog/2021/05/mi-post/ | 301 → /blog/mi-post/ |
/blog/2021/05/mi-post/?utm=a | 301 → /blog/mi-post/?utm=a |
La última fila importa: con RedirectMatch la query string también viajó al destino. La página de mod_alias lo documenta para Redirect y no dice nada explícito de RedirectMatch, así que es un comportamiento que verifiqué en 2.4.58 y que conviene repetir en tu versión.

Qué código devuelve cada forma
Sin argumento de estado, Redirect devuelve un 302 temporal. Esta es la fuente habitual de redirecciones «permanentes» que Google trata como temporales: si escribes Redirect "/a" "/b" y olvidas permanent, el 302 es el que sale. Estas son las formas que probé.
| Directiva | Código real | Cuándo |
|---|---|---|
Redirect permanent | 301 | Cambio definitivo de URL |
Redirect (sin estado) | 302 | Temporal; es el valor por defecto |
Redirect gone "/retirado" | 410, sin destino | Contenido eliminado a propósito |
Redirect 308 | 308 | Permanente que conserva el método (POST sigue siendo POST) |
Google dice que 301 y 308 indican que la página se ha movido de forma permanente, y que se usen cuando estés seguro de que la redirección no se va a revertir. Cómo trata cada código y qué pasa con la señal que se transfiere lo explico en el artículo sobre tipos de redirección y su efecto en SEO, y el resto de la familia 3xx, 4xx y 5xx está en los códigos de respuesta del servidor. En Apache, una petición POST a la ruta con Redirect 308 devolvió 308, es decir, el servidor no degrada el método; lo que haga el cliente después no lo he probado.
mod_alias frente a mod_rewrite
La documentación de Apache describe mod_rewrite como «a last resort»: con alternativas más simples produce configuraciones confusas, frágiles y difíciles de mantener. Mi criterio, que coincide con esa página, es este.
| Necesitas… | Usa |
|---|---|
| Mover una URL o una carpeta a otra | Redirect permanent |
| Mover por patrón (años, extensiones, ids) | RedirectMatch |
| Quitar o fijar la query string | RewriteRule con QSD o QSA |
| Decidir según una cabecera, cookie o variable | RewriteCond + RewriteRule, o un bloque <If> |
| Consultar una tabla de equivalencias | RewriteMap |

RewriteRule dentro del VirtualHost
En el contexto del servidor el patrón se compara con la ruta que va tras el host y antes de la query, y empieza por barra (^/productos/); en .htaccess esa barra no existe, y de ahí que las reglas no sean copiables tal cual entre los dos sitios. Estas tres reglas, probadas, muestran qué pasa con la query string.
RewriteEngine On
RewriteRule "^/productos/(.*)$" "/tienda/$1" [R=301,L]
RewriteRule "^/sin-query/(.*)$" "/tienda/$1" [R=301,L,QSD]
RewriteRule "^/con-query/(.*)$" "/tienda/$1?origen=viejo" [R=301,L,QSA]
| Petición | Location devuelto |
|---|---|
/productos/camisa?c=rojo | /tienda/camisa?c=rojo |
/sin-query/camisa?c=rojo | /tienda/camisa |
/con-query/camisa?c=rojo | /tienda/camisa?origen=viejo&c=rojo |
Una regla con R=301 sin L sigue evaluando las siguientes; en configuración central END (desde 2.3.9) detiene todo el proceso de reescritura. Y un detalle de orden que la documentación indica y que probé: cuando Redirect y RewriteRule afectan a la misma URL, en contexto de servidor o VirtualHost corre primero mod_rewrite. Con Redirect permanent "/choque" y una RewriteRule al 302 sobre la misma ruta, ganó la segunda: respondió 302 al destino de la regla de reescritura.
Las reglas no se heredan entre vhosts
Otro fallo clásico al mover reglas desde .htaccess: las reescrituras del servidor principal no pasan a los VirtualHost. Puse una regla global y la probé en dos vhosts.
| VirtualHost | Petición a la URL de la regla global |
|---|---|
Sin RewriteEngine | 404: la regla no existe para él |
RewriteEngine On + RewriteOptions Inherit | 301 a /nuevo/ |
De HTTP a HTTPS con un VirtualHost por puerto
La documentación de Apache dice que, para llevar las URLs http a https, un Redirect en un virtual host HTTP dedicado es el enfoque más limpio. No hay RewriteCond %{HTTPS} ni lógica: todo lo que llega al puerto 80 va al destino, conservando ruta y query.
<VirtualHost *:80>
ServerName www.ejemplo.test
ServerAlias ejemplo.test
Redirect permanent "/" "https://www.ejemplo.test/"
</VirtualHost>
<VirtualHost *:443>
ServerName ejemplo.test
Redirect permanent "/" "https://www.ejemplo.test/"
</VirtualHost>
<VirtualHost *:443>
ServerName www.ejemplo.test
# SSLEngine, certificados y contenido
</VirtualHost>
En mi laboratorio no tengo TLS, así que usé el puerto 8461 como «el 80» y el 8462 como «el 443» sin cifrado, con la misma estructura de VirtualHost. Esto es lo que devolvió cada Host.
| Petición | Resultado |
|---|---|
«:80», Host ejemplo.test, /guia.html?x=1 | 301 → https://www.ejemplo.test/guia.html?x=1 |
«:80», Host www.ejemplo.test, /a/b?q=1 | 301 → https://www.ejemplo.test/a/b?q=1 |
«:443», Host ejemplo.test, /servicios/?p=2 | 301 → https://www.ejemplo.test/servicios/?p=2 |
«:443», Host www.ejemplo.test, / | 200 |
Host desconocido otro.test | 301 al canónico en los dos puertos |
La última fila es una consecuencia de cómo funcionan los hosts virtuales por nombre: la primera <VirtualHost> definida para un puerto atiende cualquier Host que no coincida con ningún ServerName o ServerAlias. Por eso en mi ejemplo el vhost que redirige va primero: una petición con un dominio inesperado acaba en el canónico en lugar de servir el contenido bajo un host duplicado. Elige a conciencia cuál es tu vhost por defecto.

Qué versión del dominio elegir (https, www o sin www) y por qué debe ser una sola es otra conversación, y la cuento en el artículo sobre www, sin www y http. Aquí solo importa que el servidor la imponga con un salto, no con dos.
Una alternativa: el bloque If
Desde Apache 2.4 puedes condicionar un Redirect con <If>, que es lo que muestra la propia documentación en «When not to use mod_rewrite» para redirigir al host canónico sin RewriteCond. Funcionó: una petición con Host distinto del canónico devolvió 301 con la ruta y la query intactas. Con el host correcto (el canónico), el bloque no actúa y la petición sigue su curso normal.
RewriteMap para tablas grandes
Con cientos de URLs antiguas, una regla por URL es ilegible. RewriteMap convierte la tabla en un archivo aparte y deja una sola regla. Solo se puede definir en la configuración del servidor o del VirtualHost, no en .htaccess ni en <Directory>, y es una razón de peso para tener acceso a la configuración central en una migración grande.
# maps/redir.txt: una pareja por línea, separada por espacio
guia-seo-2019 /blog/guia-seo/
producto-viejo-1 /tienda/producto-nuevo-1/
RewriteEngine On
RewriteMap tabla "txt:/etc/apache2/maps/redir.txt"
RewriteCond "${tabla:$1|NONE}" "!=NONE"
RewriteRule "^/txt/(.+)$" "${tabla:$1}" [R=301,L]
# Versión dbm, más rápida con tablas grandes
RewriteMap tabladbm "dbm:/etc/apache2/maps/redir.map"
RewriteRule "^/dbm/(.+)$" "${tabladbm:$1|/no-existe/}" [R=301,L]
El archivo dbm se genera con httxt2dbm -i redir.txt -o redir.map. Según la documentación, un dbm es mucho más rápido porque está indexado y un texto plano no. Los resultados que vi:
| Prueba | Resultado |
|---|---|
| Clave que existe en el mapa de texto | 301 a /blog/guia-seo/ |
Clave que no existe, con RewriteCond de guarda | 404 (la regla no se aplica) |
| Clave que no existe, sin guarda ni valor por defecto | 301 a /: Apache trata un valor vacío como clave ausente y redirige a la raíz |
Línea añadida al .txt, sin reiniciar | Funcionó al momento: se recarga al cambiar su fecha de modificación |
Línea añadida al .txt con el mapa dbm | Sin cambios hasta volver a ejecutar httxt2dbm |
Esa tercera fila es el fallo de un mapa mal escrito: sin guarda ni valor por defecto, una clave desconocida se convierte en una redirección a la home, que es justo lo que se parece a un soft 404. Pon siempre un valor por defecto con | o una condición. En una migración SEO con miles de URLs, esa tabla es además el documento que cruzas con los rastreos antes y después.

Cuatro fallos que reproduje
Estos son los que no salen en la documentación de referencia y me encontré al probar. Los dos primeros se cuelan en revisiones de sitios reales.
Barra doble con un destino que termina en barra
Con Redirect permanent "/pagina-vieja" "/nuevo/", una petición a /pagina-vieja/sub/x.html?a=1 devolvió Location: /nuevo//sub/x.html?a=1. El prefijo /pagina-vieja se sustituye y el resto de la ruta, que empieza por barra, se añade detrás de la barra final del destino. Si el origen y el destino van sin barra final, el resultado es correcto: Redirect permanent "/old" "/nuevo" llevó /old/sub?x=1 a /nuevo/sub?x=1.
RedirectMatch que atrapa el index.html
Para quitar la extensión puse RedirectMatch permanent "^/(.+)\.html$" "/$1/". La home dejó de cargar y redirigió a /index/. El motivo: mod_dir reescribe / internamente a /index.html y esa subpetición también coincide con la regex. Con una exclusión funciona:
RedirectMatch permanent "^/(?!index\.html$)(.+)\.html$" "/$1/"
Resultado: / responde 200, /index.html responde 200 y /guia.html?v=2 devuelve 301 a /guia/?v=2.
Una redirección que acaba en dos saltos
Un Redirect permanent "/old" "/nuevo" hacia un directorio que existe produjo dos saltos: /old → /nuevo → /nuevo/, porque mod_dir añade la barra final con su propio 301. Lo vi siguiendo la cadena con curl -L. La solución es apuntar directamente a /nuevo/.
El vhost por defecto sirve lo que no esperabas
Ya lo vimos en la sección de HTTPS: sin un vhost de captura al principio, un Host que nadie configuró se sirve con el contenido del primer VirtualHost del puerto. Es un caso típico de contenido duplicado bajo dominios que no controlas.
Un efecto relacionado con la caché del navegador y de Googlebot: en mis pruebas la respuesta de un Redirect llegó sin Cache-Control ni Expires. No he medido cuánto tiempo guarda cada navegador un 301, así que si dudas de un destino, prueba primero con 302 y pasa a permanent cuando esté confirmado.

Aplicar los cambios sin tirar el servidor
Los cambios de la configuración central no se aplican solos. Antes de recargar, valida. apachectl configtest (equivalente a apachectl -t) analiza los archivos y responde Syntax OK o el error. Después, apachectl graceful reinicia el demonio sin abortar las conexiones abiertas; según la documentación, también ejecuta la comprobación de sintaxis antes de reiniciar.
apachectl configtest
apachectl graceful
Con graceful cargué vhosts nuevos en mi instancia sin parar el servicio. En Ubuntu y Debian, los sitios viven en sites-available y se activan con a2ensite y los módulos con a2enmod; en otras distribuciones las rutas cambian, y no he probado esas variantes.
Dos errores que el configtest detectó en mi laboratorio:
| Error | Mensaje |
|---|---|
Redirect permanent "/pagina-vieja" (sin destino) | AH00526: Syntax error on line 19 … URL to redirect to is missing |
RewriteEngine con el módulo sin cargar | Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configuration |
Cuidado: configtest valida la sintaxis, no la intención. Una regex demasiado amplia o un destino mal escrito pasan la prueba con Syntax OK.
Depurar una regla que no hace lo que debe
El orden que sigo: primero curl con la cabecera Host (así pruebas un vhost sin tocar DNS ni /etc/hosts), luego el log. Para ver la cadena entera sin resolver el dominio, --connect-to manda el nombre a tu servidor de pruebas.
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Host: www.ejemplo.test' http://127.0.0.1:8462/pagina-vieja
curl -sIL --connect-to www.ejemplo.test:80:127.0.0.1:8462 http://www.ejemplo.test/old
Si la respuesta no se explica, sube el log de mod_rewrite en el vhost afectado con LogLevel alert rewrite:trace3. Apache avisa de que un nivel por encima de trace2 ralentiza mucho el servidor y que debe usarse solo para depurar; quítalo al terminar. Para una petición a una regla global, el log mostró el patrón aplicado, el resultado y el código final:
applying pattern '^/global-viejo$' to uri '/global-viejo'
rewrite '/global-viejo' -> '/nuevo/'
redirect to http://www.ejemplo.test/nuevo/ [REDIRECT/301]
Para filtrar solo esas líneas del error_log, tail -f error_log | fgrep '[rewrite:' es lo que sugiere la documentación.

Cómo lo tengo en mi web
cristofercruz.net usa un .htaccess en la raíz, así que lo que tengo hoy no es el escenario de este artículo y prefiero decirlo claro. Lo que sí puedo afirmar leyendo el código del sitio:
- El
.htaccessno contiene ninguna redirecciónRedirectniRewriteRuleconR=301. SusRewriteRuleson dos reglas con[F,L]que bloqueantools/y.git/, másErrorDocument 404y403hacia/404.php. - No hay redirecciones de dominio en PHP: no hay lógica de cabecera
Locationsobre el host ni sobre el protocolo en el código de las páginas. - Las notas del propio proyecto marcan como «pendiente de verificar en el hosting» las redirecciones https y no-www, precisamente porque dependen del servidor.
Lo que no puedo saber desde el código: si el servidor es Apache con certeza, qué VirtualHost tiene, si hay una redirección central de HTTP a HTTPS o de apex a www, ni qué valor de AllowOverride aplica. Un hosting compartido suele gestionar eso por su cuenta, pero es una suposición mía y no un dato. La comprobación se hace desde fuera, con curl -I a las cuatro variantes del dominio, y es lo que corresponde hacer antes de tocar nada.

Si tu hosting no te deja tocar la configuración central, la alternativa es el archivo por directorio que cubro en el artículo de redirecciones en .htaccess. Y si tu servidor es otro, el equivalente en nginx usa return 301 en lugar de Redirect.
Cómo auditar las redirecciones de Apache
- Lanza
curl -Ia las cuatro variantes (http://yhttps://, con y sin www) y comprueba que tres devuelven un único 301 al canónico y una responde 200. - Sigue cada cadena con
curl -ILy cuenta los saltos. Cualquier cadena de más de un salto es un fallo a corregir. - Prueba una URL con query string y comprueba que llega entera al destino.
- Pide un Host que no existe y mira qué responde el vhost por defecto.
- Revisa los códigos: 301 o 308 para lo definitivo, 302 solo si es temporal de verdad, 410 para lo eliminado.
- Rastrea la lista antigua de URLs con un crawler y cruza el resultado con el informe de páginas de Search Console tras unas semanas.
Preguntas frecuentes
¿Redirect o RewriteRule para una redirección 301?
Para una URL fija o un grupo con un patrón simple, Redirect o RedirectMatch. La documentación de Apache pide reservar RewriteRule para cuando no hay alternativa más simple, por ejemplo condiciones sobre cabeceras o consultas a un RewriteMap.
¿Un Redirect conserva la query string?
Sí. La documentación lo indica para Redirect y yo comprobé que RedirectMatch también la conserva en 2.4.58, aunque su página no lo dice explícitamente. Con RewriteRule la query se arrastra por defecto; usa QSD para descartarla.
¿Es peor .htaccess para el SEO?
No he encontrado ninguna documentación que ligue .htaccess con un peor posicionamiento. Lo que documenta Apache es un coste de rendimiento en el servidor y de seguridad. El efecto SEO llega si ese coste se convierte en un TTFB alto.
¿Hay que reiniciar Apache tras cambiar una redirección?
En la configuración central sí hace falta recargar: apachectl configtest y después apachectl graceful. En .htaccess no, porque se lee en cada petición. Un mapa txt de RewriteMap se recargó solo al modificar el archivo en mi prueba.
¿Qué pasa con las peticiones POST en un 301?
La documentación de mod_alias avisa de que los POST se descartan. Si necesitas que la redirección permanente conserve el método, Redirect 308 devolvió 308 en mi prueba. No he comprobado cómo reacciona cada cliente.
¿Cómo evito una cadena de redirecciones?
Apunta siempre al destino final, con su barra final y su protocolo definitivo. En mi laboratorio, un destino sin barra hacia un directorio añadió un segundo salto de mod_dir.
¿RewriteMap sirve en un hosting compartido?
No: solo se puede definir en la configuración del servidor o del VirtualHost, no en .htaccess. Si no tienes acceso, tendrás que repartir la tabla en reglas individuales.
Referencias
- Apache mod_alias: Redirect y RedirectMatch Apache HTTP Server 2.4
- Apache mod_rewrite: RewriteRule, RewriteMap, flags y LogLevel Apache HTTP Server 2.4
- When not to use mod_rewrite Apache HTTP Server 2.4
- Redirecting and Remapping with mod_rewrite Apache HTTP Server 2.4
- Using RewriteMap Apache HTTP Server 2.4
- Apache HTTP Server Tutorial: .htaccess files Apache HTTP Server 2.4
- apachectl: Apache HTTP Server Control Interface Apache HTTP Server 2.4
- Redirects and Google Search Google Search Central




