Compare commits

...

2 commits

Author SHA1 Message Date
Dominic Grimm 501b9d3093
Update 2023-02-11 12:49:24 +01:00
Dominic Grimm 53e144d9a7
Update 2023-02-11 12:48:39 +01:00
24 changed files with 1235 additions and 268 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
blog/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Dominic Grimm <dominic@dergrimm.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -6,3 +6,4 @@ Dockerfile
vendor/
example/
static/
LICENSE

477
backend/Cargo.lock generated
View file

@ -30,7 +30,7 @@ dependencies = [
"actix-service",
"actix-utils",
"ahash",
"base64",
"base64 0.21.0",
"bitflags",
"brotli",
"bytes",
@ -43,14 +43,14 @@ dependencies = [
"http",
"httparse",
"httpdate",
"itoa",
"itoa 1.0.5",
"language-tags",
"local-channel",
"mime",
"percent-encoding",
"pin-project-lite",
"rand",
"sha1",
"sha1 0.10.5",
"smallvec",
"tokio",
"tokio-util",
@ -155,7 +155,7 @@ dependencies = [
"futures-core",
"futures-util",
"http",
"itoa",
"itoa 1.0.5",
"language-tags",
"log",
"mime",
@ -322,6 +322,17 @@ dependencies = [
"toml",
]
[[package]]
name = "async-trait"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -339,13 +350,14 @@ dependencies = [
"askama_actix",
"chrono",
"clap",
"comrak",
"diesel",
"env_logger",
"envconfig",
"fronma",
"lazy_static",
"log",
"pulldown-cmark",
"r2d2_redis",
"scan_dir",
"serde",
"serde_yaml 0.9.17",
@ -368,12 +380,42 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -491,6 +533,7 @@ dependencies = [
"once_cell",
"strsim",
"termcolor",
"terminal_size",
]
[[package]]
@ -525,6 +568,38 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "combine"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "comrak"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784836d0812dade01579cc0cc9b1684847044e716fd7aa6bffbc172e42199500"
dependencies = [
"clap",
"emojis",
"entities",
"memchr",
"once_cell",
"pest",
"pest_derive",
"regex",
"shell-words",
"slug",
"syntect",
"typed-arena",
"unicode_categories",
"xdg",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -633,6 +708,12 @@ dependencies = [
"syn",
]
[[package]]
name = "deunicode"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
[[package]]
name = "diesel"
version = "2.0.3"
@ -643,7 +724,7 @@ dependencies = [
"byteorder",
"chrono",
"diesel_derives",
"itoa",
"itoa 1.0.5",
"pq-sys",
"r2d2",
]
@ -670,6 +751,41 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]]
name = "emojis"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fe60b864b6544ad211d4053ced474a9b9d2c8d66b77f01d6c6bcfed10c6bf0"
dependencies = [
"phf",
]
[[package]]
name = "encoding_rs"
version = "0.8.32"
@ -679,6 +795,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "entities"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "env_logger"
version = "0.10.0"
@ -733,6 +855,16 @@ dependencies = [
"libc",
]
[[package]]
name = "fancy-regex"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
dependencies = [
"bit-set",
"regex",
]
[[package]]
name = "flate2"
version = "1.0.25"
@ -886,7 +1018,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 1.0.5",
]
[[package]]
@ -979,6 +1111,12 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.5"
@ -1021,6 +1159,15 @@ version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "link-cplusplus"
version = "1.0.8"
@ -1182,6 +1329,28 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "os_str_bytes"
version = "6.4.1"
@ -1238,6 +1407,68 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac3922aac69a40733080f53c1ce7f91dcf57e1a5f6c52f421fadec7fbdc4b69"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06646e185566b5961b4058dd107e0a7f56e77c3f484549fb119867773c0f202"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f60b2ba541577e2a0c307c8f39d1439108120eb7903adeb6497fa880c59616"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@ -1256,6 +1487,20 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "plist"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635"
dependencies = [
"base64 0.13.1",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time 0.3.17",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -1304,23 +1549,21 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [
"bitflags",
"memchr",
"unicase",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.23"
@ -1341,6 +1584,16 @@ dependencies = [
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_redis"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "182473b876b0b93e353682ec58e207dd1cb4a62278bbe0045fe52b86b74363bb"
dependencies = [
"r2d2",
"redis",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1371,6 +1624,21 @@ dependencies = [
"getrandom",
]
[[package]]
name = "redis"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4f0ceb2ec0dd769483ecd283f6615aa83dcd0be556d5294c6e659caefe7cc54"
dependencies = [
"async-trait",
"combine",
"dtoa",
"itoa 0.4.8",
"percent-encoding",
"sha1 0.6.1",
"url",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -1380,6 +1648,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
name = "regex"
version = "1.7.1"
@ -1432,6 +1711,21 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scan_dir"
version = "0.3.3"
@ -1494,7 +1788,7 @@ version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
dependencies = [
"itoa",
"itoa 1.0.5",
"ryu",
"serde",
]
@ -1506,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"itoa 1.0.5",
"ryu",
"serde",
]
@ -1530,12 +1824,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567"
dependencies = [
"indexmap",
"itoa",
"itoa 1.0.5",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]]
name = "sha1"
version = "0.10.5"
@ -1547,6 +1850,29 @@ dependencies = [
"digest",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -1556,6 +1882,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "slab"
version = "0.4.7"
@ -1565,6 +1897,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slug"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
dependencies = [
"deunicode",
]
[[package]]
name = "smallvec"
version = "1.10.0"
@ -1609,6 +1950,30 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
dependencies = [
"bincode",
"bitflags",
"fancy-regex",
"flate2",
"fnv",
"lazy_static",
"once_cell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"thiserror",
"walkdir",
"yaml-rust",
]
[[package]]
name = "termcolor"
version = "1.2.0"
@ -1618,6 +1983,36 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907"
dependencies = [
"rustix",
"windows-sys 0.42.0",
]
[[package]]
name = "thiserror"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tikv-jemalloc-sys"
version = "0.5.3+5.3.0-patched"
@ -1655,7 +2050,7 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
"itoa",
"itoa 1.0.5",
"serde",
"time-core",
"time-macros",
@ -1753,12 +2148,24 @@ dependencies = [
"once_cell",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.6.0"
@ -1795,6 +2202,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "unsafe-libyaml"
version = "0.2.5"
@ -1824,6 +2237,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -2002,6 +2426,15 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "xdg"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
dependencies = [
"dirs",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"

View file

@ -3,6 +3,7 @@ name = "backend"
version = "0.1.0"
edition = "2021"
authors = ["Dominic Grimm <dominic@dergrimm.net>"]
publish = false
[[bin]]
name = "backend"
@ -11,11 +12,11 @@ build = "build.rs"
[[bin]]
name = "blogctl"
# [profile.release]
# codegen-units = 1
# lto = "fat"
# strip = true
# panic = "abort"
[profile.release]
codegen-units = 1
lto = "fat"
strip = true
panic = "abort"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -27,13 +28,14 @@ askama = "0.11.1"
askama_actix = "0.13.0"
chrono = { version = "0.4.23", features = ["serde"] }
clap = { version = "4.1.4", features = ["derive"] }
comrak = { version = "0.16.0", features = ["shortcodes"] }
diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "chrono", "r2d2"] }
env_logger = "0.10.0"
envconfig = "0.10.0"
fronma = "0.1.1"
lazy_static = "1.4.0"
log = "0.4.17"
pulldown-cmark = { version = "0.9.2", default-features = false, features = ["simd"] }
r2d2_redis = "0.14.0"
scan_dir = "0.3.3"
serde = { version = "1.0.152", features = ["derive"] }
serde_yaml = "0.9.17"

View file

@ -10,6 +10,11 @@ WORKDIR /usr/src/static
COPY --from=css /usr/src/scss/dist ./css
RUN minify . -r -o .
FROM tdewolff/minify:latest as templates
WORKDIR /usr/src/templates
COPY ./templates .
RUN minify . -r -o .
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.67.0 as chef
FROM chef as diesel
@ -28,11 +33,16 @@ RUN cargo chef cook --recipe-path recipe.json
RUN rm -rf ./src
COPY ./build.rs .
COPY --from=static /usr/src/static ./static
COPY ./templates ./templates
COPY --from=templates /usr/src/templates ./templates
COPY ./src ./src
RUN cargo build
FROM docker.io/debian:bullseye-slim as runner
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
org.opencontainers.image.description="Personal blog backend" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://git.dergrimm.net/dergrimm/blog" \
org.opencontainers.image.url="https://git.dergrimm.net/dergrimm/blog"
RUN apt update
RUN apt install -y libpq5
RUN apt install -y ca-certificates
@ -40,12 +50,10 @@ RUN apt-get clean
RUN apt-get autoremove -y
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
WORKDIR /usr/local/bin
ENV RUST_BACKTRACE=full
COPY --from=diesel /usr/local/cargo/bin/diesel .
WORKDIR /usr/src/backend
COPY ./run.sh .
RUN chmod +x ./run.sh
COPY ./migrations ./migrations
COPY --from=builder /usr/src/backend/target/debug/backend /usr/src/backend/target/debug/blogctl ./bin/
EXPOSE 80
ENTRYPOINT [ "./run.sh" ]

View file

@ -1,42 +1,88 @@
html,
body {
width: 100vw;
min-height: 100vh;
}
* {
margin: 0;
padding: 0;
width: 100vw;
min-height: 100vh;
font-family: "JetBrains Mono", monospace;
}
// * {
// font-family: "JetBrains Mono", monospace;
// }
h1,
h2,
h3 {
text-align: center;
}
h2 {
text-decoration: underline;
}
h3 {
font-style: italic;
}
h4,
h5,
h6 {
font-weight: bold;
}
a {
text-decoration: none;
}
hr {
border: none;
border-top: medium solid black;
}
ul.dashed {
list-style-type: none;
li:before {
content: "-";
position: absolute;
// margin-left: -20px;
margin-left: -1rem;
}
}
table.border-rows {
border-collapse: collapse;
tr:first-child,
tr:not(:last-child) {
border-bottom: thin solid black;
}
th,
td {
padding: 1em;
}
th:not(:last-child),
td:not(:last-child) {
// border: thin solid black;
border-right: thin solid black;
}
}
#wrapper {
width: 75%;
margin: 0 auto;
overflow: auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
#navbar {
display: flex;
margin-bottom: 1%;
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
li {
float: left;
}
a {
display: block;
text-align: center;
padding: 1em;
text-decoration: none;
}
}
#navbar-brand {
@ -48,18 +94,138 @@ body {
}
}
.navbar-link a,
#navbar-links ul li a {
display: block;
text-align: center;
}
#navbar-links {
width: 50%;
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
float: right;
li {
float: left;
&:not(:first-of-type) a {
padding-left: 1rem;
}
}
}
}
#content {
padding: 3%;
#main-wrapper {
flex: 1;
padding: 1.5% 3%;
}
#footer {
font-size: small;
text-align: center;
}
#status-code {
text-align: center;
}
#status-code-message {
font-size: small;
}
#post-index {
li:not(:last-child) {
margin-bottom: 1rem;
}
}
.double-border {
border: 5px double black;
}
.section-margin {
margin-top: 1%;
}
.section-light-padding {
padding: 1%;
}
.breadcrumb {
padding: 1% 0;
ul {
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;
}
li:not(:last-child)::after {
display: inline-block;
margin: 0 0.5rem;
content: ">";
}
}
.wysiwyg {
hr {
width: 50%;
}
ul {
@extend ul.dashed;
}
table {
@extend table.border-rows;
margin: 0 auto;
}
p > code,
li > code,
dd > code,
td > code {
word-wrap: break-word;
box-decoration-break: clone;
padding: 0.1rem 0.3rem 0.2rem;
border: thin solid black;
}
pre {
padding: 0 1rem;
border: thin solid black;
overflow-x: scroll;
}
}
@media only screen and (max-width: 600px) {
#wrapper {
width: 100%;
}
}
@media only screen and (min-width: 600px) {
#wrapper {
width: 75%;
}
}
@media only screen and (min-width: 768px) {
}
@media only screen and (min-width: 992px) {
}
@media only screen and (min-width: 1200px) {
#wrapper {
width: 50%;
}
}

View file

@ -1,4 +1,3 @@
use diesel::QueryDsl;
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
@ -6,104 +5,23 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
use actix_web::{get, http::StatusCode, middleware, web, App, HttpResponse, HttpServer};
use actix_web_static_files::ResourceFiles;
use askama_actix::{Template, TemplateToResponse};
use diesel::prelude::*;
use backend::*;
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
mod filters {
pub fn cmark<T: std::fmt::Display>(s: T) -> ::askama::Result<String> {
let s = s.to_string();
let options = pulldown_cmark::Options::empty();
let parser = pulldown_cmark::Parser::new_ext(&s, options);
let mut html_output = String::with_capacity(s.len() * 3 / 2);
pulldown_cmark::html::push_html(&mut html_output, parser);
Ok(html_output)
}
}
#[derive(Template)]
#[template(path = "status_code.html")]
struct StatusCodeTemplate {
status_code: StatusCode,
message: Option<String>,
}
#[derive(Template)]
#[template(path = "posts/{slug}.html")]
struct PostBySlugTemplate {
post: db::models::Post,
}
async fn not_found() -> HttpResponse {
StatusCodeTemplate {
status_code: StatusCode::NOT_FOUND,
message: None,
}
.to_response()
}
#[get("/posts/{slug}")]
async fn post_by_slug(db_pool: web::Data<db::DbPool>, path: web::Path<String>) -> HttpResponse {
let slug = path.into_inner();
let conn = &mut match db_pool.get() {
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
let post = match db::schema::posts::table
.filter(db::schema::posts::slug.eq(&slug))
.filter(db::schema::posts::active)
.first::<db::models::Post>(conn)
.optional()
{
Ok(x) => x,
Err(e) => {
dbg!(&e);
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
};
match post {
Some(x) => PostBySlugTemplate { post: x }.to_response(),
None => {
let mut resp = StatusCodeTemplate {
status_code: StatusCode::NOT_FOUND,
message: None,
}
.to_response();
*resp.status_mut() = StatusCode::NOT_FOUND;
resp
}
}
}
use actix_web::{middleware, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
log::info!("Listening to requests at http://0.0.0.0:80");
log::info!(
"Listening to requests at {}",
backend::config::CONFIG.bind_url
);
HttpServer::new(move || {
let generated = generate();
App::new()
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.app_data(web::Data::new(db::pool().unwrap()))
.service(post_by_slug)
.service(ResourceFiles::new("/static", generated))
.default_service(web::route().to(not_found))
.configure(backend::web::init)
})
.bind("0.0.0.0:80")
.bind(&backend::config::CONFIG.bind_url)
.unwrap()
.run()
.await

View file

@ -9,6 +9,7 @@ use anyhow::{bail, Result};
use chrono::prelude::*;
use clap::{Parser, Subcommand};
use diesel::prelude::*;
use r2d2_redis::redis;
use scan_dir::ScanDir;
use serde::{Deserialize, Serialize};
use std::fs;
@ -27,6 +28,9 @@ struct Cli {
enum Commands {
#[clap(about = "Imports new posts")]
Import,
#[clap(about = "Clears redis cache")]
Clear,
}
#[derive(Deserialize, Serialize, Debug)]
@ -50,88 +54,137 @@ struct Post {
fn main() -> Result<()> {
match Cli::parse().commands {
Commands::Import => {
let conn = &mut db::establish_connection()?;
let db_conn = &mut db::establish_connection()?;
let redis_conn = &mut cache::establish_connection()?;
for post in ScanDir::dirs().read("/blog", |iter| {
iter.map(|(entry, _)| {
let path = entry.path().join("post.md");
let src = fs::read_to_string(&path)?;
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
Ok(x) => x,
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
};
let posts = ScanDir::dirs()
.read("/blog", |iter| {
iter.map(|(entry, _)| {
let path = entry.path().join("post.md");
let src = fs::read_to_string(&path)?;
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
Ok(x) => x,
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
};
Ok(Post {
path,
frontmatter: frontmatter.headers,
content: frontmatter.body.to_string(),
})
})
.collect::<Result<Vec<_>>>()
})?? {
let content = post.content.trim();
if let Some(id) = post.frontmatter.id {
diesel::update(db::schema::posts::table)
.filter(db::schema::posts::id.eq(id))
.set(db::models::UpdatePost {
name: Some(&post.frontmatter.name),
slug: Some(&post.frontmatter.slug),
description: Some(&post.frontmatter.description),
content: Some(content),
published_at: Some(post.frontmatter.published_at),
edited_at: Some(post.frontmatter.edited_at),
active: Some(post.frontmatter.active),
Ok(Post {
path,
frontmatter: frontmatter.headers,
content: frontmatter.body.to_string(),
})
.execute(conn)?;
} else {
let id = if let Some(id) = db::schema::posts::table
.select(db::schema::posts::id)
.filter(db::schema::posts::slug.eq(&post.frontmatter.slug))
.first::<i32>(conn)
.optional()?
{
})
.collect::<Result<Vec<_>>>()
})??
.into_iter()
.map(|post| -> Result<_> {
let trimmed = PostFrontmatter {
id: post.frontmatter.id,
name: post.frontmatter.name.trim().to_string(),
slug: post.frontmatter.slug.trim().to_string(),
description: post.frontmatter.description.trim().to_string(),
published_at: post.frontmatter.published_at,
edited_at: post.frontmatter.edited_at,
active: post.frontmatter.active,
};
let content = post.content.trim();
if let Some(id) = trimmed.id {
diesel::update(db::schema::posts::table)
.filter(db::schema::posts::id.eq(id))
.set(db::models::UpdatePost {
name: Some(&post.frontmatter.name),
slug: None,
description: Some(&post.frontmatter.description),
name: Some(&trimmed.name),
slug: Some(&trimmed.slug),
description: Some(&trimmed.description),
content: Some(content),
published_at: Some(post.frontmatter.published_at),
edited_at: Some(post.frontmatter.edited_at),
active: Some(post.frontmatter.active),
published_at: Some(trimmed.published_at),
edited_at: Some(trimmed.edited_at),
active: Some(trimmed.active),
})
.execute(conn)?;
.execute(db_conn)?;
id
Ok(id)
} else {
diesel::insert_into(db::schema::posts::table)
.values(db::models::NewPost {
name: &post.frontmatter.name,
slug: &post.frontmatter.slug,
description: &post.frontmatter.description,
content: content,
published_at: post.frontmatter.published_at,
edited_at: post.frontmatter.edited_at,
active: post.frontmatter.active,
})
.returning(db::schema::posts::id)
.get_result::<i32>(conn)?
};
let id = if let Some(id) = db::schema::posts::table
.select(db::schema::posts::id)
.filter(db::schema::posts::slug.eq(&trimmed.slug))
.first::<i32>(db_conn)
.optional()?
{
diesel::update(db::schema::posts::table)
.filter(db::schema::posts::id.eq(id))
.set(db::models::UpdatePost {
name: Some(&trimmed.name),
slug: None,
description: Some(&trimmed.description),
content: Some(content),
published_at: Some(trimmed.published_at),
edited_at: Some(trimmed.edited_at),
active: Some(trimmed.active),
})
.execute(db_conn)?;
fs::write(
post.path,
format!(
"---\n{}---\n{}",
serde_yaml::to_string(&PostFrontmatter {
id: Some(id),
..post.frontmatter
})?,
post.content
),
)?;
}
id
} else {
diesel::insert_into(db::schema::posts::table)
.values(db::models::NewPost {
name: &trimmed.name,
slug: &trimmed.slug,
description: &trimmed.description,
content: content,
published_at: trimmed.published_at,
edited_at: trimmed.edited_at,
active: trimmed.active,
})
.returning(db::schema::posts::id)
.get_result::<i32>(db_conn)?
};
fs::write(
post.path,
format!(
"---\n{}---\n\n{}\n",
serde_yaml::to_string(&PostFrontmatter {
id: Some(id),
..trimmed
})?,
content
),
)?;
Ok(id)
}
})
.collect::<Result<Vec<_>>>()?;
let ids = db::schema::posts::table
.select(db::schema::posts::id)
.load::<i32>(db_conn)?;
diesel::delete(
db::schema::posts::table
.filter(diesel::dsl::not(db::schema::posts::id.eq_any(posts))),
)
.execute(db_conn)?;
for id in ids {
redis::cmd("DEL")
.arg(cache::keys::post_content(id))
.query::<()>(redis_conn)?;
}
Ok(())
}
Commands::Clear => {
let db_conn = &mut db::establish_connection()?;
let redis_conn = &mut cache::establish_connection()?;
for id in db::schema::posts::table
.select(db::schema::posts::id)
.load::<i32>(db_conn)?
{
redis::cmd("DEL")
.arg(cache::keys::post_content(id))
.query::<()>(redis_conn)?;
}
Ok(())

28
backend/src/cache.rs Normal file
View file

@ -0,0 +1,28 @@
use anyhow::Result;
use lazy_static::lazy_static;
use r2d2_redis::{r2d2, redis, RedisConnectionManager};
use crate::config;
pub type RedisPool = r2d2::Pool<RedisConnectionManager>;
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
pub fn establish_connection() -> Result<redis::Connection> {
Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?)
}
pub fn pool() -> Result<RedisPool> {
Ok(r2d2::Pool::builder().build(RedisConnectionManager::new(
config::CONFIG.redis_url.as_str(),
)?)?)
}
lazy_static! {
pub static ref POOL: RedisPool = pool().unwrap();
}
pub mod keys {
pub fn post_content(id: i32) -> String {
format!("post_content:{}", id)
}
}

View file

@ -1,11 +1,19 @@
use envconfig::Envconfig;
use lazy_static::lazy_static;
#[derive(Envconfig, Debug)]
pub struct Config {
#[envconfig(from = "BACKEND_BIND_URL")]
pub bind_url: String,
#[envconfig(from = "BACKEND_DB_URL")]
pub db_url: String,
#[envconfig(from = "BACKEND_REDIS_URL")]
pub redis_url: String,
#[envconfig(from = "BACKEND_CACHE_POST_CONTENT_TTL")]
pub cache_post_content_ttl: usize,
}
lazy_static! {

View file

@ -1,2 +1,5 @@
pub mod cache;
pub mod config;
pub mod db;
pub mod markdown;
pub mod web;

28
backend/src/markdown.rs Normal file
View file

@ -0,0 +1,28 @@
pub fn to_html(src: &str) -> String {
let adapter = comrak::plugins::syntect::SyntectAdapter::new("InspiredGitHub");
let options = comrak::ComrakOptions {
extension: comrak::ComrakExtensionOptions {
strikethrough: true,
tagfilter: true,
table: true,
autolink: true,
tasklist: true,
superscript: true,
header_ids: Some("__blog-content_".to_string()),
footnotes: true,
description_lists: true,
front_matter_delimiter: None,
shortcodes: true,
},
parse: comrak::ComrakParseOptions {
smart: true,
..comrak::ComrakParseOptions::default()
},
..comrak::ComrakOptions::default()
};
let mut plugins = comrak::ComrakPlugins::default();
plugins.render.codefence_syntax_highlighter = Some(&adapter);
comrak::markdown_to_html_with_plugins(src, &options, &plugins)
}

176
backend/src/web/mod.rs Normal file
View file

@ -0,0 +1,176 @@
use actix_web::{get, http, web, HttpResponse};
use actix_web_static_files::ResourceFiles;
use askama_actix::TemplateToResponse;
use diesel::prelude::*;
use r2d2_redis::redis;
use std::ops::DerefMut;
use crate::{cache, config, db, markdown};
pub mod templates;
pub mod static_dir {
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
}
async fn not_found() -> HttpResponse {
let mut resp = templates::StatusCode {
status_code: http::StatusCode::NOT_FOUND,
message: Some("maybe try a correct url?".to_string()),
}
.to_response();
*resp.status_mut() = http::StatusCode::NOT_FOUND;
resp
}
#[get("/posts")]
async fn posts(db_pool: web::Data<db::DbPool>) -> HttpResponse {
let db_conn = &mut match db_pool.get() {
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
let posts = match db::schema::posts::table
.filter(db::schema::posts::active)
.order(db::schema::posts::published_at.desc())
.load::<db::models::Post>(db_conn)
{
Ok(x) => x,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
};
templates::Posts { posts }.to_response()
}
#[get("/")]
async fn index() -> HttpResponse {
templates::Index.to_response()
}
#[get("/about")]
async fn about() -> HttpResponse {
templates::About.to_response()
}
#[get("/posts/{slug}")]
async fn post_by_slug(
db_pool: web::Data<db::DbPool>,
redis_pool: web::Data<cache::RedisPool>,
path: web::Path<String>,
) -> HttpResponse {
let slug = path.into_inner();
let db_conn = &mut match db_pool.get() {
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
let redis_conn = &mut match redis_pool.get() {
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
let post_stripped: Option<(i32, String)> = match db::schema::posts::table
.select((db::schema::posts::id, db::schema::posts::name))
.filter(db::schema::posts::slug.eq(&slug))
.filter(db::schema::posts::active)
.get_result::<(i32, String)>(db_conn)
.optional()
{
Ok(x) => x,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
};
match post_stripped {
Some(stripped) => {
let (stripped_id, stripped_name) = stripped;
let key = cache::keys::post_content(stripped_id);
match match redis::cmd("GET")
.arg(&key)
.query::<Option<String>>(redis_conn.deref_mut())
{
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
} {
Some(s) => templates::PostBySlug {
name: stripped_name,
slug,
content: s,
}
.to_response(),
None => {
let post = match db::schema::posts::table
.filter(db::schema::posts::id.eq(stripped_id))
.first::<db::models::Post>(db_conn)
{
Ok(x) => x,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
};
let html = markdown::to_html(&post.content);
match redis::cmd("SET")
.arg(&key)
.arg(&html)
.query::<Option<String>>(redis_conn.deref_mut())
{
Ok(x) => x,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("{:?}", e))
}
};
if let Err(e) = redis::cmd("EXPIRE")
.arg(key)
.arg(config::CONFIG.cache_post_content_ttl)
.query::<()>(redis_conn.deref_mut())
{
return HttpResponse::InternalServerError().body(format!("{:?}", e));
}
templates::PostBySlug {
name: post.name,
slug: post.slug,
content: html,
}
.to_response()
}
}
}
None => {
let mut resp = templates::StatusCode {
status_code: http::StatusCode::NOT_FOUND,
message: Some("this post does not exists... yet".to_string()),
}
.to_response();
*resp.status_mut() = http::StatusCode::NOT_FOUND;
resp
}
}
}
fn setup_routes(cfg: &mut web::ServiceConfig) {
let generated = static_dir::generate();
cfg.service(index)
.service(about)
.service(posts)
.service(ResourceFiles::new("/static", generated))
.service(post_by_slug)
.default_service(web::route().to(not_found));
}
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.app_data(web::Data::new(db::pool().unwrap()))
.app_data(web::Data::new(cache::pool().unwrap()));
setup_routes(cfg);
}

View file

@ -0,0 +1,33 @@
use actix_web::http;
use askama_actix::Template;
use crate::db;
#[derive(Template)]
#[template(path = "status_code.html")]
pub struct StatusCode {
pub status_code: http::StatusCode,
pub message: Option<String>,
}
#[derive(Template)]
#[template(path = "web/index.html")]
pub struct Index;
#[derive(Template)]
#[template(path = "web/about.html")]
pub struct About;
#[derive(Template)]
#[template(path = "web/posts/index.html")]
pub struct Posts {
pub posts: Vec<db::models::Post>,
}
#[derive(Template)]
#[template(path = "web/posts/{slug}.html")]
pub struct PostBySlug {
pub name: String,
pub slug: String,
pub content: String,
}

View file

@ -7,7 +7,7 @@
<title>{% block title %}{% endblock %} | dergrimm's blog</title>
<link rel="preconnect" href="https://fonts.bunny.net" />
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet" />
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400,400i,700,700i" rel="stylesheet" />
<link rel="stylesheet" href="/static/css/styles.css" />
@ -15,19 +15,39 @@
</head>
<body>
<div id="wrapper">
<nav id="navbar" class="double-border">
<div id="navbar-brand"><a href="/">dergrimm's blog</a></div>
<div id="navbar-links">
<ul>
<li><a href="/posts">posts</a></li>
<li><a href="/about">about</a></li>
</ul>
</div>
</nav>
<header class="double-border section-margin section-light-padding">
<nav id="navbar">
<div id="navbar-brand" class="navbar-link"><a href="/">dergrimm's blog</a></div>
<div id="navbar-links">
<ul>
<li><a href="/posts">posts</a></li>
<li><a href="/about">about</a></li>
</ul>
</div>
</nav>
</header>
<div id="content" class="double-border">
{% block content %}{% endblock %}
<div id="main-wrapper" class="section-margin">
<nav class="breadcrumb">
<ul>
<li><a href="/">/</a></li>
{% block breadcrumb %}{% endblock %}
</ul>
</nav>
<hr />
<main>
{% block content %}{% endblock %}
</main>
</div>
<footer id="footer" class="double-border section-margin section-light-padding">
(C) 2023 Dominic Grimm
&lt;<a href='m&#97;ilto&#58;%64o%6Di&#110;&#105;c&#64;&#37;&#54;4ergr&#37;&#54;9mm&#37;2En&#101;t'>dominic&#64;&#100;e&#114;grimm&#46;net</a>&gt;
| <a href="https://git.dergrimm.net/dergrimm/blog">repo</a>
| <a href="/">dergrimm.net</a>
</footer>
</div>
</body>
</html>

View file

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ post.name }}{% endblock %}
{% block head %}{% endblock %}
{% block content %}
<div class="blog">
{{ post.content|cmark|safe }}
</div>
{% endblock %}

View file

@ -1,14 +1,19 @@
{% extends "base.html" %}
{% block title %}{{ status_code }}{% endblock %}
{% block title %}{{ status_code|lower }}{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %}
<h1>{{ status_code }}!</h1>
{% match message %}
{% when Some with (x) %}
<p>{{ x }}</p>
{% when None %}
{% endmatch %}
<div id="status-code">
<p><b>{{ status_code }}!</b></p>
{% match message %}
{% when Some with (x) %}
<p id="status-code-message"><i>{{ x }}</i></p>
{% when None %}
{% endmatch %}
<p>:(</p>
</div>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}index{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %}
<p><i>...fearlessly conquering the world of backend</i></p>
<h1>About</h1>
<p>Hey, I'm Dominic (aka. dergrimm) and am currently a student going to school in Germany.</p>
<p>
You can contact me at:
<a href='m&#97;ilto&#58;%64o%6Di&#110;&#105;c&#64;&#37;&#54;4ergr&#37;&#54;9mm&#37;2En&#101;t'>dominic&#64;&#100;e&#114;grimm&#46;net</a>
</p>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}index{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}{% endblock %}
{% block content %}
<h1>This is my site</h1>
<p>Hey, I'm Dominic (aka. dergrimm) and this is my blog!</p>
<p>I mostly plan to talk about modern tech and server side stuff like Rust, Crystal, C, databases and APIs.</p>
<p>Keep posted by following my feed.</p>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}posts{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}
<li><a href="/posts">posts</a></li>
{% endblock %}
{% block content %}
<ul id="post-index" class="dashed">
{% for post in posts %}
<li>
<span>
<a href="/posts/{{ post.slug }}">{{ post.name }}</a>
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
</span>
<br />
<span>{{ post.description }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}{{ name }}{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}
<li><a href="/posts">posts</a></li>
<li><a href="/posts/{{ slug }}">{{ slug }}</a></li>
{% endblock %}
{% block content %}
<article class="wysiwyg">
{{content|safe }}
</article>
{% endblock %}

View file

@ -1,15 +0,0 @@
---
id: 1
name: Hello world!
slug: hello_world
description: Hello world to the internet. Set up my first blog!
published_at: 2023-02-06
edited_at: null
active: true
---
# Hello world!
I just set up my first blog and am really proud of it!
So anyway, here I am.

View file

@ -2,7 +2,7 @@ version: "3"
services:
db:
image: docker.io/postgres:alpine
image: docker.io/postgres:15-alpine
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
@ -11,27 +11,45 @@ services:
- db:/var/lib/postgresql/data
adminer:
image: docker.io/adminer:standalone
image: docker.io/adminer:4-standalone
restart: always
ports:
- 8080:8080
depends_on:
- db
backend:
image: git.dergrimm.net/dergrimm/blog_backend:latest
redis:
image: docker.io/redis:7-alpine
restart: always
redis-commander:
image: rediscommander/redis-commander:latest
restart: always
environment:
REDIS_HOSTS: local:redis:6379
ports:
- 8081:8081
depends_on:
- redis
blog:
image: git.dergrimm.net/dergrimm/blog:latest
build:
context: ./backend
restart: always
command: worker
environment:
BACKEND_BIND_URL: 0.0.0.0:80
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
BACKEND_REDIS_URL: redis://redis
BACKEND_CACHE_POST_CONTENT_TTL: 3600
volumes:
- ./blog:/blog
ports:
- 80:80
depends_on:
- db
- redis
volumes:
db: