Generating websites with SPARQL and Snowman, part 2

With Rhizome's excellent ArtBase SPARQL endpoint.

1+1+1+1+1+1+1+1+1+1+1+1 by Grégory Chatonsky

In part one of this two-part series, we saw how the open source Snowman static web site generator can generate websites with data from a SPARQL endpoint. I showed how I created a sample website project with its snowman new command and then reconfigured the project to retrieve a list of artists from the Rhizome ArtBase endpoint, a repository of data about digital artworks since 1999. Here in part two I will build on that to add lists of artists’ works with links to Rhizome pages about them.

Add lists of artists works with links to more information

To add these lists of the artists’ works under their names, I started by removing the last two lines of the project’s views.yaml file (which was generated by the original snowman new command) and the templates/static.html file that the last line pointed to because I didn’t need that additional view:

  - output: "index.html"
    query: "index.rq"
    template: "index.html"
  - output: "static/index.html"
    template: "static.html"

The Snowman github readme file tells you more about views.

A lot of incremental development in Snowman consists of adding to a query such as the queries/index.rq one that I started editing in part one and then editing the corresponding display template to take advantage of the new parts of the query. I gradually worked the query in queries/index.rq up to the following. It asks for artist names and their works that have “Flash” in their list of tags:

PREFIX rt: <>
SELECT DISTINCT ?artistName ?artist ?searchTag WHERE {
   BIND("Flash" AS ?searchTag)
   ?artwork rt:P29 ?artist . 
   ?artist rdfs:label ?artistName .
   ?artwork rt:P48 ?artbaseLegacyTags .
   # Compare lower-case versions of both to make it case-insensitive
   FILTER CONTAINS(LCASE(?artbaseLegacyTags),LCASE(?searchTag))
ORDER BY (?artistName)

A few notes about this query:

  • An artwork’s rt:P48 value is a comma-delimited list of tags that have been assigned to it. (Some artworks in the dataset do not have tags assigned, so because this triple pattern is not optional, this query would not retrieve any of those.)
  • I learned about which properties (such as rt:P48) do what mostly through exploratory queries and guesswork. Visiting URLs like would then show me how good my guesses were.
  • I could have just put "Flash" as the second parameter to CONTAINS() in the FILTER line instead of storing it in a ?searchTag variable and referencing that. Storing it in a variable at the top of the query made it easier to change to other values to look for other kinds of works, as we’ll see below.

In the followup to the query revision above, the new version of the template/index.html display template shown below has three new things:

  • A slightly different title.
  • The artist name in an h2 subhead element with the search value (for example, “Flash”) appended.
  • A Snowman include function to insert more content. It names an HTML template to format the inserted content, a query to generate values for the new template, and a parameter to pass to the query: the ?artist value (a URL) retrieved by the main query above.
{{ template "base" . }}
{{ define "title" }}Rhizome Artbase Artists and Works {{ end }}

{{ define "content" }}
<h1>Rhizome Artbase Artists and Works</h1>
    {{ range . }}
<h2>Artist: {{ .artistName }} ({{ .searchTag }} and other works)</h2>
{{ include "artistsWorks.html" (query "artistsWorks.rq" .artist.String) }}
{{ end }}
{{ end }}

Below is the queries/artistsWorks.rq query referenced by the include function above. The artist value passed to it by the template above is plugged in using the <{{.}}> construct, which I believe is Go template syntax. (I tried to learn more about it, but It’s difficult to do web searches for strings like that. You can see another demonstration of it in Snowman’s inline-queries example project.)

PREFIX rt: <>
SELECT DISTINCT ?workTitle ?creationDate ?artworkPage ?artbaseLegacyTags WHERE {
  # r:Q676 is the artist Andy Cox if I need to sub it in next line for testing
  ?artwork rt:P29  <{{.}}> ;    # artwork by artist
           rdfs:label ?workTitle;
           rt:P26 ?creationDateTime .
  OPTIONAL { ?artwork rt:P48 ?artbaseLegacyTags . }
  # Don't need full ISO date value; just yyyy-mm-dd
  BIND(SUBSTR(str(?creationDateTime),1,10) AS ?creationDate)
  ?artworkPage schema:about ?artwork;
               schema:isPartOf <>.
ORDER BY (?workTitle)

I left the qname for one of the artists in a comment near the top of the query because I sometimes replaced the <{{.}}> with that qname when working out other parts of the query logic.

Remember that the include function mentioned both this new queries/artistsWorks.rq file and the template file that goes with it to format the result: template/artistsWorks.html.

  <tr><th width="200">title</th><th width="100">creation date</th><th>tags</th></tr>
    {{ range . }}
      <td><a href='{{ .artworkPage }}'>{{ .workTitle}}</a></td>
      <td>{{ .creationDate}}</td>
      <td>{{ .artbaseLegacyTags }}</td>
        {{ end }}

This creates a table for each artist’s works with a row for each one. The first cell of each row uses the URL stored in the ?artworkPage value retrieved by the artistsWorks.rq query to create a link to that page—for example, to this page for one of the retrieved works.

Once the additions and modifications have been made to the files described so far, a snowman build creates a new site/index.html file with the work lists under each artist’s name.

Looking more stylish

I added some simple CSS, but first, in the templates/layouts/default.html file in the project, I took the / out of the following line so that the generated index.html file would look for style.css in the same directory:

<link rel="stylesheet" href="/style.css">

There was already a style.css file in the project’s static directory. I replaced its contents with the following minimal CSS:

{ font-family: arial,helvetica; font-size:12pt; }

body {
    margin: .25in .5in .25in .5in; /* t,r,b,l */
    font-family: arial,helvetica; 

th {
    text-align: left;
    background: lightgray;

Another snowman build then created a version of the page that looks like the one that I previewed in part one of this series:

Preview of Snowman ArtBase project

Query for 3D works

I mentioned how I stored the string “Flash” in the ?searchTag variable of the queries/index.rq query to make it easier to have this query search for artwork tagged with other values. After changing this variable’s value to “3D” and doing another build, the top of the index.html file looked like this:

Snowman ArtBase project listing 3D works

The image at the top of this blog entry is from 1+1+1+1+1+1+1+1+1+1+1+1 by by Grégory Chatonsky, one of the works tagged as 3D.

The search for the keyword is just a simple substring search of the CSV list. If a work had been tagged with “3DogNight”, that also would have been retrieved in the search for “3D”. For a more serious search of tags, I would make a copy of the keyword list that was not only all lower-case but also had spaces removed and began and ended with commas “,like,this,”. Then, a search of that for a version of ?searchTag enclosed by commas such as “,3d,” would be more accurate.

Possible next steps

I zipped up my artbase snowman project and made it available so that you can unzip it in your own Snowman examplesdirectory and try it out. The “Getting started with Snowman” section of the Snowman home page has brief descriptions of other projects included in that examples directory as part of the Snowman distribution. Each of those demonstrates other features that you can incorporate into your own Snowman website projects. One included example that is not listed there is nested-lists-with-single-query, which shows a way to “render nested lists without the need of multiple queries or views”.

You can apply these features to you own copy of my artbase project, or you can apply them to you own new Snowman projects that you create with snowman new. Let me know how it turns out!

Comments? Reply to my tweet announcing this blog entry.