Routes in Sandhill
Sandhill processes page requests by looking at the path provided in the URL. The path of the URL is the part between the hostname and the arguments.
For example, with the URL:
The path would be: /collection/42
If Sandhill has a route defined that matches a requested path, it would proceed to render the appropriate page based on the defined route.
Defining a Route
Routes are defined as JSON files inside your instance directory (see the guide on
setting up your Sandhill instance). Specifically, route files should
be placed in instance/config/routes/
. Sandhill will automatically load all .json
files placed here when it starts.
A Simple Example
A simple example route file might look like this:
{
"routes": [
"/collections/",
"/browse/"
],
"template": "collections.html"
}
In the above definition, going to either URL for your site:
Would serve up the collections.html
file from your instance templates directory.
Per the standard setup, all HTML templates should be placed in instance/templates/
.
Route definitions follow the Flask routing mechanics. Flask is the framework which underpins Sandhill.
Variables in Route Path
This supports putting variables into the path such as
/collections/<int:collection_id>/
which can be referenced
while the page is processing. In Sandhill, these path arguments are made available
by the view_args
variable; in such that view_args.collection_id
would be
the value 42
if the path was /collections/42/
.
Flask provides the following variable types:
type | example path | description |
---|---|---|
string |
/list/<string:namespace>/ |
Any string of text without slashes (/ ) |
int |
/db/<int:db>/row/<int:row> |
Positive integer values |
float |
/round/<float:num> |
Positive decimal values |
path |
/dir/<path:subdir> |
Any string, including slashes (/ ) |
uuid |
/account/<uuid:account>/ |
A Universally unique identifier string |
Route with Data Processors
A core feature of Sandhill is how it handles loading additional data while it processes a route. These additional actions are defined in the same route file and call upon Sandhill data processors to perform specific actions.
A data processor can load dynamic data from a datastore or manipulate already loaded data to prepare it for output to the user.
Sandhill comes with a number of builtin data processors that can be used immediately, though with some knowledge of Python programming you can also define your own data processors.
Example with a Data Processor
Here's a basic route which uses a data processor to query a Solr service and retrieve a record:
{
"route": [
"/hello/<int:user_id>"
],
"methods": ["GET", "POST"],
"template": "hello.html.j2",
"data": [
{
"name": "user",
"processor": "solr.select_record",
"params": { "q": "user_id:{{ view_args.user_id }}" }
}
]
}
With the HTML template file containing Jinja2 syntax:
<!DOCTYPE html>
<html>
<body>
<h1>Hello to {{ user.firstname }} {{ user.lastname }}!</h1>
<p>{{ user.firstname }}'s user_id is {{ user.user_id }}.</p>
</body>
</html>
A request to https://example.edu/hello/33
might produce and response like:
<!DOCTYPE html>
<html>
<body>
<h1>Hello to Aaron Zahn!</h1>
<p>Aaron's user_id is 33.</p>
</body>
</html>
In this case, the solr.select_record
data processor automatically pulls the
appropriate Solr API base URL from either the instance/sandhill.cfg
file or
from an environment variable, both named SOLR_URL
. More details on the
solr
data processor is available in the
data processor documentation
and details on how to configure Sandhill is available in the
instance setup documentation.
Jinja Templating
Regarding the HTML template, Sandhill makes full use of the Jinja2 templating engine for rendering pages.
It's not just HTML templates that are Jinja processed. Data processor definitions in the route config files are Jinja rendered as well. So the following is possible:
{
"route": [
"/<string:namespace>/<int:id>"
],
"template": "record.html.j2",
"data": [
{
"name": "record",
"processor": "solr.select_record",
"params": { "q": "rec:{{ view_args.namespace }}-{{ view_args.id }}" }
},
{
"name": "parent",
"processor": "solr.select_record",
"params": { "q": "rec:{{ record.parent_rec }}" }
}
]
}
Notice how use of Jinja is available in the JSON data
section. Jinja expressions
are rendered on a per request basis. Sandhill always provides
the view_args
variable for use of route path variables. Beyond that, any variables
defined by a data processor also becomes available after that processor has run.
In the previous example, notice how the second data processor is referencing the result
of the first data processor (i.e. record
).
This is possible because data processor definitions are rendered in Sandhill sequentially. Each data processor is able to make use of data created by the processors defined before it.
Mixing Jinja and JSON
If use of Jinja expressions results in invalid JSON, the route will become unparsable. Be careful when using Jinja in JSON and ensure your output is JSON compatible. If your route is failing to load, be sure to check the Sandhill logs.
Advanced Examples
Conditional Data Processors and Implied Template
Here is a more advanced route example that shows how you can use the
when
attribute.
{
"route": [
"/item/<int:id>"
],
"data": [
{
"name": "render-id1",
"processor": "template.render",
"file": "item1.html.j2",
"when": "{{ view_args.id == 1 }}"
},
{
"name": "render",
"processor": "template.render",
"file": "item.html.j2"
}
]
}
template
attribute provided.
When a data processor returns a
FlaskResponse
or
WerkzeugReponse,
Sandhill will stop processing any further data processors and return that response immediately.
In fact, specifying the template
attribute is really just a shorthand method of appending the
template.render
processor as seen above.
The when
condition in the example is showing how you can conditionally have
a data processor excluded from running when a page is loaded if the when
condition is True
.
In this case the condition is based on one of the route URL variables (view_args.id
),
but all loaded data is available. A when
is considered True
if the value would be considered
true when evaluated in Python.
Error Handling
By default, Sandhill will not stop processing a request if a data processor fails or
returns no data. When this is not desired, the on_fail
may be set for any given
data processor.
Setting the on_fail
to a HTTP status code integer
will cause Sandhill to abort with that code should that data processor fail to return any data.
{
"route": [
"/item/<int:id>"
],
"template": "record.html.j2",
"data": [
{
"name": "record",
"processor": "solr.select_record",
"params": { "q": "ID:{{ view_args.id }}" },
"on_fail": 404
}
]
}
With the above configuration, if the call to Solr fails for any reason,
a 404 error will be returned and the abort template will be displayed instead.
When the on_fail
is not set and a failure occurs in a data processor, those
errors are simply recorded in the logs and the next processor is loaded.
Setting this attribute allows more fine-grained control over what processors
are critical to a page loading and what error is appropriate for those errors.
Some data processors have the ability to return
the HTTP code of its choice. For example, if the data processor is making an
external API call and you'd prefer it to pass back the HTTP code from the API call
on failure. If the data processor supports it, setting on_fail
to 0
will
accomplish this. The 0
value indicates that Sandhill should abort page processing
on a failure, but leave the selection of HTTP code up to the data processor.
Route Config Attributes
This section contains a summary of the available attributes for route definitions for
quick reference. But more details on any of these attributes are found above.
For full details on the data
section, see the data processors documentation.
Name | Type | Description |
---|---|---|
route /routes |
string, or list of strings | The URL pattern to match in order for this route to be selected. Both names accept either string or list. |
method /methods |
string, or list of strings | The request method to permit (e.g. GET or POST ); default GET . Both names accept either string or list. |
template |
string, optional | The name of the Jinja2 template file to attempt to render |
data |
list of JSON entries, optional | An ordered list of data processors, with each one being run in order |