1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only
use anyhow::bail;
use clap::Subcommand;
use ordinary_config::{
AssetsConfig, CompressionAlgorithm, CompressionAlgorithms, ErrorConfig, HttpCache,
HttpCacheControl, HttpCsp, OrdinaryConfig,
};
use std::io::Write;
use std::path::Path;
use tracing::instrument;
#[derive(Subcommand, Debug)]
pub enum Ssg {
/// generate an `ordinary.json` config to deploy your SSG to a running instance of `ordinaryd`
Init {
/// project domain
domain: String,
/// assets `dir_path`
dir_path: String,
/// contacts for Let's Encrypt (auto-TLS) [source](https://docs.rs/rustls-acme/latest/rustls_acme/struct.AcmeConfig.html#method.contact_push)
#[arg(long, short, value_delimiter = ',', num_args = 1..)]
contacts: Option<Vec<String>>,
/// include generated 404.html as error page
#[arg(long, short, default_value_t = false)]
error_page: bool,
/// accommodate inline styles in the generated files
#[arg(long, short('s'), default_value_t = false)]
inline_styles: bool,
/// accommodate inline scripts in the generated files
#[arg(long, short('r'), default_value_t = false)]
inline_scripts: bool,
/// accommodate inline images in the generated files
#[arg(long, short('m'), default_value_t = false)]
inline_images: bool,
},
}
impl Ssg {
#[instrument(skip_all, name = "ssg")]
pub fn handle(&self, project: &str) -> anyhow::Result<()> {
match self {
Ssg::Init {
domain,
dir_path,
contacts,
error_page,
inline_styles,
inline_scripts,
inline_images,
} => {
let error = if *error_page {
Some(ErrorConfig {
asset: Some("404.html".into()),
..Default::default()
})
} else {
None
};
let html_csp = if *inline_scripts || *inline_styles || *inline_images {
Some(HttpCsp {
script_src: if *inline_scripts {
Some("'self' 'unsafe-inline'".into())
} else {
None
},
style_src: if *inline_styles {
Some("'self' 'unsafe-inline'".into())
} else {
None
},
img_src: if *inline_images {
Some("'self' data: 'unsafe-eval'".into())
} else {
None
},
..Default::default()
})
} else {
None
};
let config = OrdinaryConfig {
domain: domain.to_owned(),
contacts: contacts.to_owned(),
version: "0.1.0".into(),
port: Some(4433),
storage_size: Some(10_000_000),
error,
assets: Some(AssetsConfig {
dir_path: Some(dir_path.to_owned()),
base_route: "/".into(),
append_index_html: Some(true),
html_csp,
http: Some(HttpCache {
cache_control: Some(HttpCacheControl {
max_age: Some(3600),
..Default::default()
}),
expires: Some(3600),
..Default::default()
}),
precompression: Some(CompressionAlgorithms(vec![
CompressionAlgorithm::All,
])),
minify_html: Some(true),
..Default::default()
}),
..Default::default()
};
let oj_path = Path::new(project).join("ordinary.json");
if oj_path.exists() {
bail!("ordinary.json already exists in the '{project}' directory")
}
let mut file = fs_err::File::create(oj_path)?;
serde_json::to_writer_pretty(&mut file, &config)?;
file.flush()?;
}
}
Ok(())
}
}