Feature.link: a card's name may point elsewhere

An https URL on a feature renders its name as an outbound link -
an announcement's programme page, a venue. Validated on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-30 14:39:58 +02:00
co-authored by Claude Fable 5
parent 7a0724af7f
commit 73efa6a4eb
3 changed files with 36 additions and 1 deletions
+10 -1
View File
@@ -945,7 +945,16 @@ fn AlternativeCard(
let name = feature.name.clone(); let name = feature.name.clone();
move || !name.is_empty() move || !name.is_empty()
}> }>
<h3>{feature.name.clone()}</h3> <h3>
{match feature.link.clone() {
Some(href) => view! {
<a href=href target="_blank" rel="noopener noreferrer">
{feature.name.clone()}
</a>
}.into_any(),
None => feature.name.clone().into_any(),
}}
</h3>
</Show> </Show>
<Show when={ <Show when={
let description = feature.description.clone(); let description = feature.description.clone();
+15
View File
@@ -237,6 +237,11 @@ pub struct Feature {
/// rendered via Iconify's public SVG API - no icon library bundled. /// rendered via Iconify's public SVG API - no icon library bundled.
#[serde(default)] #[serde(default)]
pub icon: Option<String>, pub icon: Option<String>,
/// An https URL the card's name links to - an announcement's
/// programme page, a venue. Plain-text descriptions can't carry
/// links, so this is the one place a card points elsewhere.
#[serde(default)]
pub link: Option<String>,
#[serde(default)] #[serde(default)]
pub requirements: Vec<Requirement>, pub requirements: Vec<Requirement>,
/// Live data this feature pulls in. Read-only unless `transitions` /// Live data this feature pulls in. Read-only unless `transitions`
@@ -1017,6 +1022,16 @@ pub fn validate_questions(
} }
} }
for alternative in &question.alternatives { for alternative in &question.alternatives {
for feature in &alternative.features {
if let Some(link) = &feature.link {
if !link.starts_with("https://") {
anyhow::bail!(
"question {:?} feature {:?}: link {:?} must be an https:// URL",
question.id, feature.name, link
);
}
}
}
// A dangling action is a literal dead end: the submit // A dangling action is a literal dead end: the submit
// button navigates to "Nothing here". // button navigates to "Nothing here".
if let Some(action) = &alternative.action { if let Some(action) = &alternative.action {
+11
View File
@@ -448,6 +448,17 @@ main.not-found {
margin-bottom: 0.15em; margin-bottom: 0.15em;
} }
.feature h3 a {
color: inherit;
text-decoration: none;
border-bottom: 0.06rem solid var(--line);
}
.feature h3 a:hover {
color: var(--accent);
border-bottom-color: var(--accent);
}
.feature p { .feature p {
color: var(--ink-dim); color: var(--ink-dim);
font-size: 0.95rem; font-size: 0.95rem;