cache busting

for faster load times, and seamless updates.
because for some reason ServeDir can't do it correctly, and it tells the client "yeah hey nothing changed" even if something changed
This commit is contained in:
liquidex 2024-09-04 21:48:42 +02:00
parent 9b190165ff
commit 5e6b84bed5
24 changed files with 580 additions and 203 deletions

33
Cargo.lock generated
View file

@ -213,6 +213,19 @@ dependencies = [
"digest",
]
[[package]]
name = "blake3"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -254,12 +267,13 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "cc"
version = "1.1.8"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
@ -309,6 +323,12 @@ dependencies = [
"tracing-error",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "copy_dir"
version = "0.1.3"
@ -780,6 +800,7 @@ checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown",
"serde",
]
[[package]]
@ -1250,6 +1271,7 @@ dependencies = [
"argon2",
"axum",
"base64 0.22.1",
"blake3",
"chrono",
"color-eyre",
"copy_dir",
@ -1260,6 +1282,7 @@ dependencies = [
"handlebars",
"indexmap",
"jotdown",
"mime_guess",
"rand",
"rand_chacha",
"rayon",
@ -1427,6 +1450,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"

View file

@ -4,7 +4,6 @@ use core::{
};
use alloc::vec::Vec;
use log::info;
use crate::{
ast::{Ast, NodeId, NodeKind},

View file

@ -7,6 +7,7 @@ edition = "2021"
argon2 = "0.5.3"
axum = { version = "0.7.5", features = ["macros", "ws"] }
base64 = "0.22.1"
blake3 = "1.5.4"
chrono = "0.4.38"
color-eyre = "0.6.3"
copy_dir = "0.1.3"
@ -15,8 +16,9 @@ derive_more = { version = "1.0.0", features = ["try_from"] }
eyre = "0.6.12"
haku.workspace = true
handlebars = "6.0.0"
indexmap = "2.4.0"
indexmap = { version = "2.4.0", features = ["serde"] }
jotdown = "0.5.0"
mime_guess = "2.0.5"
rand = "0.8.5"
rand_chacha = "0.3.1"
rayon = "1.10.0"

129
crates/rkgk/src/build.rs Normal file
View file

@ -0,0 +1,129 @@
use std::{
ffi::OsStr,
fs::{copy, create_dir_all, remove_dir_all, write},
};
use copy_dir::copy_dir;
use eyre::Context;
use handlebars::Handlebars;
use import_map::ImportMap;
use include_static::IncludeStatic;
use serde::Serialize;
use static_urls::StaticUrls;
use tracing::{info, instrument};
use walkdir::WalkDir;
pub mod import_map;
mod include_static;
mod static_urls;
use crate::{
config::{BuildConfig, RenderTemplateFiles},
Paths,
};
#[instrument(skip(paths, config))]
pub fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> {
info!("building static site");
_ = remove_dir_all(paths.target_dir);
create_dir_all(paths.target_dir).context("cannot create target directory")?;
copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?;
create_dir_all(paths.target_dir.join("static/wasm"))
.context("cannot create static/wasm directory")?;
copy(
paths.target_wasm_dir.join("haku_wasm.wasm"),
paths.target_dir.join("static/wasm/haku.wasm"),
)
.context("cannot copy haku.wasm file")?;
let import_map = ImportMap::generate("".into(), &config.import_roots);
write(
paths.target_dir.join("static/import_map.json"),
serde_json::to_string(&import_map)?,
)?;
let mut handlebars = Handlebars::new();
handlebars.register_helper(
"static",
Box::new(StaticUrls::new(
paths.target_dir.join("static"),
"/static".into(),
)),
);
handlebars.register_helper(
"include_static",
Box::new(IncludeStatic {
base_dir: paths.target_dir.join("static"),
}),
);
for entry in WalkDir::new("template") {
let entry = entry?;
let path = entry.path();
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
if file_name
.rsplit_once('.')
.is_some_and(|(left, _)| left.ends_with(".hbs"))
{
handlebars.register_template_file(&file_name, path)?;
info!(file_name, "registered template");
}
}
#[derive(Serialize)]
struct SingleFileData {}
#[derive(Serialize)]
struct DjotData {
title: String,
content: String,
}
for render_template in &config.render_templates {
info!(?render_template);
match &render_template.files {
RenderTemplateFiles::SingleFile { to_file } => {
let rendered = handlebars.render(&render_template.template, &SingleFileData {})?;
std::fs::write(paths.target_dir.join(to_file), rendered)?;
}
RenderTemplateFiles::Directory { from_dir, to_dir } => {
create_dir_all(paths.target_dir.join(to_dir))?;
for entry in WalkDir::new(from_dir) {
let entry = entry?;
let inner_path = entry.path().strip_prefix(from_dir)?;
if entry.path().extension() == Some(OsStr::new("dj")) {
let djot = std::fs::read_to_string(entry.path())?;
let events = jotdown::Parser::new(&djot);
let content = jotdown::html::render_to_string(events);
let title = config
.page_titles
.get(entry.path())
.cloned()
.unwrap_or_else(|| entry.path().to_string_lossy().into_owned());
let rendered = handlebars
.render(&render_template.template, &DjotData { title, content })?;
std::fs::write(
paths
.target_dir
.join(to_dir)
.join(inner_path.with_extension("html")),
rendered,
)?;
}
}
}
}
}
Ok(())
}

View file

@ -0,0 +1,67 @@
use std::{ffi::OsStr, path::PathBuf};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use tracing::warn;
use walkdir::WalkDir;
use super::static_urls::StaticUrls;
#[derive(Debug, Clone, Serialize)]
pub struct ImportMap {
pub imports: IndexMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ImportRoot {
pub name: String,
pub path: String,
}
impl ImportMap {
pub fn generate(base_url: String, import_roots: &[ImportRoot]) -> Self {
let mut import_map = ImportMap {
imports: IndexMap::new(),
};
for root in import_roots {
let static_urls = StaticUrls::new(
PathBuf::from(&root.path),
format!("{base_url}/{}", root.path),
);
for entry in WalkDir::new(&root.path) {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
warn!("directory walk failed: {error}");
continue;
}
};
if !entry.file_type().is_dir() && entry.path().extension() == Some(OsStr::new("js"))
{
let normalized_path = entry
.path()
.strip_prefix(&root.path)
.unwrap_or(entry.path())
.to_string_lossy()
.replace('\\', "/");
match static_urls.get(&normalized_path) {
Ok(url) => {
import_map
.imports
.insert(format!("{}/{normalized_path}", root.name), url);
}
Err(error) => {
warn!("could not get static url for {normalized_path}: {error}")
}
}
}
}
}
import_map.imports.sort_unstable_keys();
import_map
}
}

View file

@ -0,0 +1,31 @@
use std::path::PathBuf;
use handlebars::{
Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason,
ScopedJson,
};
use serde_json::Value;
pub struct IncludeStatic {
pub base_dir: PathBuf,
}
impl HelperDef for IncludeStatic {
fn call_inner<'reg: 'rc, 'rc>(
&self,
helper: &Helper<'rc>,
_: &'reg Handlebars<'reg>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'rc>, RenderError> {
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
return Ok(ScopedJson::Derived(Value::String(
std::fs::read_to_string(self.base_dir.join(param)).map_err(|error| {
RenderErrorReason::Other(format!("cannot read static asset {param}: {error}"))
})?,
)));
}
Err(RenderErrorReason::Other("asset path must be provided".into()).into())
}
}

View file

@ -0,0 +1,79 @@
use std::{
collections::HashMap,
fs::File,
io::{self, BufReader},
path::PathBuf,
sync::RwLock,
};
use handlebars::{
Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason,
ScopedJson,
};
use serde_json::Value;
pub struct StaticUrls {
base_dir: PathBuf,
base_url: String,
// Really annoying that we have to use an RwLock for this. We only ever generate in a
// single-threaded environment.
// Honestly it would be a lot more efficient if Handlebars just assumed single-threadedness
// and required you to clone it over to different threads.
// Stuff like this is why I really want to implement my own templating engine...
hash_cache: RwLock<HashMap<String, String>>,
}
impl StaticUrls {
pub fn new(base_dir: PathBuf, base_url: String) -> Self {
Self {
base_dir,
base_url,
hash_cache: RwLock::new(HashMap::new()),
}
}
pub fn get(&self, filename: &str) -> Result<String, io::Error> {
let hash_cache = self.hash_cache.read().unwrap();
if let Some(cached) = hash_cache.get(filename) {
return Ok(cached.to_owned());
}
drop(hash_cache);
let mut hasher = blake3::Hasher::new();
let file = BufReader::new(File::open(self.base_dir.join(filename))?);
hasher.update_reader(file)?;
// NOTE: Here the hash is truncated to 8 characters. This is fine, because we don't
// care about security here - only detecting changes in files.
let hash = format!(
"{}/{}?cache=b3-{}",
self.base_url,
filename,
&hasher.finalize().to_hex()[0..8]
);
{
let mut hash_cache = self.hash_cache.write().unwrap();
hash_cache.insert(filename.to_owned(), hash.clone());
}
Ok(hash)
}
}
impl HelperDef for StaticUrls {
fn call_inner<'reg: 'rc, 'rc>(
&self,
helper: &Helper<'rc>,
_: &'reg Handlebars<'reg>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'rc>, RenderError> {
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
return Ok(ScopedJson::Derived(Value::String(
self.get(param).map_err(|error| {
RenderErrorReason::Other(format!("cannot get asset url for {param}: {error}"))
})?,
)));
}
Err(RenderErrorReason::Other("asset path must be provided".into()).into())
}
}

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize};
use crate::wall;
use crate::{build::import_map::ImportRoot, wall};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
@ -15,6 +15,7 @@ pub struct Config {
pub struct BuildConfig {
pub render_templates: Vec<RenderTemplate>,
pub page_titles: HashMap<PathBuf, String>,
pub import_roots: Vec<ImportRoot>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -27,5 +28,6 @@ pub struct RenderTemplate {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RenderTemplateFiles {
SingleFile { to_file: PathBuf },
Directory { from_dir: PathBuf, to_dir: PathBuf },
}

View file

@ -1,29 +1,20 @@
use std::{
ffi::OsStr,
fs::{copy, create_dir_all, remove_dir_all},
net::Ipv4Addr,
path::Path,
sync::Arc,
};
use std::{fs::create_dir_all, net::Ipv4Addr, path::Path, sync::Arc};
use api::Api;
use axum::Router;
use config::{BuildConfig, Config, RenderTemplateFiles};
use copy_dir::copy_dir;
use config::Config;
use eyre::Context;
use handlebars::Handlebars;
use serde::Serialize;
use router::router;
use tokio::{fs, net::TcpListener};
use tower_http::services::{ServeDir, ServeFile};
use tracing::{info, instrument};
use walkdir::WalkDir;
use tracing::info;
mod api;
mod auto_reload;
mod build;
mod config;
mod haku;
mod id;
mod live_reload;
mod login;
mod router;
mod schema;
mod serialization;
mod wall;
@ -33,87 +24,11 @@ mod wall;
static GLOBAL_ALLOCATOR: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
struct Paths<'a> {
target_dir: &'a Path,
target_wasm_dir: &'a Path,
database_dir: &'a Path,
}
#[instrument(skip(paths, config))]
fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> {
info!("building static site");
_ = remove_dir_all(paths.target_dir);
create_dir_all(paths.target_dir).context("cannot create target directory")?;
copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?;
create_dir_all(paths.target_dir.join("static/wasm"))
.context("cannot create static/wasm directory")?;
copy(
paths.target_wasm_dir.join("haku_wasm.wasm"),
paths.target_dir.join("static/wasm/haku.wasm"),
)
.context("cannot copy haku.wasm file")?;
let mut handlebars = Handlebars::new();
for entry in WalkDir::new("template") {
let entry = entry?;
let path = entry.path();
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
if file_name
.rsplit_once('.')
.is_some_and(|(left, _)| left.ends_with(".hbs"))
{
handlebars.register_template_file(&file_name, path)?;
info!(file_name, "registered template");
}
}
#[derive(Serialize)]
struct DjotData {
title: String,
content: String,
}
for render_template in &config.render_templates {
info!(?render_template);
match &render_template.files {
RenderTemplateFiles::Directory { from_dir, to_dir } => {
create_dir_all(paths.target_dir.join(to_dir))?;
for entry in WalkDir::new(from_dir) {
let entry = entry?;
let inner_path = entry.path().strip_prefix(from_dir)?;
if entry.path().extension() == Some(OsStr::new("dj")) {
let djot = std::fs::read_to_string(entry.path())?;
let events = jotdown::Parser::new(&djot);
let content = jotdown::html::render_to_string(events);
let title = config
.page_titles
.get(entry.path())
.cloned()
.unwrap_or_else(|| entry.path().to_string_lossy().into_owned());
let rendered = handlebars
.render(&render_template.template, &DjotData { title, content })?;
std::fs::write(
paths
.target_dir
.join(to_dir)
.join(inner_path.with_extension("html")),
rendered,
)?;
}
}
}
}
}
Ok(())
#[derive(Debug, Clone, Copy)]
pub struct Paths<'a> {
pub target_dir: &'a Path,
pub target_wasm_dir: &'a Path,
pub database_dir: &'a Path,
}
pub struct Databases {
@ -152,20 +67,11 @@ async fn fallible_main() -> eyre::Result<()> {
)
.context("cannot deserialize config file")?;
build(&paths, &config.build)?;
build::build(&paths, &config.build)?;
let dbs = Arc::new(database(&config, &paths)?);
let api = Arc::new(Api { config, dbs });
let app = Router::new()
.route_service(
"/",
ServeFile::new(paths.target_dir.join("static/index.html")),
)
.nest_service("/static", ServeDir::new(paths.target_dir.join("static")))
.nest_service("/docs", ServeDir::new(paths.target_dir.join("docs")))
.nest("/api", api::router(api));
let app = app.nest("/auto-reload", live_reload::router());
let app = router(&paths, api);
let port: u16 = std::env::var("RKGK_PORT")
.unwrap_or("8080".into())

97
crates/rkgk/src/router.rs Normal file
View file

@ -0,0 +1,97 @@
use std::{path::PathBuf, sync::Arc};
use axum::{
extract::{Path, Query, State},
http::{
header::{CACHE_CONTROL, CONTENT_TYPE},
HeaderValue,
},
response::{Html, IntoResponse, Response},
routing::get,
Router,
};
use serde::Deserialize;
use crate::{
api::{self, Api},
auto_reload, Paths,
};
struct Server {
target_dir: PathBuf,
index_html: String,
four_oh_four_html: String,
}
pub fn router<S>(paths: &Paths, api: Arc<Api>) -> Router<S> {
Router::new()
.route("/", get(index))
.route("/static/*path", get(static_file))
.route("/docs/*path", get(docs))
.nest("/api", api::router(api))
.nest("/auto-reload", auto_reload::router())
.fallback(get(four_oh_four))
.with_state(Arc::new(Server {
target_dir: paths.target_dir.to_path_buf(),
index_html: std::fs::read_to_string(paths.target_dir.join("static/index.html"))
.expect("index.html does not exist"),
four_oh_four_html: std::fs::read_to_string(paths.target_dir.join("static/404.html"))
.expect("404.html does not exist"),
}))
}
async fn index(State(state): State<Arc<Server>>) -> Html<String> {
Html(state.index_html.clone())
}
async fn four_oh_four(State(state): State<Arc<Server>>) -> Html<String> {
Html(state.four_oh_four_html.clone())
}
#[derive(Deserialize)]
struct StaticFileQuery {
cache: Option<String>,
}
async fn static_file(
Path(path): Path<String>,
Query(query): Query<StaticFileQuery>,
State(state): State<Arc<Server>>,
) -> Response {
if let Ok(file) = tokio::fs::read(state.target_dir.join("static").join(&path)).await {
let mut response = file.into_response();
if let Some(content_type) = mime_guess::from_path(&path).first_raw() {
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
} else {
response.headers_mut().remove(CONTENT_TYPE);
}
if query.cache.is_some() {
response.headers_mut().insert(
CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),
);
}
response
} else {
four_oh_four(State(state)).await.into_response()
}
}
async fn docs(Path(mut path): Path<String>, state: State<Arc<Server>>) -> Html<String> {
if !path.ends_with(".html") {
path.push_str(".html")
}
if let Ok(file) = tokio::fs::read_to_string(state.target_dir.join("docs").join(&path)).await {
Html(file)
} else {
four_oh_four(state).await
}
}

View file

@ -4,7 +4,15 @@
# List of Handlebars templates to render.
render_templates = [
{ template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" }
{ template = "fonts.hbs.css", to_file = "static/fonts.css" },
{ template = "index.hbs.html", to_file = "static/index.html" },
{ template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" },
]
# List of JavaScript `import` root directories.
import_roots = [
{ name = "rkgk", path = "static" },
]
[build.page_titles]

17
static/404.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>
<p>It appears the thing you're looking for doesn't exist.</p>
<p><a href="/">Back to rakugaki</a></p>
</body>
</html>
<!-- This page could really be prettier, don't you think? -->

View file

@ -23,50 +23,6 @@ body {
/* Fonts */
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Regular"),
url("font/AtkinsonHyperlegible-Regular.ttf");
font-weight: 400;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Italic"),
url("font/AtkinsonHyperlegible-Italic.ttf");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Bold"),
url("font/AtkinsonHyperlegible-Bold.ttf");
font-weight: 700;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Bold Italic"),
url("font/AtkinsonHyperlegible-BoldItalic.ttf");
font-weight: 700;
font-style: italic;
}
@font-face {
/* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices
of Atkinson Hyperlegible. */
font-family: "Iosevka Hyperlegible";
src:
local("Iosevka Hyperlegible"),
url("font/IosevkaHyperlegible-Regular.woff2");
font-weight: 400;
}
:root {
font-family: "Atkinson Hyperlegible", sans-serif;
}

View file

@ -1,6 +1,6 @@
import { listen } from "./framework.js";
import { Viewport } from "./viewport.js";
import { Wall } from "./wall.js";
import { listen } from "rkgk/framework.js";
import { Viewport } from "rkgk/viewport.js";
import { Wall } from "rkgk/wall.js";
class CanvasRenderer extends HTMLElement {
viewport = new Viewport();

View file

@ -8,7 +8,7 @@ function makeLogFunction(level) {
}
let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming(
fetch(import.meta.resolve("./wasm/haku.wasm")),
fetch(HAKU_WASM_PATH),
{
env: {
panic(length, pMessage) {

View file

@ -1,4 +1,4 @@
import { Wall } from "./wall.js";
import { Wall } from "rkgk/wall.js";
import {
getLoginSecret,
getUserId,
@ -6,9 +6,9 @@ import {
newSession,
registerUser,
waitForLogin,
} from "./session.js";
import { debounce } from "./framework.js";
import { ReticleCursor } from "./reticle-renderer.js";
} from "rkgk/session.js";
import { debounce } from "rkgk/framework.js";
import { ReticleCursor } from "rkgk/reticle-renderer.js";
const updateInterval = 1000 / 60;

View file

@ -1,5 +1,5 @@
import { Haku } from "./haku.js";
import { Painter } from "./painter.js";
import { Haku } from "rkgk/haku.js";
import { Painter } from "rkgk/painter.js";
export class User {
nickname = "";

View file

@ -1,4 +1,4 @@
import { listen } from "./framework.js";
import { listen } from "rkgk/framework.js";
export class ResizeHandle extends HTMLElement {
constructor() {

View file

@ -1,4 +1,4 @@
import { listen } from "./framework.js";
import { listen } from "rkgk/framework.js";
let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}");

View file

@ -1,5 +1,5 @@
import { Pixmap } from "./haku.js";
import { OnlineUsers } from "./online-users.js";
import { Pixmap } from "rkgk/haku.js";
import { OnlineUsers } from "rkgk/online-users.js";
export class Chunk {
constructor(size) {

View file

@ -4,12 +4,13 @@
<head>
<meta charset="UTF-8">
<title>{{ title }} · rakugaki manual</title>
<link rel="stylesheet" href="/static/base.css">
<link rel="stylesheet" href="/static/docs.css">
<link rel="stylesheet" href="{{ static 'base.css' }}">
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
<link rel="stylesheet" href="{{ static 'docs.css' }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="/static/live-reload.js" type="module"></script>
<script src="{{ static 'live-reload.js' }}" type="module"></script>
<link rel="icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
<link rel="icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">

44
template/fonts.hbs.css Normal file
View file

@ -0,0 +1,44 @@
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Regular"),
url("{{ static 'font/AtkinsonHyperlegible-Regular.ttf' }}");
font-weight: 400;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Italic"),
url("{{ static 'font/AtkinsonHyperlegible-Italic.ttf' }}");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Bold"),
url("{{ static 'font/AtkinsonHyperlegible-Bold.ttf' }}");
font-weight: 700;
}
@font-face {
font-family: "Atkinson Hyperlegible";
src:
local("Atkinson Hyperlegible Bold Italic"),
url("{{ static 'font/AtkinsonHyperlegible-BoldItalic.ttf' }}");
font-weight: 700;
font-style: italic;
}
@font-face {
/* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices
of Atkinson Hyperlegible. */
font-family: "Iosevka Hyperlegible";
src:
local("Iosevka Hyperlegible"),
url("{{ static 'font/IosevkaHyperlegible-Regular.woff2' }}");
font-weight: 400;
}

View file

@ -6,38 +6,47 @@
<title>rakugaki</title>
<link rel="stylesheet" href="static/base.css">
<link rel="stylesheet" href="static/index.css">
<link rel="stylesheet" href="{{ static 'base.css' }}">
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
<link rel="stylesheet" href="{{ static 'index.css' }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="static/live-reload.js" type="module"></script>
<script src="static/brush-editor.js" type="module"></script>
<script src="static/canvas-renderer.js" type="module"></script>
<script src="static/connection-status.js" type="module"></script>
<script src="static/framework.js" type="module"></script>
<script src="static/resize-handle.js" type="module"></script>
<script src="static/reticle-renderer.js" type="module"></script>
<script src="static/session.js" type="module"></script>
<script src="static/throbber.js" type="module"></script>
<script src="static/viewport.js" type="module"></script>
<script src="static/welcome.js" type="module"></script>
<script src="static/index.js" type="module" defer></script>
<script type="importmap">{{{ include_static 'import_map.json' }}}</script>
<link rel="icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
<link rel="icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">
<link rel="icon" sizes="64x64" href="/static/favicon/rkgk@4x.png">
<link rel="icon" sizes="128x128" href="/static/favicon/rkgk@8x.png">
<link rel="icon" sizes="256x256" href="/static/favicon/rkgk@16x.png">
<link rel="icon" sizes="512x512" href="/static/favicon/rkgk@32x.png">
<link rel="apple-touch-icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
<link rel="apple-touch-icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">
<link rel="apple-touch-icon" sizes="64x64" href="/static/favicon/rkgk@4x.png">
<link rel="apple-touch-icon" sizes="128x128" href="/static/favicon/rkgk@8x.png">
<link rel="apple-touch-icon" sizes="256x256" href="/static/favicon/rkgk@16x.png">
<link rel="apple-touch-icon" sizes="512x512" href="/static/favicon/rkgk@32x.png">
<script>
const HAKU_WASM_PATH = "{{{ static 'wasm/haku.wasm' }}}";
</script>
<script type="module">
import "rkgk/live-reload.js";
import "rkgk/brush-editor.js";
import "rkgk/canvas-renderer.js";
import "rkgk/connection-status.js";
import "rkgk/framework.js";
import "rkgk/resize-handle.js";
import "rkgk/reticle-renderer.js";
import "rkgk/session.js";
import "rkgk/throbber.js";
import "rkgk/viewport.js";
import "rkgk/welcome.js";
import "rkgk/index.js";
</script>
<link rel="icon" sizes="16x16" href="{{ static 'favicon/rkgk@1x.png' }}">
<link rel="icon" sizes="32x32" href="{{ static 'favicon/rkgk@2x.png' }}">
<link rel="icon" sizes="64x64" href="{{ static 'favicon/rkgk@4x.png' }}">
<link rel="icon" sizes="128x128" href="{{ static 'favicon/rkgk@8x.png' }}">
<link rel="icon" sizes="256x256" href="{{ static 'favicon/rkgk@16x.png' }}">
<link rel="icon" sizes="512x512" href="{{ static 'favicon/rkgk@32x.png' }}">
<link rel="apple-touch-icon" sizes="16x16" href="{{ static 'favicon/rkgk@1x.png' }}">
<link rel="apple-touch-icon" sizes="32x32" href="{{ static 'favicon/rkgk@2x.png' }}">
<link rel="apple-touch-icon" sizes="64x64" href="{{ static 'favicon/rkgk@4x.png' }}">
<link rel="apple-touch-icon" sizes="128x128" href="{{ static 'favicon/rkgk@8x.png' }}">
<link rel="apple-touch-icon" sizes="256x256" href="{{ static 'favicon/rkgk@16x.png' }}">
<link rel="apple-touch-icon" sizes="512x512" href="{{ static 'favicon/rkgk@32x.png' }}">
</head>
<body>
@ -133,3 +142,4 @@
</main>
</body>
</html>