1use anyhow::{Context, Result};
10use std::{fs, path::Path};
11
12fn write_scaffold_file(
14 path: &Path,
15 content: impl AsRef<[u8]>,
16 label: &str,
17) -> Result<()> {
18 fs::write(path, content).with_context(|| format!("Failed to write {label}"))
19}
20
21pub fn scaffold_project(name: &str) -> Result<()> {
23 let cwd = std::env::current_dir()?;
24 scaffold_project_at(name, &cwd)
25}
26
27pub fn scaffold_project_at(name: &str, base: &Path) -> Result<()> {
29 let root = base.join(name);
30 if root.exists() {
31 anyhow::bail!("Directory '{name}' already exists");
32 }
33
34 create_scaffold_dirs(name, &root)?;
35 write_config_file(name, &root)?;
36 write_content_files(name, &root)?;
37 write_template_files(&root)?;
38 write_static_assets(&root)?;
39 write_data_files(&root)?;
40
41 println!("Created new project: {name}");
42 println!(" cd {name}");
43 println!(" ssg -f config.toml");
44
45 Ok(())
46}
47
48fn create_scaffold_dirs(name: &str, root: &Path) -> Result<()> {
50 let dirs = [
51 "",
52 "content",
53 "content/blog",
54 "templates/tera",
55 "static/css",
56 "data",
57 ];
58 for dir in &dirs {
59 fail_point!("scaffold::create-dir", |_| {
60 anyhow::bail!("injected: scaffold::create-dir")
61 });
62 fs::create_dir_all(root.join(dir))
63 .with_context(|| format!("Failed to create {name}/{dir}"))?;
64 }
65 Ok(())
66}
67
68fn write_config_file(name: &str, root: &Path) -> Result<()> {
70 fail_point!("scaffold::write-config", |_| {
71 anyhow::bail!("injected: scaffold::write-config")
72 });
73 write_scaffold_file(
74 &root.join("config.toml"),
75 format!(
76 r#"site_name = "{name}"
77content_dir = "content"
78output_dir = "public"
79template_dir = "templates"
80base_url = "http://127.0.0.1:8000"
81site_title = "{name}"
82site_description = "A site built with SSG"
83language = "en-GB"
84"#
85 ),
86 "config.toml",
87 )
88}
89
90fn write_content_files(name: &str, root: &Path) -> Result<()> {
92 fail_point!("scaffold::write-index", |_| {
93 anyhow::bail!("injected: scaffold::write-index")
94 });
95 write_scaffold_file(
96 &root.join("content/index.md"),
97 format!(
98 r"---
99title: Welcome to {name}
100description: A fast, accessible static site built with SSG
101layout: index
102---
103
104# Welcome
105
106This is your new SSG site. Edit `content/index.md` to get started.
107
108## Features
109
110- Tera templating with inheritance
111- WCAG 2.1 AA accessibility by default
112- JSON-LD structured data for SEO
113- Syntax highlighting for code blocks
114- Responsive image optimisation
115- Client-side search
116"
117 ),
118 "content/index.md",
119 )?;
120
121 fail_point!("scaffold::write-about", |_| {
122 anyhow::bail!("injected: scaffold::write-about")
123 });
124 write_scaffold_file(
125 &root.join("content/about.md"),
126 r"---
127title: About
128description: About this site
129layout: page
130---
131
132# About
133
134This page was generated by [SSG](https://static-site-generator.one).
135",
136 "content/about.md",
137 )?;
138
139 fail_point!("scaffold::write-post", |_| {
140 anyhow::bail!("injected: scaffold::write-post")
141 });
142 write_scaffold_file(
143 &root.join("content/blog/first-post.md"),
144 format!(
145 r#"---
146title: First Post
147description: My first blog post
148layout: post
149date: 2026-01-01
150author: {name} Team
151tags:
152 - welcome
153 - getting-started
154categories:
155 - blog
156---
157
158# First Post
159
160Welcome to **{name}**! This is your first blog post.
161
162## Code Example
163
164```rust
165fn main() {{
166 println!("Hello from {name}!");
167}}
168```
169
170{{{{< tip >}}}}
171Edit this file at `content/blog/first-post.md`.
172{{{{< /tip >}}}}
173"#
174 ),
175 "content/blog/first-post.md",
176 )
177}
178
179fn write_template_files(root: &Path) -> Result<()> {
181 fail_point!("scaffold::write-base", |_| {
182 anyhow::bail!("injected: scaffold::write-base")
183 });
184 write_scaffold_file(
185 &root.join("templates/tera/base.html"),
186 r##"<!DOCTYPE html>
187<html lang="{{ site.language | default(value='en') }}">
188<head>
189 <meta charset="utf-8">
190 <meta name="viewport" content="width=device-width, initial-scale=1">
191 <title>{% block title %}{{ page.title | default(value="Untitled") }}{% if site.title %} — {{ site.title }}{% endif %}{% endblock %}</title>
192 {% if page.description %}<meta name="description" content="{{ page.description }}">{% endif %}
193 <link rel="stylesheet" href="/css/style.css">
194 {% block head_extra %}{% endblock %}
195</head>
196<body>
197 <a href="#main-content" class="sr-only">Skip to main content</a>
198 <header role="banner">
199 <nav aria-label="Main navigation">
200 <a href="/">{{ site.name | default(value="Home") }}</a>
201 <a href="/about.html">About</a>
202 </nav>
203 </header>
204 <main id="main-content" role="main">
205 {% block content %}{% endblock %}
206 </main>
207 <footer role="contentinfo">
208 <p>© {{ site.name | default(value="") }}. Built with <a href="https://static-site-generator.one">SSG</a>.</p>
209 </footer>
210</body>
211</html>
212"##,
213 "templates/tera/base.html",
214 )?;
215
216 fail_point!("scaffold::write-page-tpl", |_| {
217 anyhow::bail!("injected: scaffold::write-page-tpl")
218 });
219 write_scaffold_file(
220 &root.join("templates/tera/page.html"),
221 r#"{% extends "base.html" %}
222{% block content %}{{ page.content | safe }}{% endblock %}
223"#,
224 "templates/tera/page.html",
225 )?;
226
227 fail_point!("scaffold::write-post-tpl", |_| {
228 anyhow::bail!("injected: scaffold::write-post-tpl")
229 });
230 write_scaffold_file(
231 &root.join("templates/tera/post.html"),
232 r#"{% extends "base.html" %}
233{% block content %}
234<article>
235 <header>
236 <h1>{{ page.title | default(value="") }}</h1>
237 {% if page.date %}<time datetime="{{ page.date }}">{{ page.date }}</time>{% endif %}
238 {% if page.author %}<span class="author">by {{ page.author }}</span>{% endif %}
239 {% if page.content %}<span class="reading-time">{{ page.content | reading_time }}</span>{% endif %}
240 </header>
241 <div class="post-body">{{ page.content | safe }}</div>
242 {% if page.tags %}
243 <footer>
244 <ul class="tags" aria-label="Tags">
245 {% for tag in page.tags %}<li><a href="/tags/{{ tag | slugify }}/">{{ tag }}</a></li>{% endfor %}
246 </ul>
247 </footer>
248 {% endif %}
249</article>
250{% endblock %}
251"#,
252 "templates/tera/post.html",
253 )?;
254
255 fail_point!("scaffold::write-index-tpl", |_| {
256 anyhow::bail!("injected: scaffold::write-index-tpl")
257 });
258 write_scaffold_file(
259 &root.join("templates/tera/index.html"),
260 r#"{% extends "base.html" %}
261{% block title %}{{ site.title | default(value="Home") }}{% endblock %}
262{% block content %}
263<section>{{ page.content | safe }}</section>
264{% endblock %}
265"#,
266 "templates/tera/index.html",
267 )
268}
269
270fn write_static_assets(root: &Path) -> Result<()> {
272 fail_point!("scaffold::write-css", |_| {
273 anyhow::bail!("injected: scaffold::write-css")
274 });
275 write_scaffold_file(
276 &root.join("static/css/style.css"),
277 r#"/* SSG Default Styles */
278:root {
279 --text: #1a1a2e;
280 --bg: #ffffff;
281 --accent: #0066cc;
282 --muted: #6b7280;
283 --border: #e5e7eb;
284 --radius: 6px;
285}
286@media (prefers-color-scheme: dark) {
287 :root {
288 --text: #e6edf3;
289 --bg: #0d1117;
290 --accent: #58a6ff;
291 --muted: #8b949e;
292 --border: #30363d;
293 }
294}
295*, *::before, *::after { box-sizing: border-box; }
296body {
297 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
298 color: var(--text);
299 background: var(--bg);
300 line-height: 1.6;
301 max-width: 48rem;
302 margin: 0 auto;
303 padding: 1rem 1.5rem;
304}
305a { color: var(--accent); }
306nav { display: flex; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--border); }
307main { padding: 2rem 0; }
308footer { border-top: 1px solid var(--border); padding: 1rem 0; color: var(--muted); font-size: 0.875rem; }
309pre { background: #f6f8fa; border: 1px solid var(--border); border-radius: var(--radius); padding: 1em; overflow-x: auto; }
310@media (prefers-color-scheme: dark) { pre { background: #161b22; } }
311code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 0.875em; }
312.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
313.admonition { border-left: 4px solid var(--accent); padding: 0.75rem 1rem; margin: 1rem 0; border-radius: var(--radius); }
314.admonition-title { font-weight: 600; margin-bottom: 0.25rem; }
315.tags { list-style: none; padding: 0; display: flex; gap: 0.5rem; }
316.tags li a { background: var(--border); padding: 0.25rem 0.5rem; border-radius: var(--radius); text-decoration: none; font-size: 0.875rem; }
317article time, article .author, article .reading-time { color: var(--muted); font-size: 0.875rem; margin-right: 1rem; }
318"#,
319 "static/css/style.css",
320 )
321}
322
323fn write_data_files(root: &Path) -> Result<()> {
325 fail_point!("scaffold::write-nav", |_| {
326 anyhow::bail!("injected: scaffold::write-nav")
327 });
328 write_scaffold_file(
329 &root.join("data/nav.toml"),
330 r#"[[links]]
331name = "Home"
332url = "/"
333
334[[links]]
335name = "About"
336url = "/about.html"
337
338[[links]]
339name = "Blog"
340url = "/blog/"
341"#,
342 "data/nav.toml",
343 )
344}
345
346#[cfg(test)]
347#[allow(clippy::unwrap_used, clippy::expect_used)]
348mod tests {
349 use super::*;
350 use tempfile::tempdir;
351
352 #[test]
357 fn scaffold_project_at_creates_complete_directory_structure() {
358 let dir = tempdir().unwrap();
359 let name = "test-site";
360 let project = dir.path().join(name);
361 scaffold_project_at(name, dir.path()).unwrap();
362
363 for sub in [
365 "",
366 "content",
367 "content/blog",
368 "templates/tera",
369 "static/css",
370 "data",
371 ] {
372 assert!(
373 project.join(sub).exists(),
374 "subdirectory `{sub}` should have been created"
375 );
376 }
377 }
378
379 #[test]
380 fn scaffold_project_at_writes_all_expected_files() {
381 let dir = tempdir().unwrap();
382 let name = "demo";
383 let project = dir.path().join(name);
384 scaffold_project_at(name, dir.path()).unwrap();
385
386 for file in [
387 "config.toml",
388 "content/index.md",
389 "content/about.md",
390 "content/blog/first-post.md",
391 "templates/tera/base.html",
392 "templates/tera/page.html",
393 "templates/tera/post.html",
394 "templates/tera/index.html",
395 "static/css/style.css",
396 "data/nav.toml",
397 ] {
398 assert!(
399 project.join(file).exists(),
400 "file `{file}` should have been scaffolded"
401 );
402 }
403 }
404
405 #[test]
410 fn scaffold_project_at_injects_project_name_into_config() {
411 let dir = tempdir().unwrap();
412 let name = "my-cool-site";
413 scaffold_project_at(name, dir.path()).unwrap();
414
415 let config =
416 fs::read_to_string(dir.path().join(name).join("config.toml"))
417 .unwrap();
418 assert!(config.contains(&format!(r#"site_name = "{name}""#)));
419 assert!(config.contains(&format!(r#"site_title = "{name}""#)));
420 assert!(config.contains(r#"language = "en-GB""#));
421 }
422
423 #[test]
424 fn scaffold_project_at_injects_project_name_into_index_md() {
425 let dir = tempdir().unwrap();
426 let name = "hello";
427 scaffold_project_at(name, dir.path()).unwrap();
428
429 let index =
430 fs::read_to_string(dir.path().join(name).join("content/index.md"))
431 .unwrap();
432 assert!(index.contains(&format!("title: Welcome to {name}")));
433 assert!(index.contains("layout: index"));
434 }
435
436 #[test]
437 fn scaffold_project_at_injects_project_name_into_first_post() {
438 let dir = tempdir().unwrap();
439 let name = "projectx";
440 scaffold_project_at(name, dir.path()).unwrap();
441
442 let post = fs::read_to_string(
443 dir.path().join(name).join("content/blog/first-post.md"),
444 )
445 .unwrap();
446 assert!(post.contains(&format!("author: {name} Team")));
447 assert!(post.contains(&format!("Welcome to **{name}**")));
448 assert!(post.contains(&format!(r#"println!("Hello from {name}!");"#)));
449 }
450
451 #[test]
452 fn scaffold_project_at_static_assets_include_dark_mode_block() {
453 let dir = tempdir().unwrap();
456 scaffold_project_at("a", dir.path()).unwrap();
457 let css = fs::read_to_string(
458 dir.path().join("a").join("static/css/style.css"),
459 )
460 .unwrap();
461 assert!(css.contains("@media (prefers-color-scheme: dark)"));
462 assert!(css.contains(".sr-only"));
463 }
464
465 #[test]
466 fn scaffold_project_at_base_template_has_accessibility_landmarks() {
467 let dir = tempdir().unwrap();
468 scaffold_project_at("x", dir.path()).unwrap();
469 let base = fs::read_to_string(
470 dir.path().join("x").join("templates/tera/base.html"),
471 )
472 .unwrap();
473 assert!(base.contains(r#"role="banner""#));
474 assert!(base.contains(r#"role="main""#));
475 assert!(base.contains(r#"role="contentinfo""#));
476 assert!(base.contains(r#"aria-label="Main navigation""#));
477 assert!(base.contains(r#"class="sr-only""#));
478 }
479
480 #[test]
481 fn scaffold_project_at_nav_toml_has_three_default_links() {
482 let dir = tempdir().unwrap();
483 scaffold_project_at("y", dir.path()).unwrap();
484 let nav =
485 fs::read_to_string(dir.path().join("y").join("data/nav.toml"))
486 .unwrap();
487 assert_eq!(nav.matches("[[links]]").count(), 3);
488 assert!(nav.contains(r#"name = "Home""#));
489 assert!(nav.contains(r#"name = "About""#));
490 assert!(nav.contains(r#"name = "Blog""#));
491 }
492
493 #[test]
498 fn scaffold_project_at_refuses_to_overwrite_existing_directory() {
499 let dir = tempdir().unwrap();
502 let name = "existing";
503 fs::create_dir(dir.path().join(name)).unwrap();
504
505 let err = scaffold_project_at(name, dir.path()).unwrap_err();
506 let msg = format!("{err}");
507 assert!(
508 msg.contains("already exists"),
509 "error should mention `already exists`: {msg}"
510 );
511 }
512
513 #[test]
514 fn scaffold_project_at_refuses_to_overwrite_existing_file() {
515 let dir = tempdir().unwrap();
518 let name = "blocker";
519 fs::write(dir.path().join(name), "i exist").unwrap();
520
521 assert!(scaffold_project_at(name, dir.path()).is_err());
522 }
523
524 #[test]
529 fn scaffold_project_uses_current_working_directory() {
530 let dir = tempdir().unwrap();
534 let prev = std::env::current_dir().expect("read current dir");
535 std::env::set_current_dir(&dir).expect("pushd");
536
537 let result = scaffold_project("from-cwd");
538
539 std::env::set_current_dir(&prev).expect("popd");
541
542 result.expect("scaffold should succeed in a fresh cwd");
543 assert!(dir.path().join("from-cwd").join("config.toml").exists());
544 }
545
546 #[test]
551 fn write_scaffold_file_creates_file() {
552 let dir = tempdir().unwrap();
553 let path = dir.path().join("test.txt");
554 write_scaffold_file(&path, "hello", "test.txt").unwrap();
555 assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
556 }
557
558 #[test]
559 fn write_scaffold_file_overwrites_existing() {
560 let dir = tempdir().unwrap();
561 let path = dir.path().join("test.txt");
562 fs::write(&path, "old").unwrap();
563 write_scaffold_file(&path, "new", "test.txt").unwrap();
564 assert_eq!(fs::read_to_string(&path).unwrap(), "new");
565 }
566
567 #[test]
568 fn write_scaffold_file_error_has_label() {
569 let err = write_scaffold_file(
570 Path::new("/no/such/dir/file.txt"),
571 "x",
572 "my-label",
573 )
574 .unwrap_err();
575 let msg = format!("{err:#}");
576 assert!(
577 msg.contains("my-label"),
578 "error should include the label: {msg}"
579 );
580 }
581
582 #[test]
583 fn write_scaffold_file_empty_content() {
584 let dir = tempdir().unwrap();
585 let path = dir.path().join("empty.txt");
586 write_scaffold_file(&path, "", "empty.txt").unwrap();
587 assert_eq!(fs::read_to_string(&path).unwrap(), "");
588 }
589
590 #[test]
591 fn write_scaffold_file_binary_content() {
592 let dir = tempdir().unwrap();
593 let path = dir.path().join("bin.dat");
594 let data: &[u8] = &[0x00, 0xFF, 0xAB, 0xCD];
595 write_scaffold_file(&path, data, "bin.dat").unwrap();
596 assert_eq!(fs::read(&path).unwrap(), data);
597 }
598
599 #[test]
604 fn create_scaffold_dirs_creates_all_expected_dirs() {
605 let dir = tempdir().unwrap();
606 let root = dir.path().join("proj");
607 create_scaffold_dirs("proj", &root).unwrap();
608
609 for sub in [
610 "",
611 "content",
612 "content/blog",
613 "templates/tera",
614 "static/css",
615 "data",
616 ] {
617 assert!(root.join(sub).exists(), "{sub} should exist");
618 }
619 }
620
621 #[test]
622 fn create_scaffold_dirs_idempotent() {
623 let dir = tempdir().unwrap();
624 let root = dir.path().join("proj");
625 create_scaffold_dirs("proj", &root).unwrap();
626 create_scaffold_dirs("proj", &root).unwrap();
628 }
629
630 #[test]
635 fn write_config_file_content() {
636 let dir = tempdir().unwrap();
637 let root = dir.path().join("proj");
638 fs::create_dir_all(&root).unwrap();
639 write_config_file("my-site", &root).unwrap();
640
641 let content = fs::read_to_string(root.join("config.toml")).unwrap();
642 assert!(content.contains(r#"site_name = "my-site""#));
643 assert!(content.contains(r#"site_title = "my-site""#));
644 assert!(content.contains(r#"content_dir = "content""#));
645 assert!(content.contains(r#"output_dir = "public""#));
646 assert!(content.contains(r#"template_dir = "templates""#));
647 assert!(content.contains("http://127.0.0.1:8000"));
648 assert!(content.contains(r#"language = "en-GB""#));
649 }
650
651 #[test]
656 fn write_content_files_creates_all_content() {
657 let dir = tempdir().unwrap();
658 let root = dir.path().join("proj");
659 fs::create_dir_all(root.join("content/blog")).unwrap();
660 write_content_files("test-proj", &root).unwrap();
661
662 assert!(root.join("content/index.md").exists());
663 assert!(root.join("content/about.md").exists());
664 assert!(root.join("content/blog/first-post.md").exists());
665 }
666
667 #[test]
668 fn write_content_files_about_has_correct_frontmatter() {
669 let dir = tempdir().unwrap();
670 let root = dir.path().join("proj");
671 fs::create_dir_all(root.join("content/blog")).unwrap();
672 write_content_files("test-proj", &root).unwrap();
673
674 let about = fs::read_to_string(root.join("content/about.md")).unwrap();
675 assert!(about.contains("title: About"));
676 assert!(about.contains("layout: page"));
677 assert!(about.contains("static-site-generator.one"));
678 }
679
680 #[test]
681 fn write_content_files_index_has_features_list() {
682 let dir = tempdir().unwrap();
683 let root = dir.path().join("proj");
684 fs::create_dir_all(root.join("content/blog")).unwrap();
685 write_content_files("proj", &root).unwrap();
686
687 let index = fs::read_to_string(root.join("content/index.md")).unwrap();
688 assert!(index.contains("## Features"));
689 assert!(index.contains("- Tera templating"));
690 }
691
692 #[test]
693 fn write_content_files_first_post_has_tags_and_code() {
694 let dir = tempdir().unwrap();
695 let root = dir.path().join("proj");
696 fs::create_dir_all(root.join("content/blog")).unwrap();
697 write_content_files("proj", &root).unwrap();
698
699 let post = fs::read_to_string(root.join("content/blog/first-post.md"))
700 .unwrap();
701 assert!(post.contains("tags:"));
702 assert!(post.contains("- welcome"));
703 assert!(post.contains("```rust"));
704 assert!(post.contains("categories:"));
705 }
706
707 #[test]
712 fn write_template_files_creates_all_templates() {
713 let dir = tempdir().unwrap();
714 let root = dir.path().join("proj");
715 fs::create_dir_all(root.join("templates/tera")).unwrap();
716 write_template_files(&root).unwrap();
717
718 for file in [
719 "templates/tera/base.html",
720 "templates/tera/page.html",
721 "templates/tera/post.html",
722 "templates/tera/index.html",
723 ] {
724 assert!(root.join(file).exists(), "{file} should exist");
725 }
726 }
727
728 #[test]
729 fn write_template_files_page_extends_base() {
730 let dir = tempdir().unwrap();
731 let root = dir.path().join("proj");
732 fs::create_dir_all(root.join("templates/tera")).unwrap();
733 write_template_files(&root).unwrap();
734
735 let page =
736 fs::read_to_string(root.join("templates/tera/page.html")).unwrap();
737 assert!(page.contains(r#"extends "base.html""#));
738 assert!(page.contains("block content"));
739 }
740
741 #[test]
742 fn write_template_files_post_has_article_structure() {
743 let dir = tempdir().unwrap();
744 let root = dir.path().join("proj");
745 fs::create_dir_all(root.join("templates/tera")).unwrap();
746 write_template_files(&root).unwrap();
747
748 let post =
749 fs::read_to_string(root.join("templates/tera/post.html")).unwrap();
750 assert!(post.contains("<article>"));
751 assert!(post.contains("page.title"));
752 assert!(post.contains("page.date"));
753 assert!(post.contains("page.tags"));
754 assert!(post.contains("reading_time"));
755 }
756
757 #[test]
758 fn write_template_files_index_extends_base() {
759 let dir = tempdir().unwrap();
760 let root = dir.path().join("proj");
761 fs::create_dir_all(root.join("templates/tera")).unwrap();
762 write_template_files(&root).unwrap();
763
764 let index =
765 fs::read_to_string(root.join("templates/tera/index.html")).unwrap();
766 assert!(index.contains(r#"extends "base.html""#));
767 assert!(index.contains("site.title"));
768 }
769
770 #[test]
775 fn write_static_assets_creates_stylesheet() {
776 let dir = tempdir().unwrap();
777 let root = dir.path().join("proj");
778 fs::create_dir_all(root.join("static/css")).unwrap();
779 write_static_assets(&root).unwrap();
780
781 let css =
782 fs::read_to_string(root.join("static/css/style.css")).unwrap();
783 assert!(css.contains(":root"));
784 assert!(css.contains("--text:"));
785 assert!(css.contains("--bg:"));
786 assert!(css.contains("--accent:"));
787 assert!(css.contains("box-sizing: border-box"));
788 }
789
790 #[test]
795 fn write_data_files_creates_nav_toml() {
796 let dir = tempdir().unwrap();
797 let root = dir.path().join("proj");
798 fs::create_dir_all(root.join("data")).unwrap();
799 write_data_files(&root).unwrap();
800
801 let nav = fs::read_to_string(root.join("data/nav.toml")).unwrap();
802 assert_eq!(nav.matches("[[links]]").count(), 3);
803 assert!(nav.contains(r#"url = "/""#));
804 assert!(nav.contains(r#"url = "/about.html""#));
805 assert!(nav.contains(r#"url = "/blog/""#));
806 }
807
808 #[test]
813 fn scaffold_project_at_special_chars_in_name() {
814 let dir = tempdir().unwrap();
815 let name = "my-cool_site.2026";
816 scaffold_project_at(name, dir.path()).unwrap();
817
818 let config =
819 fs::read_to_string(dir.path().join(name).join("config.toml"))
820 .unwrap();
821 assert!(config.contains(&format!(r#"site_name = "{name}""#)));
822 }
823
824 #[test]
825 fn scaffold_project_at_single_char_name() {
826 let dir = tempdir().unwrap();
827 scaffold_project_at("z", dir.path()).unwrap();
828 assert!(dir.path().join("z").join("config.toml").exists());
829 }
830
831 #[test]
836 fn scaffold_project_at_base_template_is_valid_html() {
837 let dir = tempdir().unwrap();
838 scaffold_project_at("t", dir.path()).unwrap();
839 let base = fs::read_to_string(
840 dir.path().join("t").join("templates/tera/base.html"),
841 )
842 .unwrap();
843 assert!(base.starts_with("<!DOCTYPE html>"));
844 assert!(base.contains("</html>"));
845 assert!(base.contains("<head>"));
846 assert!(base.contains("</head>"));
847 assert!(base.contains("<body>"));
848 assert!(base.contains("</body>"));
849 }
850
851 #[test]
852 fn scaffold_project_at_config_has_all_required_keys() {
853 let dir = tempdir().unwrap();
854 scaffold_project_at("k", dir.path()).unwrap();
855 let config =
856 fs::read_to_string(dir.path().join("k").join("config.toml"))
857 .unwrap();
858 for key in [
859 "site_name",
860 "content_dir",
861 "output_dir",
862 "template_dir",
863 "base_url",
864 "site_title",
865 "site_description",
866 "language",
867 ] {
868 assert!(
869 config.contains(key),
870 "config.toml should contain key: {key}"
871 );
872 }
873 }
874}