Skip to content

Commit

Permalink
feat(gateway): improved templates, error template
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed May 26, 2023
1 parent 8ec71db commit 3203992
Show file tree
Hide file tree
Showing 33 changed files with 1,162 additions and 1,233 deletions.
23 changes: 8 additions & 15 deletions gateway/assets/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
# Required Assets for the Gateway

> DAG and Directory HTML for HTTP gateway
> HTTP Gateway Templates.
## Updating

When making updates to the templates, please note the following:

1. Make your changes to the (human-friendly) source documents in the `src` directory.
2. Before testing or releasing, go to `assets/` and run `go generate .`.
To update the templates, make changes to the files in `html/` and `css/`.

## Testing

1. Make sure you have [Go](https://golang.org/dl/) installed
2. Start the test server, which lives in its own directory:

```bash
> cd test
> go run .
```
1. Make sure you have [Go](https://golang.org/dl/) installed.
2. From the `assets/` directory, start the test server: `go run test/main.go`.

This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. Here you have two pages:
This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. Here you have three pages:

- [`localhost:3000/dag`](http://localhost:3000/dag) for the DAG template preview; and
- [`localhost:3000/directory`](http://localhost:3000/directory) for the Directory template preview.
- [`localhost:3000/directory`](http://localhost:3000/directory) for the Directory template preview; and
- [`localhost:3000/error?code=500`](http://localhost:3000/error?status=500) for the Error template preview, you can replace `500` by a different status code.

If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `go generate .` to generate the minified artifact that the test is looking for.
Every time you refresh, the template will be reloaded.
64 changes: 25 additions & 39 deletions gateway/assets/assets.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//go:generate ./build.sh
package assets

import (
Expand All @@ -9,24 +8,23 @@ import (
"strconv"

"html/template"
"net/url"
"path"
"strings"

"github.com/cespare/xxhash/v2"

ipfspath "github.com/ipfs/boxo/path"
)

//go:embed dag-index.html directory-index.html knownIcons.txt
var asset embed.FS
//go:embed *.html *.css
var assets embed.FS

// AssetHash a non-cryptographic hash of all embedded assets
var AssetHash string

var (
DirectoryTemplate *template.Template
DagTemplate *template.Template
ErrorTemplate *template.Template
)

func init() {
Expand All @@ -36,7 +34,7 @@ func init() {

func initAssetsHash() {
sum := xxhash.New()
err := fs.WalkDir(asset, ".", func(path string, d fs.DirEntry, err error) error {
err := fs.WalkDir(assets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand All @@ -45,7 +43,7 @@ func initAssetsHash() {
return nil
}

file, err := asset.Open(path)
file, err := assets.Open(path)
if err != nil {
return err
}
Expand All @@ -61,60 +59,48 @@ func initAssetsHash() {
}

func initTemplates() {
knownIconsBytes, err := asset.ReadFile("knownIcons.txt")
var err error

// Directory listing template
DirectoryTemplate, err = BuildTemplate(assets, "directory.html")
if err != nil {
panic(err)
}
knownIcons := make(map[string]struct{})
for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") {
knownIcons[ext] = struct{}{}
}

// helper to guess the type/icon for it by the extension name
iconFromExt := func(name string) string {
ext := path.Ext(name)
_, ok := knownIcons[ext]
if !ok {
// default blank icon
return "ipfs-_blank"
}
return "ipfs-" + ext[1:] // slice of the first dot
}

// custom template-escaping function to escape a full path, including '#' and '?'
urlEscape := func(rawUrl string) string {
pathURL := url.URL{Path: rawUrl}
return pathURL.String()
}

// Directory listing template
dirIndexBytes, err := asset.ReadFile("directory-index.html")
// DAG Index template
DagTemplate, err = BuildTemplate(assets, "dag.html")
if err != nil {
panic(err)
}

DirectoryTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{
"iconFromExt": iconFromExt,
"urlEscape": urlEscape,
}).Parse(string(dirIndexBytes)))

// DAG Index template
dagIndexBytes, err := asset.ReadFile("dag-index.html")
// Error template
ErrorTemplate, err = BuildTemplate(assets, "error.html")
if err != nil {
panic(err)
}
}

DagTemplate = template.Must(template.New("dir").Parse(string(dagIndexBytes)))
type GlobalData struct {
SupportURL string
}

type DagTemplateData struct {
GlobalData
Path string
CID string
CodecName string
CodecHex string
}

type ErrorTemplateData struct {
GlobalData
StatusCode int
StatusText string
Error string
}

type DirectoryTemplateData struct {
GlobalData
GatewayURL string
DNSLink bool
Listing []DirectoryItem
Expand Down
14 changes: 0 additions & 14 deletions gateway/assets/build.sh

This file was deleted.

67 changes: 0 additions & 67 deletions gateway/assets/dag-index.html

This file was deleted.

33 changes: 33 additions & 0 deletions gateway/assets/dag.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="A DAG hosted on IPFS: {{ .Path }}.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
<title>{{ .Path }}</title>
<link rel="stylesheet">
</head>
<body>
<header></header>
<main id="main">
<header>
<div>
<strong>CID: <code translate="no">{{.CID}}</code></strong>
</div>
<div>
<strong>Codec</strong>: <code translate="no">{{.CodecName}} ({{.CodecHex}})</code>
</div>
</header>
<div>
<p>Download as:</p>

<ul>
<li><a href="?format=raw" rel="nofollow">Raw Block</a> (no conversion)</li>
<li><a href="?format=dag-json" rel="nofollow">Valid DAG-JSON</a> (specs at <a href="https://ipld.io/specs/codecs/dag-json/spec/" target="_blank" rel="noopener noreferrer">IPLD</a> and <a href="https://www.iana.org/assignments/media-types/application/vnd.ipld.dag-json" target="_blank" rel="noopener noreferrer">IANA</a>)</li>
<li><a href="?format=dag-cbor" rel="nofollow">Valid DAG-CBOR</a> (specs at <a href="https://ipld.io/specs/codecs/dag-cbor/spec/" target="_blank" rel="noopener noreferrer">IPLD</a> and <a href="https://www.iana.org/assignments/media-types/application/vnd.ipld.dag-cbor" target="_blank" rel="noopener noreferrer">IANA</a>)</li>
</ul>
</div>
</main>
</body>
</html>
99 changes: 0 additions & 99 deletions gateway/assets/directory-index.html

This file was deleted.

74 changes: 74 additions & 0 deletions gateway/assets/directory.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!DOCTYPE html>
{{ $root := . }}
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="A directory of content-addressed files hosted on IPFS.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
<title>{{ .Path }}</title>
<link rel="stylesheet">
</head>
<body>
<header></header>
<main id="main">
<header class="flex flex-wrap">
<div>
<strong>
Index of
{{ range .Breadcrumbs -}}
/{{ if .Path }}<a href="{{ $root.GatewayURL }}{{ .Path | urlEscape }}">{{ .Name }}</a>{{ else }}{{ .Name }}{{ end }}
{{- else }}
{{ .Path }}
{{ end }}
</strong>
{{ if .Hash }}
<div class="ipfs-hash" translate="no">
{{- .Hash -}}
</div>
{{ end }}
</div>
{{ if .Size }}
<div class="nowrap flex-shrink ml-auto">
<strong title="Cumulative size of IPFS DAG (data + metadata)">&nbsp;{{ .Size }}</strong>
</div>
{{ end }}
</header>
<div class="table-responsive">
<table>
{{ if .BackLink }}
<tr>
<td class="type-icon">
<div class="ipfs-_blank">&nbsp;</div>
</td>
<td>
<a href="{{.BackLink | urlEscape}}">..</a>
</td>
<td></td>
<td></td>
</tr>
{{ end }}
{{ range .Listing }}
<tr>
<td class="type-icon">
<div class="{{iconFromExt .Name}}">&nbsp;</div>
</td>
<td>
<a href="{{ .Path | urlEscape }}">{{ .Name }}</a>
</td>
<td class="nowrap">
{{ if .Hash }}
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.tech/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
{{ .ShortHash }}
</a>
{{ end }}
</td>
<td class="nowrap" title="Cumulative size of IPFS DAG (data + metadata)">{{ .Size }}</td>
</tr>
{{ end }}
</table>
</div>

</main>
</body>
</html>
55 changes: 55 additions & 0 deletions gateway/assets/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="A {{ .StatusCode }} {{ .StatusText }} error has occurred when trying to fetch content from the IPFS network.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
<title>{{ .StatusCode }} {{ .StatusText }}</title>
<link rel="stylesheet">
</head>
<body>
<header></header>
<main id="main">
<header>
<strong>{{ .StatusCode }} {{ .StatusText }}</strong>
</header>

<div>
{{ if eq .StatusCode 400 }}
<p>Your request is invalid. Please check the error below for more information.</p>
{{ else if eq .StatusCode 404 }}
<p>The content path you requested cannot be found. There's likely an invalid or missing DAG node.</p>
{{ else if eq .StatusCode 406 }}
<p>This gateway is unable to return the data in the format requested by the client.</p>
{{ else if eq .StatusCode 410 }}
<p>This gateway refuses to return the requested data.</p>
{{ else if eq .StatusCode 412 }}
<p>This gateway is unable to return the requested data under the conditions sent by the client.</p>
{{ else if eq .StatusCode 429 }}
<p>You have made too many requests. Please wait some time and try again.</p>
{{ else if eq .StatusCode 451 }}
<p>This gateway is not allowed to return the requested data due to legal reasons.</p>
{{ else if eq .StatusCode 500 }}
<p>This gateway was unable to return the requested data due to an internal error. Please check the error below for more information.</p>
{{ else if eq .StatusCode 502 }}
<p>The gateway backed was unable to fullfil your request due to an error.</p>
{{ else if eq .StatusCode 504 }}
<p>The gateway backend was unable to fullfil your request due to a timeout.</p>
{{ end }}

<pre class="terminal wrap">{{ .Error }}</pre>

<p>How you can proceed:</p>
<ul>
<li>Check the <a href="https://discuss.ipfs.tech/c/help/13" rel="noopener noreferrer">Discussion Forums</a> for similar errors.</li>
<li>Try diagnosing your request with the <a href="https://docs.ipfs.tech/reference/diagnostic-tools/" rel="noopener noreferrer">diagnostic tools</a>.</li>
<li>Self-host and run an <a href="https://docs.ipfs.tech/concepts/ipfs-implementations/" rel="noopener noreferrer">IPFS client</a> that verifies your data.</li>
{{ if or (eq .StatusCode 400) (eq .StatusCode 404) }}
<li>Inspect the <a href="https://cid.ipfs.tech/" rel="noopener noreferrer">CID</a> or <a href="https://explore.ipld.io/" rel="noopener noreferrer">DAG</a>.</li>
{{ end }}
</ul>
</div>
</main>
</body>
</html>
12 changes: 12 additions & 0 deletions gateway/assets/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<header id="header">
<div class="ipfs-logo">&nbsp;</div>
<nav>
<a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About<span class="dn-mobile"> IPFS</span></a>
<a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install<span class="dn-mobile"> IPFS</span></a>
{{ with .SupportURL }}
<a href="{{ . }}" target="_blank" rel="noopener noreferrer" title="Support">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.4 21"><circle cx="7.5" cy="4.8" r="1"/><circle cx="11.1" cy="4.8" r="1"/><path d="M12.7 8.4c-0.5-1.5-1.9-2.5-3.5-2.5 -1.6 0-3 1-3.5 2.5H12.7z"/><path d="M8.5 9.7H5c-0.5 0.8-0.7 1.7-0.7 2.7 0 2.6 1.8 4.8 4.2 5.2V9.7z"/><path d="M13.4 9.7H9.9v7.9c2.4-0.4 4.2-2.5 4.2-5.2C14.1 11.4 13.9 10.5 13.4 9.7z"/><circle cx="15.7" cy="12.9" r="1"/><circle cx="15.1" cy="15.4" r="1"/><circle cx="15.3" cy="10.4" r="1"/><circle cx="2.7" cy="12.9" r="1"/><circle cx="3.3" cy="15.4" r="1"/><circle cx="3.1" cy="10.4" r="1"/></svg>
</a>
{{ end }}
</nav>
</header>
Loading

0 comments on commit 3203992

Please sign in to comment.