Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Check site
run: ./scripts/build.sh check

- name: Build site
run: ./scripts/build.sh

- name: Check internal links
run: ./scripts/check-links.sh public scripts/link-allowlist.txt
2 changes: 1 addition & 1 deletion content/run/get-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ There are a few things in here worth calling out:

- `defradb.url: localhost:9181` is the Host client's _internal_ DefraDB API, not the Generator client's. Inside the Host client container, DefraDB binds to `9181` on `localhost`. The Generator client's API happens to use the same number because they're both DefraDB; we'll remap the published ports in the next step so they don't collide.
- `bootstrap_peers` is the only Generator-specific value. The Host client learns everything else (schemas, signed data) from the Generator client over P2P once it connects.
- `minimum_attestations: 1` means the Host client will serve data as soon as it has one signature on it. Production setups use higher values to require independent confirmation from multiple Generator clients. See the [Host overview](/run/run-a-host/) for more on attestations.
- `minimum_attestations: 1` means the Host client will serve data as soon as it has one signature on it. Production setups use higher values to require independent confirmation from multiple Generator clients. See the [Host overview](/run/run-a-host/hardware-requirements/) for more on attestations.
- `hub_base_url` points to ShinzoHub. We're not registering anything here, but the Host client expects the field to be present.

## Start the Host client
Expand Down
2 changes: 1 addition & 1 deletion content/run/operations/troubleshooting/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ The Generator client falls back to HTTP polling. Check that `GETH_WS_URL` is cor

### What is the difference between a Host and a Generator?

Generator clients are the write side: they read raw blocks from an execution node, sign the data, gossip it over P2P, and provide attestations to prove the data is correct. Host clients are the read side: they receive those signed primitives, verify them, run WebAssembly lens transforms to build Views, and serve the results to applications over GraphQL. A Generator client talks to a source-chain node, but a Host client never does. See the [Run a Host](/run/run-a-host/) overview for more.
Generator clients are the write side: they read raw blocks from an execution node, sign the data, gossip it over P2P, and provide attestations to prove the data is correct. Host clients are the read side: they receive those signed primitives, verify them, run WebAssembly lens transforms to build Views, and serve the results to applications over GraphQL. A Generator client talks to a source-chain node, but a Host client never does. See the [Run a Host](/run/run-a-host/hardware-requirements/) overview for more.

### Do I need to run my own Generator client to run a Host client?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ chown -R 1001:1001 /root/shinzo-data
| `GOMEMLIMIT=5GiB` | Go runtime soft memory limit; keep below `mem_limit`. Scale both up on bigger hosts. |

{% admonition(type="info") %}
See the [Generator client config reference](../../../run-a-generator/config-reference/index.md) for a detailed list of available configuration options.
See the [Generator client config reference](../../../run-a-generator/config-reference/) for a detailed list of available configuration options.
{% end %}

## Start the client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ On first startup, the Host downloads signed snapshot files from a trustless inde
## Prerequisites

- Docker installed on the VM.
- A trustless indexer serving snapshots over HTTP. The trustless indexer must have `SNAPSHOT_ENABLED=true` and be reachable over HTTPS or HTTP. See the [nginx with TLS scenario](/run/run-a-generator/deployment-examples/nginx-tls-snapshots/) for how to set up a trustless indexer that serves snapshots.
- A trustless indexer serving snapshots over HTTP. The trustless indexer must have `SNAPSHOT_ENABLED=true` and be reachable over HTTPS or HTTP. See the [nginx with TLS scenario](/run/run-a-generator/deployment-examples/nginx-with-snapshots/) for how to set up a trustless indexer that serves snapshots.
- The block range you want to bootstrap. The trustless indexer must have snapshot files covering that range.

## Config file
Expand Down Expand Up @@ -139,7 +139,7 @@ You should see log lines indicating snapshot downloads and imports, followed by

- Snapshot bootstrap only runs on first startup when DefraDB has no existing data. If the Host already has data for the requested block range, bootstrap is skipped.
- The `indexer_url` in the shipped `config.yaml` is `http://35.206.105.60:8080`. This is a development trustless indexer. Replace it with your own trustless indexer URL or a production trustless indexer that has snapshots enabled.
- The trustless indexer must have `SNAPSHOT_ENABLED=true` on the Generator side. If the trustless indexer is not producing snapshot files, the `/snapshots` endpoint will return nothing and bootstrap will fail. See the [nginx with TLS scenario](/run/run-a-generator/deployment-examples/nginx-tls-snapshots/) for setting up a trustless indexer that serves snapshots.
- The trustless indexer must have `SNAPSHOT_ENABLED=true` on the Generator side. If the trustless indexer is not producing snapshot files, the `/snapshots` endpoint will return nothing and bootstrap will fail. See the [nginx with TLS scenario](/run/run-a-generator/deployment-examples/nginx-with-snapshots/) for setting up a trustless indexer that serves snapshots.
- The `DEFRA_URL` env var overrides `defradb.url` at runtime and is read by the Host client (`config/config.go`). The `docker run` above does not set it, so the DefraDB URL comes from `defradb.url` in the YAML config. See [environment variables](/run/run-a-host/config-reference#environment-variables).
- The `DEFRA_KEYRING_SECRET` env var uses the `DEFRA_` prefix. The Generator client uses `DEFRADB_KEYRING_SECRET` with the `DEFRADB_` prefix. The two clients use different env var names for the same concept. See [environment variables](/run/run-a-host/config-reference#environment-variables).
- `LOG_LEVEL`, `LOG_SOURCE`, and `LOG_STACKTRACE` env vars appear in some deployment scripts but are not read by the Host client. They have been omitted from the `docker run` above. See [env vars that are not read](/run/run-a-host/config-reference#env-vars-that-are-not-read).
Expand Down
2 changes: 1 addition & 1 deletion content/run/run-a-host/install/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A Host client pulls primitive blockchain data from Generator clients, runs Lens

- **Docker** (for the Docker path), or **[Go 1.25+](https://go.dev/dl/) and Make** (to build from source).
- **Access to a running Generator client.** The Host client doesn't produce data itself. It receives primitive block data from a Generator client over libp2p and transforms it, so it needs at least one Generator client to sync from. The Generator client doesn't have to be your own, you just need one you can reach and its libp2p multiaddr. A public Generator client you can point at is coming; this page will link it here once it's live. Until then, run your own. The [Operator Quickstart](/run/run-a-host/quickstart/) walks through standing up a Generator client and a Host client together.
- **Hardware** that meets the [recommendations](/run/run-a-host/).
- **Hardware** that meets the [recommendations](/run/run-a-host/hardware-requirements/).

There are two ways to install: [Docker](#use-docker) (recommended) or [build from source](#build-from-source).

Expand Down
2 changes: 1 addition & 1 deletion content/understand/core-concepts/privacy/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A standard Host client ships configured for the public network. On startup it do
- Dials the public bootstrap peers shipped in `config.yaml` to reach Generator clients it doesn't control.
- Contacts ShinzoHub (`shinzo.hub_base_url`) to fetch every registered View and subscribe to live registration events.

A third step is opt-in: registering on the [Host registry](../register) so other nodes can discover and replicate from it. This is a manual, wallet-signed action from the registration app, not something the Host client does on startup.
A third step is opt-in: registering on the [Host registry](/run/run-a-host/register/) so other nodes can discover and replicate from it. This is a manual, wallet-signed action from the registration app, not something the Host client does on startup.

A private Host client connection closes some or all of these. How many you close decides how private the setup is.

Expand Down
2 changes: 1 addition & 1 deletion content/understand/how-it-works/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Generator clients are the entry point. Reserved for validators at mainnet launch

#### Hosts

Hosts are the workhorses. They receive primitives, maintain attestation records, run Views, and serve the resulting view documents to subscribers. Anyone can run a Host. See [Run a Host](/run/run-a-host/) for operational details and [Host Client reference](/reference/components/host-client/) for internals.
Hosts are the workhorses. They receive primitives, maintain attestation records, run Views, and serve the resulting view documents to subscribers. Anyone can run a Host. See [Run a Host](/run/run-a-host/hardware-requirements/) for operational details and [Host Client reference](/reference/components/host-client/) for internals.

#### Developers

Expand Down
25 changes: 25 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ Zola installation behavior:
- Otherwise the script downloads the Zola release matching `ZOLA_VERSION`.
- `ZOLA_VERSION` is read from the environment first, then from `wrangler.toml`.

#### Subcommands

- `./scripts/build.sh` (or `./scripts/build.sh build`): production build. This is
what Cloudflare Pages runs.
- `./scripts/build.sh check`: runs `zola check` (validates `@/` internal links
as errors per `[link_checker]` in `config.toml`, and external links as
warnings). Used by CI; does not write output.

### check-links.sh

Checks internal links in the built site output (`public/`). Unlike
`zola check`, which only validates `@/`-prefixed links, this script checks
**all** internal links regardless of syntax — absolute (`/path/`), relative
(`../path/`), and `@/`-prefixed — by inspecting the built HTML.

Run it after `zola build`:

```shell
./scripts/check-links.sh public
```

It verifies that every internal `<a href>` resolves to an existing file and
that fragment anchors (`#section`) match an `id` in the target page. Exits
non-zero if any broken links are found. Used by CI.

### generate-llms.sh

Generates `llms.txt` and `llms-full.txt` into the build output directory (defaults to `./public`).
Expand Down
23 changes: 18 additions & 5 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,24 @@ main() {

ensure_zola

echo "Building site with Zola..." >&2
zola build

echo "Generating llms.txt outputs..." >&2
"$SCRIPT_DIR/generate-llms.sh"
local cmd="${1:-build}"
case "$cmd" in
build)
echo "Building site with Zola..." >&2
zola build

echo "Generating llms.txt outputs..." >&2
"$SCRIPT_DIR/generate-llms.sh"
;;
check)
echo "Checking site with Zola..." >&2
zola check
;;
*)
echo "error: unknown command '$cmd' (expected: build|check)" >&2
return 1
;;
esac
}

main "$@"
220 changes: 220 additions & 0 deletions scripts/check-links.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
# Check internal links in built Zola site output.
#
# Walks all .html files in the output directory, extracts href attributes from
# <a> tags, and verifies that internal links resolve to existing files. Handles
# Zola's directory-style URL scheme (e.g. /path/ -> path/index.html) and
# verifies fragment anchors (#section) against id attributes in the target page.
#
# Usage: check-links.sh <output-dir> [allowlist]
# Exit: 0 if all links resolve, 1 if broken links are found, 2 on usage error.
#
# The optional allowlist file lists hrefs to suppress (one per line, # comments).
# Use it to keep CI green while known-broken links are being fixed; remove
# entries as they are fixed.

set -euo pipefail

export LC_ALL=C

readonly ANCHOR_RE='<[aA][[:space:]][^>]*href=("[^"]*"|'"'"'[^'"'"']*'"'"')'
readonly ENTITY_RE='&#(x[0-9a-fA-F]+|[0-9]+);'

usage() {
echo "usage: check-links.sh <output-dir> [allowlist]" >&2
exit 2
}

# Print $1 with numeric (&#NN; / &#xHH;) and the common named HTML entities
# decoded, mirroring what an HTML parser hands to the link checker.
decode_entities() {
local s="$1" ent num code oct ch
while [[ $s =~ $ENTITY_RE ]]; do
ent="${BASH_REMATCH[0]}"
num="${BASH_REMATCH[1]}"
if [[ $num == x* ]]; then
code=$((16#${num#x}))
else
code=$((10#$num))
fi
if ((code >= 0x20 && code <= 0x7e)); then
printf -v oct '%03o' "$code"
printf -v ch "\\$oct"
s="${s//"$ent"/$ch}"
else
break
fi
done
s="${s//&lt;/<}"
s="${s//&gt;/>}"
s="${s//&quot;/\"}"
s="${s//&apos;/\'}"
s="${s//&amp;/&}"
printf '%s' "$s"
}

# True if the file has an id attribute matching the fragment.
fragment_exists() {
grep -qF "id=\"$2\"" -- "$1" || grep -qF "id='$2'" -- "$1"
}

# Print $1 lexically normalized (resolves ../ segments, like Path.resolve()).
# Falls back to the input as-is when no parent directory exists to cd into.
normalize_path() {
local p="$1" base out
while [[ $p == */ && $p != "/" ]]; do p="${p%/}"; done
out="$(cd "$p" 2>/dev/null && pwd -P)" || out=""
if [[ -z $out ]]; then
base="${p##*/}"
p="${p%/*}"
if [[ -n $p ]]; then
out="$(cd "$p" 2>/dev/null && pwd -P)" || out=""
[[ -n $out ]] && out="$out/$base"
fi
fi
[[ -n $out ]] || out="$1"
printf '%s\n' "$out"
}

# Set RESOLVED to the file an internal target maps to, or leave it empty.
# Directory-style URLs map to <dir>/index.html; extension-less paths also
# try a sibling .html file.
resolve_target() {
RESOLVED=""
if [[ -f $1 ]]; then
RESOLVED="$1"
elif [[ -d $1 && -f $1/index.html ]]; then
RESOLVED="$1/index.html"
else
local base="${1##*/}"
if [[ $base != ?*.* && -f "$1.html" ]]; then
RESOLVED="$1.html"
fi
fi
}

main() {
if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
fi

OUTPUT_DIR="$(cd "$1" 2>/dev/null && pwd -P)" || {
echo "error: $1 is not a directory" >&2
exit 2
}

local ALLOW=()
if [[ $# -eq 2 ]]; then
if [[ ! -f $2 ]]; then
echo "error: allowlist file not found: $2" >&2
exit 2
fi
local line
while IFS= read -r line || [[ -n $line ]]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z $line || $line == \#* ]] && continue
ALLOW+=("$line")
done < "$2"
fi

local BROKEN=()
local checked=0 skipped=0 files_scanned=0
local html_file rel_src match line_no href path_part fragment
local target resolved norm show

while IFS= read -r html_file; do
files_scanned=$((files_scanned + 1))
if [[ ! -r $html_file ]]; then
echo "warning: could not read $html_file" >&2
continue
fi
rel_src="${html_file#"$OUTPUT_DIR"/}"

while IFS= read -r match; do
line_no="${match%%:*}"
href="${match#*:}"
href="${href#*href=?}"
href="${href%?}"
[[ -n $href ]] || continue
href="$(decode_entities "$href")"

case $href in
http://*|https://*|mailto:*|tel:*|javascript:*|data:*|ftp://*|file:*) continue ;;
esac

if [[ ${#ALLOW[@]} -gt 0 ]]; then
local allowed=0 entry
for entry in "${ALLOW[@]}"; do
[[ $entry == "$href" ]] && { allowed=1; break; }
done
if [[ $allowed -eq 1 ]]; then
skipped=$((skipped + 1))
continue
fi
fi

path_part="${href%%#*}"
if [[ $href == *"#"* ]]; then fragment="${href#*#}"; else fragment=""; fi

if [[ -z $path_part ]]; then
if [[ -n $fragment ]] && ! fragment_exists "$html_file" "$fragment"; then
BROKEN+=(" $rel_src:$line_no: $href (missing #$fragment)")
fi
continue
fi

checked=$((checked + 1))

if [[ $path_part == /* ]]; then
while [[ $path_part == /* ]]; do path_part="${path_part#/}"; done
if [[ -n $path_part ]]; then
target="$OUTPUT_DIR/$path_part"
else
target="$OUTPUT_DIR"
fi
else
target="${html_file%/*}/$path_part"
fi
while [[ $target == */ && $target != "/" ]]; do target="${target%/}"; done

resolve_target "$target"

if [[ -z $RESOLVED ]]; then
if [[ $path_part == /* || $href == /* ]]; then
norm="$target"
else
norm="$(normalize_path "$target")"
fi
if [[ $norm == "$OUTPUT_DIR" ]]; then
show="."
elif [[ $norm == "$OUTPUT_DIR"/* ]]; then
show="${norm#"$OUTPUT_DIR"/}"
else
show="$norm"
fi
BROKEN+=(" $rel_src:$line_no: $href (not found: $show)")
elif [[ -n $fragment ]] && ! fragment_exists "$RESOLVED" "$fragment"; then
BROKEN+=(" $rel_src:$line_no: $href (missing #$fragment)")
fi
done < <(grep -noE "$ANCHOR_RE" -- "$html_file" || true)
done < <(find "$OUTPUT_DIR" -type f -name '*.html' | sort)

local summary="Checked $checked internal links across $files_scanned HTML files."
[[ $skipped -gt 0 ]] && summary="$summary Skipped $skipped allowlisted."
echo "$summary"

if [[ ${#BROKEN[@]} -gt 0 ]]; then
echo "" >&2
echo "Broken link(s) (${#BROKEN[@]}):" >&2
local entry
for entry in "${BROKEN[@]}"; do
echo "$entry" >&2
done
exit 1
fi

echo "All internal links resolve."
}

main "$@"
7 changes: 7 additions & 0 deletions scripts/link-allowlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Known broken internal links — fix and remove from this file.
#
# Currently empty. Add hrefs to suppress (one per line, # comments) to keep CI
# green while known-broken links are being fixed. Remove entries as they are
# fixed so the checker re-enforces them.
#
# See scripts/check-links.sh for the checker that consumes this file.
6 changes: 3 additions & 3 deletions templates/macros/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ <h4 class="footer__get-started-heading">Get started building with Shinzo</h4>
<li class="footer__nav-column">
<div class="footer__column-title">Who it's for</div>
<ul class="footer__column-links">
<li><a class="footer__link" href="/run/run-a-generator/">Chain Validators</a></li>
<li><a class="footer__link" href="/run/run-a-host/">Data Hosts</a></li>
<li><a class="footer__link" href="/run/run-a-generator/hardware-requirements/">Chain Validators</a></li>
<li><a class="footer__link" href="/run/run-a-host/hardware-requirements/">Data Hosts</a></li>
<li><a class="footer__link" href="/build/create-a-view/">Builders</a></li>
<li><a class="footer__link" href="/understand/core-concepts/">Core concepts</a></li>
<li><a class="footer__link" href="/understand/core-concepts/views/">Core concepts</a></li>
<li><a class="footer__link" href="/understand/get-help/">Get help</a></li>
</ul>
</li>
Expand Down
Loading