diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 0bc8db3..0000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -dist -node_modules -.cache -.github -scripts -*.d.ts \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 25e5024..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: ["eslint:recommended"], - overrides: [], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, - plugins: [], - rules: { - "no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - }, - ], - }, -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5089776..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: ci - -on: - push: - branches: [main] - pull_request: - -jobs: - lint-and-build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: oven-sh/setup-bun@v1 - with: - bun-version: 1 - - - name: install - run: bun install - - name: lint - run: bun run lint - - name: build - run: bun run build diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..496596b --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,19 @@ +name: deploy +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: + - main +jobs: + Deploy-Production: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: install vercel cli + run: npm install --global vercel@latest + - name: build + run: cargo run + - name: deploy + run: vercel deploy --token ${{ secrets.VERCEL_TOKEN }} --prod diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..cb3cfe1 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,23 @@ +name: tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo check --verbose + - name: Check format + run: cargo fmt --check + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index b2820b4..84b4a15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -node_modules .DS_Store - dist - .env* - -.cache \ No newline at end of file +.cache +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..436a6f8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "generator" +version = "0.1.0" +dependencies = [ + "markdown", +] + +[[package]] +name = "markdown" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4b20e3e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "markdown", + "generator" +] diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 58293c5..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/generator/Cargo.toml b/generator/Cargo.toml new file mode 100644 index 0000000..925fde9 --- /dev/null +++ b/generator/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "generator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +markdown = { path = "../markdown" } diff --git a/generator/src/main.rs b/generator/src/main.rs new file mode 100644 index 0000000..9699e0a --- /dev/null +++ b/generator/src/main.rs @@ -0,0 +1,415 @@ +extern crate markdown; + +use std::fs; +use std::path::Path; + +trait Generator { + fn get_md_files(&self) -> Vec; + fn generate_styles(&self); + fn generate_scripts(&self); + fn posts_template(&self, content: String) -> String; + fn post_template(&self, content: String, title: String, created_at: String) -> String; + fn generate_posts(&self, md_files: Vec); + fn generate_post(&self, md_files: Vec); + fn generate_rss(&self, md_files: Vec); + fn generate(&self, md_files: Vec); +} + +fn create_dir(path: &str) { + match fs::create_dir_all(path) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + } + } +} + +struct HtmlGenerator; + +impl Generator for HtmlGenerator { + fn get_md_files(&self) -> Vec { + let mut files = Vec::new(); + let paths = match fs::read_dir("./posts") { + Ok(paths) => paths, + Err(_) => { + println!("posts ディレクトリが存在しません"); + return files; + } + }; + for path in paths { + let path = path.unwrap().path(); + if path.is_file() { + let file_name = path.file_name().unwrap().to_str().unwrap(); + if file_name.ends_with(".md") { + files.push(file_name.to_string()); + } + } + } + files + } + + fn generate_styles(&self) { + let paths = match fs::read_dir("./styles") { + Ok(paths) => paths, + Err(_) => { + println!("styles ディレクトリが存在しません"); + return; + } + }; + + for path in paths { + let path = path.unwrap().path(); + if path.is_file() { + let file_name = path.file_name().unwrap().to_str().unwrap(); + let dest = Path::new("./dist").join("styles").join(file_name); + create_dir(dest.parent().unwrap().to_str().unwrap()); + match fs::copy(path, dest) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + } + } + } + } + } + + fn generate_scripts(&self) { + let paths = match fs::read_dir("./scripts") { + Ok(paths) => paths, + Err(_) => { + println!("scripts ディレクトリが存在しません"); + return; + } + }; + + for path in paths { + let path = path.unwrap().path(); + if path.is_file() { + let file_name = path.file_name().unwrap().to_str().unwrap(); + let dest = Path::new("./dist").join("scripts").join(file_name); + create_dir(dest.parent().unwrap().to_str().unwrap()); + match fs::copy(path, dest) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + } + } + } + } + } + + fn posts_template(&self, content: String) -> String { + format!( + r#" + + + home | blog.takurinton.dev + + + + + + + + + + + + + + +
+ + blog.takurinton.dev + +
+
+
+

記事一覧

+
+ {} +
+ + + +"#, + content + ) + } + + fn post_template(&self, content: String, title: String, created_at: String) -> String { + format!( + r#" + + + {} | blog.takurinton.dev + + + + + + + + + + + + + +
+ + blog.takurinton.dev + +
+
+

{}

+

{}

+ {} +
+ + + +"#, + title, title, created_at, content + ) + } + + fn generate_posts(&self, md_files: Vec) { + let mut html = String::new(); + // ファイル名を数字にしてソート + let mut md_files = md_files + .iter() + .map(|file| { + let id = file.replace(".md", "").parse::().unwrap(); + (id, file) + }) + .collect::>(); + md_files.sort_by(|a, b| b.0.cmp(&a.0)); + for md_file in md_files { + let md_file = md_file.1; + let md = match fs::read_to_string(Path::new("./posts").join(&md_file)) { + Ok(md) => md, + Err(_) => { + println!("{} ファイルが読み込めません", md_file); + continue; + } + }; + + let frontmatter = markdown::get_frontmatter(&md); + let id = match frontmatter.get("id") { + Some(id) => id.to_string(), + None => { + println!("{} IDが取得できません", md_file); + continue; + } + }; + let title = match frontmatter.get("title") { + Some(title) => title.to_string(), + None => { + println!("{} タイトルが取得できません", md_file); + continue; + } + }; + let created_at = match frontmatter.get("created_at") { + Some(created_at) => created_at.to_string(), + None => { + println!("{} 作成日が取得できません", md_file); + continue; + } + }; + + let tokens = markdown::tokenize(&md); + let rendered_html = markdown::render_html(tokens); + let content = markdown::html_to_string(rendered_html); + let content = truncate_to_char_boundary(&content, 300); + + html.push_str(&format!( + r#"
+{} +

{}

+

{}

+
+ "#, + id, title, created_at, content + )); + } + html = self.posts_template(html); + + let path = Path::new("./dist").join("index.html"); + create_dir(path.parent().unwrap().to_str().unwrap()); + match fs::write(path, html) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + } + } + + println!("created: dist/index.html"); + } + + fn generate_post(&self, md_files: Vec) { + for md_file in md_files { + let md = match fs::read_to_string(Path::new("./posts").join(&md_file)) { + Ok(md) => md, + Err(_) => { + println!("{} ファイルが読み込めません", md_file); + continue; + } + }; + + let frontmatter = markdown::get_frontmatter(&md); + let title = match frontmatter.get("title") { + Some(title) => title.to_string(), + None => { + println!("{} タイトルが取得できません", md_file); + continue; + } + }; + let created_at = match frontmatter.get("created_at") { + Some(created_at) => created_at.to_string(), + None => { + println!("{} 作成日が取得できません", md_file); + continue; + } + }; + let tokens = markdown::tokenize(&md); + let html = markdown::render_html(tokens); + let html = self.post_template(html, title, created_at); + + let pathname = md_file.replace(".md", ""); + let path = Path::new(format!("./dist/post/{}", pathname).as_str()).join("index.html"); + create_dir(path.parent().unwrap().to_str().unwrap()); + match fs::write(path, html) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + continue; + } + } + println!( + "created: posts/{} -> dist/{}", + md_file, + format!("post/{}/index.html", pathname) + ); + } + } + + fn generate_rss(&self, md_files: Vec) { + let rss_feed = md_files + .iter() + .map(|md_file| { + let md = match fs::read_to_string(Path::new("./posts").join(md_file)) { + Ok(md) => md, + Err(_) => { + println!("{} ファイルが読み込めません", md_file); + return String::new(); + } + }; + + let frontmatter = markdown::get_frontmatter(&md); + let title = match frontmatter.get("title") { + Some(title) => title.to_string(), + None => { + println!("{} タイトルが取得できません", md_file); + return String::new(); + } + }; + let created_at = match frontmatter.get("created_at") { + Some(created_at) => created_at.to_string(), + None => { + println!("{} 作成日が取得できません", md_file); + return String::new(); + } + }; + let description = match frontmatter.get("description") { + Some(description) => description.to_string(), + None => { + println!("{} 説明が取得できません", md_file); + return String::new(); + } + }; + + format!( + r#" + + {} + https://blog.takurinton.dev/post/{}/index.html + {} + {} + + "#, + title, + md_file.replace(".md", ""), + description, + created_at + ) + }) + .collect::(); + + let rss = format!( + r#" + + + + たくりんとん + https://blog.takurinton.dev + たくりんとんのブログです + en + takurinton@takurinton.com (takurinton) + takurinton@takurinton.com (takurinton) + + https://takurinton.dev/me.jpeg + たくりんとん + https://blog.takurinton.dev + 32 + 32 + + {} + + + "#, + rss_feed + ); + + let path = Path::new("./dist").join("rss.xml"); + create_dir(path.parent().unwrap().to_str().unwrap()); + match fs::write(path, rss) { + Ok(_) => { + println!("created: dist/rss.xml"); + } + Err(err) => { + println!("{:?}", err); + } + } + } + + fn generate(&self, md_files: Vec) { + self.generate_posts(md_files.clone()); + self.generate_post(md_files.clone()); + self.generate_rss(md_files.clone()); + } +} + +fn truncate_to_char_boundary(s: &str, max_chars: usize) -> String { + s.char_indices() + .nth(max_chars) + .map(|(idx, _)| &s[..idx]) + .unwrap_or(s) + .to_string() +} + +fn main() { + let generator = HtmlGenerator; + let md_files = generator.get_md_files(); + generator.generate_styles(); + generator.generate_scripts(); + generator.generate(md_files) +} diff --git a/markdown/Cargo.toml b/markdown/Cargo.toml new file mode 100644 index 0000000..fcf7439 --- /dev/null +++ b/markdown/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "markdown" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "1.0.0" diff --git a/markdown/src/lib.rs b/markdown/src/lib.rs new file mode 100644 index 0000000..50fcf2a --- /dev/null +++ b/markdown/src/lib.rs @@ -0,0 +1,621 @@ +extern crate regex; + +use regex::Regex; +use std::collections::{HashMap, VecDeque}; + +#[derive(Debug)] +pub enum Token { + Heading { level: usize, content: String }, + ListItem(String), + OrderedList(Vec), + Bold(String), + Italic(String), + Link { text: String, url: String }, + CodeBlock { language: String, content: String }, + InlineCode(String), + Paragraph(String), + BlockQuote(String), + Image { src: String, alt: String }, + + // === ここから下はカスタムのシンタックス === + LinkCard(String), + Twitter(String), +} + +// MEMO: tokenizer に frontmatter があるのはおかしいので修正する +pub fn tokenize(input: &str) -> VecDeque { + let front_matter_re = Regex::new(r"---\n([\s\S]*?)\n---\n\n([\s\S]*)").unwrap(); + let captures = match front_matter_re.captures(input) { + Some(captures) => captures, + None => { + // 何もしない + return VecDeque::new(); + } + }; + + let input = captures.get(2).unwrap().as_str().to_string(); + let mut tokens = VecDeque::new(); + + let heading_regex = Regex::new(r"^(#{1,6}) (.+)$").unwrap(); + let list_item_regex = Regex::new(r"^\* (.+)$").unwrap(); + let ordered_list_regex = Regex::new(r"^\d+\. (.+)$").unwrap(); + let bold_regex = Regex::new(r"\*\*(.+?)\*\*").unwrap(); + let italic_regex = Regex::new(r"\*(.+?)\*").unwrap(); + let link_regex = Regex::new(r"\[(.+?)\]\((.+?)\)").unwrap(); + let code_block_start_regex = Regex::new(r"^```(\w*)").unwrap(); + let inline_code_regex = Regex::new(r"`(.+?)`").unwrap(); + let block_quote_regex = Regex::new(r"^> (.+)").unwrap(); + let image_regex = Regex::new(r"!\[(.+?)\]\((.+?)\)").unwrap(); + + // === ここから下はカスタムのシンタックス === + let link_card = Regex::new(r"^@og\[(.*)\]").unwrap(); + // MEMO: Twitter card、もう正しく動く保証がないので廃止にしてもいいかもしれない + let twitter = Regex::new(r"^@twitter\[(.*)\]").unwrap(); + + let mut in_code_block = false; + let mut code_block_content = String::new(); + let mut code_block_language = String::new(); + + for line in input.lines() { + if in_code_block { + if code_block_start_regex.is_match(line) { + tokens.push_back(Token::CodeBlock { + language: code_block_language.clone(), + content: code_block_content.clone(), + }); + code_block_content.clear(); + code_block_language.clear(); + in_code_block = false; + } else { + code_block_content.push_str(line); + code_block_content.push('\n'); + } + continue; + } + + if code_block_start_regex.is_match(line) { + let captures = code_block_start_regex.captures(line).unwrap(); + code_block_language = captures.get(1).map_or("", |m| m.as_str()).to_string(); + in_code_block = true; + continue; + } + + if heading_regex.is_match(line) { + let captures = heading_regex.captures(line).unwrap(); + let level = captures.get(1).unwrap().as_str().len(); + let content = captures.get(2).unwrap().as_str(); + tokens.push_back(Token::Heading { + level, + content: content.to_string(), + }); + } else if image_regex.is_match(line) { + let captures = image_regex.captures(line).unwrap(); + let alt = captures.get(1).unwrap().as_str(); + let src = captures.get(2).unwrap().as_str(); + tokens.push_back(Token::Image { + src: src.to_string(), + alt: alt.to_string(), + }); + } else if list_item_regex.is_match(line) { + let captures = list_item_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::ListItem(content.to_string())); + } else if ordered_list_regex.is_match(line) { + let captures = ordered_list_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::OrderedList(vec![content.to_string()])); + } else if bold_regex.is_match(line) { + let captures = bold_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::Bold(content.to_string())); + } else if italic_regex.is_match(line) { + let captures = italic_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::Italic(content.to_string())); + } else if link_regex.is_match(line) { + let captures = link_regex.captures(line).unwrap(); + let text = captures.get(1).unwrap().as_str(); + let url = captures.get(2).unwrap().as_str(); + tokens.push_back(Token::Link { + text: text.to_string(), + url: url.to_string(), + }); + } else if inline_code_regex.is_match(line) { + let captures = inline_code_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::InlineCode(content.to_string())); + } else if block_quote_regex.is_match(line) { + let captures = block_quote_regex.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::BlockQuote(content.to_string())); + } else if link_card.is_match(line) { + let captures = link_card.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::LinkCard(content.to_string())) + } else if twitter.is_match(line) { + let captures = twitter.captures(line).unwrap(); + let content = captures.get(1).unwrap().as_str(); + tokens.push_back(Token::Twitter(content.to_string())) + } else { + tokens.push_back(Token::Paragraph(line.to_string())); + } + } + + if in_code_block { + tokens.push_back(Token::CodeBlock { + language: code_block_language, + content: code_block_content, + }); + } + + tokens +} + +pub fn get_frontmatter(input: &str) -> HashMap { + let frontmatter_re = Regex::new(r"---\n([\s\S]*?)\n---\n\n([\s\S]*)").unwrap(); + let frontmatter_list = + Regex::new(r"id:([\s\S]*)\ntitle:([\s\S]*)\ndescription:([\s\S]*)\ncreated_at:([\s\S]*)") + .unwrap(); + + let captures = match frontmatter_re.captures(input) { + Some(captures) => captures, + None => return HashMap::new(), + }; + let frontmatter = captures.get(1).unwrap().as_str(); + + let captures = match frontmatter_list.captures(frontmatter) { + Some(captures) => captures, + None => return HashMap::new(), + }; + + let id = captures.get(1).unwrap().as_str().trim().to_string(); + let title = captures.get(2).unwrap().as_str().trim().to_string(); + let description = captures.get(3).unwrap().as_str().trim().to_string(); + let created_at = captures.get(4).unwrap().as_str().trim().to_string(); + + let mut map = HashMap::new(); + map.insert("id".to_string(), id); + map.insert("title".to_string(), title); + map.insert("description".to_string(), description); + map.insert("created_at".to_string(), created_at); + + map +} + +pub fn render_html(tokens: VecDeque) -> String { + let mut output = String::new(); + + for token in tokens { + match token { + Token::Heading { level, content } => { + output.push_str(&format!("{}\n", level, content, level)); + } + Token::ListItem(content) => { + output.push_str(&format!("
  • {}
  • \n", content)); + } + Token::Paragraph(content) => { + output.push_str(&format!("

    {}

    \n", content)); + } + Token::Bold(content) => { + output.push_str(&format!("{}\n", content)); + } + Token::Italic(content) => { + output.push_str(&format!("{}\n", content)); + } + Token::Link { text, url } => { + output.push_str(&format!("{}\n", url, text)); + } + Token::CodeBlock { language, content } => { + output.push_str(&format!( + "
    {}
    \n", + language, content + )); + } + Token::InlineCode(content) => { + output.push_str(&format!("{}\n", content)); + } + Token::BlockQuote(content) => { + output.push_str(&format!("
    {}
    \n", content)); + } + Token::Image { src, alt } => { + output.push_str(&format!("\"{}\"\n", src, alt)); + } + Token::LinkCard(content) => { + output.push_str(&format!( + r#"
    +
    +
    +
    + +
    +
    +

    loading...

    +
    +
    +
    + "#, + content, content, + )); + } + Token::Twitter(_) => { + // output.push_str(&format!( + // "
    \n", + // content + // )); + output.push_str("

    twitter card is not supported anymore...

    \n") + } + _ => {} + } + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tokenize_heading() { + let input = "# Title"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Heading { level, content } => { + assert_eq!(*level, 1); + assert_eq!(content, "Title"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_tokenize_paragraph() { + let input = "Normal text here."; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Paragraph(content) => { + assert_eq!(content, "Normal text here."); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_list() { + let input = "* Item 1\n"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + println!("{:?}", tokens); + match tokens.get(0).unwrap() { + Token::ListItem(content) => { + assert_eq!(content, "Item 1"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_ordered_list() { + let input = "1. Item 1\n"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::OrderedList(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], "Item 1"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_bold() { + let input = "**Bold text**"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Bold(content) => { + assert_eq!(content, "Bold text"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_italic() { + let input = "*Italic text*"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Italic(content) => { + assert_eq!(content, "Italic text"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_link() { + let input = "[Link text](https://example.com)"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Link { text, url } => { + assert_eq!(text, "Link text"); + assert_eq!(url, "https://example.com"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_code_block() { + let input = "```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::CodeBlock { language, content } => { + assert_eq!(language, "rust"); + assert_eq!( + content, + "fn main() {\n println!(\"Hello, world!\");\n}\n" + ); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_inline_code() { + let input = "`let x = 42;`"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::InlineCode(content) => { + assert_eq!(content, "let x = 42;"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_block_quote() { + let input = "> This is a block quote."; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::BlockQuote(content) => { + assert_eq!(content, "This is a block quote."); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_image() { + let input = "![Alt text](https://example.com/image.jpg)"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Image { src, alt } => { + assert_eq!(src, "https://example.com/image.jpg"); + assert_eq!(alt, "Alt text"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_tokenize_link_card() { + let input = "@og[https://example.com]"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::LinkCard(content) => { + assert_eq!(content, "https://example.com"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_tokenize_twitter() { + let input = "@twitter[https://twitter.com/foo/status/11111111]"; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 1); + match tokens.get(0).unwrap() { + Token::Twitter(content) => { + assert_eq!(content, "https://twitter.com/foo/status/11111111"); + } + _ => panic!("Unexpected token"), + } + } + + #[test] + fn test_render_html_heading() { + let tokens = VecDeque::from(vec![Token::Heading { + level: 1, + content: "Title".to_string(), + }]); + let html = render_html(tokens); + assert_eq!(html, "

    Title

    \n"); + } + + #[test] + fn test_render_html_list() { + let tokens = VecDeque::from(vec![Token::ListItem("Item 1".to_string())]); + let html = render_html(tokens); + assert_eq!(html, "
  • Item 1
  • \n"); + } + + #[test] + fn test_render_html_paragraph() { + let tokens = VecDeque::from(vec![Token::Paragraph("Normal text here.".to_string())]); + let html = render_html(tokens); + assert_eq!(html, "

    Normal text here.

    \n"); + } + + #[test] + fn test_render_html_bold() { + let tokens = VecDeque::from(vec![Token::Bold("Bold text".to_string())]); + let html = render_html(tokens); + assert_eq!(html, "Bold text\n"); + } + + #[test] + fn test_render_html_italic() { + let tokens = VecDeque::from(vec![Token::Italic("Italic text".to_string())]); + let html = render_html(tokens); + assert_eq!(html, "Italic text\n"); + } + + #[test] + fn test_render_html_link() { + let tokens = VecDeque::from(vec![Token::Link { + text: "Link text".to_string(), + url: "https://example.com".to_string(), + }]); + let html = render_html(tokens); + assert_eq!(html, "Link text\n"); + } + + #[test] + fn test_render_html_code_block() { + let tokens = VecDeque::from(vec![Token::CodeBlock { + language: "rust".to_string(), + content: "fn main() {\n println!(\"Hello, world!\");\n}\n".to_string(), + }]); + let html = render_html(tokens); + assert_eq!(html, "
    fn main() {\n    println!(\"Hello, world!\");\n}\n
    \n"); + } + + #[test] + fn test_render_html_inline_code() { + let tokens = VecDeque::from(vec![Token::InlineCode("let x = 42;".to_string())]); + let html = render_html(tokens); + assert_eq!(html, "let x = 42;\n"); + } + + #[test] + fn test_render_html_block_quote() { + let tokens = VecDeque::from(vec![Token::BlockQuote( + "This is a block quote.".to_string(), + )]); + let html = render_html(tokens); + assert_eq!(html, "
    This is a block quote.
    \n"); + } + + #[test] + fn test_render_html_image() { + let tokens = VecDeque::from(vec![Token::Image { + src: "https://example.com/image.jpg".to_string(), + alt: "Alt text".to_string(), + }]); + let html = render_html(tokens); + assert_eq!( + html, + "\"Alt\n" + ); + } + + #[test] + fn test_render_html_link_card() { + let tokens = VecDeque::from(vec![Token::LinkCard("https://example.com".to_string())]); + let html = render_html(tokens); + assert_eq!( + html, + r#"
    +
    +
    +
    + +
    +
    +

    loading...

    +
    +
    +
    + "# + ); + } + + #[test] + fn test_render_html_twitter() { + let tokens = VecDeque::from(vec![Token::Twitter( + "https://twitter.com/foo/status/11111111".to_string(), + )]); + let html = render_html(tokens); + assert_eq!(html, "

    twitter card is not supported anymore...

    \n"); + } + + #[test] + fn test_tokenize() { + let input = r#" +# Title +## Subtitle +### Sub-subtitle +* Item 1 +* Item 2 +* Item 3 +Normal text here. +"#; + let tokens = tokenize(input); + assert_eq!(tokens.len(), 8); + } + + #[test] + fn test_render_html() { + let tokens = VecDeque::from(vec![ + Token::Heading { + level: 1, + content: "Title".to_string(), + }, + Token::Heading { + level: 2, + content: "Subtitle".to_string(), + }, + Token::Heading { + level: 3, + content: "Sub-subtitle".to_string(), + }, + Token::ListItem("Item 1".to_string()), + Token::ListItem("Item 2".to_string()), + Token::ListItem("Item 3".to_string()), + Token::Paragraph("Normal text here.".to_string()), + ]); + let html = render_html(tokens); + assert_eq!( + html, + "

    Title

    \n

    Subtitle

    \n

    Sub-subtitle

    \n
  • Item 1
  • \n
  • Item 2
  • \n
  • Item 3
  • \n

    Normal text here.

    \n" + ); + } +} + +pub fn html_to_string(html: String) -> String { + // Markdownの一部の表現をプレーンテキストに変換 + let html_bold_italic_removed = html + .replace("*", "") + .replace("_", "") + .replace("`", "") + .replace("~", ""); + + let html_tag_re = Regex::new(r"<[^>]*>").unwrap(); + let notag = html_tag_re + .replace_all(&html_bold_italic_removed, "") + .to_string(); + + let break_to_space = notag.replace(&['\r', '\n'][..], " "); + let space_re = Regex::new(r"\s+").unwrap(); + let space_removed = space_re.replace_all(&break_to_space, " ").to_string(); + + // 特定のパターンを除去 + let twitter_re = Regex::new(r"@twitter\[.*?\]").unwrap(); + let without_twitter = twitter_re.replace_all(&space_removed, "").to_string(); + + let og_re = Regex::new(r"@og\[.*?\]").unwrap(); + let result = og_re.replace_all(&without_twitter, "").to_string(); + + result +} diff --git a/package.json b/package.json deleted file mode 100644 index 6f3efa3..0000000 --- a/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "blog.takurinton.dev", - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "wmr", - "build": "NODE_ENV=production wmr build --prerender", - "serve": "wmr serve", - "lint": "eslint './{public,src}/**/*.{js,jsx,ts,tsx}'", - "gen": "bun run ./scripts/gen-template.js" - }, - "alias": { - "react": "preact/compat" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.8.0", - "@typescript-eslint/parser": "6.8.0", - "eslint": "8.52.0", - "eslint-config-preact": "1.3.0", - "typescript": "5.2.2", - "wmr": "3.8.0" - }, - "dependencies": { - "hoofd": "1.7.0", - "marked": "12.0.0", - "preact": "10.18.1", - "preact-iso": "2.5.0" - }, - "author": "takurinton", - "eslintConfig": { - "extends": "preact" - } -} diff --git a/plugins/feed/index.js b/plugins/feed/index.js deleted file mode 100644 index 2f98f8a..0000000 --- a/plugins/feed/index.js +++ /dev/null @@ -1,57 +0,0 @@ -import { writeFileSync, readFileSync } from "fs"; - -export default function feedPlugin({ plugins, cwd, prod }, opts) { - plugins.push(feed({ cwd, prod, ...opts })); -} - -function feed() { - return { - name: "feed", - async load() { - makeFeed(); - }, - }; -} - -function makeFeed() { - const posts = JSON.parse( - readFileSync(`${process.cwd()}/public/contents/posts.json`) - ); - - const feed = ` - - - - たくりんとん - https://blog.takurinton.dev - たくりんとんのブログです - en - takurinton@takurinton.com (takurinton) - takurinton@takurinton.com (takurinton) - - https://takurinton.dev/me.jpeg - たくりんとん - https://blog.takurinton.dev - 32 - 32 - - ${posts - .map( - (content) => ` - - ${content.title} | たくりんとんのブログ - https://blog.takurinton.dev/post/${content.id} - ${new Date(content.created_at).toUTCString()} - ${content.description} - https://blog.takurinton.dev/post/${content.id} - ` - ) - .join("")} - - `; - - writeFileSync(`${process.cwd()}/dist/rss.xml`, feed, (err) => { - if (err) throw err; - console.log(`posts.json updated.`); - }); -} diff --git a/plugins/sentence/index.js b/plugins/sentence/index.js deleted file mode 100644 index 5092506..0000000 --- a/plugins/sentence/index.js +++ /dev/null @@ -1,67 +0,0 @@ -import { writeFileSync, readFileSync, readdirSync } from "fs"; -import { marked } from "marked"; - -export default function sentensePlugin({ plugins, cwd, prod }, opts) { - plugins.push(sentense({ cwd, prod, ...opts })); -} - -function sentense() { - return { - name: "feed", - enforce: "pre", - configResolved() { - makeListObject(); - }, - }; -} - -function markdownToString(markdown) { - const html = marked(markdown); - const notTag = html.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, ""); - const breakToSpace = notTag.replace(/(\r\n|\n|\r)/gm, " "); - const removeTwitter = breakToSpace.replace(/@twitter\[.*\]/g, ""); - const removeOg = removeTwitter.replace(/@og\[.*\]/g, ""); - return removeOg; -} - -const FRONTMATTER = /---\n([\s\S]*?)\n---\n\n([\s\S]*)/; -const FRONTMATTER_LIST = - /id:([\s\S]*)\ntitle:([\s\S]*)\ndescription:([\s\S]*)\ncreated_at:([\s\S]*)/; - -function makeListObject() { - const files = readdirSync(`${process.cwd()}/public/contents`); - const posts = files.filter((file) => file.match(/\.md$/)); - const contents = posts.map((post) => { - const file = readFileSync( - `${process.cwd()}/public/contents/${post}`, - "utf8" - ); - const md = file.match(FRONTMATTER); - const id = md[1].match(FRONTMATTER_LIST)[1]; - const c = markdownToString(md[2]).slice(0, 200); - - return { id, content: `${c}...` }; - }); - - const postList = readFileSync(`${process.cwd()}/public/contents/posts.json`); - const newPosts = JSON.parse(postList).map((post) => { - contents.forEach((content) => { - if (Number(post.id) === Number(content.id)) { - post = { - ...post, - description: content.content, - }; - } - }); - return post; - }); - - writeFileSync( - `${process.cwd()}/public/contents/posts.json`, - JSON.stringify(newPosts), - (err) => { - if (err) throw err; - console.log(`posts.json updated.`); - } - ); -} diff --git a/public/contents/1.md b/posts/1.md similarity index 100% rename from public/contents/1.md rename to posts/1.md diff --git a/public/contents/103.md b/posts/103.md similarity index 100% rename from public/contents/103.md rename to posts/103.md diff --git a/public/contents/105.md b/posts/105.md similarity index 100% rename from public/contents/105.md rename to posts/105.md diff --git a/public/contents/108.md b/posts/108.md similarity index 100% rename from public/contents/108.md rename to posts/108.md diff --git a/public/contents/109.md b/posts/109.md similarity index 100% rename from public/contents/109.md rename to posts/109.md diff --git a/public/contents/110.md b/posts/110.md similarity index 100% rename from public/contents/110.md rename to posts/110.md diff --git a/public/contents/111.md b/posts/111.md similarity index 100% rename from public/contents/111.md rename to posts/111.md diff --git a/public/contents/112.md b/posts/112.md similarity index 99% rename from public/contents/112.md rename to posts/112.md index cfad0c3..04cfe73 100644 --- a/public/contents/112.md +++ b/posts/112.md @@ -15,7 +15,7 @@ created_at: 2022-12-31 去年に倣って GitHub の草を載せるとこんな感じです、去年と比べるとあまりコードを書かなかった 1 年だったように感じます。 -![GitHub 2022](../contents/images/2022-github.png) +![GitHub 2022](/images/2022-github.png) # ざっくり時系列 diff --git a/public/contents/12.md b/posts/12.md similarity index 100% rename from public/contents/12.md rename to posts/12.md diff --git a/public/contents/14.md b/posts/14.md similarity index 100% rename from public/contents/14.md rename to posts/14.md diff --git a/public/contents/16.md b/posts/16.md similarity index 100% rename from public/contents/16.md rename to posts/16.md diff --git a/public/contents/17.md b/posts/17.md similarity index 100% rename from public/contents/17.md rename to posts/17.md diff --git a/public/contents/18.md b/posts/18.md similarity index 100% rename from public/contents/18.md rename to posts/18.md diff --git a/public/contents/19.md b/posts/19.md similarity index 100% rename from public/contents/19.md rename to posts/19.md diff --git a/public/contents/2.md b/posts/2.md similarity index 100% rename from public/contents/2.md rename to posts/2.md diff --git a/public/contents/21.md b/posts/21.md similarity index 100% rename from public/contents/21.md rename to posts/21.md diff --git a/public/contents/22.md b/posts/22.md similarity index 100% rename from public/contents/22.md rename to posts/22.md diff --git a/public/contents/23.md b/posts/23.md similarity index 100% rename from public/contents/23.md rename to posts/23.md diff --git a/public/contents/26.md b/posts/26.md similarity index 100% rename from public/contents/26.md rename to posts/26.md diff --git a/public/contents/29.md b/posts/29.md similarity index 100% rename from public/contents/29.md rename to posts/29.md diff --git a/public/contents/30.md b/posts/30.md similarity index 100% rename from public/contents/30.md rename to posts/30.md diff --git a/public/contents/31.md b/posts/31.md similarity index 100% rename from public/contents/31.md rename to posts/31.md diff --git a/public/contents/32.md b/posts/32.md similarity index 100% rename from public/contents/32.md rename to posts/32.md diff --git a/public/contents/33.md b/posts/33.md similarity index 100% rename from public/contents/33.md rename to posts/33.md diff --git a/public/contents/34.md b/posts/34.md similarity index 100% rename from public/contents/34.md rename to posts/34.md diff --git a/public/contents/35.md b/posts/35.md similarity index 100% rename from public/contents/35.md rename to posts/35.md diff --git a/public/contents/36.md b/posts/36.md similarity index 100% rename from public/contents/36.md rename to posts/36.md diff --git a/public/contents/37.md b/posts/37.md similarity index 100% rename from public/contents/37.md rename to posts/37.md diff --git a/public/contents/38.md b/posts/38.md similarity index 100% rename from public/contents/38.md rename to posts/38.md diff --git a/public/contents/39.md b/posts/39.md similarity index 100% rename from public/contents/39.md rename to posts/39.md diff --git a/public/contents/40.md b/posts/40.md similarity index 100% rename from public/contents/40.md rename to posts/40.md diff --git a/public/contents/42.md b/posts/42.md similarity index 100% rename from public/contents/42.md rename to posts/42.md diff --git a/public/contents/49.md b/posts/49.md similarity index 100% rename from public/contents/49.md rename to posts/49.md diff --git a/public/contents/5.md b/posts/5.md similarity index 100% rename from public/contents/5.md rename to posts/5.md diff --git a/public/contents/50.md b/posts/50.md similarity index 100% rename from public/contents/50.md rename to posts/50.md diff --git a/public/contents/52.md b/posts/52.md similarity index 100% rename from public/contents/52.md rename to posts/52.md diff --git a/public/contents/55.md b/posts/55.md similarity index 100% rename from public/contents/55.md rename to posts/55.md diff --git a/public/contents/56.md b/posts/56.md similarity index 100% rename from public/contents/56.md rename to posts/56.md diff --git a/public/contents/57.md b/posts/57.md similarity index 100% rename from public/contents/57.md rename to posts/57.md diff --git a/public/contents/59.md b/posts/59.md similarity index 100% rename from public/contents/59.md rename to posts/59.md diff --git a/public/contents/6.md b/posts/6.md similarity index 100% rename from public/contents/6.md rename to posts/6.md diff --git a/public/contents/67.md b/posts/67.md similarity index 100% rename from public/contents/67.md rename to posts/67.md diff --git a/public/contents/68.md b/posts/68.md similarity index 100% rename from public/contents/68.md rename to posts/68.md diff --git a/public/contents/71.md b/posts/71.md similarity index 100% rename from public/contents/71.md rename to posts/71.md diff --git a/public/contents/72.md b/posts/72.md similarity index 100% rename from public/contents/72.md rename to posts/72.md diff --git a/public/contents/74.md b/posts/74.md similarity index 100% rename from public/contents/74.md rename to posts/74.md diff --git a/public/contents/75.md b/posts/75.md similarity index 100% rename from public/contents/75.md rename to posts/75.md diff --git a/public/contents/76.md b/posts/76.md similarity index 100% rename from public/contents/76.md rename to posts/76.md diff --git a/public/contents/77.md b/posts/77.md similarity index 100% rename from public/contents/77.md rename to posts/77.md diff --git a/public/contents/78.md b/posts/78.md similarity index 100% rename from public/contents/78.md rename to posts/78.md diff --git a/public/contents/79.md b/posts/79.md similarity index 100% rename from public/contents/79.md rename to posts/79.md diff --git a/public/contents/8.md b/posts/8.md similarity index 100% rename from public/contents/8.md rename to posts/8.md diff --git a/public/contents/81.md b/posts/81.md similarity index 100% rename from public/contents/81.md rename to posts/81.md diff --git a/public/contents/83.md b/posts/83.md similarity index 100% rename from public/contents/83.md rename to posts/83.md diff --git a/public/contents/84.md b/posts/84.md similarity index 100% rename from public/contents/84.md rename to posts/84.md diff --git a/public/contents/87.md b/posts/87.md similarity index 100% rename from public/contents/87.md rename to posts/87.md diff --git a/public/contents/88.md b/posts/88.md similarity index 100% rename from public/contents/88.md rename to posts/88.md diff --git a/public/contents/9.md b/posts/9.md similarity index 100% rename from public/contents/9.md rename to posts/9.md diff --git a/public/contents/90.md b/posts/90.md similarity index 100% rename from public/contents/90.md rename to posts/90.md diff --git a/public/contents/92.md b/posts/92.md similarity index 100% rename from public/contents/92.md rename to posts/92.md diff --git a/public/contents/93.md b/posts/93.md similarity index 100% rename from public/contents/93.md rename to posts/93.md diff --git a/public/contents/94.md b/posts/94.md similarity index 100% rename from public/contents/94.md rename to posts/94.md diff --git a/public/contents/images/05d27e14-31b8-463e-a0e3-1219b469956c.png b/posts/images/05d27e14-31b8-463e-a0e3-1219b469956c.png similarity index 100% rename from public/contents/images/05d27e14-31b8-463e-a0e3-1219b469956c.png rename to posts/images/05d27e14-31b8-463e-a0e3-1219b469956c.png diff --git a/public/contents/images/063e4b1c-60aa-488a-ae38-47e7295e10c5.png b/posts/images/063e4b1c-60aa-488a-ae38-47e7295e10c5.png similarity index 100% rename from public/contents/images/063e4b1c-60aa-488a-ae38-47e7295e10c5.png rename to posts/images/063e4b1c-60aa-488a-ae38-47e7295e10c5.png diff --git a/public/contents/images/08700026-b2e3-4069-8181-f92fa8843830.png b/posts/images/08700026-b2e3-4069-8181-f92fa8843830.png similarity index 100% rename from public/contents/images/08700026-b2e3-4069-8181-f92fa8843830.png rename to posts/images/08700026-b2e3-4069-8181-f92fa8843830.png diff --git a/public/contents/images/142afb4f-6c93-4b5d-b568-2244a81e747d.png b/posts/images/142afb4f-6c93-4b5d-b568-2244a81e747d.png similarity index 100% rename from public/contents/images/142afb4f-6c93-4b5d-b568-2244a81e747d.png rename to posts/images/142afb4f-6c93-4b5d-b568-2244a81e747d.png diff --git a/public/contents/images/2022-github.png b/posts/images/2022-github.png similarity index 100% rename from public/contents/images/2022-github.png rename to posts/images/2022-github.png diff --git a/public/contents/images/463b713b-f599-4fbd-914d-27a5ab36da28.png b/posts/images/463b713b-f599-4fbd-914d-27a5ab36da28.png similarity index 100% rename from public/contents/images/463b713b-f599-4fbd-914d-27a5ab36da28.png rename to posts/images/463b713b-f599-4fbd-914d-27a5ab36da28.png diff --git a/public/contents/images/46a3440f-3695-440a-8036-a643bcad5ca7.png b/posts/images/46a3440f-3695-440a-8036-a643bcad5ca7.png similarity index 100% rename from public/contents/images/46a3440f-3695-440a-8036-a643bcad5ca7.png rename to posts/images/46a3440f-3695-440a-8036-a643bcad5ca7.png diff --git a/public/contents/images/8238670a-b90f-495e-9cc7-fca5a0772d3e.png b/posts/images/8238670a-b90f-495e-9cc7-fca5a0772d3e.png similarity index 100% rename from public/contents/images/8238670a-b90f-495e-9cc7-fca5a0772d3e.png rename to posts/images/8238670a-b90f-495e-9cc7-fca5a0772d3e.png diff --git a/public/contents/images/842df78c-937e-416b-b617-6a757dd1416a.png b/posts/images/842df78c-937e-416b-b617-6a757dd1416a.png similarity index 100% rename from public/contents/images/842df78c-937e-416b-b617-6a757dd1416a.png rename to posts/images/842df78c-937e-416b-b617-6a757dd1416a.png diff --git a/public/contents/images/913a7331-d28b-4a10-b319-102fe23d91f8.png b/posts/images/913a7331-d28b-4a10-b319-102fe23d91f8.png similarity index 100% rename from public/contents/images/913a7331-d28b-4a10-b319-102fe23d91f8.png rename to posts/images/913a7331-d28b-4a10-b319-102fe23d91f8.png diff --git a/public/contents/images/9f5a271e-42d0-4b85-89af-99f43743c984.png b/posts/images/9f5a271e-42d0-4b85-89af-99f43743c984.png similarity index 100% rename from public/contents/images/9f5a271e-42d0-4b85-89af-99f43743c984.png rename to posts/images/9f5a271e-42d0-4b85-89af-99f43743c984.png diff --git a/public/contents/images/aws-billing.png b/posts/images/aws-billing.png similarity index 100% rename from public/contents/images/aws-billing.png rename to posts/images/aws-billing.png diff --git a/public/contents/images/ef951453-429a-4360-bd1e-3f75d6dbfa66.png b/posts/images/ef951453-429a-4360-bd1e-3f75d6dbfa66.png similarity index 100% rename from public/contents/images/ef951453-429a-4360-bd1e-3f75d6dbfa66.png rename to posts/images/ef951453-429a-4360-bd1e-3f75d6dbfa66.png diff --git a/public/contents/images/fc1f3423-3c96-4816-b5bf-caa13c241161.png b/posts/images/fc1f3423-3c96-4816-b5bf-caa13c241161.png similarity index 100% rename from public/contents/images/fc1f3423-3c96-4816-b5bf-caa13c241161.png rename to posts/images/fc1f3423-3c96-4816-b5bf-caa13c241161.png diff --git a/public/contents/images/fd0fd8ca-1996-4f2f-9590-624cdcb52a91.png b/posts/images/fd0fd8ca-1996-4f2f-9590-624cdcb52a91.png similarity index 100% rename from public/contents/images/fd0fd8ca-1996-4f2f-9590-624cdcb52a91.png rename to posts/images/fd0fd8ca-1996-4f2f-9590-624cdcb52a91.png diff --git a/public/contents/images/takurinton.jpeg b/posts/images/takurinton.jpeg similarity index 100% rename from public/contents/images/takurinton.jpeg rename to posts/images/takurinton.jpeg diff --git a/public/contents/posts.json b/public/contents/posts.json deleted file mode 100644 index 96136a6..0000000 --- a/public/contents/posts.json +++ /dev/null @@ -1,272 +0,0 @@ -[ - { - "id": "5", - "title": "OngaqJS触ってみた", - "description": "はじめに こんにちは ポートフォリオ(おふざけ入ってるあれです)にリンク貼ってあるOngaqJSについてのソースコードをまとめてみたので記事にしました。 #OngaqJSとはなんぞや? OngaqJSとは、JavaScriptで音楽が作成できるというAPIです。 Keyの取得はこちらから 有料枠と無料枠があり、使える楽器の種類などが異なる模様(有料会員になろうかな) ぽちぽちしていけば簡単に登録で...", - "created_at": "2020-04-24" - }, - { - "id": "9", - "title": "ブログを引っ越した話", - "description": "はじめに こんにちは。疲れた。 てことで今回は僕がはじめてAWSを使ったときに詰まったところについてまとめたいと思います!!! また、この記事はQiitaにも全く同じことが書いてありますので、見やすい方がいいなと思ったら素直にQiitaに言ってください。 URLはこちらです 僕のレベル AWSって何???? まず最初に言っておきますが、私はこのレベルの人間です。AWSって言葉は知ってるけど中身は知...", - "created_at": "2020-06-10" - }, - { - "id": "12", - "title": "サポーターズの1on1面談に参加した話", - "description": "こんにちは こんばんは、先日サポーターズの1on1面談イベントに参加したので記事にしたいと思います。 簡単にまとめると、とっても楽しかったし充実した時間になったけど、その分周りのエンジニア志望の学生との差を痛感してとても刺激がもらえたイベントでした!!!! 1on1面談イベントってなんだよって思った方はこちらのリンクを参照してみてください。 #事前に準備したこと このイベントは、各ターンの最初に学...", - "created_at": "2020-06-11" - }, - { - "id": "17", - "title": "Treasureに参加した話", - "description": "はじめに こんにちは。僕です。 以前の記事にも書きましたが、VOYAGE GROUPのTreasureというインターンに参加したのでまとめたいと思います。 結論から言うと最高のインターンでした!!(迷惑かけてばっかりだったけど) 来年以降インターンシップに行きたいと思ってる人はぜひTreasureにも申し込んでみてください! そもそもTreasureってなんだ TreasureとはVOYAGE G...", - "created_at": "2020-09-02" - }, - { - "id": "18", - "title": "サイバーエージェントのインターンに参加した話", - "description": "こんにちは どうも、僕です。 今回はサイバーエージェントの就業型インターンに参加して、Ameba事業本部という部署で少しだけコード書かせてもらったのでそのことについて記事にしたいと思います。 参加した経緯 これは遡ること2月、、、 僕が某企業のアドテクのインターンに参加して、楽しかったことを大学の先輩のYさんに報告しました。 僕「この前アドテクのインターンに参加して、DSP周りのコードを簡単に...", - "created_at": "2020-09-13" - }, - { - "id": "19", - "title": "楽天のインターンに参加した話", - "description": "こんにちは どうも、僕です。今回は夏の締めくくりに参加した楽天のインターンについて書きたいと思います。楽天のインターンはここまでのインターンの中で一番辛かったような気がします(言語的な問題)海外で仕事できる気がしませんでした。そんな感じでまとめていきます。 選考 インターンには選考がつきものです。選考内容はコーディングテストと面接で、両方とも1日で終わらせます。参加者全員。 コーディングテストはc...", - "created_at": "2020-09-25" - }, - { - "id": "21", - "title": "Skywayでビデオ通話", - "description": "はじめに こんにちは、ブログに MaterialUIを導入してルンルンの僕です。今回はSkyWayで1対1の音声通話、またそれを文字起こしをしてGASに投げる処理までを書いたのでまとめたいと思います。GitHub Pages でデプロイしたサイトはこちらになります。レポジトリはこちらになります。 SkyWayって何? SkyWayとは、Webでリアルタイムコミュニケーションを実現する標準技術、We...", - "created_at": "2020-10-09" - }, - { - "id": "23", - "title": "Django + RDS", - "description": "こんにちは どうも、僕です。今回はAWSのEC2にデプロイしたDjangoのプロジェクトをRDSに接続する方法について書きたいと思います。Djangoを理解してればとても簡単。困ることなんてありません。ではいきます。 バックアップを取る DjangoではデフォルトのDBとしてプロジェクトを作成したときにsqlite3のテーブルがついてきます。しかし、RDSに乗り換えるときにはsqlite3のデータ...", - "created_at": "2020-10-15" - }, - { - "id": "26", - "title": "easyjsonを使ってみた", - "description": "こんにちは どうも、僕です。この記事は随分前のインターン期間中に自分のために書いた記事を転載してます。 GoでJSON使う時ってだいぶめんどくさいんですよね。まあ型による安心感がバケモンなのでやった方がいいんですけど。 GoでJSONを捌く時はstructを使用します。クラスとかはないのでこれでいきます。  Unmarshal 早速やってみます。 例えばこんな感じのjsonがくるとします。 v...", - "created_at": "2020-10-21" - }, - { - "id": "29", - "title": "4日間でポートフォリオを作り替えた", - "description": "こんにちは どうもこんにちは。僕です。最近あったことといえば、Vのインターン落ちて落ち込んでるところに人事の方からのなぐさめのDMがきてさらに泣きそうになったところでRからの内定もらってなんだかメンタルが忙しいことです。 今回は僕がポートフォリオを作り替えた話(需要あるのか?)について話していこうと思います。 なんで作り替えたの? まずここから作り替えた理由としては3つあります。 Nex...", - "created_at": "2020-11-14" - }, - { - "id": "30", - "title": "Goのdeferに注意する", - "description": "はじめに 今日開発してて遭遇したエラーについて話します。短めです。よろしくお願いします。 状況 GoでAPIサーバを開発してる時に、エラーハンドリングについての実装をしていた。現状の問題としては、DBと接続する時にアプリケーションサーバ(NginxやらGolangやら)が生きてる状態でDBサーバ(今で言うRDS)が死んでる時にDBのIPアドレスとポートがクライアント側に渡されてしまうという問題が起...", - "created_at": "2020-11-15" - }, - { - "id": "31", - "title": "DjangoでUser認証機能を作る", - "description": "こんにちは これですどうも、僕です。この記事はKITアドベントカレンダーの4日目の記事になります。今回はユーザー認証の機能を実装してみようと思います。 やり方 Djangoではデフォルトでユーザーモデルが定義されています。今回はそれを書き換えることでユーザー認証の仕組みを作成して行こうと思います。最近だと外部の認証に任せるパターンも増えていますが、こっちの方が楽だと感じることもちょこちょこある...", - "created_at": "2020-12-04" - }, - { - "id": "33", - "title": "pyparsing触ってみた", - "description": "こんにちは こんにちは、僕です。最近、pyparsingというPythonのライブラリを使用していて、面白いなあと思ったので記事にしてみました。元々自分は言語解析などに興味があって(NLPとか)、今回は形式言語解析になりますがまとめたいと思います。 pyparsingとは? これです。ドキュメント The pyparsing module is an alternative approach ...", - "created_at": "2020-11-24" - }, - { - "id": "34", - "title": "DRFのViewについてまとめる", - "description": "こんにちは どうも、こんにちは、僕です。今日はDRFのなんちゃらAPIViewについてまとめたいと思います。自分の推しはAPIViewです。対戦お願いします! DRFとは? そんなもんは自分で調べてください(辛辣) 参考までにこれは前回の記事でさらっと書いてしまったAPIView周りの深堀りみたいなイメージで描いていきます。 今回使うモデル 今回使うモデルはとてもシンプルで、以下のよう...", - "created_at": "2020-11-27" - }, - { - "id": "35", - "title": "asgiを触ってみた", - "description": "こんにちは どうも、僕です。今回はDjangoでasgiを使用してみたということでチュートリアルを自分なりにまとめたいと思います。最近のブログ暇つぶしみたいな感じだったので今回は久しぶりに新しい技術に触れたアウトプット感があって良きです。浅い asgiって何? まず最初にasgiについて説明したいと思います。そもそもPythonで鯖を立てるためにはwsgiとasgiの2種類があります。 ws...", - "created_at": "2020-11-27" - }, - { - "id": "36", - "title": "asgiを触ってみたつづき", - "description": "どうも こんにちは、僕です。今回は前回の記事の続きを書いていきたいと思います。まだまだリファクタリングしないといけないのですが、現状動くものをのせる的な感じで。 前回なにしたか 前回はぽよりました。(適当)まあ、簡単にソケットでチャットを作りましょうみたいなことをしました。 今回はなにするか そこに今回はユーザーを定義してさらにチャットを保存していくみたいな実装をしました。簡単な実装しかしてい...", - "created_at": "2020-11-30" - }, - { - "id": "38", - "title": "Goの名前付き戻り値", - "description": "こんにちは どうも僕です。この記事はKITアドベントカレンダー11日目の記事になります。今回はGolangの名前付き戻り値について簡単にまとめてみました。 そもそも名前付き戻り値って何? まずこれですよね、僕も最初はわからんってなってましたし、今もよくわかってないです。簡単にいうと、戻り値に名前をつけておくことができます。比較は以下のコードで。 // 通常の関数 func nomalFun...", - "created_at": "2020-12-11" - }, - { - "id": "40", - "title": "フロントエンドの今", - "description": "こんにちは どうも、僕です。今日はフロントエンドについて書いて行こうと思います。(抽象的〜〜〜) 僕はフロントエンドの人間ではないのであまり詳しくはないのですが、そんな僕でも知っていることや最低限意識していることなどを簡単にですがまとめていきたいと思います。 最近のフロントエンド 最近はフロントエンドの進化が早く、バックエンド側まで侵食しかけている印象を受けます。SPAとはみたいなことを思うよ...", - "created_at": "2020-12-15" - }, - { - "id": "42", - "title": "2020年振り返り", - "description": "こんにちは どうも,僕です.これを書いているときはクリスマスで,TLのみんなはコード書いてるんだろうなって思いながら書いてます.今年一年,某病気にほぼ潰されてしまいましたが,個人的には充実した1年間になったと感じています.インターン,就職活動,技術的な成長,その他もろもろ人生の分岐点となるようなことが多かった気がします.それを割と雑に振り返っていきたいと思います. 2020年という年 今年は大...", - "created_at": "2020-12-25" - }, - { - "id": "49", - "title": "JWTについて学ぶ", - "description": "はじめに どうも,僕です.今日はGolangでJWTを実装したけどちょっとつまづいたことが多かったので記事にしたいと思います.これ実装したのだいぶ前なので思い出しながら頑張っていこうと思います. JWTについて そもそもJWTとはなんなのかについて簡単におさらいをしておきたいと思います.JWTとは,JSON Web Tokenの略で属性情報(Claim)をJSONの中に丸め込むことで個人を識別...", - "created_at": "2021-01-11" - }, - { - "id": "56", - "title": "マルコフ連鎖実装してみた", - "description": "こんにちは どうも,僕です.今回はみんな大好きマルコフ連鎖についてです.コードはこれ.与えられた文章をもとにして新しい文章を生成するやつを実装しました.ではやっていきます. マルコフ連鎖って何? マルコフ連鎖とはどうやら離散マルコフ過程の別称のようです.知らんかった.Twitterで見かけてググってみたら出てきてほへーってなりました.今回は文章を自動生成するためのマルコフ連鎖を実装していきます...", - "created_at": "2021-02-15" - }, - { - "id": "59", - "title": "バンドルツール作る", - "description": "こんにちは どうも、僕です。最近バンドルツールを作った(というか作ってる途中)なのでその様子を記事にします。 まだ作ってる途中なのと、あまりきれいな構成ではないので多目に見てください。ではやっていきます。 技術選定 プログラムを実行するためのものと、バンドラーを作成するための補助として使うもののそれぞれを別に悩むことなく以下のように選定しました。 deno プログラムの実行に関してですが、...", - "created_at": "2021-06-05" - }, - { - "id": "67", - "title": "iframe のスクロール", - "description": "はじめに こんにちは、どうも僕です。Intersection Observer API を使ってスクロール率を用いてコンテンツの表示を操作するためにコードを書いていたのですが、ちょっとこけたのでまとめます。 Intersection Observer API とは Intersection Observer API とは、ターゲットとなる要素が指定した監視対象の要素が指定した viewport...", - "created_at": "2021-07-17" - }, - { - "id": "68", - "title": "日報?を作った", - "description": "こんにちは どうも、僕です。 今回は Twitter API を使って自分のツイートを自動で拾ってきてそれをもとに140文字以内の任意の文章を生成してツイートする bot を作成したのでその様子を記事にします。なお、@takurinton ではないアカウント(知ってる人は知ってる)なのでそこはご了承ください。 使用技術 JavaScript バンドルするのがめんどくさいので CommonJS べ...", - "created_at": "2021-07-21" - }, - { - "id": "71", - "title": "dumb-init とは", - "description": "はじめに こんにちは、どうも僕です。Dockerfile を読んだり書いたりしてる時に出てきた dumb-init を知らなくて気になったので調査しました。 dumb-init とは dumb-init とは Linux コンテナ用の最小限の init システムで、PID が 1 になるように作られています。最小限のコンテナとは、Docker などの小さめの環境のことを指していています。C で...", - "created_at": "2021-08-18" - }, - { - "id": "72", - "title": "インフラのパフォチュー", - "description": "はじめに どうも、僕です。先日、ISUCON に出場して、学生枠で6位、5位と2000点差で惜しくも本戦出場を逃しました。(チームメンバーに助けられまくってたので僕はなにもしてませんが) 普段はフロントエンドの実装をメインとしてやっているのですが、最近仕事で Docker や k8s 周りを触ることが多く、少しサーバサイドやインフラについて少し興味が出てきたときに出場した ISUCON だった...", - "created_at": "2021-08-29" - }, - { - "id": "74", - "title": "Preact の change event", - "description": "こんにちは どうも、僕です。先日、Preact を使用して、簡単なアプリケーションのプロトコーディングをしていて、input タグのイベントハンドラを呼ぼうとしたらうまく動かくてハマりました。 以下のように定義していたのですが、結論としては、onChange ではなくて onInput を使わなければならないようです。 const HogeComponent = (): JSX.Element...", - "created_at": "2021-09-19" - }, - { - "id": "75", - "title": "GraphQL の print と parse", - "description": "こんにちは どうも、僕です。最近、業務や趣味で GraphQL の AST や query を動的にいじるようなことをしていて、その中で print 関数や parse 関数を脳死で使っていたのですが、ふと中身がどうなっているのか気になったため、ちょっと調べてみました。なお、今回は、AST の見方などは書きません。 print と parse とは parse 関数 print 関数とは、Grap...", - "created_at": "2021-09-19" - }, - { - "id": "77", - "title": "GraphQL の parse エディタ", - "description": "こんにちは どうも、僕です。シルバーウィークなのでこれまで書きたくても時間がなくて書けなかった記事をどんどん投下しています。今回は、最近作ってる GraphQL の DocumentNode を parse して作ったエディタから動的に query を生成する画面のプロトコーディングをしたのでそれを簡単にまとめます。 概要 このツイートの感じです。 https://twitter.com/taku...", - "created_at": "2021-09-20" - }, - { - "id": "78", - "title": "自サイトのアクセス可視化", - "description": "こんにちは どうも、僕です。1ヶ月ほど前からポートフォリオとブログのトラッキングを始めて、だいぶデータが溜まってきたのでどうしようかなと思っていました。ちょうど、昨日(9月23日)は祝日で、数日前に研究の中間発表も終わり(学部の研究発表なんてたかが知れてるだろという声はさておき)、たまには休日っぽいことでもするかと思っていたので、ポートフォリオとブログのログを可視化するというコードを書いてみました...", - "created_at": "2021-09-23" - }, - { - "id": "79", - "title": "自サイトのアクセス可視化2", - "description": "こんにちは どうも、僕です。この記事は 自サイトのアクセス可視化 の続きです。前回は AST をこねくり回して、form をいじる実装をしました。今回はそのアクセスごとの詳細画面を作成し、そこにグラフをつけて見やすくするみたいなことをしてみました。まだまだきれいではないのですが、これからきれいになります、きっと。 概要 前回、ちょっとだけ作っていた Detail.tsx を拡張します。詳細ページで...", - "created_at": "2021-09-25" - }, - { - "id": "81", - "title": "プレイドのインターン終了した", - "description": "こんにちは どうも、僕です。会社のブログ にも書いたのですが、プレイドのインターンを終了しました。理由としては、これから色々忙しくなるからであって、別にクビになったわけではないです。ここでは、なんというか、こんなこと感じたみたいなことを書きます。会社のブログは事実ベース、こっちのブログは感情ベースです。 あのブログに関して あのブログ、実は1ヶ月前くらいから書いていて、めちゃくちゃ時間かけました。...", - "created_at": "2021-09-30" - }, - { - "id": "83", - "title": "addEventListener の第3引数", - "description": "こんにちは どうも、僕です。addEventListener の第3引数って知ってますか。僕は知りませんでした。いや、厳密に言うと、存在は知ってたけど理解してませんでした。理解するために、DOMのイベントフローとともに見ていきたいと思います。 イベントフローとは まずはイベントフローとは何かについて考えます。イベントフローとは、DOM に対するイベントの委任(伝播)のことで、例えば以下のような...", - "created_at": "2021-10-13" - }, - { - "id": "84", - "title": "React のメモ化", - "description": "こんにちは どうも、僕です。プレイドのインターンで人生で初めて業務でフロントエンドを書いてから3ヶ月半が経ちました。プレイドでは Vue と Svelte を書いていて、最後の2週間くらいは React を書いていましたが、まだまだよくわからない点が多く、特に hooks 周りを自分はしっかり理解してませんでした。プレイドでは Vue と Svelte を書いていて、最後の2週間くらいは Reac...", - "created_at": "2021-10-13" - }, - { - "id": "87", - "title": "JPHACKS に出場した", - "description": "こんにちは どうも、僕です。人生初ハッカソン、出場しました。総合すると、毎日忙しかったものの、とても楽しくコードを書くことができたと思います。また、一緒に出てくれたメンバーや、今回のハッカソンに出場していた皆さん、また、開催してくれた方々には感謝しかありません、ありがとうございました。 作品 https://github.com/jphacks/F_2111 チームメンバー チームメンバーは、...", - "created_at": "2021-10-30" - }, - { - "id": "88", - "title": "forwardRef を習得した", - "description": "こんにちは どうも、僕です。最近色々あって、Material UI のコードを読んでいるのですが、forwardRef がさまざまな箇所で出てきて理解できないと辛いので勉強しました。 ref とは ref とは、簡単に言うと、dom であれば element、それ以外の要素であればその class のインスタンスに対してアタッチするためのものです。class component だと、要素 +...", - "created_at": "2021-11-01" - }, - { - "id": "93", - "title": "検索を作る", - "description": "こんにちは どうも、僕です。検索エンジンを自作しようとしてうまくは行きましたが、想定していた規模には耐えることができなかったという内容の記事です。実際、パフォーマンスにこだわると厳しい点が多かったり、検索結果が帰ってこなかったりしてだいぶしんどい実装になってしまいました。もっとうまくやる方法があったら教えてください。 今回の手法と想定する規模 今回は、転置インデックスを作成して戦います。転置イ...", - "created_at": "2021-11-15" - }, - { - "id": "94", - "title": "MySQL の FULLTEXT とは", - "description": "こんにちは どうも、僕です。MySQL(MariaDB)に搭載されてる最強な検索、FULLITEXT INDEX について書きます。前回のブログ で、検索をアプリケーション側で実装する方法について書きましたが、今回はそれのデータベース側からのアプローチです。このレイヤーでデータを操作するのは賛否が分かれる部分だとは思いますが、簡単に実装してみたのでやっていきます。 そもそも何 インデックスです。全...", - "created_at": "2021-11-16" - }, - { - "id": "103", - "title": "結合時のアルゴリズム", - "description": "こんにちは どうも、僕です。SQL アンチパターンを読んでいて、「外部結合をすると処理のコストが指数関数的に上がっていってしまいます」との記述があり、よくわからなかったので調べました。 JOIN アルゴリズム そもそも、 SQL の JOIN には、以下の3種類のアルゴリズムが使用されることが多いです。オラクルと、postges には全部使用されていますが、MySQL には nested loo...", - "created_at": "2021-12-18" - }, - { - "id": "105", - "title": "2021年振り返り", - "description": "こんにちは どうも、僕です。2021年もそろそろ終わります。2021年は色々なことがありましたが、簡単にまとめると、機会と出逢いに恵まれた1年だったと思っています。さまざまな優秀なエンジニアの方々と仕事をさせていただいたり、コミュニティへ招待していただいたりをしていました。自分自身の技術的な成長があったかどうかは微妙ですが、エンジニアとして少しずつ地に足がついてきた実感はあります。来年も頑張って行...", - "created_at": "2021-12-29" - }, - { - "id": "108", - "title": "Svelte と SPA", - "description": "こんにちは どうも、僕です。最近、Svelte が市民権を獲得してきているようで、フロントエンド開発において、選択肢に入ってくる機会が増えてきたと思います。また、Twitter などでも、React よりも Svelte!のような内容のものや、Svelte を勉強してみるといった内容のものが増えてきたように感じます。それ自体はいいことであり、Svelte が選択肢に入ってくることは面白いとは思うの...", - "created_at": "2022-01-14" - }, - { - "id": 109, - "title": "ブログをSSGにした", - "description": "こんにちは どうも、僕です。今回はブログを SSG にしてみたという話をします。年始に SSR をするような構成にしたばかりですが、色々あって(後述)作り替えてみました。 SSR にする記事は以下からどうぞ。 ブログを作り直した Rust で GraphQL server を書いてみた このブログのソースコードはこちらtakurinton/ssg-blog なぜ作り直したか そもそもなぜ作り直...", - "created_at": "2022-08-14" - }, - { - "id": 110, - "title": "wmr の prerender 時に明示的な fetch の定義が不要になる", - "description": "こんにちは どうも、僕です。 先日、ブログを新しくしました。=> ブログを SSG にしたwmr の prerender mode を使って SSG にしましたが、自分の実装したような wmr の prerender で .md ファイルを使ったブログを書くようなパターンだと、prerender 関数の中で fetch API をオーバーライドしてあげないとプロダクションビルドをする際に f...", - "created_at": "2022-09-26" - }, - { - "id": 111, - "title": "SHA-256 algorithm", - "description": "こんにちは どうも、僕です。今週猫がきます。 猫の名前について、友人と雑談をしていて、 名前なんて記号なんだから呼びやすくて聞こえが良ければなんでもいいよ と言われたので、記号だなんてそんなのハッシュではないかと思ったと同時に、ハッシュという名前はとてもいいなと思いました。ハッシュという名前にしっくりきたので名前はハッシュにしようと思ったものの、筆者はコンピュータサイエンスの学部を出ているのに...", - "created_at": "2022-10-10" - }, - { - "id": 112, - "title": "2022年振り返り", - "description": "こんにちは 今年は散々な年だったため書かなくて良いかなと思っていましたが、こういうのは後から見てみてこういうのもあったなーと振り返るものな気がするので一応書いておきます。 去年の振り返りはこちらです。 去年に倣って GitHub の草を載せるとこんな感じです、去年と比べるとあまりコードを書かなかった 1 年だったように感じます。 ざっくり時系列 1 月 卒論の提出期日を間違えてました。 とよ...", - "created_at": "2022-12-31" - } -] diff --git a/public/header/index.js b/public/header/index.js deleted file mode 100644 index 831a982..0000000 --- a/public/header/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// eslint-disable-next-line no-unused-vars -import { h } from "preact"; -import styles from "./style.module.css"; - -export default function Header() { - return ( -
    - - blog.takurinton.dev - -
    - ); -} diff --git a/public/header/style.module.css b/public/header/style.module.css deleted file mode 100644 index 732b6a0..0000000 --- a/public/header/style.module.css +++ /dev/null @@ -1,13 +0,0 @@ -header { - display: flex; -} - -header .title { - font-size: var(--font-size-xlarge); - color: var(--font-black); - padding: var(--padding-medium); -} - -header .title:hover { - color: var(--color-primary-main); -} diff --git a/public/index.html b/public/index.html deleted file mode 100644 index df470a3..0000000 --- a/public/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/public/index.js b/public/index.js deleted file mode 100644 index 1dcf366..0000000 --- a/public/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import { LocationProvider, Router, ErrorBoundary, hydrate } from "preact-iso"; -import NotFound from "./pages/_404"; -import Header from "./header"; -import Home from "./pages/home"; -import Post from "./pages/post"; - -export function App() { - return ( - -
    -
    - - - - - - - -
    -
    - ); -} - -if (typeof window !== "undefined") { - hydrate(, document.body); -} - -export async function prerender() { - return (await import("./prerender.js")).prerender(); -} diff --git a/public/pages/_404.js b/public/pages/_404.js deleted file mode 100644 index e183e6f..0000000 --- a/public/pages/_404.js +++ /dev/null @@ -1,8 +0,0 @@ -const NotFound = () => ( -
    -

    404: Not Found

    -

    It's gone :(

    -
    -); - -export default NotFound; diff --git a/public/pages/home/index.js b/public/pages/home/index.js deleted file mode 100644 index 737c9fb..0000000 --- a/public/pages/home/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import styles from "./style.module.css"; -import posts from "../../contents/posts.json"; -import { useMetas } from "src/hooks/useMetas"; - -const sortPosts = (posts) => { - return posts - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) - .map((post) => post); -}; - -export default function Home() { - const title = "takurinton | Home"; - const description = `takurinton のブログです`; - useMetas({ title, description }); - - return ( -
    -
    -

    記事一覧

    -
    - {sortPosts(posts).map((post) => ( -
    - - {post.title} - -

    {post.created_at}

    -

    {post.description}

    -
    - ))} -
    - ); -} diff --git a/public/pages/post/index.js b/public/pages/post/index.js deleted file mode 100644 index bb83303..0000000 --- a/public/pages/post/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useRoute } from "preact-iso"; -import styles from "./style.module.css"; -import { usePost } from "src/hooks/usePost"; -import { useMetas } from "src/hooks/useMetas"; -import { useClientAssets } from "src/hooks/useClientAssets"; - -export default function Post() { - const { params } = useRoute(); - const { title, description, createdAt, content } = usePost(params.id); - useMetas({ title, description }); - useClientAssets(content); - - return ( -
    -

    {title}

    -

    {createdAt}

    -
    -
    - ); -} diff --git a/public/prerender.js b/public/prerender.js deleted file mode 100644 index bb46fdc..0000000 --- a/public/prerender.js +++ /dev/null @@ -1,22 +0,0 @@ -import { prerender as ssr } from "preact-iso"; -import { toStatic } from "hoofd/preact"; - -export async function prerender(vnode) { - const res = await ssr(vnode); - - const head = toStatic(); - const elements = new Set([ - ...head.links.map((props) => ({ type: "link", props })), - ...head.metas.map((props) => ({ type: "meta", props })), - ...head.scripts.map((props) => ({ type: "script", props })), - ]); - - return { - ...res, - head: { - title: head.title, - lang: "en", - elements, - }, - }; -} diff --git a/scripts/gen-template.js b/scripts/gen-template.js deleted file mode 100644 index fadc801..0000000 --- a/scripts/gen-template.js +++ /dev/null @@ -1,56 +0,0 @@ -import { writeFileSync, readFileSync } from "fs"; -import { createInterface } from "readline"; - -(() => { - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - readline.question("title: ", (title) => { - const posts = JSON.parse( - readFileSync(`${process.cwd()}/public/contents/posts.json`, "utf8") - ); - const ids = posts.map((post) => post.id); - const newId = Math.max(...ids) + 1; - - const date = new Date(); - let newPost = { - id: newId, - title, - description: `${title} について`, - created_at: `${date.getFullYear()}-${(date.getMonth() + 1) - .toString() - .padStart(2, "0")}-${date - .getDate() - .toString() - .padStart(2, "0")}`.replace(/\n/g, ""), - }; - - writeFileSync( - `${process.cwd()}/public/contents/${newId}.md`, - `--- -id: ${newPost.id} -title: ${newPost.title} -description: ${newPost.description} -created_at: ${newPost.created_at} ---- -`, - (err) => { - if (err) throw err; - console.log(`${newId} created.`); - } - ); - - const newPosts = [...posts, newPost]; - writeFileSync( - `${process.cwd()}/public/contents/posts.json`, - JSON.stringify(newPosts), - (err) => { - if (err) throw err; - console.log(`posts.json updated.`); - } - ); - readline.close(); - }); -})(); diff --git a/src/utils/setupClientAssets.js b/scripts/index.js similarity index 76% rename from src/utils/setupClientAssets.js rename to scripts/index.js index a3f6bb8..6c00093 100644 --- a/src/utils/setupClientAssets.js +++ b/scripts/index.js @@ -1,11 +1,50 @@ -import { getHtml } from "src/md/getHtml"; - const CACHE = new Map(); +/** + * @param {string} url + * @returns {Promise<{title: string, description: string, image: string}>} + */ +const getMetaTags = async (url) => { + if (!url) { + return { + title: "", + description: "", + image: "", + }; + } + const data = await fetch(`https://meta.takur.in/api/?url=${url}`); + const json = await data.json(); + return json; +}; + +/** + * @param {string} url + * @param {number} id + * @returns {Promise} + */ +export const getHtml = async (url, id) => { + const { title, description, image } = await getMetaTags(url); + + return ` + + `; +}; + /** * Set external link card when OG embedding exists */ -export const setupClientAssets = () => { +const setupClientAssets = () => { (async () => { const og = document.getElementsByClassName("og"); Array.from(og).forEach((element) => { @@ -114,3 +153,5 @@ export const setupClientAssets = () => { document.head.appendChild(style); } }; + +setupClientAssets(); diff --git a/src/hooks/useClientAssets.js b/src/hooks/useClientAssets.js deleted file mode 100644 index fa98e88..0000000 --- a/src/hooks/useClientAssets.js +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from "preact/hooks"; -import { setupClientAssets } from "src/utils/setupClientAssets"; -import { setupHighlightjs } from "src/utils/setupHighlightjs"; -import { setupTwitter } from "src/utils/setupTwitter"; - -/** - * Perform client rendering when: - * 1. Call highlightjs when markdown code tag exists - * 2. Call widget when Twitter embed exists - * 3. Call renderer when OG embedding exists - * - * @param {string} content - */ -export const useClientAssets = (content) => { - useEffect(() => { - if (/
    .*/.test(content)) {
    -      setupHighlightjs();
    -    }
    -  }, []);
    -
    -  useEffect(() => {
    -    if (/