diff --git a/.gitignore b/.gitignore
index b56bec2..9128043 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@
*storybook.log
-**.env
\ No newline at end of file
+**.env
+
+/public/sitemap.xml
\ No newline at end of file
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index bfa7745..6486547 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -18,8 +18,8 @@ export function meta({ }: Route.MetaArgs) {
const members: MemberCardProps[] = [
{ name: "Naoto Kido", role: "owner", description: "card.memberCard.descriptions.naoido", stacks: ['Kubernetes', 'AWS', 'Java', 'Go-wordmark', 'Flutter'], headerImage: "/assets/images/headers/naoido.webp", iconImage: "https://avatars.githubusercontent.com/u/54303857", githubName: "naoido" },
{ name: "Naoki Miura", role: "coowner", description: "card.memberCard.descriptions.thirdlf03", stacks: ['Go-wordmark', 'Javascript', 'Rust', 'Python', 'Neovim'], headerImage: "/assets/images/headers/thirdlf03.webp", iconImage: "https://avatars.githubusercontent.com/u/114989748", githubName: "thirdlf03" },
- { name: "Takumi Matsubara", role: "frontend", description: "card.memberCard.descriptions.annkoatama", stacks: ['Typescript', 'Swift', 'React'], headerImage: "/assets/images/headers/annkoatama.webp", iconImage: "https://avatars.githubusercontent.com/u/152017354", githubName: "AnnkoATAMA" },
- { name: "Kentaro Doi", role: "backend", description: "card.memberCard.descriptions.kenta-afk", stacks: ['Javascript', 'Python', 'php', 'Laravel', 'Typescript'], headerImage: "/assets/images/headers/kenta_afk.webp", iconImage: "https://avatars.githubusercontent.com/u/148222450", githubName: "kenta-afk" },
+ { name: "Takumi Matsubara", role: "frontend", description: "card.memberCard.descriptions.annkoatama", stacks: ['Typescript', 'Python', 'Java', 'Swift', 'React'], headerImage: "/assets/images/headers/annkoatama.webp", iconImage: "https://avatars.githubusercontent.com/u/152017354", githubName: "AnnkoATAMA" },
+ { name: "Kentaro Doi", role: "backend", description: "card.memberCard.descriptions.kenta-afk", stacks: ['Python', 'php', 'Laravel', 'Typescript', 'Javascript'], headerImage: "/assets/images/headers/kenta_afk.webp", iconImage: "https://avatars.githubusercontent.com/u/148222450", githubName: "kenta-afk" },
]
const notices: NoticeCardProps[] = [
diff --git a/package.json b/package.json
index d3c16e9..a570038 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,9 @@
"private": true,
"type": "module",
"scripts": {
+ "gen-sitemap": "node ./scripts/gen-sitemap.js",
+ "prebuild": "npm run gen-sitemap",
"build": "react-router build",
- "postbuild": "cp _routes.json build/client/_routes.json",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
diff --git a/_routes.json b/public/_routes.json
similarity index 100%
rename from _routes.json
rename to public/_routes.json
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..0e19d85
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://object-t.com/sitemap.xml
\ No newline at end of file
diff --git a/scripts/gen-sitemap.js b/scripts/gen-sitemap.js
new file mode 100644
index 0000000..6877e31
--- /dev/null
+++ b/scripts/gen-sitemap.js
@@ -0,0 +1,95 @@
+import { execSync } from "child_process";
+import fs from "fs";
+import path from "path";
+
+const BASE_URL = "https://object-t.com/blog";
+
+const repoDir = "tmp";
+const repoPath = path.resolve(repoDir);
+
+const urlEntries = [];
+
+try {
+ if (fs.existsSync(repoPath)) {
+ console.log(`[INFO] Removing existing directory: ${repoPath}`);
+ fs.rmSync(repoPath, { recursive: true, force: true });
+ }
+
+ console.log(`[INFO] Cloning repository into ${repoPath}...`);
+ execSync(`git clone https://github.com/object-t/object-t-blog ${repoDir}`, { stdio: 'inherit' });
+
+ const articlesJsonPath = path.join(repoPath, "articles.json");
+ if (!fs.existsSync(articlesJsonPath)) {
+ throw new Error(`articles.json not found in the cloned repository at ${articlesJsonPath}`);
+ }
+ console.log(`[INFO] Reading articles list from ${articlesJsonPath}...`);
+ const raw = fs.readFileSync(articlesJsonPath, "utf-8");
+ const data = JSON.parse(raw);
+
+ console.log("[INFO] Processing articles for sitemap...");
+ for (const article of data) {
+ const id = article.id;
+ if (!id || typeof id !== 'string' || !id.endsWith(".md")) {
+ console.warn(`[WARN] Skipping invalid or non-markdown entry: ${JSON.stringify(article)}`);
+ continue;
+ }
+
+ const slug = id.replace(/\.md$/, '');
+ const loc = `${BASE_URL.replace(/\/$/, '')}/${slug}`;
+
+ console.log(`[INFO] Processing ID: ${id} -> ${loc}`);
+ const filePathInRepo = path.join("articles", id);
+
+ try {
+ const command = `git log -1 --format="%cI" -- "${filePathInRepo}"`;
+ const lastmodRaw = execSync(command, {
+ encoding: "utf-8",
+ cwd: repoPath,
+ }).trim();
+
+ if (!lastmodRaw) {
+ console.warn(`[WARN] Lastmod is empty for ${id}. Skipping sitemap entry.`);
+ continue;
+ }
+ const lastmod = new Date(lastmodRaw).toISOString();
+ console.log(`[INFO] Lastmod: ${lastmod}`);
+
+ urlEntries.push(`
+
+ ${loc}
+ ${lastmod}
+ `);
+ } catch (error) {
+ const stderr = error.stderr?.toString('utf-8').trim();
+ console.error(`[ERROR] Error getting git log for ${id}: ${stderr || error.message}. Skipping sitemap entry.`);
+ }
+ }
+ console.log("[INFO] Finished processing articles.");
+
+ const sitemapXml = `
+${urlEntries.join('')}
+`;
+
+ console.log("\n--- Generated Sitemap ---");
+ console.log(sitemapXml);
+
+ try {
+ const outputFilePath = "public/sitemap.xml";
+ fs.writeFileSync(outputFilePath, sitemapXml, "utf-8");
+ console.log(`\n[INFO] Sitemap successfully written to ${outputFilePath}`);
+ } catch (writeError) {
+ console.error(`\n[ERROR] Failed to write sitemap to file: ${writeError.message}`);
+ }
+} catch (error) {
+ console.error("\n[ERROR] An overall error occurred:", error.message);
+} finally {
+ if (fs.existsSync(repoPath)) {
+ console.log(`[INFO] Cleaning up ${repoPath}...`);
+ try {
+ fs.rmSync(repoPath, { recursive: true, force: true });
+ console.log("[INFO] Cleanup successful.");
+ } catch (cleanupError) {
+ console.error(`[ERROR] Error during cleanup: ${cleanupError.message}`);
+ }
+ }
+}
\ No newline at end of file