Skip to content
Merged

main #328

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "シフトリ",
"url": "https://shiftori.app",
"logo": "https://shiftori.app/logo512.png"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "シフトリ",
"url": "https://shiftori.app",
"inLanguage": "ja-JP"
}
</script>
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 2 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
User-agent: *
Disallow: /dashboard
Disallow: /shiftboard
Disallow: /shifts
Disallow: /welcome
Allow: /

Sitemap: https://shiftori.app/sitemap.xml
3 changes: 3 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://shiftori.app</loc>
<lastmod>2026-04-16</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://shiftori.app/privacy</loc>
<lastmod>2026-04-16</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://shiftori.app/terms</loc>
<lastmod>2026-04-16</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
Expand Down
15 changes: 15 additions & 0 deletions src/components/features/LandingPage/faqs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type Faq = {
q: string;
a: string;
};

export const faqs: Faq[] = [
{
q: "料金はかかりますか?",
a: "無料です",
},
{
q: "スタッフがメールアドレスを持っていない場合は?",
a: "現在はメールアドレスが必須です 将来的にほかの方法にも対応予定です",
},
];
17 changes: 1 addition & 16 deletions src/components/features/LandingPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -160,22 +161,6 @@ const PointCard = ({ icon: Icon, title, body }: Point) => (
</VStack>
);

type Faq = {
q: string;
a: string;
};

const faqs: Faq[] = [
{
q: "料金はかかりますか?",
a: "無料です",
},
{
q: "スタッフがメールアドレスを持っていない場合は?",
a: "現在はメールアドレスが必須です 将来的にほかの方法にも対応予定です",
},
];

const FaqSection = () => (
<Box bg="gray.50" px={{ base: 4, lg: 12 }} py={{ base: 12, lg: 24 }}>
<VStack mx="auto" w="full" maxW="768px" gap={{ base: 6, lg: 8 }} align="stretch">
Expand Down
64 changes: 64 additions & 0 deletions src/helpers/seo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* SEO helpers for TanStack Router's `head` option.
*
* TanStack Router's public `meta` type is typed as HTML `<meta>` attributes,
* but its runtime also recognizes `{ title }` (renders `<title>`) and
* `{ "script:ld+json": payload }` (renders a JSON-LD `<script>`). The entries
* here produce runtime-valid tags; the final array is cast so the `head`
* option accepts it.
* @link https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management
*/
import type { JSX } from "react";

const SITE_NAME = "シフトリ";
const SITE_URL = "https://shiftori.app";

type MetaList = NonNullable<JSX.IntrinsicElements["meta"]>[];

type MetaEntry =
| { title: string }
| { name: string; content: string }
| { property: string; content: string }
| { "script:ld+json": Record<string, unknown> };

type BuildMetaOptions = {
title: string;
description?: string;
/** Set true for pages that should not be indexed (magic-link pages, authed pages). */
noindex?: boolean;
/** Canonical path (e.g. "/terms"). Adds `og:url`. */
canonical?: string;
};

/**
* Build route meta tags for TanStack Router's `head` option.
* - Appends the site name to the title (except when the title is the site name itself)
* - Mirrors description to `og:description`
* - Adds `robots: noindex, nofollow` when `noindex` is set
*/
export const buildMeta = ({ title, description, noindex, canonical }: BuildMetaOptions): MetaList => {
const fullTitle = title === SITE_NAME ? title : `${title} | ${SITE_NAME}`;
const entries: MetaEntry[] = [{ title: fullTitle }, { property: "og:title", content: fullTitle }];

if (description) {
entries.push({ name: "description", content: description });
entries.push({ property: "og:description", content: description });
}

if (canonical) {
entries.push({ property: "og:url", content: `${SITE_URL}${canonical}` });
}

if (noindex) {
entries.push({ name: "robots", content: "noindex, nofollow" });
}

return entries as unknown as MetaList;
};

/**
* Wrap a JSON-LD payload as a `head.meta` entry. Rendered as
* `<script type="application/ld+json">...</script>` by TanStack Router.
*/
export const jsonLdMeta = (payload: Record<string, unknown>): MetaList =>
[{ "script:ld+json": payload }] as unknown as MetaList;
3 changes: 2 additions & 1 deletion src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,6 +16,7 @@ const PageViewTracker = () => {
export const Route = createRootRoute({
component: () => (
<>
<HeadContent />
<PageViewTracker />
<Outlet />
<Toaster />
Expand Down
4 changes: 4 additions & 0 deletions src/routes/_auth/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
4 changes: 4 additions & 0 deletions src/routes/_auth/shiftboard.$recruitmentId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
4 changes: 4 additions & 0 deletions src/routes/_unregistered/shifts.reissue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => ({
recruitmentId: search.recruitmentId as string,
}),
head: () => ({
meta: buildMeta({ title: "シフト閲覧リンクの再発行", noindex: true }),
}),
component: ReissueRoute,
});

Expand Down
4 changes: 4 additions & 0 deletions src/routes/_unregistered/shifts.submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => ({
token: (search.token as string) || undefined,
}),
head: () => ({
meta: buildMeta({ title: "希望シフト提出", noindex: true }),
}),
component: ShiftSubmitRoute,
});

Expand Down
4 changes: 4 additions & 0 deletions src/routes/_unregistered/shifts.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => ({
token: (search.token as string) || undefined,
}),
head: () => ({
meta: buildMeta({ title: "シフト確認", noindex: true }),
}),
component: ShiftViewRoute,
});

Expand Down
4 changes: 4 additions & 0 deletions src/routes/_unregistered/welcome.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
21 changes: 21 additions & 0 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
8 changes: 8 additions & 0 deletions src/routes/privacy.tsx
Original file line number Diff line number Diff line change
@@ -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,
});
8 changes: 8 additions & 0 deletions src/routes/terms.tsx
Original file line number Diff line number Diff line change
@@ -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,
});
Loading