Switch over to WebAssembly, Rust and Yew #35
1
.env.local
Normal file
1
.env.local
Normal file
@ -0,0 +1 @@
|
|||||||
|
OPENAI_API_KEY=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
|
48
.gitignore
vendored
48
.gitignore
vendored
@ -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
71
.swcrc
Normal 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
393
Cargo.lock
generated
@ -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"
|
||||||
|
22
Cargo.toml
22
Cargo.toml
@ -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
130
README.md
@ -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
9
Trunk.toml
Normal 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" ]
|
@ -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
24
package.json
Normal 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
9
postcss.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-import": {},
|
||||||
|
"tailwindcss/nesting": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
||||||
|
},
|
||||||
|
};
|
51
src/app.rs
51
src/app.rs
@ -1,8 +1,51 @@
|
|||||||
use yew::{function_component, html, Html};
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
use yew_router::Switch;
|
||||||
|
|
||||||
#[function_component(App)]
|
#[cfg(feature = "static")]
|
||||||
pub fn app() -> Html {
|
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! {
|
html! {
|
||||||
<h1>{"Hello, World!"}</h1>
|
<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)]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub fn app(props: &AppProps) -> Html {
|
||||||
|
#[cfg(feature = "static")]
|
||||||
|
{
|
||||||
|
let history = AnyHistory::from(MemoryHistory::default());
|
||||||
|
history.push(&props.url);
|
||||||
|
html! {
|
||||||
|
<Router history={history}>
|
||||||
|
<AppContent />
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "static"))]
|
||||||
|
html! {
|
||||||
|
<BrowserRouter>
|
||||||
|
<AppContent />
|
||||||
|
</BrowserRouter>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
@ -1 +1,2 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod pages;
|
||||||
|
34
src/pages.rs
Normal file
34
src/pages.rs
Normal 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
6
src/pages/about.rs
Normal 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
6
src/pages/blog.rs
Normal 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
11
src/pages/blog_post.rs
Normal 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
6
src/pages/disclaimer.rs
Normal 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
22
src/pages/home.rs
Normal 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
4
style/main.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@import "tailwind.css";
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
}
|
3
style/tailwind.css
Normal file
3
style/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
16
tailwind.config.js
Normal file
16
tailwind.config.js
Normal 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
103
tools/finalize.js
Normal 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
18
tools/prepare.sh
Normal 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
|
Loading…
Reference in New Issue
Block a user