100줄의 Rust로 정적 사이트 생성기 만들기

즉, 핫 리로드 및 임베디드 웹 서버가 포함된 정확히 100줄(템플릿 제외)을 의미합니다 😃

원래 내 블로그에 게시됨: https://kerkour.com/rust-static-site-generator

개념적으로 정적 사이트 생성기는 간단합니다.

일부 파일을 입력으로 사용하고, 종종 마크다운하고, 렌더링하고, 미리 정의된 템플릿과 병합하고, 모든 것을 원시 HTML 파일로 출력합니다. 간단하고 기본입니다.

우리는 파일이 변경될 때 웹사이트를 미리 볼 수 있도록 웹 서버를 내장할 것입니다.



다음은 사용할 Cargo.toml 파일입니다.

Cargo.toml

[package]
name = "rust_static_site_generator"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
pulldown-cmark = "0.8.0"
hotwatch = "0.4"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
walkdir = "2"
axum = "0.2"
tower-http = { version = "0.1", features = ["fs"] }


템플릿



템플릿의 경우 format! 매크로와 함께 일반 Rust 문자열을 사용합니다.

템플릿.rs

pub const HEADER: &str = r#"<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

"#;

pub fn render_body(body: &str) -> String {
    format!(
        r#"  <body>
    <nav>
        <a href="/">Home</a>
    </nav>
    <br />
    {}
  </body>"#,
        body
    )
}

pub const FOOTER: &str = r#"

</html>
"#;


변경 시 사이트 재구축



파일 변경 사항을 감지하기 위해 hotwatch , notify 위에 간단한 래퍼를 사용하여 몇 줄을 절약할 수 있습니다.

시작 시 웹사이트를 먼저 빌드한 다음 content 폴더에서 변경 사항이 감지될 때마다.

main.rs

use axum::{http::StatusCode, service, Router};
use std::{convert::Infallible, fs, net::SocketAddr, path::Path, thread, time::Duration};
use tower_http::services::ServeDir;

mod templates;
const CONTENT_DIR: &str = "content";
const PUBLIC_DIR: &str = "public";

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    rebuild_site(CONTENT_DIR, PUBLIC_DIR).expect("Rebuilding site");

    tokio::task::spawn_blocking(move || {
        println!("listenning for changes: {}", CONTENT_DIR);
        let mut hotwatch = hotwatch::Hotwatch::new().expect("hotwatch failed to initialize!");
        hotwatch
            .watch(CONTENT_DIR, |_| {
                println!("Rebuilding site");
                rebuild_site(CONTENT_DIR, PUBLIC_DIR).expect("Rebuilding site");
            })
            .expect("failed to watch content folder!");
        loop {
            thread::sleep(Duration::from_secs(1));
        }
    });

    // ...
}


우리는 잔인한 방법으로 웹 사이트를 구축합니다.
  • 전체public 폴더
  • 를 삭제합니다.
  • .md 폴더의 모든 content 파일을 반복합니다
  • .
  • 비어 있는 새 폴더public에서 HTML 파일로 렌더링합니다
  • .
  • 각 파일에 대해 상위 폴더가 있는지 확인합니다(예: content/blog/hello.md -> public/blog/hello.html에서 blog 하위 폴더가 보존됨)

  • 또한 정적 사이트의 인덱스에 추가하기 위해 생성된 모든 HTML 파일 목록을 유지합니다.

    main.rs

    fn rebuild_site(content_dir: &str, output_dir: &str) -> Result<(), anyhow::Error> {
        let _ = fs::remove_dir_all(output_dir);
    
        let markdown_files: Vec<String> = walkdir::WalkDir::new(content_dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().display().to_string().ends_with(".md"))
            .map(|e| e.path().display().to_string())
            .collect();
        let mut html_files = Vec::with_capacity(markdown_files.len());
    
        for file in &markdown_files {
            let mut html = templates::HEADER.to_owned();
            let markdown = fs::read_to_string(&file)?;
            let parser = pulldown_cmark::Parser::new_ext(&markdown,  pulldown_cmark::Options::all());
    
            let mut body = String::new();
            pulldown_cmark::html::push_html(&mut body, parser);
    
            html.push_str(templates::render_body(&body).as_str());
            html.push_str(templates::FOOTER);
    
            let html_file = file
                .replace(content_dir, output_dir)
                .replace(".md", ".html");
            let folder = Path::new(&html_file).parent().unwrap();
            let _ = fs::create_dir_all(folder);
            fs::write(&html_file, html)?;
    
            html_files.push(html_file);
        }
    
        write_index(html_files, output_dir)?;
        Ok(())
    }
    


    인덱스 생성



    사이트의 모든 페이지를 구축한 후 방문자가 페이지를 탐색할 수 있도록 색인을 렌더링해야 합니다. 이를 위해 페이지 목록을 HTML 링크로 렌더링합니다.

    main.rs

    fn write_index(files: Vec<String>, output_dir: &str) -> Result<(), anyhow::Error> {
        let mut html = templates::HEADER.to_owned();
        let body = files
            .into_iter()
            .map(|file| {
                let file = file.trim_start_matches(output_dir);
                let title = file.trim_start_matches("/").trim_end_matches(".html");
                format!(r#"<a href="{}">{}</a>"#, file, title)
            })
            .collect::<Vec<String>>()
            .join("<br />\n");
    
        html.push_str(templates::render_body(&body).as_str());
        html.push_str(templates::FOOTER);
    
        let index_path = Path::new(&output_dir).join("index.html");
        fs::write(index_path, html)?;
        Ok(())
    }
    


    웹 서버



    마지막으로 콘텐츠를 작성하고 편집할 때 페이지를 미리 보려면 웹 서버가 필요합니다. axum의 새로운 tokio's team 프레임워크를 선택한 이유는 API가 매우 훌륭하고 직관적이며 hyper 위에 구축되어 매우 안정적이기 때문입니다.

    main.rs

    #[tokio::main]
    async fn main() -> Result<(), anyhow::Error> {
        // ...
    
        let app = Router::new().nest(
            "/",
            service::get(ServeDir::new(PUBLIC_DIR)).handle_error(|error: std::io::Error| {
                Ok::<_, Infallible>((
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Unhandled internal error: {}", error),
                ))
            }),
        );
    
        let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
        println!("serving site on {}", addr);
        axum::Server::bind(&addr)
            .serve(app.into_make_service())
            .await?;
    
        Ok(())
    }
    


    그리고 마지막 누락된 부분: Markdown 페이지!

    내용/hello-world.md

    # Hellow world
    
    Cool
    


    다음을 실행하여 정적 사이트를 로컬로 제공할 수 있습니다.

    $ cargo run
    


    그런 다음 방문하십시오 http://localhost:8080





    귀엽죠?

    더 나아가



    물론, 우리의 작은 아기는 완벽하지 않으며 신뢰할 수 있는 정적 사이트 생성기로 바꾸려면 더 많은 작업이 필요하지만 후렴구를 알고 있습니다: 독자를 위한 연습으로 남겨둡니다 😉
  • CSS 추가(예: pico.css 또는 mvp.css )
  • 사용자 정의 가능한 템플릿
  • 오류 및 에지 케이스 처리
  • 클라이언트측 핫 리로드
  • 변경된 파일만 재구축
  • sitemap.xml (index.html처럼 쉽게)
  • 그리고 더 많은 것들...

  • 코드는 GitHub에 있습니다.



    평소와 같이 GitHub에서 코드를 찾을 수 있습니다: github.com/skerkour/kerkour.com (저장소에 별표를 표시하는 것을 잊지 마세요 🙏).

    좋은 웹페이지 즐겨찾기