Skip to content

Commit f185a5d

Browse files
committed
feat(config): add host and base_path support for site URLs
1 parent 8a423ab commit f185a5d

File tree

19 files changed

+386
-134
lines changed

19 files changed

+386
-134
lines changed

.github/workflows/deploy-pages.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
config: config.toml
3232
output: public
3333
working-directory: examples/blog
34+
host: https://longcipher.github.io
35+
base-path: /typstify
3436

3537
- name: Deploy to GitHub Pages
3638
uses: JamesIves/github-pages-deploy-action@v4

.github/workflows/deploy-worker.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
config: config.toml
2727
output: public
2828
working-directory: examples/blog
29+
host: https://typstify-example-blog.workers.dev
30+
base-path: ""
2931

3032
- name: Fix permissions
3133
if: always()

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ inputs:
2323
description: 'Include draft posts in build'
2424
required: false
2525
default: 'false'
26+
host:
27+
description: 'Override site host (e.g., https://example.com)'
28+
required: false
29+
base-path:
30+
description: 'Override site base path (e.g., /my-blog)'
31+
required: false
2632
working-directory:
2733
description: 'Working directory (relative to repository root)'
2834
required: false

bin/typstify/src/cmd/build.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,22 @@ use super::check::quick_validate;
1111
/// Run the build command.
1212
///
1313
/// Builds the static site from content files to the output directory.
14-
pub fn run(config_path: &Path, output: &Path, drafts: bool) -> Result<()> {
14+
pub fn run(
15+
config_path: &Path,
16+
output: &Path,
17+
drafts: bool,
18+
host: Option<&str>,
19+
base_path: Option<&str>,
20+
) -> Result<()> {
1521
let start = Instant::now();
16-
tracing::info!(?config_path, ?output, drafts, "Starting build");
22+
tracing::info!(
23+
?config_path,
24+
?output,
25+
drafts,
26+
?host,
27+
?base_path,
28+
"Starting build"
29+
);
1730

1831
// Load configuration
1932
let mut config = Config::load(config_path).wrap_err("Failed to load configuration")?;
@@ -35,6 +48,18 @@ pub fn run(config_path: &Path, output: &Path, drafts: bool) -> Result<()> {
3548
// Include drafts if flag is set
3649
config.build.drafts = drafts;
3750

51+
// Override host if specified via CLI
52+
if let Some(h) = host {
53+
tracing::info!(host = h, "Overriding site host from CLI");
54+
config.site.host = h.to_string();
55+
}
56+
57+
// Override base_path if specified via CLI
58+
if let Some(bp) = base_path {
59+
tracing::info!(base_path = bp, "Overriding site base_path from CLI");
60+
config.site.base_path = bp.to_string();
61+
}
62+
3863
tracing::debug!(?config, "Loaded configuration");
3964

4065
// Create builder with content and output directories

bin/typstify/src/cmd/check.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,11 @@ fn check_directories(result: &mut ValidationResult) {
250250

251251
/// Check configuration values for common issues.
252252
fn check_config_values(config: &Config, result: &mut ValidationResult) {
253-
// Check base_url
254-
if config.site.base_url.is_empty() {
255-
result.add_warning("site.base_url is empty");
256-
} else if !config.site.base_url.starts_with("http") {
257-
result.add_warning("site.base_url should start with http:// or https://");
253+
// Check host
254+
if config.site.host.is_empty() {
255+
result.add_warning("site.host is empty");
256+
} else if !config.site.host.starts_with("http") {
257+
result.add_warning("site.host should start with http:// or https://");
258258
}
259259

260260
// Check title

bin/typstify/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
//! use typstify::cmd;
1818
//!
1919
//! // Build a static site
20-
//! cmd::build::run(Path::new("config.toml"), Path::new("public"), false).unwrap();
20+
//! cmd::build::run(
21+
//! Path::new("config.toml"),
22+
//! Path::new("public"),
23+
//! false,
24+
//! None,
25+
//! None,
26+
//! )
27+
//! .unwrap();
2128
//! ```
2229
2330
pub mod cmd;

bin/typstify/src/main.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ enum Commands {
3838
/// Include draft posts
3939
#[arg(long)]
4040
drafts: bool,
41+
/// Override site host (e.g., https://example.com)
42+
#[arg(long)]
43+
host: Option<String>,
44+
/// Override site base path (e.g., /my-blog)
45+
#[arg(long)]
46+
base_path: Option<String>,
4147
},
4248
/// Start development server with live reload
4349
Watch {
@@ -72,8 +78,19 @@ async fn main() -> Result<()> {
7278
typstify::init_tracing(cli.verbose);
7379

7480
match cli.command {
75-
Commands::Build { output, drafts } => {
76-
typstify::cmd::build::run(&cli.config, &output, drafts)?;
81+
Commands::Build {
82+
output,
83+
drafts,
84+
host,
85+
base_path,
86+
} => {
87+
typstify::cmd::build::run(
88+
&cli.config,
89+
&output,
90+
drafts,
91+
host.as_deref(),
92+
base_path.as_deref(),
93+
)?;
7794
}
7895
Commands::Watch { port, open } => {
7996
typstify::cmd::watch::run(&cli.config, port, open).await?;
@@ -104,9 +121,16 @@ mod tests {
104121
assert_eq!(cli.verbose, 0);
105122

106123
match cli.command {
107-
Commands::Build { output, drafts } => {
124+
Commands::Build {
125+
output,
126+
drafts,
127+
host,
128+
base_path,
129+
} => {
108130
assert_eq!(output, std::path::PathBuf::from("dist"));
109131
assert!(!drafts);
132+
assert!(host.is_none());
133+
assert!(base_path.is_none());
110134
}
111135
_ => panic!("Expected Build command"),
112136
}
@@ -179,4 +203,27 @@ mod tests {
179203
let cli = Cli::parse_from(args);
180204
assert_eq!(cli.config, std::path::PathBuf::from("site.toml"));
181205
}
206+
207+
#[test]
208+
fn test_cli_build_with_host_and_base_path() {
209+
let args = [
210+
"typstify",
211+
"build",
212+
"--host",
213+
"https://example.com",
214+
"--base-path",
215+
"/blog",
216+
];
217+
let cli = Cli::parse_from(args);
218+
219+
match cli.command {
220+
Commands::Build {
221+
host, base_path, ..
222+
} => {
223+
assert_eq!(host.as_deref(), Some("https://example.com"));
224+
assert_eq!(base_path.as_deref(), Some("/blog"));
225+
}
226+
_ => panic!("Expected Build command"),
227+
}
228+
}
182229
}

crates/typstify-core/src/config.rs

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ pub struct SiteConfig {
4343
/// Site title.
4444
pub title: String,
4545

46-
/// Base URL for the site (e.g., "https://example.com").
47-
pub base_url: String,
46+
/// Host URL for the site (e.g., "https://example.com").
47+
/// This is the origin without any path.
48+
pub host: String,
49+
50+
/// Base path for subdirectory deployments (e.g., "/typstify").
51+
/// Empty string for root deployments.
52+
#[serde(default)]
53+
pub base_path: String,
4854

4955
/// Default language code.
5056
#[serde(default = "default_language")]
@@ -280,25 +286,45 @@ impl Config {
280286
return Err(CoreError::config("site.title cannot be empty"));
281287
}
282288

283-
if self.site.base_url.is_empty() {
284-
return Err(CoreError::config("site.base_url cannot be empty"));
289+
if self.site.host.is_empty() {
290+
return Err(CoreError::config("site.host cannot be empty"));
285291
}
286292

287-
// Ensure base_url doesn't have trailing slash
288-
if self.site.base_url.ends_with('/') {
289-
tracing::warn!("site.base_url should not have a trailing slash");
293+
// Ensure host doesn't have trailing slash
294+
if self.site.host.ends_with('/') {
295+
tracing::warn!("site.host should not have a trailing slash");
296+
}
297+
298+
// Ensure base_path starts with / if not empty
299+
if !self.site.base_path.is_empty() && !self.site.base_path.starts_with('/') {
300+
tracing::warn!("site.base_path should start with /");
290301
}
291302

292303
Ok(())
293304
}
294305

306+
/// Get the full base URL (host + base_path).
307+
#[must_use]
308+
pub fn base_url(&self) -> String {
309+
let host = self.site.host.trim_end_matches('/');
310+
let base_path = self.site.base_path.trim_end_matches('/');
311+
format!("{host}{base_path}")
312+
}
313+
295314
/// Get the full URL for a path.
296315
pub fn url_for(&self, path: &str) -> String {
297-
let base = self.site.base_url.trim_end_matches('/');
316+
let base = self.base_url();
298317
let path = path.trim_start_matches('/');
299318
format!("{base}/{path}")
300319
}
301320

321+
/// Get the base path for URL generation.
322+
/// Returns the configured base_path, ensuring no trailing slash.
323+
#[must_use]
324+
pub fn base_path(&self) -> &str {
325+
self.site.base_path.trim_end_matches('/')
326+
}
327+
302328
/// Check if a language code is configured (either as default or in languages map).
303329
#[must_use]
304330
pub fn has_language(&self, lang: &str) -> bool {
@@ -355,7 +381,7 @@ mod tests {
355381
r#"
356382
[site]
357383
title = "Test Site"
358-
base_url = "https://example.com"
384+
host = "https://example.com"
359385
default_language = "en"
360386
361387
[languages.zh]
@@ -392,7 +418,7 @@ paginate = 20
392418
let config = Config::load(&config_path).expect("load config");
393419

394420
assert_eq!(config.site.title, "Test Site");
395-
assert_eq!(config.site.base_url, "https://example.com");
421+
assert_eq!(config.site.host, "https://example.com");
396422
assert_eq!(config.site.default_language, "en");
397423
assert!(config.has_language("en"));
398424
assert!(config.has_language("zh"));
@@ -416,7 +442,7 @@ paginate = 20
416442
let minimal_config = r#"
417443
[site]
418444
title = "Minimal Site"
419-
base_url = "https://example.com"
445+
host = "https://example.com"
420446
"#;
421447
std::fs::write(&config_path, minimal_config).expect("write");
422448

@@ -430,37 +456,14 @@ base_url = "https://example.com"
430456
assert_eq!(config.rss.limit, 20);
431457
}
432458

433-
#[test]
434-
fn test_url_for() {
435-
let dir = tempfile::tempdir().expect("create temp dir");
436-
let config_path = dir.path().join("config.toml");
437-
let config_content = r#"
438-
[site]
439-
title = "Test"
440-
base_url = "https://example.com"
441-
"#;
442-
std::fs::write(&config_path, config_content).expect("write");
443-
444-
let config = Config::load(&config_path).expect("load config");
445-
446-
assert_eq!(
447-
config.url_for("/posts/hello"),
448-
"https://example.com/posts/hello"
449-
);
450-
assert_eq!(
451-
config.url_for("posts/hello"),
452-
"https://example.com/posts/hello"
453-
);
454-
}
455-
456459
#[test]
457460
fn test_config_validation_empty_title() {
458461
let dir = tempfile::tempdir().expect("create temp dir");
459462
let config_path = dir.path().join("config.toml");
460463
let config_content = r#"
461464
[site]
462465
title = ""
463-
base_url = "https://example.com"
466+
host = "https://example.com"
464467
"#;
465468
std::fs::write(&config_path, config_content).expect("write");
466469

@@ -480,4 +483,73 @@ base_url = "https://example.com"
480483
assert!(result.is_err());
481484
assert!(result.unwrap_err().to_string().contains("not found"));
482485
}
486+
487+
#[test]
488+
fn test_base_path_empty() {
489+
let dir = tempfile::tempdir().expect("create temp dir");
490+
let config_path = dir.path().join("config.toml");
491+
let config_content = r#"
492+
[site]
493+
title = "Test"
494+
host = "https://example.com"
495+
"#;
496+
std::fs::write(&config_path, config_content).expect("write");
497+
let config = Config::load(&config_path).expect("load");
498+
assert_eq!(config.base_path(), "");
499+
assert_eq!(config.base_url(), "https://example.com");
500+
}
501+
502+
#[test]
503+
fn test_base_path_subdirectory() {
504+
let dir = tempfile::tempdir().expect("create temp dir");
505+
let config_path = dir.path().join("config.toml");
506+
let config_content = r#"
507+
[site]
508+
title = "Test"
509+
host = "https://longcipher.github.io"
510+
base_path = "/typstify"
511+
"#;
512+
std::fs::write(&config_path, config_content).expect("write");
513+
let config = Config::load(&config_path).expect("load");
514+
assert_eq!(config.base_path(), "/typstify");
515+
assert_eq!(config.base_url(), "https://longcipher.github.io/typstify");
516+
}
517+
518+
#[test]
519+
fn test_base_path_with_trailing_slash() {
520+
let dir = tempfile::tempdir().expect("create temp dir");
521+
let config_path = dir.path().join("config.toml");
522+
let config_content = r#"
523+
[site]
524+
title = "Test"
525+
host = "https://longcipher.github.io/"
526+
base_path = "/typstify/"
527+
"#;
528+
std::fs::write(&config_path, config_content).expect("write");
529+
let config = Config::load(&config_path).expect("load");
530+
assert_eq!(config.base_path(), "/typstify");
531+
assert_eq!(config.base_url(), "https://longcipher.github.io/typstify");
532+
}
533+
534+
#[test]
535+
fn test_url_for() {
536+
let dir = tempfile::tempdir().expect("create temp dir");
537+
let config_path = dir.path().join("config.toml");
538+
let config_content = r#"
539+
[site]
540+
title = "Test"
541+
host = "https://example.com"
542+
base_path = "/blog"
543+
"#;
544+
std::fs::write(&config_path, config_content).expect("write");
545+
let config = Config::load(&config_path).expect("load");
546+
assert_eq!(
547+
config.url_for("/posts/hello"),
548+
"https://example.com/blog/posts/hello"
549+
);
550+
assert_eq!(
551+
config.url_for("posts/hello"),
552+
"https://example.com/blog/posts/hello"
553+
);
554+
}
483555
}

0 commit comments

Comments
 (0)