diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-03 16:15:55 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-03 16:15:55 +0000 |
| commit | dd1b44132199aa72c2b699e1160fbe6b885f0ef6 (patch) | |
| tree | a4f3ea50770f8ef22d5f5140e79bbfddec563b0d | |
| parent | 2591dbf7498e2300a42928ac9fbec454732e66f0 (diff) | |
landing page: display relay inforamtion document
| -rw-r--r-- | src/http/landing.rs | 133 | ||||
| -rw-r--r-- | src/http/nip11.rs | 8 | ||||
| -rw-r--r-- | templates/landing.html | 59 |
3 files changed, 189 insertions, 11 deletions
diff --git a/src/http/landing.rs b/src/http/landing.rs index c76a7e6..8ab4a68 100644 --- a/src/http/landing.rs +++ b/src/http/landing.rs | |||
| @@ -398,7 +398,7 @@ fn generate_nip_cards(nip11: &RelayInformationDocument) -> String { | |||
| 398 | if let Some(meta) = metadata.get(nip) { | 398 | if let Some(meta) = metadata.get(nip) { |
| 399 | html.push_str(&format!( | 399 | html.push_str(&format!( |
| 400 | r#"<div class="card"> | 400 | r#"<div class="card"> |
| 401 | <div class="card-title"><span class="badge">NIP-{:02}</span> {}</div> | 401 | <div class="card-title"><span class="badge badge-nip">NIP-{:02}</span> {}</div> |
| 402 | <div class="card-desc">{}</div> | 402 | <div class="card-desc">{}</div> |
| 403 | </div>"#, | 403 | </div>"#, |
| 404 | nip, meta.title, meta.description | 404 | nip, meta.title, meta.description |
| @@ -407,7 +407,7 @@ fn generate_nip_cards(nip11: &RelayInformationDocument) -> String { | |||
| 407 | // Fallback for unknown NIPs - still show them | 407 | // Fallback for unknown NIPs - still show them |
| 408 | html.push_str(&format!( | 408 | html.push_str(&format!( |
| 409 | r#"<div class="card"> | 409 | r#"<div class="card"> |
| 410 | <div class="card-title"><span class="badge">NIP-{:02}</span></div> | 410 | <div class="card-title"><span class="badge badge-nip">NIP-{:02}</span></div> |
| 411 | </div>"#, | 411 | </div>"#, |
| 412 | nip | 412 | nip |
| 413 | )); | 413 | )); |
| @@ -418,6 +418,129 @@ fn generate_nip_cards(nip11: &RelayInformationDocument) -> String { | |||
| 418 | html | 418 | html |
| 419 | } | 419 | } |
| 420 | 420 | ||
| 421 | /// Escape HTML special characters | ||
| 422 | fn escape_html(s: &str) -> String { | ||
| 423 | s.replace('&', "&") | ||
| 424 | .replace('<', "<") | ||
| 425 | .replace('>', ">") | ||
| 426 | .replace('"', """) | ||
| 427 | .replace('\'', "'") | ||
| 428 | } | ||
| 429 | |||
| 430 | /// Generate table rows for NIP-11 relay information | ||
| 431 | fn generate_relay_info_rows(nip11: &RelayInformationDocument) -> String { | ||
| 432 | let mut html = String::new(); | ||
| 433 | |||
| 434 | // Follow NIP-11 document order: | ||
| 435 | // name | ||
| 436 | html.push_str(&format!( | ||
| 437 | r#"<tr><th>name</th><td>{}</td></tr>"#, | ||
| 438 | escape_html(&nip11.name) | ||
| 439 | )); | ||
| 440 | html.push('\n'); | ||
| 441 | |||
| 442 | // description | ||
| 443 | html.push_str(&format!( | ||
| 444 | r#"<tr><th>description</th><td>{}</td></tr>"#, | ||
| 445 | escape_html(&nip11.description) | ||
| 446 | )); | ||
| 447 | html.push('\n'); | ||
| 448 | |||
| 449 | // pubkey (if present) | ||
| 450 | if let Some(ref pubkey) = nip11.pubkey { | ||
| 451 | html.push_str(&format!( | ||
| 452 | r#"<tr><th>pubkey</th><td><code>{}</code></td></tr>"#, | ||
| 453 | escape_html(pubkey) | ||
| 454 | )); | ||
| 455 | html.push('\n'); | ||
| 456 | } | ||
| 457 | |||
| 458 | // contact (if present) | ||
| 459 | if let Some(ref contact) = nip11.contact { | ||
| 460 | html.push_str(&format!( | ||
| 461 | r#"<tr><th>contact</th><td>{}</td></tr>"#, | ||
| 462 | escape_html(contact) | ||
| 463 | )); | ||
| 464 | html.push('\n'); | ||
| 465 | } | ||
| 466 | |||
| 467 | // supported_nips | ||
| 468 | let nips_str = nip11 | ||
| 469 | .supported_nips | ||
| 470 | .iter() | ||
| 471 | .map(|n| n.to_string()) | ||
| 472 | .collect::<Vec<_>>() | ||
| 473 | .join(", "); | ||
| 474 | html.push_str(&format!( | ||
| 475 | r#"<tr><th>supported_nips</th><td>{}</td></tr>"#, | ||
| 476 | nips_str | ||
| 477 | )); | ||
| 478 | html.push('\n'); | ||
| 479 | |||
| 480 | // software | ||
| 481 | html.push_str(&format!( | ||
| 482 | r#"<tr><th>software</th><td><a href="{}">{}</a></td></tr>"#, | ||
| 483 | escape_html(&nip11.software), | ||
| 484 | escape_html(&nip11.software) | ||
| 485 | )); | ||
| 486 | html.push('\n'); | ||
| 487 | |||
| 488 | // version | ||
| 489 | html.push_str(&format!( | ||
| 490 | r#"<tr><th>version</th><td>{}</td></tr>"#, | ||
| 491 | escape_html(&nip11.version) | ||
| 492 | )); | ||
| 493 | html.push('\n'); | ||
| 494 | |||
| 495 | // icon (if present) | ||
| 496 | if let Some(ref icon) = nip11.icon { | ||
| 497 | html.push_str(&format!( | ||
| 498 | r#"<tr><th>icon</th><td><a href="{}">{}</a></td></tr>"#, | ||
| 499 | escape_html(icon), | ||
| 500 | escape_html(icon) | ||
| 501 | )); | ||
| 502 | html.push('\n'); | ||
| 503 | } | ||
| 504 | |||
| 505 | // GRASP-01 Extensions: | ||
| 506 | // supported_grasps | ||
| 507 | let grasps_str = nip11.supported_grasps.join(", "); | ||
| 508 | html.push_str(&format!( | ||
| 509 | r#"<tr><th>supported_grasps</th><td>{}</td></tr>"#, | ||
| 510 | escape_html(&grasps_str) | ||
| 511 | )); | ||
| 512 | html.push('\n'); | ||
| 513 | |||
| 514 | // repo_acceptance_criteria | ||
| 515 | html.push_str(&format!( | ||
| 516 | r#"<tr><th>repo_acceptance_criteria</th><td>{}</td></tr>"#, | ||
| 517 | escape_html(&nip11.repo_acceptance_criteria) | ||
| 518 | )); | ||
| 519 | html.push('\n'); | ||
| 520 | |||
| 521 | // curation (if present) | ||
| 522 | if let Some(ref curation) = nip11.curation { | ||
| 523 | html.push_str(&format!( | ||
| 524 | r#"<tr><th>curation</th><td>{}</td></tr>"#, | ||
| 525 | escape_html(curation) | ||
| 526 | )); | ||
| 527 | html.push('\n'); | ||
| 528 | } | ||
| 529 | |||
| 530 | html | ||
| 531 | } | ||
| 532 | |||
| 533 | /// Generate the relay icon HTML for the header | ||
| 534 | fn generate_relay_icon_html(nip11: &RelayInformationDocument) -> String { | ||
| 535 | match &nip11.icon { | ||
| 536 | Some(icon_url) => format!( | ||
| 537 | r#"<img src="{}" alt="Relay Icon" class="relay-icon">"#, | ||
| 538 | escape_html(icon_url) | ||
| 539 | ), | ||
| 540 | None => String::new(), | ||
| 541 | } | ||
| 542 | } | ||
| 543 | |||
| 421 | /// Generate the HTML landing page | 544 | /// Generate the HTML landing page |
| 422 | pub fn get_html(config: &Config) -> String { | 545 | pub fn get_html(config: &Config) -> String { |
| 423 | // Get NIP-11 document for supported NIPs and GRASPs | 546 | // Get NIP-11 document for supported NIPs and GRASPs |
| @@ -430,6 +553,8 @@ pub fn get_html(config: &Config) -> String { | |||
| 430 | let hero_tags = generate_hero_tags(&nip11); | 553 | let hero_tags = generate_hero_tags(&nip11); |
| 431 | let grasp_cards = generate_grasp_cards(&nip11); | 554 | let grasp_cards = generate_grasp_cards(&nip11); |
| 432 | let nip_cards = generate_nip_cards(&nip11); | 555 | let nip_cards = generate_nip_cards(&nip11); |
| 556 | let relay_info_rows = generate_relay_info_rows(&nip11); | ||
| 557 | let relay_icon = generate_relay_icon_html(&nip11); | ||
| 433 | 558 | ||
| 434 | format!( | 559 | format!( |
| 435 | include_str!("../../templates/landing.html"), | 560 | include_str!("../../templates/landing.html"), |
| @@ -437,12 +562,16 @@ pub fn get_html(config: &Config) -> String { | |||
| 437 | relay_name = config.relay_name(), | 562 | relay_name = config.relay_name(), |
| 438 | relay_description = config.relay_description, | 563 | relay_description = config.relay_description, |
| 439 | version = get_version(), | 564 | version = get_version(), |
| 565 | nip11_version = escape_html(&nip11.version), | ||
| 440 | curation = curation, | 566 | curation = curation, |
| 567 | repo_acceptance_criteria = escape_html(&nip11.repo_acceptance_criteria), | ||
| 441 | theme_toggle = get_theme_toggle_html(), | 568 | theme_toggle = get_theme_toggle_html(), |
| 442 | theme_script = get_theme_script(), | 569 | theme_script = get_theme_script(), |
| 443 | hero_tags = hero_tags, | 570 | hero_tags = hero_tags, |
| 444 | grasp_cards = grasp_cards, | 571 | grasp_cards = grasp_cards, |
| 445 | nip_cards = nip_cards, | 572 | nip_cards = nip_cards, |
| 573 | relay_info_rows = relay_info_rows, | ||
| 574 | relay_icon = relay_icon, | ||
| 446 | ) | 575 | ) |
| 447 | } | 576 | } |
| 448 | 577 | ||
diff --git a/src/http/nip11.rs b/src/http/nip11.rs index 2a37385..1ed80de 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs | |||
| @@ -76,11 +76,7 @@ impl RelayInformationDocument { | |||
| 76 | 76 | ||
| 77 | // GRASP-01 Extensions | 77 | // GRASP-01 Extensions |
| 78 | supported_grasps: vec!["GRASP-01".to_string()], | 78 | supported_grasps: vec!["GRASP-01".to_string()], |
| 79 | repo_acceptance_criteria: format!( | 79 | repo_acceptance_criteria: "None".to_string(), |
| 80 | "Repositories must list this relay ({}) in both 'clone' and 'relays' tags of kind 30617 announcements. \ | ||
| 81 | All other events must reference accepted repositories or accepted events.", | ||
| 82 | config.domain | ||
| 83 | ), | ||
| 84 | curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy | 80 | curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy |
| 85 | } | 81 | } |
| 86 | } | 82 | } |
| @@ -118,7 +114,7 @@ mod tests { | |||
| 118 | assert!(doc.supported_nips.contains(&34)); | 114 | assert!(doc.supported_nips.contains(&34)); |
| 119 | assert!(doc.supported_nips.contains(&77)); | 115 | assert!(doc.supported_nips.contains(&77)); |
| 120 | assert_eq!(doc.supported_grasps, vec!["GRASP-01"]); | 116 | assert_eq!(doc.supported_grasps, vec!["GRASP-01"]); |
| 121 | assert!(doc.repo_acceptance_criteria.contains("relay.example.com")); | 117 | assert!(doc.repo_acceptance_criteria.contains("None")); |
| 122 | assert!(doc.curation.is_none()); | 118 | assert!(doc.curation.is_none()); |
| 123 | assert_eq!( | 119 | assert_eq!( |
| 124 | doc.icon, | 120 | doc.icon, |
diff --git a/templates/landing.html b/templates/landing.html index 6dc2833..41bb1e5 100644 --- a/templates/landing.html +++ b/templates/landing.html | |||
| @@ -27,9 +27,13 @@ | |||
| 27 | gap: 16px; | 27 | gap: 16px; |
| 28 | margin-bottom: 24px; | 28 | margin-bottom: 24px; |
| 29 | }} | 29 | }} |
| 30 | .logo {{ | 30 | .relay-icon {{ |
| 31 | width: 48px; | 31 | width: 48px; |
| 32 | height: 48px; | 32 | height: 48px; |
| 33 | max-width: 48px; | ||
| 34 | max-height: 48px; | ||
| 35 | object-fit: contain; | ||
| 36 | border-radius: 8px; | ||
| 33 | }} | 37 | }} |
| 34 | h1 {{ | 38 | h1 {{ |
| 35 | font-size: 2rem; | 39 | font-size: 2rem; |
| @@ -123,6 +127,9 @@ | |||
| 123 | font-weight: 500; | 127 | font-weight: 500; |
| 124 | margin-right: 8px; | 128 | margin-right: 8px; |
| 125 | }} | 129 | }} |
| 130 | .badge-nip {{ | ||
| 131 | background: var(--success); | ||
| 132 | }} | ||
| 126 | code {{ padding: 2px 8px; }} | 133 | code {{ padding: 2px 8px; }} |
| 127 | .tag-container {{ | 134 | .tag-container {{ |
| 128 | margin: 16px 0; | 135 | margin: 16px 0; |
| @@ -169,6 +176,37 @@ | |||
| 169 | background: var(--brand-light); | 176 | background: var(--brand-light); |
| 170 | text-decoration: none; | 177 | text-decoration: none; |
| 171 | }} | 178 | }} |
| 179 | /* Relay Information Document table styles */ | ||
| 180 | .info-table {{ | ||
| 181 | width: 100%; | ||
| 182 | border-collapse: collapse; | ||
| 183 | font-size: 0.875rem; | ||
| 184 | }} | ||
| 185 | .info-table th {{ | ||
| 186 | text-align: left; | ||
| 187 | padding: 8px 12px; | ||
| 188 | color: var(--text-muted); | ||
| 189 | font-weight: 500; | ||
| 190 | width: 140px; | ||
| 191 | vertical-align: top; | ||
| 192 | border-bottom: 1px solid var(--border); | ||
| 193 | }} | ||
| 194 | .info-table td {{ | ||
| 195 | padding: 8px 12px; | ||
| 196 | color: var(--text); | ||
| 197 | border-bottom: 1px solid var(--border); | ||
| 198 | word-break: break-all; | ||
| 199 | }} | ||
| 200 | .info-table tr:last-child th, | ||
| 201 | .info-table tr:last-child td {{ | ||
| 202 | border-bottom: none; | ||
| 203 | }} | ||
| 204 | .info-table a {{ | ||
| 205 | color: var(--brand-light); | ||
| 206 | }} | ||
| 207 | .info-table code {{ | ||
| 208 | font-size: 0.75rem; | ||
| 209 | }} | ||
| 172 | .footer {{ margin-top: 64px; }} | 210 | .footer {{ margin-top: 64px; }} |
| 173 | .footer-separator {{ margin: 0 0.5em; opacity: 0.5; }} | 211 | .footer-separator {{ margin: 0 0.5em; opacity: 0.5; }} |
| 174 | </style> | 212 | </style> |
| @@ -178,6 +216,7 @@ | |||
| 178 | <header class="hero"> | 216 | <header class="hero"> |
| 179 | <div class="hero-content"> | 217 | <div class="hero-content"> |
| 180 | <div class="header"> | 218 | <div class="header"> |
| 219 | {relay_icon} | ||
| 181 | <h1>{relay_name}</h1> | 220 | <h1>{relay_name}</h1> |
| 182 | </div> | 221 | </div> |
| 183 | 222 | ||
| @@ -187,11 +226,16 @@ | |||
| 187 | Browse Repositories on GitWorkshop.dev → | 226 | Browse Repositories on GitWorkshop.dev → |
| 188 | </a> | 227 | </a> |
| 189 | 228 | ||
| 190 | <div class="tag-container" style="margin-bottom: 12px;"> | 229 | <div class="tag-container" style="margin-bottom: 8px;"> |
| 191 | <span class="tag-label">Curation:</span> | 230 | <span class="tag-label">Curation:</span> |
| 192 | <span class="tag-label">{curation}</span> | 231 | <span class="tag-label">{curation}</span> |
| 193 | </div> | 232 | </div> |
| 194 | 233 | ||
| 234 | <div class="tag-container" style="margin-bottom: 12px;"> | ||
| 235 | <span class="tag-label">Repo Acceptance Criteria:</span> | ||
| 236 | <span class="tag-label" style="max-width: 600px; text-align: center;">{repo_acceptance_criteria}</span> | ||
| 237 | </div> | ||
| 238 | |||
| 195 | <div class="tag-container" style="margin-top: 12px;"> | 239 | <div class="tag-container" style="margin-top: 12px;"> |
| 196 | <span class="tag-label">Supports:</span> | 240 | <span class="tag-label">Supports:</span> |
| 197 | {hero_tags} | 241 | {hero_tags} |
| @@ -205,7 +249,7 @@ | |||
| 205 | <path d="M10.6731 30.6348C8.83687 30.6346 7.34885 29.1458 7.34885 27.3096C7.34891 26.2473 7.84783 25.303 8.62326 24.6943C8.21265 23.3055 7.86571 22.049 7.45334 20.6758C6.90247 18.8412 7.4492 16.8197 8.93576 15.5605L15.7512 9.78906C15.6931 9.54286 15.6614 9.28642 15.6613 9.02246C15.6613 7.51617 16.6628 6.24465 18.0363 5.83594L18.0363 -1.11215e-06C18.511 0.000462658 18.4612 0.000975391 18.9856 0.000975533C19.5102 0.000975578 19.5802 -1.11589e-06 19.9367 -9.46012e-07L19.9367 5.83594C21.3097 6.24503 22.3108 7.5166 22.3108 9.02246C22.3107 9.29118 22.2792 9.55249 22.219 9.80273L29.0783 15.6123C30.5229 16.8359 31.1022 18.8013 30.5539 20.6133L29.3254 24.6758C30.1142 25.2837 30.6232 26.2367 30.6233 27.3096C30.6233 29.1459 29.1344 30.6348 27.2981 30.6348C25.4619 30.6346 23.9738 29.1458 23.9738 27.3096C23.974 25.4734 25.4619 23.9846 27.2981 23.9844C27.3814 23.9844 27.4643 23.9891 27.5461 23.9951L28.7356 20.0625C29.0645 18.9753 28.7166 17.7966 27.8498 17.0625L21.2424 11.4648C20.8746 11.8048 20.4294 12.0622 19.9367 12.209L19.9367 18.9258C21.0425 19.3175 21.836 20.3694 21.8362 21.6094C21.8362 23.1834 20.5596 24.46 18.9856 24.46C17.4117 24.4598 16.136 23.1833 16.136 21.6094C16.1361 20.3689 16.93 19.3172 18.0363 18.9258L18.0363 12.21C17.5395 12.0622 17.0916 11.801 16.7219 11.457L10.1643 17.0107C9.27919 17.7605 8.93068 18.9867 9.27365 20.1289C9.68708 21.5056 10.0175 22.7009 10.3986 23.998C10.4892 23.9906 10.5806 23.9844 10.6731 23.9844C12.5093 23.9844 13.9981 25.4733 13.9983 27.3096C13.9983 29.1459 12.5094 30.6348 10.6731 30.6348Z" fill="white"/> | 249 | <path d="M10.6731 30.6348C8.83687 30.6346 7.34885 29.1458 7.34885 27.3096C7.34891 26.2473 7.84783 25.303 8.62326 24.6943C8.21265 23.3055 7.86571 22.049 7.45334 20.6758C6.90247 18.8412 7.4492 16.8197 8.93576 15.5605L15.7512 9.78906C15.6931 9.54286 15.6614 9.28642 15.6613 9.02246C15.6613 7.51617 16.6628 6.24465 18.0363 5.83594L18.0363 -1.11215e-06C18.511 0.000462658 18.4612 0.000975391 18.9856 0.000975533C19.5102 0.000975578 19.5802 -1.11589e-06 19.9367 -9.46012e-07L19.9367 5.83594C21.3097 6.24503 22.3108 7.5166 22.3108 9.02246C22.3107 9.29118 22.2792 9.55249 22.219 9.80273L29.0783 15.6123C30.5229 16.8359 31.1022 18.8013 30.5539 20.6133L29.3254 24.6758C30.1142 25.2837 30.6232 26.2367 30.6233 27.3096C30.6233 29.1459 29.1344 30.6348 27.2981 30.6348C25.4619 30.6346 23.9738 29.1458 23.9738 27.3096C23.974 25.4734 25.4619 23.9846 27.2981 23.9844C27.3814 23.9844 27.4643 23.9891 27.5461 23.9951L28.7356 20.0625C29.0645 18.9753 28.7166 17.7966 27.8498 17.0625L21.2424 11.4648C20.8746 11.8048 20.4294 12.0622 19.9367 12.209L19.9367 18.9258C21.0425 19.3175 21.836 20.3694 21.8362 21.6094C21.8362 23.1834 20.5596 24.46 18.9856 24.46C17.4117 24.4598 16.136 23.1833 16.136 21.6094C16.1361 20.3689 16.93 19.3172 18.0363 18.9258L18.0363 12.21C17.5395 12.0622 17.0916 11.801 16.7219 11.457L10.1643 17.0107C9.27919 17.7605 8.93068 18.9867 9.27365 20.1289C9.68708 21.5056 10.0175 22.7009 10.3986 23.998C10.4892 23.9906 10.5806 23.9844 10.6731 23.9844C12.5093 23.9844 13.9981 25.4733 13.9983 27.3096C13.9983 29.1459 12.5094 30.6348 10.6731 30.6348Z" fill="white"/> |
| 206 | </svg> | 250 | </svg> |
| 207 | <div class="software-content"> | 251 | <div class="software-content"> |
| 208 | <h3 class="software-heading"><a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">Grasp Server</a> Powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">ngit-grasp</a></h3> | 252 | <h3 class="software-heading"><a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">Grasp Server</a> Powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">ngit-grasp</a> ({nip11_version})</h3> |
| 209 | <p class="software-desc">Git hosting distributed across relays using Nostr for authorization. <a href="https://ngit.dev/grasp">Find out more...</a></p> | 253 | <p class="software-desc">Git hosting distributed across relays using Nostr for authorization. <a href="https://ngit.dev/grasp">Find out more...</a></p> |
| 210 | </div> | 254 | </div> |
| 211 | </div> | 255 | </div> |
| @@ -234,6 +278,15 @@ | |||
| 234 | {nip_cards} | 278 | {nip_cards} |
| 235 | </div> | 279 | </div> |
| 236 | 280 | ||
| 281 | <div class="section"> | ||
| 282 | <div class="section-title">Relay Information Document</div> | ||
| 283 | <div class="card"> | ||
| 284 | <table class="info-table"> | ||
| 285 | {relay_info_rows} | ||
| 286 | </table> | ||
| 287 | </div> | ||
| 288 | </div> | ||
| 289 | |||
| 237 | <div class="footer"> | 290 | <div class="footer"> |
| 238 | <span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed | 291 | <span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed |
| 239 | </div> | 292 | </div> |