Switch over to WebAssembly, Rust and Yew #35

Merged
BlakeRain merged 87 commits from yew-static into main 2023-08-30 18:01:40 +00:00
26 changed files with 2587 additions and 577 deletions
Showing only changes of commit 8e349cdc23 - Show all commits

1
.env.local Normal file
View File

@ -0,0 +1 @@
OPENAI_API_KEY=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

48
.gitignore vendored
View File

@ -1,47 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Test data
/public/data
/public/feeds
/public/robots.txt
/public/sitemap.xml
/public/sitemap-0.xml
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
/.netlify
# misc
.DS_Store .DS_Store
*.pem /dist
/node_modules
# debug /output
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# Optimized images
/public/content/**/optimized
/public/content/images.sha256.json
/target /target

71
.swcrc Normal file
View File

@ -0,0 +1,71 @@
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": false
},
"target": "es2017",
"loose": false,
"minify": {
"compress": {
"arguments": false,
"arrows": true,
"booleans": true,
"booleans_as_integers": false,
"collapse_vars": true,
"comparisons": true,
"computed_props": true,
"conditionals": true,
"dead_code": true,
"directives": true,
"drop_console": false,
"drop_debugger": true,
"evaluate": true,
"expression": false,
"hoist_funs": false,
"hoist_props": true,
"hoist_vars": false,
"if_return": true,
"join_vars": true,
"keep_classnames": false,
"keep_fargs": true,
"keep_fnames": false,
"keep_infinity": false,
"loops": true,
"negate_iife": true,
"properties": true,
"reduce_funcs": false,
"reduce_vars": false,
"side_effects": true,
"switches": true,
"typeofs": true,
"unsafe": false,
"unsafe_arrows": false,
"unsafe_comps": false,
"unsafe_Function": false,
"unsafe_math": false,
"unsafe_symbols": false,
"unsafe_methods": false,
"unsafe_proto": false,
"unsafe_regexp": false,
"unsafe_undefined": false,
"unused": true,
"const_to_let": true,
"pristine_globals": true
},
"mangle": {
"toplevel": false,
"keep_classnames": false,
"keep_fnames": false,
"keep_private_props": false,
"ie8": false,
"safari10": false
}
}
},
"module": {
"type": "es6"
},
"minify": true,
"isModule": true
}

393
Cargo.lock generated
View File

@ -26,127 +26,18 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]] [[package]]
name = "anymap2" name = "anymap2"
version = "0.13.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "async-trait"
version = "0.1.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"mime",
"rustversion",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.68" version = "0.3.68"
@ -222,53 +113,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -586,12 +430,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.2" version = "0.3.2"
@ -618,64 +456,12 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.9",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]] [[package]]
name = "implicit-clone" name = "implicit-clone"
version = "0.3.6" version = "0.3.6"
@ -712,26 +498,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jemalloc-sys"
version = "0.5.4+5.3.0-patched"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6c1946e1cea1788cbfde01c993b52a10e2da07f4bac608228d1bed20bfebf2"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "jemallocator"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0de374a9f8e63150e6f5e8a60cc14c668226d7a347d8aee1a45766e3c4dd3bc"
dependencies = [
"jemalloc-sys",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.64" version = "0.3.64"
@ -769,34 +535,12 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "matchit"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.1" version = "0.7.1"
@ -1106,16 +850,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_path_to_error"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
dependencies = [
"itoa",
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1141,16 +875,10 @@ dependencies = [
name = "site" name = "site"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"axum",
"clap",
"env_logger", "env_logger",
"futures", "futures",
"hyper",
"jemallocator",
"log", "log",
"tokio", "tokio",
"tower",
"tower-http",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-logger", "wasm-logger",
"yew", "yew",
@ -1173,16 +901,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "socket2"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.3" version = "0.5.3"
@ -1193,12 +911,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -1221,12 +933,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.2.0" version = "1.2.0"
@ -1270,7 +976,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.3", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys",
] ]
@ -1297,72 +1003,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82"
dependencies = [
"bitflags 2.4.0",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.37" version = "0.1.37"
@ -1370,7 +1010,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@ -1396,21 +1035,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.11" version = "1.0.11"
@ -1423,27 +1047,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -5,21 +5,23 @@ edition = "2021"
publish = false publish = false
[features] [features]
ssr = ["yew/ssr"] hydration = [
hydration = ["yew/hydration"] "yew/hydration"
]
[[bin]] static = [
name = "site-hydrate" "yew/ssr"
required-features = [
"hydrate"
] ]
[[bin]] [[bin]]
name = "site-build" name = "site-build"
required-features = [ required-features = [
"ssr" "static",
] ]
[[bin]]
name = "site"
[dependencies] [dependencies]
yew = { version = "0.20" } yew = { version = "0.20" }
yew-router = { version = "0.17" } yew-router = { version = "0.17" }
@ -38,11 +40,5 @@ wasm-logger = { version = "0.2" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.32", features = ["full"] } tokio = { version = "1.32", features = ["full"] }
axum = { version = "0.6" }
tower = { version = "0.4", features = ["make"] }
tower-http = { version = "0.4", features = ["fs"] }
env_logger = { version = "0.10" } env_logger = { version = "0.10" }
clap = { version = "4.3", features = ["derive"] }
hyper = { version = "0.14", features = ["server", "http1"] }
jemallocator = { version = "0.5" }

130
README.md
View File

@ -1,127 +1,33 @@
# blakerain.com # blakerain.com
![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)
![Next JS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white)
![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)
![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white)
![AmazonDynamoDB](https://img.shields.io/badge/DynamoDB-4053D6?style=for-the-badge&logo=Amazon%20DynamoDB&logoColor=white) ![AmazonDynamoDB](https://img.shields.io/badge/DynamoDB-4053D6?style=for-the-badge&logo=Amazon%20DynamoDB&logoColor=white)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white) ![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) ![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)
This repository contains the sources for my website: [blakerain.com]. The website is built using [Next.js]. It also This repository contains the sources for my website: [blakerain.com]. The website is built using [Yew]. It also
includes some analytics code that is written in Rust and runs in AWS. The website is stored in an [S3] bucket and served includes some analytics code that is written in Rust and runs in AWS. The website is stored in an [S3] bucket and served
using the AWS [CloudFront] CDN. Deployment from this repository is performed by a GitHub workflow. using the AWS [CloudFront] CDN. Deployment from this repository is performed by a GitHub workflow.
## Analytics ## Cargo Features
The analytics for the site is quite simple, consisting of a couple of AWS [lambda] functions and a [DynamoDB] table. The There are a number of Cargo feature flags that are used during development and during release
two lambda functions provide both the API and a back-end trigger function to perform accumulation. builds. These features are as follows:
![Layout](https://github.com/BlakeRain/blakerain.com/blob/main/public/content/site-analytics/analytics-layout.drawio.png?raw=true) - `static` feature is set when we want to build the static rendering application, called
`site-build`, which will generate the static HTML pages for the site.
- `hydration` feature is set when we're building the WebAssembly for hydration into a statically
rendered page.
The domain `pv.blakerain.com` is aliased to an AWS [API Gateway] domain. The domain is mapped to an API which allows During development, neither `static` nor `hydration` are set. This allows commands like `trunk
`GET` and `POST` requests and forwards these requests to the [api] Lambda function. The Lambda function receives these serve` to serve the WebAssembly and nicely rebuild upon file changes and so on.
HTTP `GET` or `POST` requests encoded as JSON (see the [input format] for a Lambda function for a proxy integration).
The Lambda function processes the request and then returns an HTTP response (see the [output format]).
The lambda function response to the following resources: During a release build, we first build the site using `trunk build` with the `hydration` feature
enabled. This will build a WebAssembly module with the hydration support. Afterwards, we use
`cargo run` to run the `site-build` app with the `static` feature set, which allows us to generate
all the static pages.
| Method | URL | Description | [blakerain.com]: https://blakerain.com/
|--------|-----------------------|--------------------------------------------| [Yew]: https://yew.rs/
| `GET` | `/pv.gif` | Records a page view | [S3]: https://aws.amazon.com/s3/
| `POST` | `/append` | Update a page view record | [CloudFront]: https://aws.amazon.com/cloudfront/
| `POST` | `/api/auth/signin` | Authenticate access to the analytics |
| `POST` | `/api/views/week` | Get page view stats for a specific week |
| `POST` | `/api/views/month` | Get page view stats for a specific month |
| `POST` | `/api/browsers/week` | Get browser stats for a specific week |
| `POST` | `/api/browsers/month` | Get browser stats for a specific month |
| `POST` | `/api/pages/week` | Get the total page count for a given week |
| `POST` | `/api/pages/month` | Get the total page count for a given month |
### Recording Page Views
When a visitor loads the site, if they visit a page that includes the `<Analytics>` component, a request is made to the
`pv.blakerain.com` domain. Specifically, it tries to load an image from `https://pv.blakerain.com/pv.gif`. The URL
includes a query-string that encodes the various information about the site visit.
When the `GET` request for `pv.gif` image is received by the lambda function, it attempts to extract meaningful values
from the query string, and should it find any, record them as a page view into the DynamoDB table. The record for a page
view contains the following items:
| Field | Type | Description |
|------------------|----------|----------------------------------------------------------|
| `Path` | `String` | The path to the page being visited |
| `Section` | `String` | The string `view-` followed by the UUID of the page view |
| `Time` | `String` | The `OffsetDateTime` for the page view |
| `UserAgent` | `String` | The user agent string for the visitor's web browser |
| `ViewportWidth` | `Number` | The width of the viewport (i.e.: size of the window) |
| `ViewportHeight` | `Number` | The height of the viewport |
| `ScreenWidth` | `Number` | The width of the screen |
| `ScreenHeight` | `Number` | The height of the screen |
| `Timezone` | `String` | The visitor's timezone |
| `Referrer` | `String` | The referrer for the page view (e.g. `google.com`) |
| `Duration` | `Number` | The number of seconds the visitor has been on this page |
| `Scroll` | `Number` | The maximum distance that was scrolled (as a percentage) |
Of the above fields, the only fields that are required are the `Path`, `Section` and `Time` fields. All other fields are
entirely optional, and are only included in the record if they are present in the query-string given in the `GET`
request and can be parsed.
Once the `<Analytics>` component has mounted, it adds event listeners for the `scroll`, `visibilitychange` and
`pagehide` events. When the visitor scrolls the page, the `Analytics` component keeps track of the maximum scroll
distance. When the visibility of the page is changed, and the page is no longer visible, we also record the time at
which the page was hidden. Once the page is visible again, we record the total amount of time that the page was hidden.
This allows us to calculate the total amount of time that the page was visible.
When the page is hidden, or the `<Analytics>` component is unmounted, we send an update to the analytics record using a
`POST` request to the `/append` URL. This allows us to update the duration of a visit to the page and the maximum scroll
distance. The `POST` request send to the `/append` URL contains a JSON object with the following fields:
| Field | Type | Description |
|------------|----------|-----------------------------------------------------|
| `path` | `String` | The path to the page being visited |
| `uuid` | `String` | The UUID of the page view |
| `duration` | `Number` | The updated duration spent on the page (in seconds) |
| `scroll` | `Number` | The maximum scroll distance (as a percentage) |
In order to transmit updates to the page view, we use the `Navigator.sendBeacon` function (see the MDN documentation for
[sendBeacon]). Using the `sendBeacon` function allows us to send data, even when the browser is about to unload the
page.
### Accumulating Page Views
Once a new page view has been recorded in the DynamoDB table, I also want to update the accumulation of these page
views in a number of dimensions. The following table describes the dimensions that we want to record. This is given as
the subject (such as the page being visited, or the site as a whole) and the time-frame being accumulated (daily, weekly,
etc). The table also provides an example of the primary key and hash key used in the DynamoDB table for this recording.
| Object | Time-frame | Primary Key | Hash key | Description |
|-------------|------------|----------------------------|---------------------|--------------------------------------------------------|
| Total Views | Day | `page-view-day-2022-12-02` | `/page` | Number of views for a specific day (2nd December 2022) |
| | Week | `page-view-week-2022-48` | `/page` | Number of views for a specific week (week 48, 2022) |
| | Month | `page-view-month-2022-12` | `/page` | Number of views for a specific month (December 2022) |
| Page | Day | `/post/some-blog-post` | `Day-2022-12-02-18` | Number of views of page at hour of day (here at 18:00) |
| | Week | `/post/some-blog-post` | `Week-2022-48-05` | Number of views of page on day of week |
| | Month | `/post/some-blog-post` | `Month-2022-12-02` | Number of views of page on day of month |
| Entire Site | Day | `/site` | `Day-2022-12-02-18` | Number of views of page at hour of day (here at 18:00) |
| | Week | `/site` | `Week-2022-48-05` | Number of views of page on day of week |
| | Month | `/site` | `Month-2022-12-02` | Number of views of page on day of month |
Accumulating this data for each page view requires performing nine updates to the DynamoDB table. Performing these
updates whenever a request is made to the `pv.gif` image by the client is less than ideal. For this reason, a [trigger]
Lambda function is included which receives events from the DynamoDB table. If the event received by the trigger function
is the insertion of a new page view record, the function performs the above record updates.
[blakerain.com]: https://blakerain.com
[next.js]: https://nextjs.org
[s3]: https://aws.amazon.com/s3/
[cloudfront]: https://aws.amazon.com/cloudfront/
[lambda]: https://aws.amazon.com/lambda/
[dynamodb]: https://aws.amazon.com/dynamodb/
[api gateway]: https://aws.amazon.com/api-gateway/
[api]: https://github.com/BlakeRain/blakerain.com/blob/main/lambda/src/bin/api.rs
[trigger]: https://github.com/BlakeRain/blakerain.com/blob/main/lambda/src/bin/trigger.rs
[input format]: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
[output format]: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
[sendbeacon]: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon

9
Trunk.toml Normal file
View File

@ -0,0 +1,9 @@
[[hooks]]
stage = "pre_build"
command = "bash"
command_arguments = [ "tools/prepare.sh" ]
[[hooks]]
stage = "post_build"
command = "node"
command_arguments = [ "tools/finalize.js" ]

View File

@ -3,11 +3,6 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link <link data-trunk rel="rust" data-bin="site" />
data-trunk
rel="rust"
data-bin="site-hydrate"
data-cargo-features="hydration"
/>
</head> </head>
</html> </html>

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "site",
"version": "0.1.0",
"license": "MIT",
"private": true,
"scripts": {
"postcss-build": "postcss style/main.css -o target/main.css"
},
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.57",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.14",
"cssnano": "^6.0.1",
"html-minifier": "^4.0.0",
"postcss-cli": "^10.1.0",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.3.2"
},
"volta": {
"node": "18.17.1",
"yarn": "4.0.0-rc.48"
}
}

9
postcss.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
},
};

View File

@ -1,8 +1,51 @@
use yew::{function_component, html, Html}; use yew::{function_component, html, Html, Properties};
use yew_router::Switch;
#[cfg(feature = "static")]
use yew_router::{
history::{AnyHistory, History, MemoryHistory},
Router,
};
#[cfg(not(feature = "static"))]
use yew_router::BrowserRouter;
use crate::pages::Route;
#[function_component(AppContent)]
fn app_content() -> Html {
html! {
<main>
<Switch<Route> render={Route::switch} />
</main>
}
}
#[derive(Properties, PartialEq)]
#[cfg_attr(not(feature = "static"), derive(Default))]
pub struct AppProps {
#[cfg(feature = "static")]
pub url: String,
}
#[function_component(App)] #[function_component(App)]
pub fn app() -> Html { #[allow(unused_variables)]
pub fn app(props: &AppProps) -> Html {
#[cfg(feature = "static")]
{
let history = AnyHistory::from(MemoryHistory::default());
history.push(&props.url);
html! { html! {
<h1>{"Hello, World!"}</h1> <Router history={history}>
<AppContent />
</Router>
}
}
#[cfg(not(feature = "static"))]
html! {
<BrowserRouter>
<AppContent />
</BrowserRouter>
} }
} }

View File

@ -1,8 +1,135 @@
use site::app::App; use std::path::{Path, PathBuf};
use site::app::{App, AppProps};
use yew::ServerRenderer; use yew::ServerRenderer;
#[tokio::main] struct Template {
async fn main() { content: String,
let renderer = ServerRenderer::<App>::new(); index: usize,
println!("{}", renderer.render().await); }
impl Template {
async fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
log::info!("Loading template from: {:?}", path.as_ref());
let content = tokio::fs::read_to_string(path).await?;
let Some(index) = content.find("</body>") else {
log::error!("Failed to find index of '</body>' close tag in 'dist/index.html'");
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed index.html"));
};
Ok(Self { content, index })
}
async fn render(&self, app: ServerRenderer<App>) -> String {
let mut result = String::with_capacity(self.content.len());
result.push_str(&self.content[..self.index]);
app.render_to_string(&mut result).await;
result.push_str(&self.content[self.index..]);
result
}
}
async fn render_route(template: &Template, url: String) -> String {
let render = ServerRenderer::<App>::with_props(move || AppProps { url });
template.render(render).await
}
struct RenderRoute {
pub url: String,
pub path: PathBuf,
}
const STATIC_ROUTES: &[&str] = &["/", "/about", "/blog", "/disclaimer"];
async fn collect_routes(root_dir: impl AsRef<Path>) -> std::io::Result<Vec<RenderRoute>> {
let root_dir = root_dir.as_ref();
let mut routes = Vec::new();
for route in STATIC_ROUTES {
routes.push(RenderRoute {
url: route.to_string(),
path: if *route == "/" {
PathBuf::from("index.html")
} else {
PathBuf::from(&route[1..]).with_extension("html")
},
})
}
let posts_dir = root_dir.join("content").join("posts");
log::info!("Collecting blog posts from: {posts_dir:?}");
let mut posts = tokio::fs::read_dir(posts_dir).await?;
while let Some(entry) = posts.next_entry().await? {
let filename = PathBuf::from(entry.file_name());
let url = format!(
"/blog/{}",
filename
.file_stem()
.expect("there to be a filename")
.to_str()
.unwrap()
);
let path = Path::new("blog").join(filename.with_extension("html"));
routes.push(RenderRoute { url, path })
}
Ok(routes)
}
async fn copy_resources(
dist_dir: impl AsRef<Path>,
out_dir: impl AsRef<Path>,
) -> std::io::Result<()> {
let mut resources = tokio::fs::read_dir(dist_dir).await?;
while let Some(entry) = resources.next_entry().await? {
let Ok(file_type) = entry.file_type().await else {
log::error!("Could not get file type for: {:?}", entry.path());
continue;
};
if file_type.is_file() {
if entry.file_name() == "index.html" {
continue;
}
let path = entry.path();
log::info!("Copying resource: {:?}", path);
tokio::fs::copy(path, out_dir.as_ref().join(entry.file_name())).await?;
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dist_dir = root_dir.clone().join("dist");
log::info!("Established distribution directory: {dist_dir:?}");
let template = Template::load(dist_dir.join("index.html")).await?;
let out_dir = root_dir.join("output");
log::info!("Creating output directory: {out_dir:?}");
tokio::fs::remove_dir_all(&out_dir).await?;
tokio::fs::create_dir_all(&out_dir).await?;
copy_resources(&dist_dir, &out_dir).await?;
for RenderRoute { url, path } in collect_routes(&root_dir).await? {
log::info!("Rendering route: {url}");
let html = render_route(&template, url).await;
let path = out_dir.clone().join(path);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
log::info!("Writing route to file: {path:?}");
tokio::fs::write(path, html).await?;
}
Ok(())
} }

View File

@ -1,5 +1,12 @@
use site::app::App;
fn main() { fn main() {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
#[cfg(feature = "hydration")]
yew::Renderer::<App>::new().hydrate(); yew::Renderer::<App>::new().hydrate();
#[cfg(not(feature = "hydration"))]
yew::Renderer::<App>::new().render();
} }

View File

@ -1 +1,2 @@
pub mod app; pub mod app;
pub mod pages;

34
src/pages.rs Normal file
View File

@ -0,0 +1,34 @@
mod about;
mod blog;
mod blog_post;
mod disclaimer;
mod home;
use yew::{html, Html};
use yew_router::Routable;
#[derive(Debug, Clone, PartialEq, Eq, Routable)]
pub enum Route {
#[at("/")]
Home,
#[at("/about")]
About,
#[at("/blog")]
Blog,
#[at("/blog/{slug}")]
BlogPost { slug: String },
#[at("/disclaimer")]
Disclaimer,
}
impl Route {
pub fn switch(self) -> Html {
match self {
Self::Home => html! { <home::Page /> },
Self::About => html! { <about::Page /> },
Self::Blog => html! { <blog::Page /> },
Self::BlogPost { slug } => html! { <blog_post::Page slug={slug} /> },
Self::Disclaimer => html! { <disclaimer::Page /> },
}
}
}

6
src/pages/about.rs Normal file
View File

@ -0,0 +1,6 @@
use yew::{function_component, html, Html};
#[function_component(Page)]
pub fn page() -> Html {
html! { "About" }
}

6
src/pages/blog.rs Normal file
View File

@ -0,0 +1,6 @@
use yew::{function_component, html, Html};
#[function_component(Page)]
pub fn page() -> Html {
html! { "Blog" }
}

11
src/pages/blog_post.rs Normal file
View File

@ -0,0 +1,11 @@
use yew::{function_component, html, Html, Properties};
#[derive(Properties, PartialEq)]
pub struct PageProps {
pub slug: String,
}
#[function_component(Page)]
pub fn page(props: &PageProps) -> Html {
html! { <h1>{format!("Blog post '{}'", props.slug)}</h1> }
}

6
src/pages/disclaimer.rs Normal file
View File

@ -0,0 +1,6 @@
use yew::{function_component, html, Html};
#[function_component(Page)]
pub fn page() -> Html {
html! { "Disclaimer" }
}

22
src/pages/home.rs Normal file
View File

@ -0,0 +1,22 @@
use yew::{function_component, html, use_state, Callback, Html};
use yew_router::prelude::Link;
use crate::pages::Route;
#[function_component(Page)]
pub fn page() -> Html {
let count = use_state(|| 0);
let button_click = {
let count = count.clone();
Callback::from(move |_| count.set(*count + 1))
};
html! {
<>
<h1>{"Home"}</h1>
<button type="button" onclick={button_click}>{format!("Clicked {} times", *count)}</button>
<Link<Route> to={Route::About}>{"About"}</Link<Route>>
</>
}
}

4
style/main.css Normal file
View File

@ -0,0 +1,4 @@
@import "tailwind.css";
@layer components {
}

3
style/tailwind.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

16
tailwind.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
theme: {
extend: {
colors: {
primary: {
dark: "#0D1B2A",
DEFAULT: "#1B263B",
light: "#415A77",
lighter: "#778DA9",
},
},
},
},
content: ["./src/**/*.rs", "./index.html", "./style/**/*.css"],
plugins: [require("@tailwindcss/forms")],
};

103
tools/finalize.js Normal file
View File

@ -0,0 +1,103 @@
const fs = require("fs").promises;
const path = require("path");
const swc = require("@swc/core");
const minify_html = require("html-minifier").minify;
async function load_swc_config() {
try {
const content = await fs.readFile(
path.join(process.env.TRUNK_SOURCE_DIR, ".swcrc"),
"utf8"
);
return JSON.parse(content);
} catch (err) {
console.log(`Error reading file from disk: ${err}`);
}
}
async function* find_files(dir) {
const dirents = await fs.readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = path.resolve(dir, dirent.name);
if (dirent.isDirectory()) {
yield* find_files(res);
} else {
yield res;
}
}
}
async function min_js(filepath) {
const swc_config = await load_swc_config();
const output = await swc.transformFile(filepath, swc_config, swc_config);
await fs.writeFile(filepath, output.code);
return output.code.length;
}
async function min_html(filepath) {
const content = await fs.readFile(filepath, "utf8");
const output = minify_html(content, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
html5: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: false,
removeRedundantAttributes: true,
sortAttributes: true,
sortClassName: true,
trimCustomFragments: true,
});
await fs.writeFile(filepath, output);
return output.length;
}
const format = new Intl.NumberFormat();
function fmt(number) {
return format.format(number);
}
async function minify_javascript() {
console.log("Minifying HTML and JavaScript ...");
const staging_dir = process.env.TRUNK_STAGING_DIR;
for await (const filepath of find_files(staging_dir)) {
const minifier = filepath.endsWith(".js")
? min_js
: filepath.endsWith(".html")
? min_html
: null;
if (!minifier) {
continue;
}
console.log(" Minifying " + path.basename(filepath));
const size_before = (await fs.stat(filepath)).size;
const size_after = await minifier(filepath);
const delta = size_before - size_after;
const percent = (delta / size_before) * 100;
console.log(
` From ${fmt(size_before)} bytes to ${fmt(
size_after
)} bytes saving ${fmt(delta)} bytes (${percent.toFixed(2)}%)`
);
}
}
async function main() {
if (process.env.TRUNK_PROFILE == "release") {
await minify_javascript();
}
}
main().then(() => {});

18
tools/prepare.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
echo "Running 'yarn install' to install front-end dependencies ..."
yarn install
if [[ "$TRUNK_PROFILE" = "debug" ]]; then
LABEL="Debug"
ENV=""
elif [[ "$TRUNK_PROFILE" = "release" ]]; then
LABEL="Release"
ENV=""
else
echo "Unrecognized 'TRUNK_PROFILE' value '$TRUN_PROFILE'; expected either 'debug' or 'release'"
exit 1
fi
echo "Running 'yarn postcss-build' to build CSS ($LABEL) ..."
NODE_ENV=$ENV yarn postcss-build

2025
yarn.lock Normal file

File diff suppressed because it is too large Load Diff