chore: added custom og and favicon
dabcad72
20 file(s) · +96 −3
| 15 | 15 | | [**OG**](apps/og) | Open Graph tag inspector | [](https://railway.com/deploy/OdXBt_?referralCode=JGcIp6) | |
|
| 16 | 16 | | [**Shrink**](apps/shrink) | Image compression and resizing | [](https://railway.com/deploy/enYUFb?referralCode=JGcIp6) | |
|
| 17 | 17 | | [**Cellar**](apps/cellar) | Minimal wine collection tracker | [](https://railway.com/deploy/MNprVh?referralCode=JGcIp6) | |
|
| 18 | - | | [**Posts**](apps/posts) | Minimal CMS blog with admin interface | | |
|
| 18 | + | | [**Posts**](apps/posts) | Minimal CMS blog with admin interface | [](https://railway.com/deploy/tYtJYp?referralCode=JGcIp6) | |
|
| 19 | 19 | ||
| 20 | 20 | ## Shared Crates |
|
| 21 | 21 |
| 70 | 70 | ||
| 71 | 71 | ## Deployment |
|
| 72 | 72 | ||
| 73 | + | ### Railway |
|
| 74 | + | ||
| 75 | + | [](https://railway.com/deploy/tYtJYp?referralCode=JGcIp6) |
|
| 76 | + | ||
| 73 | 77 | ### Docker (recommended) |
|
| 74 | 78 | ||
| 75 | 79 | ```bash |
| 156 | 156 | [], |
|
| 157 | 157 | ) |
|
| 158 | 158 | .ok(); |
|
| 159 | + | conn.execute( |
|
| 160 | + | "INSERT OR IGNORE INTO settings (key, value) VALUES ('favicon_url', '')", |
|
| 161 | + | [], |
|
| 162 | + | ) |
|
| 163 | + | .ok(); |
|
| 164 | + | conn.execute( |
|
| 165 | + | "INSERT OR IGNORE INTO settings (key, value) VALUES ('og_image_url', '')", |
|
| 166 | + | [], |
|
| 167 | + | ) |
|
| 168 | + | .ok(); |
|
| 159 | 169 | ||
| 160 | 170 | Arc::new(Mutex::new(conn)) |
|
| 161 | 171 | } |
| 40 | 40 | struct BaseTemplate { |
|
| 41 | 41 | blog_title: String, |
|
| 42 | 42 | nav_links: Vec<NavLink>, |
|
| 43 | + | favicon_url: String, |
|
| 44 | + | og_image_url: String, |
|
| 43 | 45 | } |
|
| 44 | 46 | ||
| 45 | 47 | #[derive(Template)] |
|
| 60 | 62 | intro_html: String, |
|
| 61 | 63 | posts: Vec<Post>, |
|
| 62 | 64 | nav_links: Vec<NavLink>, |
|
| 65 | + | favicon_url: String, |
|
| 66 | + | og_image_url: String, |
|
| 63 | 67 | } |
|
| 64 | 68 | ||
| 65 | 69 | #[derive(Template)] |
|
| 69 | 73 | nav_links: Vec<NavLink>, |
|
| 70 | 74 | post: Post, |
|
| 71 | 75 | rendered_content: String, |
|
| 76 | + | favicon_url: String, |
|
| 77 | + | og_image_url: String, |
|
| 72 | 78 | } |
|
| 73 | 79 | ||
| 74 | 80 | #[derive(Template)] |
|
| 78 | 84 | nav_links: Vec<NavLink>, |
|
| 79 | 85 | page: Page, |
|
| 80 | 86 | rendered_content: String, |
|
| 87 | + | favicon_url: String, |
|
| 88 | + | og_image_url: String, |
|
| 81 | 89 | } |
|
| 82 | 90 | ||
| 83 | 91 | #[derive(Template)] |
|
| 115 | 123 | nav_links: String, |
|
| 116 | 124 | custom_css: String, |
|
| 117 | 125 | default_css: String, |
|
| 126 | + | favicon_url: String, |
|
| 127 | + | og_image_url: String, |
|
| 118 | 128 | success: bool, |
|
| 119 | 129 | } |
|
| 120 | 130 | ||
| 124 | 134 | blog_title: String, |
|
| 125 | 135 | nav_links: Vec<NavLink>, |
|
| 126 | 136 | posts: Vec<Post>, |
|
| 137 | + | favicon_url: String, |
|
| 138 | + | og_image_url: String, |
|
| 127 | 139 | } |
|
| 128 | 140 | ||
| 129 | 141 | #[derive(Template)] |
|
| 239 | 251 | intro_content: String, |
|
| 240 | 252 | nav_links: String, |
|
| 241 | 253 | custom_css: String, |
|
| 254 | + | favicon_url: String, |
|
| 255 | + | og_image_url: String, |
|
| 242 | 256 | } |
|
| 243 | 257 | ||
| 244 | 258 | // --- Helpers --- |
|
| 340 | 354 | parse_nav_links(&raw) |
|
| 341 | 355 | } |
|
| 342 | 356 | ||
| 357 | + | fn get_favicon_url(db: &Db) -> String { |
|
| 358 | + | db::get_setting(db, "favicon_url") |
|
| 359 | + | .ok() |
|
| 360 | + | .flatten() |
|
| 361 | + | .unwrap_or_default() |
|
| 362 | + | } |
|
| 363 | + | ||
| 364 | + | fn get_og_image_url(db: &Db) -> String { |
|
| 365 | + | db::get_setting(db, "og_image_url") |
|
| 366 | + | .ok() |
|
| 367 | + | .flatten() |
|
| 368 | + | .unwrap_or_default() |
|
| 369 | + | } |
|
| 370 | + | ||
| 343 | 371 | fn render_latest_posts_embed(posts: &[&Post]) -> String { |
|
| 344 | 372 | let mut html = String::from("<div class=\"post-list\">"); |
|
| 345 | 373 | for post in posts { |
|
| 480 | 508 | intro_html = intro_html.replace("{{latest_posts}}", &embed_html); |
|
| 481 | 509 | } |
|
| 482 | 510 | ||
| 511 | + | let favicon_url = get_favicon_url(&state.db); |
|
| 512 | + | let og_image_url = get_og_image_url(&state.db); |
|
| 483 | 513 | WebTemplate(IndexTemplate { |
|
| 484 | 514 | blog_title, |
|
| 485 | 515 | blog_description, |
|
| 486 | 516 | intro_html, |
|
| 487 | 517 | posts, |
|
| 488 | 518 | nav_links, |
|
| 519 | + | favicon_url, |
|
| 520 | + | og_image_url, |
|
| 489 | 521 | }) |
|
| 490 | 522 | .into_response() |
|
| 491 | 523 | } |
|
| 505 | 537 | let rendered_content = render_markdown(&post.content); |
|
| 506 | 538 | let blog_title = get_blog_title(&state.db); |
|
| 507 | 539 | let nav_links = get_nav_links(&state.db); |
|
| 540 | + | let favicon_url = get_favicon_url(&state.db); |
|
| 541 | + | let og_image_url = get_og_image_url(&state.db); |
|
| 508 | 542 | WebTemplate(PostTemplate { |
|
| 509 | 543 | blog_title, |
|
| 510 | 544 | nav_links, |
|
| 511 | 545 | post, |
|
| 512 | 546 | rendered_content, |
|
| 547 | + | favicon_url, |
|
| 548 | + | og_image_url, |
|
| 513 | 549 | }) |
|
| 514 | 550 | .into_response() |
|
| 515 | 551 | } |
|
| 530 | 566 | let rendered_content = render_markdown(&page.content); |
|
| 531 | 567 | let blog_title = get_blog_title(&state.db); |
|
| 532 | 568 | let nav_links = get_nav_links(&state.db); |
|
| 569 | + | let favicon_url = get_favicon_url(&state.db); |
|
| 570 | + | let og_image_url = get_og_image_url(&state.db); |
|
| 533 | 571 | WebTemplate(PageTemplate { |
|
| 534 | 572 | blog_title, |
|
| 535 | 573 | nav_links, |
|
| 536 | 574 | page, |
|
| 537 | 575 | rendered_content, |
|
| 576 | + | favicon_url, |
|
| 577 | + | og_image_url, |
|
| 538 | 578 | }) |
|
| 539 | 579 | .into_response() |
|
| 540 | 580 | } |
|
| 549 | 589 | async fn public_posts_list(State(state): State<Arc<AppState>>) -> Response { |
|
| 550 | 590 | let blog_title = get_blog_title(&state.db); |
|
| 551 | 591 | let nav_links = get_nav_links(&state.db); |
|
| 592 | + | let favicon_url = get_favicon_url(&state.db); |
|
| 593 | + | let og_image_url = get_og_image_url(&state.db); |
|
| 552 | 594 | ||
| 553 | 595 | match db::get_published_posts(&state.db) { |
|
| 554 | 596 | Ok(posts) => WebTemplate(PostsListTemplate { |
|
| 555 | 597 | blog_title, |
|
| 556 | 598 | nav_links, |
|
| 557 | 599 | posts, |
|
| 600 | + | favicon_url, |
|
| 601 | + | og_image_url, |
|
| 558 | 602 | }) |
|
| 559 | 603 | .into_response(), |
|
| 560 | 604 | Err(e) => { |
|
| 875 | 919 | let intro_content = db::get_setting(&state.db, "intro_content").ok().flatten().unwrap_or_default(); |
|
| 876 | 920 | let nav_links = db::get_setting(&state.db, "nav_links").ok().flatten().unwrap_or_default(); |
|
| 877 | 921 | let custom_css = db::get_setting(&state.db, "custom_css").ok().flatten().unwrap_or_default(); |
|
| 922 | + | let favicon_url = db::get_setting(&state.db, "favicon_url").ok().flatten().unwrap_or_default(); |
|
| 923 | + | let og_image_url = db::get_setting(&state.db, "og_image_url").ok().flatten().unwrap_or_default(); |
|
| 878 | 924 | let default_css = Static::get("styles.css") |
|
| 879 | 925 | .map(|f| String::from_utf8_lossy(&f.data).into_owned()) |
|
| 880 | 926 | .unwrap_or_default(); |
|
| 886 | 932 | nav_links, |
|
| 887 | 933 | custom_css, |
|
| 888 | 934 | default_css, |
|
| 935 | + | favicon_url, |
|
| 936 | + | og_image_url, |
|
| 889 | 937 | success: q.success, |
|
| 890 | 938 | }) |
|
| 891 | 939 | .into_response() |
|
| 901 | 949 | let _ = db::set_setting(&state.db, "intro_content", &form.intro_content); |
|
| 902 | 950 | let _ = db::set_setting(&state.db, "nav_links", &form.nav_links); |
|
| 903 | 951 | let _ = db::set_setting(&state.db, "custom_css", &form.custom_css); |
|
| 952 | + | let _ = db::set_setting(&state.db, "favicon_url", form.favicon_url.trim()); |
|
| 953 | + | let _ = db::set_setting(&state.db, "og_image_url", form.og_image_url.trim()); |
|
| 904 | 954 | Redirect::to("/admin/settings?success=true").into_response() |
|
| 905 | 955 | } |
|
| 906 | 956 | ||
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
| 1 | + | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} |
| 12 | 12 | <input type="text" id="blog_description" name="blog_description" value="{{ blog_description }}"> |
|
| 13 | 13 | <label for="nav_links">navigation links (format: [label](url) [label2](url2))</label> |
|
| 14 | 14 | <textarea id="nav_links" name="nav_links" class="nav-links-input">{{ nav_links }}</textarea> |
|
| 15 | + | <label for="favicon_url">favicon URL (leave empty for default)</label> |
|
| 16 | + | <input type="text" id="favicon_url" name="favicon_url" value="{{ favicon_url }}" placeholder="https://example.com/favicon.png"> |
|
| 17 | + | <label for="og_image_url">default OG image URL (used when posts don't have their own)</label> |
|
| 18 | + | <input type="text" id="og_image_url" name="og_image_url" value="{{ og_image_url }}" placeholder="https://example.com/og.png"> |
|
| 15 | 19 | <label for="intro_content">intro content (markdown, shown on homepage — use {{latest_posts}} to embed recent posts)</label> |
|
| 16 | 20 | <textarea id="intro_content" name="intro_content" class="post-content">{{ intro_content }}</textarea> |
|
| 17 | 21 | <div class="switch-row"> |
| 4 | 4 | <meta charset="UTF-8"> |
|
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
| 6 | 6 | <title>{% block title %}{{ blog_title }}{% endblock %}</title> |
|
| 7 | - | <link rel="icon" href="/static/favicons/favicon.ico"> |
|
| 7 | + | {% if !favicon_url.is_empty() %} |
|
| 8 | + | <link rel="apple-touch-icon" sizes="180x180" href="{{ favicon_url }}"> |
|
| 9 | + | <link rel="icon" type="image/png" sizes="32x32" href="{{ favicon_url }}"> |
|
| 10 | + | <link rel="icon" type="image/png" sizes="16x16" href="{{ favicon_url }}"> |
|
| 11 | + | {% else %} |
|
| 12 | + | <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> |
|
| 13 | + | <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> |
|
| 14 | + | <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"> |
|
| 15 | + | <link rel="manifest" href="/static/site.webmanifest"> |
|
| 16 | + | {% endif %} |
|
| 8 | 17 | <meta name="theme-color" content="#121113" /> |
|
| 9 | 18 | {% block meta %}{% endblock %} |
|
| 10 | 19 | <link rel="stylesheet" href="/static/styles.css"> |
| 5 | 5 | <meta property="og:title" content="{{ blog_title }}"> |
|
| 6 | 6 | <meta property="og:description" content="{{ blog_description }}"> |
|
| 7 | 7 | <meta property="og:type" content="website"> |
|
| 8 | + | {% if !og_image_url.is_empty() %} |
|
| 9 | + | <meta property="og:image" content="{{ og_image_url }}"> |
|
| 10 | + | {% endif %} |
|
| 8 | 11 | {% endblock %} |
|
| 9 | 12 | {% block content %} |
|
| 10 | 13 | {% if !intro_html.is_empty() %} |
| 1 | 1 | {% extends "base.html" %} |
|
| 2 | 2 | {% block title %}{{ page.title }} — {{ blog_title }}{% endblock %} |
|
| 3 | + | {% block meta %} |
|
| 4 | + | {% if !og_image_url.is_empty() %} |
|
| 5 | + | <meta property="og:image" content="{{ og_image_url }}"> |
|
| 6 | + | {% endif %} |
|
| 7 | + | {% endblock %} |
|
| 3 | 8 | {% block content %} |
|
| 4 | 9 | <div class="page-header"> |
|
| 5 | 10 | <h1>{{ page.title }}</h1> |
| 5 | 5 | <meta name="description" content="{{ post.meta_description.as_deref().unwrap_or_default() }}"> |
|
| 6 | 6 | <meta property="og:description" content="{{ post.meta_description.as_deref().unwrap_or_default() }}"> |
|
| 7 | 7 | {% endif %} |
|
| 8 | - | {% if post.meta_image.is_some() %} |
|
| 8 | + | {% if post.meta_image.is_some() && !post.meta_image.as_deref().unwrap_or_default().is_empty() %} |
|
| 9 | 9 | <meta property="og:image" content="{{ post.meta_image.as_deref().unwrap_or_default() }}"> |
|
| 10 | + | {% else if !og_image_url.is_empty() %} |
|
| 11 | + | <meta property="og:image" content="{{ og_image_url }}"> |
|
| 10 | 12 | {% endif %} |
|
| 11 | 13 | {% if post.canonical_url.is_some() %} |
|
| 12 | 14 | <link rel="canonical" href="{{ post.canonical_url.as_deref().unwrap_or_default() }}"> |
| 1 | 1 | {% extends "base.html" %} |
|
| 2 | 2 | {% block title %}Posts — {{ blog_title }}{% endblock %} |
|
| 3 | + | {% block meta %} |
|
| 4 | + | {% if !og_image_url.is_empty() %} |
|
| 5 | + | <meta property="og:image" content="{{ og_image_url }}"> |
|
| 6 | + | {% endif %} |
|
| 7 | + | {% endblock %} |
|
| 3 | 8 | {% block content %} |
|
| 4 | 9 | <h1>Posts</h1> |
|
| 5 | 10 | {% if posts.is_empty() %} |