diff --git a/index.html b/index.html
index a0d84537..03063c04 100644
--- a/index.html
+++ b/index.html
@@ -61,6 +61,24 @@
}
}
+
+
diff --git a/public/robots.txt b/public/robots.txt
index bc7dd244..aa3b2d12 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,6 +1,8 @@
User-agent: *
Disallow: /dashboard
Disallow: /shiftboard
+Disallow: /shifts
+Disallow: /welcome
Allow: /
Sitemap: https://shiftori.app/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 02ba08ea..c3e3d225 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -2,16 +2,19 @@
https://shiftori.app
+ 2026-04-16
monthly
1.0
https://shiftori.app/privacy
+ 2026-04-16
yearly
0.3
https://shiftori.app/terms
+ 2026-04-16
yearly
0.3
diff --git a/src/components/features/LandingPage/faqs.ts b/src/components/features/LandingPage/faqs.ts
new file mode 100644
index 00000000..e2d4df5c
--- /dev/null
+++ b/src/components/features/LandingPage/faqs.ts
@@ -0,0 +1,15 @@
+export type Faq = {
+ q: string;
+ a: string;
+};
+
+export const faqs: Faq[] = [
+ {
+ q: "料金はかかりますか?",
+ a: "無料です",
+ },
+ {
+ q: "スタッフがメールアドレスを持っていない場合は?",
+ a: "現在はメールアドレスが必須です 将来的にほかの方法にも対応予定です",
+ },
+];
diff --git a/src/components/features/LandingPage/index.tsx b/src/components/features/LandingPage/index.tsx
index 016b888c..9cb50deb 100644
--- a/src/components/features/LandingPage/index.tsx
+++ b/src/components/features/LandingPage/index.tsx
@@ -4,6 +4,7 @@ import { Link as RouterLink } from "@tanstack/react-router";
import type { ReactNode } from "react";
import type { IconType } from "react-icons";
import { LuCalendarCheck, LuLink, LuSend } from "react-icons/lu";
+import { type Faq, faqs } from "./faqs";
export const LandingPage = () => {
return (
@@ -160,22 +161,6 @@ const PointCard = ({ icon: Icon, title, body }: Point) => (
);
-type Faq = {
- q: string;
- a: string;
-};
-
-const faqs: Faq[] = [
- {
- q: "料金はかかりますか?",
- a: "無料です",
- },
- {
- q: "スタッフがメールアドレスを持っていない場合は?",
- a: "現在はメールアドレスが必須です 将来的にほかの方法にも対応予定です",
- },
-];
-
const FaqSection = () => (
diff --git a/src/helpers/seo/index.ts b/src/helpers/seo/index.ts
new file mode 100644
index 00000000..fc2f72da
--- /dev/null
+++ b/src/helpers/seo/index.ts
@@ -0,0 +1,64 @@
+/**
+ * SEO helpers for TanStack Router's `head` option.
+ *
+ * TanStack Router's public `meta` type is typed as HTML `` attributes,
+ * but its runtime also recognizes `{ title }` (renders ``) and
+ * `{ "script:ld+json": payload }` (renders a JSON-LD `` by TanStack Router.
+ */
+export const jsonLdMeta = (payload: Record): MetaList =>
+ [{ "script:ld+json": payload }] as unknown as MetaList;
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 9e8cdece..c3dd0304 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,4 +1,4 @@
-import { createRootRoute, Outlet, useRouterState } from "@tanstack/react-router";
+import { createRootRoute, HeadContent, Outlet, useRouterState } from "@tanstack/react-router";
import { useEffect } from "react";
import { Toaster } from "@/src/components/ui/toaster";
import { sendPageView } from "@/src/helpers/gtm";
@@ -16,6 +16,7 @@ const PageViewTracker = () => {
export const Route = createRootRoute({
component: () => (
<>
+
diff --git a/src/routes/_auth/dashboard.tsx b/src/routes/_auth/dashboard.tsx
index d207ddfd..70ec2963 100644
--- a/src/routes/_auth/dashboard.tsx
+++ b/src/routes/_auth/dashboard.tsx
@@ -5,8 +5,12 @@ import { api } from "@/convex/_generated/api";
import { DashboardContent } from "@/src/components/features/Dashboard/DashboardContent";
import { Animation } from "@/src/components/templates/Animation";
import { RootContentWrapper } from "@/src/components/templates/RootContentWrapper";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/_auth/dashboard")({
+ head: () => ({
+ meta: buildMeta({ title: "ダッシュボード", noindex: true }),
+ }),
component: DashboardPage,
});
diff --git a/src/routes/_auth/shiftboard.$recruitmentId.tsx b/src/routes/_auth/shiftboard.$recruitmentId.tsx
index 137d2fba..803ef64d 100644
--- a/src/routes/_auth/shiftboard.$recruitmentId.tsx
+++ b/src/routes/_auth/shiftboard.$recruitmentId.tsx
@@ -5,8 +5,12 @@ import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { ShiftBoardPage } from "@/src/components/features/ShiftBoard/ShiftBoardPage";
import { Animation } from "@/src/components/templates/Animation";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/_auth/shiftboard/$recruitmentId")({
+ head: () => ({
+ meta: buildMeta({ title: "シフト表", noindex: true }),
+ }),
component: ShiftBoardRoute,
});
diff --git a/src/routes/_unregistered/shifts.reissue.tsx b/src/routes/_unregistered/shifts.reissue.tsx
index 354ed390..a180038b 100644
--- a/src/routes/_unregistered/shifts.reissue.tsx
+++ b/src/routes/_unregistered/shifts.reissue.tsx
@@ -9,11 +9,15 @@ import { ReissueDone } from "@/src/components/features/StaffView/ReissueDone";
import { ReissueForm } from "@/src/components/features/StaffView/ReissueForm";
import { StaffLayout } from "@/src/components/templates/StaffLayout";
import { FullPageSpinner } from "@/src/components/ui/FullPageSpinner";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/_unregistered/shifts/reissue")({
validateSearch: (search: Record) => ({
recruitmentId: search.recruitmentId as string,
}),
+ head: () => ({
+ meta: buildMeta({ title: "シフト閲覧リンクの再発行", noindex: true }),
+ }),
component: ReissueRoute,
});
diff --git a/src/routes/_unregistered/shifts.submit.tsx b/src/routes/_unregistered/shifts.submit.tsx
index ea24e741..1ef794f2 100644
--- a/src/routes/_unregistered/shifts.submit.tsx
+++ b/src/routes/_unregistered/shifts.submit.tsx
@@ -8,12 +8,16 @@ import { ShiftSubmitPage } from "@/src/components/features/StaffSubmit/ShiftSubm
import { RateLimitedView } from "@/src/components/features/StaffView/RateLimitedView";
import { ErrorBoundary } from "@/src/components/ui/ErrorBoundary";
import { FullPageSpinner } from "@/src/components/ui/FullPageSpinner";
+import { buildMeta } from "@/src/helpers/seo";
import { useStaffSession } from "@/src/hooks/useStaffSession";
export const Route = createFileRoute("/_unregistered/shifts/submit")({
validateSearch: (search: Record) => ({
token: (search.token as string) || undefined,
}),
+ head: () => ({
+ meta: buildMeta({ title: "希望シフト提出", noindex: true }),
+ }),
component: ShiftSubmitRoute,
});
diff --git a/src/routes/_unregistered/shifts.view.tsx b/src/routes/_unregistered/shifts.view.tsx
index f0841ab5..be127b5c 100644
--- a/src/routes/_unregistered/shifts.view.tsx
+++ b/src/routes/_unregistered/shifts.view.tsx
@@ -8,12 +8,16 @@ import { ShiftViewPage } from "@/src/components/features/StaffView/ShiftViewPage
import { StaffLayout } from "@/src/components/templates/StaffLayout";
import { ErrorBoundary } from "@/src/components/ui/ErrorBoundary";
import { FullPageSpinner } from "@/src/components/ui/FullPageSpinner";
+import { buildMeta } from "@/src/helpers/seo";
import { useStaffSession } from "@/src/hooks/useStaffSession";
export const Route = createFileRoute("/_unregistered/shifts/view")({
validateSearch: (search: Record) => ({
token: (search.token as string) || undefined,
}),
+ head: () => ({
+ meta: buildMeta({ title: "シフト確認", noindex: true }),
+ }),
component: ShiftViewRoute,
});
diff --git a/src/routes/_unregistered/welcome.tsx b/src/routes/_unregistered/welcome.tsx
index 4a15d5d9..43aed9fd 100644
--- a/src/routes/_unregistered/welcome.tsx
+++ b/src/routes/_unregistered/welcome.tsx
@@ -1,7 +1,11 @@
import { Box, Text } from "@chakra-ui/react";
import { createFileRoute } from "@tanstack/react-router";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/_unregistered/welcome")({
+ head: () => ({
+ meta: buildMeta({ title: "Welcome", noindex: true }),
+ }),
component: RouteComponent,
});
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index a1899680..01aad0a2 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,8 +1,29 @@
import { useAuth } from "@clerk/clerk-react";
import { createFileRoute, Navigate } from "@tanstack/react-router";
import { LandingPage } from "@/src/components/features/LandingPage";
+import { faqs } from "@/src/components/features/LandingPage/faqs";
+import { buildMeta, jsonLdMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/")({
+ head: () => ({
+ meta: [
+ ...buildMeta({
+ title: "シフトリ",
+ description:
+ "少人数のお店のシフト作成をもっとラクに リンクを送るだけで希望シフトを収集 スタッフのアカウント登録も不要 無料ではじめられるシフト管理ツール",
+ canonical: "/",
+ }),
+ ...jsonLdMeta({
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: faqs.map((f) => ({
+ "@type": "Question",
+ name: f.q,
+ acceptedAnswer: { "@type": "Answer", text: f.a },
+ })),
+ }),
+ ],
+ }),
component: IndexPage,
});
diff --git a/src/routes/privacy.tsx b/src/routes/privacy.tsx
index 7eb1c256..3b64225a 100644
--- a/src/routes/privacy.tsx
+++ b/src/routes/privacy.tsx
@@ -1,6 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { PrivacyPolicy } from "@/src/components/features/PrivacyPolicy";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/privacy")({
+ head: () => ({
+ meta: buildMeta({
+ title: "プライバシーポリシー",
+ description: "シフトリのプライバシーポリシー",
+ canonical: "/privacy",
+ }),
+ }),
component: PrivacyPolicy,
});
diff --git a/src/routes/terms.tsx b/src/routes/terms.tsx
index 98f75ed9..c0b27dce 100644
--- a/src/routes/terms.tsx
+++ b/src/routes/terms.tsx
@@ -1,6 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { Terms } from "@/src/components/features/Terms";
+import { buildMeta } from "@/src/helpers/seo";
export const Route = createFileRoute("/terms")({
+ head: () => ({
+ meta: buildMeta({
+ title: "利用規約",
+ description: "シフトリの利用規約",
+ canonical: "/terms",
+ }),
+ }),
component: Terms,
});