@@ -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]
357383title = "Test Site"
358- base_url = "https://example.com"
384+ host = "https://example.com"
359385default_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]
418444title = "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]
462465title = ""
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