1use dioxus::prelude::*;
26
27const BASE_URL: &str = "https://logicaffeine.com";
28const LOGO_URL: &str = "https://logicaffeine.com/assets/logo.jpeg";
29const ORG_NAME: &str = "LOGICAFFEINE";
30const GITHUB_URL: &str = "https://github.com/Brahmastra-Labs/logicaffeine";
31
32fn organization() -> serde_json::Value {
33 serde_json::json!({
34 "@context": "https://schema.org",
35 "@type": "Organization",
36 "name": ORG_NAME,
37 "url": BASE_URL,
38 "logo": LOGO_URL,
39 "description": "Turn everyday English into rigorous First-Order Logic. Debug your thoughts with precision.",
40 "sameAs": [GITHUB_URL, "https://x.com/logicaffeine"],
41 })
42}
43
44pub fn organization_schema() -> String {
46 organization().to_string()
47}
48
49pub fn organization_contact_schema(email: &str, contact_path: &str) -> String {
52 let mut org = organization();
53 org["contactPoint"] = serde_json::json!({
54 "@type": "ContactPoint",
55 "contactType": "sales",
56 "email": email,
57 "url": format!("{BASE_URL}{contact_path}"),
58 "availableLanguage": "English",
59 });
60 org.to_string()
61}
62
63pub fn website_schema() -> String {
65 format!(
66 r#"{{
67 "@context": "https://schema.org",
68 "@type": "WebSite",
69 "name": "{ORG_NAME}",
70 "url": "{BASE_URL}",
71 "description": "Turn everyday English into rigorous First-Order Logic",
72 "potentialAction": {{
73 "@type": "SearchAction",
74 "target": "{BASE_URL}/registry?q={{search_term}}",
75 "query-input": "required name=search_term"
76 }}
77}}"#
78 )
79}
80
81pub fn software_application_schema() -> String {
83 format!(
84 r#"{{
85 "@context": "https://schema.org",
86 "@type": "SoftwareApplication",
87 "name": "LOGICAFFEINE Studio",
88 "applicationCategory": "DeveloperApplication",
89 "operatingSystem": "Web Browser",
90 "description": "Interactive playground for experimenting with First-Order Logic translations",
91 "url": "{BASE_URL}/studio",
92 "offers": {{
93 "@type": "Offer",
94 "price": "0",
95 "priceCurrency": "USD"
96 }},
97 "provider": {{
98 "@type": "Organization",
99 "name": "{ORG_NAME}",
100 "url": "{BASE_URL}"
101 }}
102}}"#
103 )
104}
105
106pub fn course_schema() -> String {
108 format!(
109 r#"{{
110 "@context": "https://schema.org",
111 "@type": "Course",
112 "name": "First-Order Logic Fundamentals",
113 "description": "Learn to translate everyday English into rigorous First-Order Logic through interactive exercises and real-world examples.",
114 "url": "{BASE_URL}/learn",
115 "provider": {{
116 "@type": "Organization",
117 "name": "{ORG_NAME}",
118 "url": "{BASE_URL}"
119 }},
120 "educationalLevel": "Beginner to Advanced",
121 "isAccessibleForFree": true,
122 "inLanguage": "en",
123 "teaches": ["First-Order Logic", "Formal Logic", "Logical Reasoning", "Symbolic Logic"]
124}}"#
125 )
126}
127
128pub fn article_schema(
130 headline: &str,
131 description: &str,
132 date_published: &str,
133 slug: &str,
134) -> String {
135 format!(
136 r#"{{
137 "@context": "https://schema.org",
138 "@type": "Article",
139 "headline": "{headline}",
140 "description": "{description}",
141 "datePublished": "{date_published}",
142 "dateModified": "{date_published}",
143 "url": "{BASE_URL}/news/{slug}",
144 "author": {{
145 "@type": "Organization",
146 "name": "{ORG_NAME}",
147 "url": "{BASE_URL}"
148 }},
149 "publisher": {{
150 "@type": "Organization",
151 "name": "{ORG_NAME}",
152 "logo": {{
153 "@type": "ImageObject",
154 "url": "{LOGO_URL}"
155 }}
156 }}
157}}"#
158 )
159}
160
161pub fn faq_schema(questions: &[(&str, &str)]) -> String {
164 let items: Vec<serde_json::Value> = questions
165 .iter()
166 .map(|(question, answer)| {
167 serde_json::json!({
168 "@type": "Question",
169 "name": question,
170 "acceptedAnswer": { "@type": "Answer", "text": answer },
171 })
172 })
173 .collect();
174 serde_json::json!({
175 "@context": "https://schema.org",
176 "@type": "FAQPage",
177 "mainEntity": items,
178 })
179 .to_string()
180}
181
182pub fn tech_article_schema(title: &str, description: &str, path: &str) -> String {
184 format!(
185 r#"{{
186 "@context": "https://schema.org",
187 "@type": "TechArticle",
188 "headline": "{title}",
189 "description": "{description}",
190 "url": "{BASE_URL}{path}",
191 "author": {{
192 "@type": "Organization",
193 "name": "{ORG_NAME}"
194 }},
195 "publisher": {{
196 "@type": "Organization",
197 "name": "{ORG_NAME}",
198 "logo": {{
199 "@type": "ImageObject",
200 "url": "{LOGO_URL}"
201 }}
202 }},
203 "proficiencyLevel": "Expert"
204}}"#
205 )
206}
207
208pub struct BreadcrumbItem {
210 pub name: &'static str,
211 pub path: &'static str,
212}
213
214pub fn breadcrumb_schema(items: &[BreadcrumbItem]) -> String {
216 let list_items: Vec<String> = items
217 .iter()
218 .enumerate()
219 .map(|(i, item)| {
220 format!(
221 r#"{{
222 "@type": "ListItem",
223 "position": {},
224 "name": "{}",
225 "item": "{}{}"
226 }}"#,
227 i + 1,
228 item.name,
229 BASE_URL,
230 item.path
231 )
232 })
233 .collect();
234
235 format!(
236 r#"{{
237 "@context": "https://schema.org",
238 "@type": "BreadcrumbList",
239 "itemListElement": [
240 {}
241 ]
242}}"#,
243 list_items.join(",\n ")
244 )
245}
246
247pub fn roadmap_schema() -> String {
251 let milestones = crate::ui::pages::roadmap_data::get_milestones();
252 let items: Vec<serde_json::Value> = milestones
253 .iter()
254 .enumerate()
255 .map(|(i, m)| {
256 serde_json::json!({
257 "@type": "ListItem",
258 "position": i + 1,
259 "name": m.title,
260 "description": format!("{} — {}", m.status.label(), first_sentence(m.description)),
261 })
262 })
263 .collect();
264 serde_json::json!({
265 "@context": "https://schema.org",
266 "@type": "ItemList",
267 "name": "LOGICAFFEINE Development Roadmap",
268 "description": "Where LOGOS stands: finished milestones, work in progress, and what is planned next.",
269 "url": format!("{BASE_URL}/roadmap"),
270 "numberOfItems": milestones.len(),
271 "itemListElement": items,
272 })
273 .to_string()
274}
275
276fn first_sentence(text: &str) -> &str {
278 match text.find(". ") {
279 Some(end) => &text[..=end],
280 None => text,
281 }
282}
283
284pub fn webpage_schema(name: &str, description: &str, path: &str) -> String {
286 format!(
287 r#"{{
288 "@context": "https://schema.org",
289 "@type": "WebPage",
290 "name": "{name}",
291 "description": "{description}",
292 "url": "{BASE_URL}{path}",
293 "isPartOf": {{
294 "@type": "WebSite",
295 "name": "{ORG_NAME}",
296 "url": "{BASE_URL}"
297 }},
298 "publisher": {{
299 "@type": "Organization",
300 "name": "{ORG_NAME}",
301 "url": "{BASE_URL}"
302 }}
303}}"#
304 )
305}
306
307pub struct PageMeta {
309 pub title: &'static str,
310 pub description: &'static str,
311 pub canonical_path: &'static str,
312 pub og_image: Option<&'static str>,
313}
314
315impl Default for PageMeta {
317 fn default() -> Self {
318 Self {
319 title: "LOGICAFFEINE | Debug Your Thoughts",
320 description: "Humanity's last programming language. Transform plain English into compiled Rust code with Z3-powered verification. Debug your thoughts with mathematical certainty.",
321 canonical_path: "/",
322 og_image: Some("/assets/OG-photo.png"),
323 }
324 }
325}
326
327pub mod pages {
329 use super::PageMeta;
330
331 pub const LANDING: PageMeta = PageMeta {
332 title: "LOGICAFFEINE | Code we can all read",
333 description: "LOGOS is an English programming language with logic and mathematics built in. Bring your own coding agent: give it the instructions, then read every line it writes.",
334 canonical_path: "/",
335 og_image: Some("/assets/OG-photo.png"),
336 };
337
338 pub const LEARN: PageMeta = PageMeta {
339 title: "Learn First-Order Logic | LOGICAFFEINE",
340 description: "Master First-Order Logic through interactive exercises. From syllogisms to modal logic, learn to reason precisely.",
341 canonical_path: "/learn",
342 og_image: Some("/assets/OG-photo.png"),
343 };
344
345 pub const STUDIO: PageMeta = PageMeta {
346 title: "Studio | LOGICAFFEINE",
347 description: "Interactive playground for experimenting with First-Order Logic translations. Try examples and see results in real-time.",
348 canonical_path: "/studio",
349 og_image: Some("/assets/OG-photo.png"),
350 };
351
352 pub const GUIDE: PageMeta = PageMeta {
353 title: "Documentation | LOGICAFFEINE",
354 description:
355 "Comprehensive guide to LOGICAFFEINE syntax, features, and First-Order Logic concepts.",
356 canonical_path: "/guide",
357 og_image: Some("/assets/OG-photo.png"),
358 };
359
360 pub const CONTACT: PageMeta = PageMeta {
361 title: "Contact | LOGICAFFEINE",
362 description: "Reach the team behind LOGOS, the open source English programming language under the MIT License: questions, bugs, working together.",
363 canonical_path: "/contact",
364 og_image: Some("/assets/OG-photo.png"),
365 };
366
367 pub const CRATES: PageMeta = PageMeta {
368 title: "Crates Documentation | LOGICAFFEINE",
369 description: "The Rust crates behind LOGOS: the English-to-logic front end, the compiler and its execution tiers, the proof kernel, and the tools.",
370 canonical_path: "/crates",
371 og_image: Some("/assets/OG-photo.png"),
372 };
373
374 pub const ROADMAP: PageMeta = PageMeta {
375 title: "Roadmap | LOGICAFFEINE",
376 description:
377 "See what's coming next for LOGICAFFEINE. Track our progress and upcoming features.",
378 canonical_path: "/roadmap",
379 og_image: Some("/assets/OG-photo.png"),
380 };
381
382 pub const NEWS: PageMeta = PageMeta {
383 title: "News | LOGICAFFEINE",
384 description: "Latest updates, release notes, and announcements from LOGICAFFEINE.",
385 canonical_path: "/news",
386 og_image: Some("/assets/OG-photo.png"),
387 };
388
389 pub const PRIVACY: PageMeta = PageMeta {
390 title: "Privacy Policy | LOGICAFFEINE",
391 description: "How LOGICAFFEINE collects, uses, and protects your personal information. Read our full privacy policy.",
392 canonical_path: "/privacy",
393 og_image: Some("/assets/OG-photo.png"),
394 };
395
396 pub const TERMS: PageMeta = PageMeta {
397 title: "Terms of Service | LOGICAFFEINE",
398 description: "Terms and conditions for using LOGICAFFEINE and its MIT-licensed software.",
399 canonical_path: "/terms",
400 og_image: Some("/assets/OG-photo.png"),
401 };
402
403 pub const REGISTRY: PageMeta = PageMeta {
404 title: "Package Registry | LOGICAFFEINE",
405 description:
406 "Browse and discover community-contributed logic modules and packages for LOGICAFFEINE.",
407 canonical_path: "/registry",
408 og_image: Some("/assets/OG-photo.png"),
409 };
410
411 pub const BENCHMARKS: PageMeta = PageMeta {
412 title: "Benchmarks | LOGICAFFEINE",
413 description: "LOGOS performance benchmarks across 13 languages. Near-native speed from English source code.",
414 canonical_path: "/benchmarks",
415 og_image: Some("/assets/OG-photo.png"),
416 };
417}
418
419#[component]
428pub fn PageHead(
429 title: String,
430 description: String,
431 canonical_path: String,
432 #[props(default = String::from("/assets/OG-photo.png"))] og_image: String,
433) -> Element {
434 let canonical_url = format!("{}{}", BASE_URL, canonical_path);
435 let image_url = format!("{}{}", BASE_URL, og_image);
436 let default_card = og_image == "/assets/OG-photo.png";
439
440 #[cfg(target_arch = "wasm32")]
441 {
442 update_head_meta(&title, &description, &canonical_url, &image_url);
443 }
444
445 rsx! {
446 document::Title { "{title}" }
447 if cfg!(not(target_arch = "wasm32")) {
448 document::Meta { name: "title", content: title.clone() }
449 document::Meta { name: "description", content: description.clone() }
450 document::Meta { property: "og:type", content: "website" }
451 document::Meta { property: "og:url", content: canonical_url.clone() }
452 document::Meta { property: "og:title", content: title.clone() }
453 document::Meta { property: "og:description", content: description.clone() }
454 document::Meta { property: "og:image", content: image_url.clone() }
455 if default_card {
456 document::Meta { property: "og:image:width", content: "1200" }
457 document::Meta { property: "og:image:height", content: "630" }
458 document::Meta { property: "og:image:alt", content: "LOGICAFFEINE | Debug Your Thoughts" }
459 }
460 document::Meta { name: "twitter:url", content: canonical_url.clone() }
461 document::Meta { name: "twitter:title", content: title.clone() }
462 document::Meta { name: "twitter:description", content: description.clone() }
463 document::Meta { name: "twitter:image", content: image_url.clone() }
464 document::Link { rel: "canonical", href: canonical_url.clone() }
465 }
466 }
467}
468
469#[cfg(target_arch = "wasm32")]
474fn update_head_meta(title: &str, description: &str, canonical_url: &str, image_url: &str) {
475 let Some(window) = web_sys::window() else {
476 return;
477 };
478 let Some(doc) = window.document() else { return };
479 let Some(head) = doc.head() else { return };
480
481 let set = |selector: &str, key_attr: &str, key: &str, attr: &str, value: &str| {
482 if let Ok(Some(el)) = doc.query_selector(selector) {
483 let _ = el.set_attribute(attr, value);
484 } else if let Ok(el) = doc.create_element(if key_attr == "rel" { "link" } else { "meta" }) {
485 let _ = el.set_attribute(key_attr, key);
486 let _ = el.set_attribute(attr, value);
487 let _ = head.append_child(&el);
488 }
489 };
490 let set_name = |name: &str, value: &str| {
491 set(
492 &format!("meta[name='{name}']"),
493 "name",
494 name,
495 "content",
496 value,
497 );
498 };
499 let set_property = |property: &str, value: &str| {
500 set(
501 &format!("meta[property='{property}']"),
502 "property",
503 property,
504 "content",
505 value,
506 );
507 };
508
509 set_name("description", description);
511 set_name("title", title);
512 set_property("og:type", "website");
514 set_property("og:url", canonical_url);
515 set_property("og:title", title);
516 set_property("og:description", description);
517 set_property("og:image", image_url);
518 set_name("twitter:url", canonical_url);
520 set_name("twitter:title", title);
521 set_name("twitter:description", description);
522 set_name("twitter:image", image_url);
523 set(
525 "link[rel='canonical']",
526 "rel",
527 "canonical",
528 "href",
529 canonical_url,
530 );
531}
532
533#[component]
535pub fn JsonLd(schema: String) -> Element {
536 rsx! {
537 script {
538 r#type: "application/ld+json",
539 dangerous_inner_html: "{schema}"
540 }
541 }
542}
543
544#[component]
546pub fn JsonLdMultiple(schemas: Vec<String>) -> Element {
547 rsx! {
548 for schema in schemas.iter() {
549 script {
550 r#type: "application/ld+json",
551 dangerous_inner_html: "{schema}"
552 }
553 }
554 }
555}