Enrutamiento
El enrutador se encarga de todo lo relacionado con las URL para que no tengas que pensar más en ellas. Se lo mostraremos:
- cómo configurar el router para que las URLs sean como tú quieres
- algunas notas sobre la redirección SEO
- y le mostraremos cómo escribir su propio enrutador
Las URLs más humanas (o URLs chulas o bonitas) son más usables, más memorables y contribuyen positivamente al SEO. Nette tiene esto en mente y satisface plenamente los deseos de los desarrolladores. Puedes diseñar la estructura de URL de tu aplicación exactamente como quieras. Incluso puede diseñarla después de que la aplicación esté lista, ya que puede hacerse sin ningún cambio de código o plantilla. Se define de forma elegante en un único lugar, en el enrutador, y no está dispersa en forma de anotaciones en todos los presentadores.
El enrutador en Nette es especial porque es bidireccional, puede tanto decodificar URLs de peticiones HTTP como crear enlaces. Así que juega un papel vital en la aplicación de Nette, porque decide qué presentador y qué acción ejecutarán la solicitud actual, y también se utiliza para la generación de URL en la plantilla, etc.
Sin embargo, el enrutador no se limita a este uso, puede utilizarlo en aplicaciones donde los presentadores no se utilizan en absoluto, para las API REST, etc. Más en la sección uso separado.
Colección de rutas
La forma más agradable de definir las direcciones URL en la aplicación es a través de la clase Nette\Application\Routers\RouteList. La definición consiste en una lista de las llamadas rutas, es decir, máscaras de direcciones URL y sus presentadores y acciones asociadas mediante una API sencilla. No es necesario dar nombre a las rutas.
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
El ejemplo dice que si abrimos https://any-domain.com/rss.xml
con la acción rss
, si
https://domain.com/article/12
con la acción view
, etc. Si no se encuentra una ruta adecuada, Nette
Application responde lanzando una excepción BadRequestException, que aparece al
usuario como una página de error 404 Not Found.
Orden de las rutas
El orden en que se enumeran las rutas es muy importante porque se evalúan secuencialmente de arriba a abajo. La regla es que declaremos las rutas de específica a general:
// INCORRECTO: <rss.xml> coincide con la primera ruta y la malinterpreta como <slug>.
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');
// BUENO
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');
Las rutas también se evalúan de arriba a abajo cuando se generan los enlaces:
// INCORRECTO: genera un enlace a 'Feed:rss' como 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');
// BUENO
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
No le ocultaremos que se necesita cierta habilidad para construir una lista correctamente. Hasta que te pongas a ello, el panel de en rutamiento será una herramienta útil.
Máscara y parámetros
La máscara describe la ruta relativa basada en la raíz del sitio. La máscara más simple es una URL estática:
$router->addRoute('products', 'Products:default');
A menudo, las máscaras contienen los llamados parámetros. Van entre corchetes angulares (por ejemplo
<year>
) y se pasan al presentador de destino, por ejemplo al método renderShow(int $year)
o al
parámetro persistente $year
:
$router->addRoute('chronicle/<year>', 'History:show');
El ejemplo dice que si abrimos https://any-domain.com/chronicle/2020
y la acción show
con el
parámetro year: 2020
.
Podemos especificar un valor por defecto para los parámetros directamente en la máscara y así se convierte en opcional:
$router->addRoute('chronicle/<year=2020>', 'History:show');
La ruta aceptará ahora la URL https://any-domain.com/chronicle/
con el parámetro year: 2020
.
Por supuesto, el nombre del presentador y la acción también pueden ser un parámetro. Por ejemplo:
$router->addRoute('<presenter>/<action>', 'Home:default');
Esta ruta acepta, por ejemplo, una URL de la forma /article/edit
resp. /catalog/list
y las traduce a
presentadores y acciones Article:edit
resp. Catalog:list
.
También da a los parámetros presenter
y action
valores por defectoHome
y
default
y, por tanto, son opcionales. Así, la ruta también acepta una URL /article
y la traduce como
Article:default
. O viceversa, un enlace a Product:default
genera una ruta /product
, un
enlace al valor por defecto Home:default
genera una ruta /
.
La máscara puede describir no sólo la ruta relativa basada en la raíz del sitio, sino también la ruta absoluta cuando comienza con una barra, o incluso toda la URL absoluta cuando comienza con dos barras:
// ruta relativa a la raíz del documento de la aplicación
$router->addRoute('<presenter>/<action>', /* ... */);
// ruta absoluta, relativa al nombre del servidor
$router->addRoute('/<presenter>/<action>', /* ... */);
// URL absoluta incluyendo nombre de host (pero relativa al esquema)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);
// URL absoluta incluyendo esquema
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);
Expresiones de validación
Se puede especificar una condición de validación para cada parámetro utilizando una expresión regular. Por ejemplo, establezcamos que id
sea sólo numérico, utilizando \d+
regexp:
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
La expresión regular por defecto para todos los parámetros es [^/]+
es decir, todo excepto la barra. Si un
parámetro también debe coincidir con una barra oblicua, la expresión regular será .+
.
// acepta https://example.com/a/b/c, la ruta es 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
Secuencias opcionales
Los corchetes indican las partes opcionales de la máscara. Cualquier parte de la máscara puede ser opcional, incluidas las que contienen parámetros:
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// URL aceptadas: Parámetros:
// /en/download lang => en, name => download
// /download lang => null, name => download
Por supuesto, cuando un parámetro forma parte de una secuencia opcional, también se convierte en opcional. Si no tiene un valor por defecto, será nulo.
Las secciones opcionales también pueden estar en el dominio:
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
Las secuencias pueden anidarse y combinarse libremente:
$router->addRoute(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
'Home:default',
);
// URL aceptadas:
// /en/hello
// /en-us/hello
// /hello
// /hello/page-12
El generador de URL intenta que la URL sea lo más corta posible, por lo que se omite lo que se puede omitir. Así, por
ejemplo, una ruta index[.html]
genera una ruta /index
. Puede invertir este comportamiento escribiendo un
signo de exclamación después del corchete izquierdo:
// acepta tanto /hello como /hello.html, genera /hello
$router->addRoute('<name>[.html]', /* ... */);
// acepta tanto /hello como /hello.html, genera /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
Los parámetros opcionales (es decir, los parámetros que tienen un valor por defecto) sin corchetes se comportan como si estuvieran envueltos de esta manera:
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);
// igual a:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
Para cambiar cómo se genera la barra más a la derecha, es decir, en lugar de /home/
obtener un
/home
, ajustar la ruta de esta manera:
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
Comodines
En la máscara de ruta absoluta, podemos utilizar los siguientes comodines para evitar, por ejemplo, la necesidad de escribir un dominio en la máscara, que puede diferir en el entorno de desarrollo y de producción:
%tld%
= dominio de primer nivel, por ejemplocom
oorg
%sld%
= dominio de segundo nivel, por ejemploexample
%domain%
= dominio sin subdominios, p. ej.example.com
%host%
= host completo, por ejemplowww.example.com
%basePath%
= ruta al directorio raíz
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);
Notación avanzada
El objetivo de una ruta, normalmente escrito en la forma Presenter:action
, también puede expresarse utilizando
una matriz que defina parámetros individuales y sus valores por defecto:
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Home',
'action' => 'default',
]);
Para una especificación más detallada, se puede utilizar una forma aún más extendida, en la que además de los valores por
defecto, se pueden establecer otras propiedades de los parámetros, como una expresión regular de validación (véase el
parámetro id
):
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::Value => 'Home',
],
'action' => [
Route::Value => 'default',
],
'id' => [
Route::Pattern => '\d+',
],
]);
Es importante señalar que si los parámetros definidos en la matriz no se incluyen en la máscara de ruta, sus valores no podrán modificarse, ni siquiera utilizando parámetros de consulta especificados tras un signo de interrogación en la URL.
Filtros y traducciones
Es una buena práctica escribir el código fuente en inglés, pero ¿qué pasa si necesitas que tu sitio web tenga la URL traducida a otro idioma? Rutas simples como:
$router->addRoute('<presenter>/<action>', 'Home:default');
generarán URL en inglés, como /product/123
o /cart
. Si queremos que los presentadores y las
acciones de la URL se traduzcan al alemán (por ejemplo, /produkt/123
o /einkaufswagen
), podemos
utilizar un diccionario de traducción. Para añadirlo, ya necesitamos una variante „más locuaz“ del segundo parámetro:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterTable => [
// cadena en URL => presentador
'produkt' => 'Product',
'einkaufswagen' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::Value => 'default',
Route::FilterTable => [
'liste' => 'list',
],
],
]);
Se pueden utilizar varias claves de diccionario para el mismo presentador. Se crearán varios alias para él. La última clave se considera la variante canónica (es decir, la que aparecerá en la URL generada).
De este modo, la tabla de traducción puede aplicarse a cualquier parámetro. Sin embargo, si la traducción no existe, se toma
el valor original. Podemos cambiar este comportamiento añadiendo Route::FilterStrict => true
y la ruta rechazará
entonces la URL si el valor no está en el diccionario.
Además del diccionario de traducción en forma de array, es posible establecer funciones de traducción propias:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>/<id>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterIn => function (string $s): string { /* ... */ },
Route::FilterOut => function (string $s): string { /* ... */ },
],
'action' => 'default',
'id' => null,
]);
La función Route::FilterIn
convierte entre el parámetro en la URL y la cadena, que luego se pasa al presentador,
la función FilterOut
asegura la conversión en la dirección opuesta.
Los parámetros presenter
, action
y module
ya tienen filtros predefinidos que convierten
entre el estilo PascalCase resp. camelCase y kebab-case utilizado en la URL. El valor por defecto de los parámetros ya está
escrito en la forma transformada, por lo que, por ejemplo, en el caso de un presentador, escribimos
<presenter=ProductEdit>
en lugar de <presenter=product-edit>
.
Filtros generales
Además de filtros para parámetros específicos, también puede definir filtros generales que reciben una matriz asociativa de
todos los parámetros que pueden modificar de cualquier manera y luego devolver. Los filtros generales se definen con la tecla
null
.
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Home',
'action' => 'default',
null => [
Route::FilterIn => function (array $params): array { /* ... */ },
Route::FilterOut => function (array $params): array { /* ... */ },
],
]);
Los filtros generales le dan la posibilidad de ajustar el comportamiento de la ruta de absolutamente cualquier manera. Podemos
utilizarlos, por ejemplo, para modificar parámetros en función de otros parámetros. Por ejemplo, la traducción
<presenter>
y <action>
en función del valor actual del parámetro
<lang>
.
Si un parámetro tiene definido un filtro personalizado y existe al mismo tiempo un filtro general, el personalizado
FilterIn
se ejecuta antes que el general y viceversa el general FilterOut
se ejecuta antes que el
personalizado. Así, dentro del filtro general están los valores de los parámetros presenter
resp.
action
escritos en estilo PascalCase resp. camelCase.
Indicador unidireccional
Las rutas unidireccionales se utilizan para preservar la funcionalidad de las URL antiguas que la aplicación ya no genera pero
que aún acepta. Las marcamos con OneWay
:
// antigua URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// nueva URL /producto/123
$router->addRoute('product/<id>', 'Product:detail');
Al acceder a la URL antigua, el presentador redirige automáticamente a la URL nueva para que los motores de búsqueda no indexen estas páginas dos veces (véase SEO y canonización).
Enrutamiento dinámico con llamadas de retorno
El enrutamiento dinámico con callbacks permite asignar directamente funciones (callbacks) a las rutas, que se ejecutarán cuando se visite la ruta especificada. Esta característica flexible le permite crear rápida y eficientemente varios puntos finales para su aplicación:
$router->addRoute('test', function () {
echo 'You are at the /test address';
});
También puede definir parámetros en la máscara, que se pasarán automáticamente a su devolución de llamada:
$router->addRoute('<lang cs|en>', function (string $lang) {
echo match ($lang) {
'cs' => 'Welcome to the Czech version of our website!',
'en' => 'Welcome to the English version of our website!',
};
});
Módulos
Si tenemos más rutas que pertenecen a un módulo, podemos utilizar withModule()
para
agruparlas:
$router = new RouteList;
$router->withModule('Forum') // los siguientes routers forman parte del módulo Forum
->addRoute('rss', 'Feed:rss') // el presentador es Forum:Feed
->addRoute('<presenter>/<action>')
->withModule('Admin') // los siguientes routers forman parte del módulo Forum:Admin
->addRoute('sign:in', 'Sign:in');
Una alternativa es utilizar el parámetro module
:
// URL manage/dashboard/default se asigna al presentador Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin',
]);
Subdominios
Las colecciones de rutas pueden agruparse por subdominios:
$router = new RouteList;
$router->withDomain('example.com')
->addRoute('rss', 'Feed:rss')
->addRoute('<presenter>/<action>');
También puede utilizar comodines en su nombre de dominio:
$router = new RouteList;
$router->withDomain('example.%tld%')
// ...
Prefijo de ruta
Las colecciones de rutas pueden agruparse por ruta en la URL:
$router = new RouteList;
$router->withPath('eshop')
->addRoute('rss', 'Feed:rss') // coincide con URL /eshop/rss
->addRoute('<presenter>/<action>'); // coincide con la URL /eshop/<presentador>/<acción>
Combinaciones
Los usos anteriores pueden combinarse:
$router = (new RouteList)
->withDomain('admin.example.com')
->withModule('Admin')
->addRoute(/* ... */)
->addRoute(/* ... */)
->end()
->withModule('Images')
->addRoute(/* ... */)
->end()
->end()
->withDomain('example.com')
->withPath('export')
->addRoute(/* ... */)
// ...
Parámetros de consulta
Las máscaras también pueden contener parámetros de consulta (parámetros después del signo de interrogación en la URL). No pueden definir una expresión de validación, pero pueden cambiar el nombre con el que se pasan al presentador:
// utilizar el parámetro de consulta 'cat' como 'categoryId' en la aplicación
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
Parámetros Foo
Ahora vamos más a fondo. Los parámetros Foo son básicamente parámetros sin nombre que permiten coincidir con una expresión
regular. La siguiente ruta coincide con /index
, /index.html
, /index.htm
y
/index.php
:
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
También es posible definir explícitamente una cadena que se utilizará para generar la URL. La cadena debe colocarse
directamente después del signo de interrogación. La siguiente ruta es similar a la anterior, pero genera
/index.html
en lugar de /index
porque la cadena .html
está configurada como „valor
generado“.
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
Integración
Para conectar nuestro router a la aplicación, debemos informar al contenedor DI sobre él. La forma más sencilla es preparar
la fábrica que construirá el objeto router y decirle a la configuración del contenedor que lo utilice. Digamos que escribimos
un método para este propósito App\Core\RouterFactory::createRouter()
:
namespace App\Core;
use Nette\Application\Routers\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute(/* ... */);
return $router;
}
}
Luego escribimos en configuración:
services:
- App\Core\RouterFactory::createRouter
Cualquier dependencia, como una conexión de base de datos, etc., se pasa al método de fábrica como sus parámetros utilizando autowiring:
public static function createRouter(Nette\Database\Connection $db): RouteList
{
// ...
}
SimpleRouter
Un enrutador mucho más simple que Route Collection es SimpleRouter. Se puede utilizar
cuando no hay necesidad de un formato de URL específico, cuando mod_rewrite
(o alternativas) no está disponible
o cuando simplemente no queremos molestarnos con URLs fáciles de usar todavía.
Genera direcciones más o menos de esta forma:
http://example.com/?presenter=Product&action=detail&id=123
El parámetro del constructor SimpleRouter
es un presentador y una acción por defecto, es decir, la acción que
se ejecutará si abrimos, por ejemplo, http://example.com/
sin parámetros adicionales.
// por defecto el presentador es 'Home' y la acción es 'default
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
Recomendamos definir SimpleRouter directamente en la configuración:
services:
- Nette\Application\Routers\SimpleRouter('Home:default')
SEO y Canonización
El framework aumenta el SEO (optimización para motores de búsqueda) al evitar la duplicación de contenidos en distintas URL.
Si varias direcciones enlazan a un mismo destino, por ejemplo /index
y /index.html
, el framework
determina la primera como primaria (canónica) y redirige las demás a ella utilizando el código HTTP 301. Gracias a esto, los
motores de búsqueda no indexarán las páginas dos veces y no romperán su page rank. .
Este proceso se denomina canonización. La URL canónica es la generada por el enrutador, es decir, por la primera ruta coincidente de la colección sin la bandera OneWay. Por lo tanto, en la colección, enumeramos primero las rutas primarias.
La canonización la realiza el presentador, más en el capítulo canonización.
HTTPS
Para utilizar el protocolo HTTPS, es necesario activarlo en el alojamiento y configurar el servidor.
La redirección de todo el sitio a HTTPS debe realizarse a nivel de servidor, por ejemplo utilizando el archivo .htaccess en el directorio raíz de nuestra aplicación, con código HTTP 301. La configuración puede variar dependiendo del hosting y tiene un aspecto similar a este:
<IfModule mod_rewrite.c>
RewriteEngine On
...
RewriteCond %{HTTPS} off
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
...
</IfModule>
El router genera una URL con el mismo protocolo con el que se cargó la página, por lo que no es necesario configurar nada más.
Sin embargo, si excepcionalmente necesitamos que diferentes rutas se ejecuten bajo diferentes protocolos, lo pondremos en la máscara de ruta:
// Generará una dirección HTTP
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);
// Generará una dirección HTTPS
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
Debugging Router
La barra de enrutamiento mostrada en Tracy Bar es una herramienta útil que muestra una lista de rutas y también los parámetros que el enrutador ha obtenido de la URL.
La barra verde con el símbolo ✓ representa la ruta que coincide con la URL actual, las barras azules con los símbolos ≈ indican las rutas que también coincidirían con la URL si el verde no las superara. Vemos más adelante el presentador actual y la acción.
Al mismo tiempo, si hay una redirección inesperada debido a la canonicalización, es útil mirar en la barra redirect para ver cómo entendió originalmente el enrutador la URL y por qué redirigió.
Al depurar el enrutador, se recomienda abrir Herramientas de desarrollo en el navegador (Ctrl+Mayús+I o Cmd+Opción+I) y desactivar la caché en el panel Red para que las redirecciones no se almacenen en ella.
Rendimiento
El número de rutas afecta a la velocidad del router. Su número no debería exceder de unas pocas docenas. Si su sitio tiene una estructura de URL demasiado complicada, puede escribir un enrutador personalizado.
Si el enrutador no tiene dependencias, como en una base de datos, y su fábrica no tiene argumentos, podemos serializar su forma compilada directamente en un contenedor DI y así hacer la aplicación ligeramente más rápida.
routing:
cache: true
Enrutador personalizado
Las siguientes líneas están pensadas para usuarios muy avanzados. Puedes crear tu propio enrutador y, naturalmente, añadirlo a tu colección de rutas. El enrutador es una implementación de la interfaz Nette\Routing\Router con dos métodos:
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;
class MyRouter implements Nette\Routing\Router
{
public function match(HttpRequest $httpRequest): ?array
{
// ...
}
public function constructUrl(array $params, UrlScript $refUrl): ?string
{
// ...
}
}
El método match
procesa la $httpRequest actual, de la que se puede recuperar no sólo
la URL, sino también las cabeceras, etc., en un array que contiene el nombre del presentador y sus parámetros. Si no puede
procesar la petición, devuelve null. Al procesar la solicitud, debemos devolver al menos el presentador y la acción. El nombre
del presentador está completo e incluye cualquier módulo:
[
'presenter' => 'Front:Home',
'action' => 'default',
]
El método constructUrl
, por otro lado, genera una URL absoluta a partir de la matriz de parámetros. Puede
utilizar la información del parámetro $refUrl
, que es la URL actual.
Para añadir un enrutador personalizado a la colección de rutas, utilice add()
:
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
Uso separado
Por uso separado, nos referimos al uso de las capacidades del router en una aplicación que no utiliza Nette Application y presentadores. Casi todo lo que hemos mostrado en este capítulo se aplica a ella, con las siguientes diferencias:
- para las colecciones de rutas utilizamos la clase Nette\Routing\RouteList
- como clase de enrutador simple Nette\Routing\SimpleRouter
- como no existe el par
Presenter:action
, utilizamos la notación Advanced
Así que de nuevo crearemos un método que construirá un enrutador, por ejemplo
namespace App\Core;
use Nette\Routing\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute('rss.xml', [
'controller' => 'RssFeedController',
]);
$router->addRoute('article/<id \d+>', [
'controller' => 'ArticleController',
]);
// ...
return $router;
}
}
Si usas un contenedor DI, que es lo que recomendamos, añade de nuevo el método a la configuración y luego obtén el router junto con la petición HTTP del contenedor:
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
O crearemos los objetos directamente:
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
Ahora tenemos que dejar que el router para trabajar:
$params = $router->match($httpRequest);
if ($params === null) {
// no se encuentra ninguna ruta coincidente, enviaremos un error 404
exit;
}
// procesamos los parámetros recibidos
$controller = $params['controller'];
// ...
Y viceversa, usaremos el router para crear el enlace:
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());