diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6fed7f..11c8bad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
### Added
+* **SEO and link-preview metadata for the published site (#479, #480, #483).** The homepage now ships Open Graph and Twitter Card tags with a generated 1200×630 brand card (`src/homepage/og-card.png`), a canonical URL, an inline SVG favicon, a `SoftwareApplication` JSON-LD block, and a keyword-forward `
`. The Pages build (`scripts/build_pages_site.sh`) additionally emits a root `robots.txt` referencing both the root and docs sitemaps and a root `sitemap.xml` covering the homepage, and publishes the social card. PyPI keywords in `pyproject.toml` expanded from 6 to 19 discovery terms. Closes #479, #480, #483.
+
* **`canarchy tui` is now a full-screen Textual app that streams the CAN bus live.** The TUI was a line-oriented shell that reprinted six panes after each command finished — it had no live/async streaming (`/capture` ran to completion then dumped a batch), no interactivity (no pane focus, filter, sort, row navigation, or scrollback), and hard-coded tiny backlogs. It is now a full-screen [Textual](https://textual.textualize.io/) application whose headline feature is watching the bus in real time. The engine/view split is preserved: the deterministic fold layer in `src/canarchy/tui.py` (`TuiState`, `_update_state`, and per-pane row extractors) stays as the tested model, a new `src/canarchy/tui_app.py` consumes it as the view, and a new `src/canarchy/tui_capture.py` `CaptureSession` streams frames through the existing `LocalTransport.capture_stream_events` generator on a daemon thread + queue (mirroring the transport gateway pattern), drained on the UI loop so one frame at a time flows through the same folding logic batch results use. Panes (Bus Status, Live Traffic, Decoded Signals, J1939 summary ribbon + table, UDS, an append-only Alerts log) are interactive Textual widgets: `/capture ` starts a background live capture and `/stop` (or the `x` key) ends it; `/filter [text]` and `/sort [column]` filter and sort a pane; arrow keys navigate rows; `[`/`]` resize the operator-controlled backlog; `space` pauses the live feed; `c` clears; `ctrl+f` maximizes the focused pane. The command entry still runs real CANarchy commands and slash hotkeys through the shared parser, and the nested-front-end guard (`TUI_COMMAND_UNSUPPORTED`) is retained. `textual` is now a core dependency. **Breaking:** the interactive-only rewrite retires the text-mode shell and the one-shot `tui --command` mode; the TUI requires an interactive terminal and a non-TTY invocation prints guidance and exits non-zero — use the individual commands for scripted/automation runs. New Pilot-based app tests (`tests/test_tui_app.py`) and `CaptureSession` unit tests (`tests/test_tui_capture.py`) drive the scaffold backend's deterministic frame stream; the fold-layer snapshot tests are unchanged. A remaining follow-up: a finite-timeout `capture_stream` loop so live capture stops instantly on real hardware.
* **`datasets convert`/`stream` gain a `decoded-signal-csv` source format for pre-decoded per-ID signal CSVs like SynCAN (#461).** `datasets convert`/`stream` only understood raw arbitration-ID + payload-byte sources (`hcrl-csv`, `candump`, `comma-rlog`), but a meaningful class of public CAN intrusion-detection/ML datasets — SynCAN being a well-known MIT-licensed example — publish data as pre-decoded, normalized per-ID signal values (`Label,Time,ID,Signal1_of_ID,...`) rather than frames. Ingesting them previously meant hand-rolling a one-off byte-packing script. A new `--source-format decoded-signal-csv` (also a `datasets_convert` MCP enum value) handles this shape: it auto-detects the `Time`/`ID`/optional `Label` columns plus any per-ID signal columns, maps each row's `ID` token to a deterministic arbitration ID (`0x`-hex, plain decimal, a trailing integer like `id10` -> `10`, else a stable SHA-1-derived 11-bit ID), and packs the present normalized signal values into payload bytes as big-endian uint16 (`round(value * 0xFFFF)`, clamped), skipping empty signal cells. Real `Time` and `Label` are preserved — `Label` surfaces as `payload.label` in JSONL output, like `hcrl-csv`. The reconstructed payload bytes are synthetic (derived from normalized signal values, not captured verbatim), so the output suits frame-level analysis (`stats`, `re_*`, `compare`) with real timing/ID structure rather than byte-exact ECU replay; this is documented inline. The adapter (`src/canarchy/dataset_convert.py`) is pure and streaming, with new unit/CLI tests (`tests/test_dataset_provider.py`) over a SynCAN-style fixture and updated command/design specs. Closes #461.
* **`fuzz identify` — narrow a fuzz log to the culprit frame by replaying bisected windows (#464).** After a fuzz campaign or a captured anomaly, an operator knows *that* some frame in a log caused an effect but not *which* one; CANarchy had replay/fuzz/guided-fuzz primitives but no explicit narrowing workflow (CaringCaribou's `fuzzer identify`). A new `canarchy fuzz identify ` loads a candump capture or a JSONL fuzz log / replay plan and bisects toward the culprit: each round replays the lower half of the current candidate range, and the operator records whether the effect reproduced. Observations are supplied non-interactively via `--observe effect|no-effect` (repeatable) and/or a JSON `--observations` file, so the workflow stays automation-friendly with no hidden prompts. The narrowing engine (`src/canarchy/fuzz_identify.py`) is pure and deterministic — given the frame count and observations it returns the candidate range, the next window, and a resolved culprit in `ceil(log2(N))` rounds — so it is exhaustively unit-tested without hardware. One round runs per invocation: with an `--interface` (and without `--dry-run`) it replays the next window live behind the active-transmit safety model (`stderr` preflight, `--ack-active`) and reports the state so the operator records an observation and re-invokes; with a complete observation sequence up front it resolves the culprit with no transmission. JSON output includes the replayed/next window, the recorded observations, the candidate range and frames, the resolved culprit, a `confidence` value and `rationale`, and provenance (`source`, `frame_count`, `planned_rounds`). `--max-window` refuses to replay an over-large window (`FUZZ_IDENTIFY_WINDOW_TOO_LARGE`); invalid logs/observations return structured errors (exit 1). `fuzz identify` is a stateful, multi-round human-in-the-loop workflow and stays CLI-only (a documented MCP exclusion). New module + unit/CLI tests (`tests/test_fuzz_identify.py`) and design/test specs (`fuzz-identify.md`). Closes #464.
diff --git a/pyproject.toml b/pyproject.toml
index d7cfee0..83e8904 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,12 @@ description = "CLI-first CAN security research toolkit"
readme = "README.md"
requires-python = ">=3.12"
license = "GPL-3.0-or-later"
-keywords = ["can", "can-bus", "j1939", "uds", "security", "cli"]
+keywords = [
+ "can", "can-bus", "canfd", "j1939", "uds", "doip", "iso-tp",
+ "automotive", "automotive-security", "car-hacking", "vehicle",
+ "socketcan", "candump", "dbc",
+ "fuzzing", "reverse-engineering", "security", "cli", "mcp",
+]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
diff --git a/scripts/build_pages_site.sh b/scripts/build_pages_site.sh
index cb9949a..a8f2ba2 100644
--- a/scripts/build_pages_site.sh
+++ b/scripts/build_pages_site.sh
@@ -10,6 +10,32 @@ uv run mkdocs build --strict
cp "$repo_root/src/homepage/index.html" "$site_root/index.html"
cp "$repo_root/src/homepage/site-brutalist.jsx" "$site_root/site-brutalist.jsx"
+cp "$repo_root/src/homepage/og-card.png" "$site_root/og-card.png"
+
+site_url="https://hexsecs.github.io/canarchy"
+
+# On a GitHub *project* page this file is served from /canarchy/robots.txt,
+# which crawlers do not treat as authoritative (only the host root counts),
+# so the sitemaps below must also be submitted directly to search engines
+# (e.g. Google Search Console / Bing Webmaster Tools). The file still serves
+# SEO tooling that probes it and becomes authoritative if the site ever
+# moves to a custom domain served from the root.
+cat > "$site_root/robots.txt" < "$site_root/sitemap.xml" <
+
+
+ $site_url/
+
+
+EOF
if [[ ! -f "$site_root/index.html" ]]; then
echo "missing published homepage: $site_root/index.html" >&2
diff --git a/src/homepage/index.html b/src/homepage/index.html
index 3919ab6..aa50679 100644
--- a/src/homepage/index.html
+++ b/src/homepage/index.html
@@ -3,8 +3,41 @@
- CANarchy — CAN bus security for the humans and agents
-
+ CANarchy — CAN bus & J1939 security research toolkit (CLI, TUI, MCP)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/homepage/og-card.png b/src/homepage/og-card.png
new file mode 100644
index 0000000..fc7e6a4
Binary files /dev/null and b/src/homepage/og-card.png differ