Skip to content

Commit 0d7ee22

Browse files
Copilotna-trium-144
andcommitted
Add sections.yml script and new [lang]/[pageId] route
Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent 1c39ee8 commit 0d7ee22

73 files changed

Lines changed: 1066 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/[docs_id]/markdown.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export function Heading({
6464
children: ReactNode;
6565
}) {
6666
switch (level) {
67+
case 0:
68+
return null;
6769
case 1:
6870
return <h1 className="text-2xl font-bold my-4">{children}</h1>;
6971
case 2:

app/[docs_id]/pageContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function PageContent(props: PageContentProps) {
2929
props.splitMdContent.map((section, i) => ({
3030
...section,
3131
inView: false,
32-
sectionId: `${props.docs_id}-${i}`,
32+
sectionId: section.id || `${props.docs_id}-${i}`,
3333
}))
3434
);
3535

@@ -38,7 +38,7 @@ export function PageContent(props: PageContentProps) {
3838
const newContent = props.splitMdContent.map((section, i) => ({
3939
...section,
4040
inView: false,
41-
sectionId: `${props.docs_id}-${i}`,
41+
sectionId: section.id || `${props.docs_id}-${i}`,
4242
}));
4343
setDynamicMdContent(newContent);
4444
setSidebarMdContent(props.docs_id, newContent);

app/[docs_id]/splitMarkdown.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import remarkParse from "remark-parse";
33
import remarkGfm from "remark-gfm";
44

55
export interface MarkdownSection {
6+
id: string;
67
level: number;
78
title: string;
89
content: string;
@@ -31,6 +32,7 @@ export function splitMarkdown(content: string): MarkdownSection[] {
3132
}
3233
}
3334
sections.push({
35+
id: "",
3436
title: splitContent[startLine - 1].replace(/#+\s*/, "").trim(),
3537
content: splitContent
3638
.slice(startLine - 1 + 1, endLine ? endLine - 1 : undefined)

app/[lang]/[pageId]/page.tsx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Metadata } from "next";
2+
import { notFound } from "next/navigation";
3+
import { getCloudflareContext } from "@opennextjs/cloudflare";
4+
import { readFile } from "node:fs/promises";
5+
import { join } from "node:path";
6+
import { MarkdownSection } from "../[docs_id]/splitMarkdown";
7+
import { PageContent } from "../[docs_id]/pageContent";
8+
import { ChatHistoryProvider } from "../[docs_id]/chatHistory";
9+
import { getChatFromCache, initContext } from "@/lib/chatHistory";
10+
import { pagesList } from "@/pagesList";
11+
import { isCloudflare } from "@/lib/detectCloudflare";
12+
13+
async function readDocFile(
14+
lang: string,
15+
pageId: string,
16+
filename: string
17+
): Promise<string> {
18+
try {
19+
if (isCloudflare()) {
20+
const cfAssets = getCloudflareContext().env.ASSETS;
21+
const res = await cfAssets!.fetch(
22+
`https://assets.local/docs/${lang}/${pageId}/${filename}`
23+
);
24+
if (!res.ok) notFound();
25+
return await res.text();
26+
} else {
27+
return await readFile(
28+
join(process.cwd(), "public", "docs", lang, pageId, filename),
29+
"utf-8"
30+
);
31+
}
32+
} catch {
33+
notFound();
34+
}
35+
}
36+
37+
/**
38+
* YAMLフロントマターをパースしてid, title, level, bodyを返す。
39+
* フロントマターがない場合はid/titleを空文字、levelを0で返す。
40+
*/
41+
function parseFrontmatter(content: string): {
42+
id: string;
43+
title: string;
44+
level: number;
45+
body: string;
46+
} {
47+
if (!content.startsWith("---\n")) {
48+
return { id: "", title: "", level: 0, body: content };
49+
}
50+
const endIdx = content.indexOf("\n---\n", 4);
51+
if (endIdx === -1) {
52+
return { id: "", title: "", level: 0, body: content };
53+
}
54+
const fm = content.slice(4, endIdx);
55+
const body = content.slice(endIdx + 5);
56+
57+
const id = fm.match(/^id:\s*(.+)$/m)?.[1]?.trim() ?? "";
58+
let title = fm.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? "";
59+
// YAMLクォートを除去
60+
if (
61+
(title.startsWith("'") && title.endsWith("'")) ||
62+
(title.startsWith('"') && title.endsWith('"'))
63+
) {
64+
title = title.slice(1, -1);
65+
}
66+
const level = parseInt(fm.match(/^level:\s*(\d+)$/m)?.[1] ?? "2");
67+
return { id, title, level, body };
68+
}
69+
70+
/**
71+
* public/docs/{lang}/{pageId}/ 以下のmdファイルを結合して MarkdownSection[] を返す。
72+
*/
73+
async function getMarkdownSections(
74+
lang: string,
75+
pageId: string,
76+
pageTitle: string
77+
): Promise<MarkdownSection[]> {
78+
const sectionsYml = await readDocFile(lang, pageId, "sections.yml");
79+
const files = sectionsYml
80+
.split("\n")
81+
.filter((l) => l.trim().startsWith("- "))
82+
.map((l) => l.trim().slice(2).trim())
83+
.filter(Boolean);
84+
85+
const sections: MarkdownSection[] = [];
86+
for (const file of files) {
87+
const raw = await readDocFile(lang, pageId, file);
88+
if (file === "-intro.md") {
89+
// イントロセクションはフロントマターなし・見出しなし
90+
sections.push({
91+
id: `${lang}-${pageId}-intro`,
92+
level: 1,
93+
title: pageTitle,
94+
content: raw.trim(),
95+
rawContent: raw.trim(),
96+
});
97+
} else {
98+
const { id, title, level, body } = parseFrontmatter(raw);
99+
// bodyには見出し行が含まれるので、contentとしては見出しを除いた本文のみを渡す
100+
const content = body.replace(/^#{1,6} [^\n]*\n?/, "").trim();
101+
sections.push({
102+
id,
103+
level,
104+
title,
105+
content,
106+
rawContent: body.trim(),
107+
});
108+
}
109+
}
110+
return sections;
111+
}
112+
113+
export async function generateMetadata({
114+
params,
115+
}: {
116+
params: Promise<{ lang: string; pageId: string }>;
117+
}): Promise<Metadata> {
118+
const { lang, pageId } = await params;
119+
const langEntry = pagesList.find((l) => l.id === lang);
120+
const pageEntry = langEntry?.pages.find((p) => p.slug === pageId);
121+
if (!langEntry || !pageEntry) notFound();
122+
123+
const pageIndex = langEntry!.pages.findIndex((p) => p.slug === pageId);
124+
// pageIndex will be >= 0 since pageEntry was found via the same pages array
125+
return {
126+
title: `${langEntry!.lang}-${pageIndex + 1}. ${pageEntry!.title}`,
127+
};
128+
}
129+
130+
export default async function Page({
131+
params,
132+
}: {
133+
params: Promise<{ lang: string; pageId: string }>;
134+
}) {
135+
const { lang, pageId } = await params;
136+
137+
const langEntry = pagesList.find((l) => l.id === lang);
138+
const pageEntry = langEntry?.pages.find((p) => p.slug === pageId);
139+
if (!langEntry || !pageEntry) notFound();
140+
141+
const docsId = `${lang}/${pageId}`;
142+
const sections = await getMarkdownSections(lang, pageId, pageEntry!.title);
143+
144+
// AI用のドキュメント全文(見出し付きで結合)
145+
const documentContent = sections
146+
.map((s) =>
147+
s.level > 0 ? `${"#".repeat(s.level)} ${s.title}\n\n${s.content}` : s.content
148+
)
149+
.join("\n\n");
150+
151+
const context = await initContext();
152+
const initialChatHistories = await getChatFromCache(docsId, context);
153+
154+
return (
155+
<ChatHistoryProvider
156+
initialChatHistories={initialChatHistories}
157+
docs_id={docsId}
158+
>
159+
<PageContent
160+
documentContent={documentContent}
161+
splitMdContent={sections}
162+
docs_id={docsId}
163+
/>
164+
</ChatHistoryProvider>
165+
);
166+
}

app/navbar.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ function PageTitle() {
1414
}
1515

1616
const currentDocsId = pathname.replace(/^\//, "");
17-
const currentGroup = pagesList.find((group) => currentDocsId.startsWith(group.id));
18-
const currentPage = currentGroup?.pages.find((page) => `${currentGroup.id}-${page.id}` === currentDocsId);
17+
const currentGroup = pagesList.find((group) => currentDocsId.startsWith(`${group.id}/`));
18+
const pageIndex = currentGroup?.pages.findIndex((page) => `${currentGroup.id}/${page.slug}` === currentDocsId) ?? -1;
19+
const currentPage = pageIndex >= 0 ? currentGroup?.pages[pageIndex] : undefined;
1920
if(currentPage){
2021
return <>
21-
<span className="text-base mr-2">{currentGroup?.lang}-{currentPage.id}.</span>
22+
<span className="text-base mr-2">{currentGroup?.lang}-{pageIndex + 1}.</span>
2223
<span>{currentPage.title}</span>
2324
</>
2425
}

app/pagesList.ts

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,108 +6,109 @@ export const pagesList = [
66
// TODO: これをいい感じの文章に変える↓
77
description: "Pythonの基礎から応用までを学べるチュートリアル",
88
pages: [
9-
{ id: 1, title: "環境構築と基本思想" },
10-
{ id: 2, title: "基本構文とデータ型" },
11-
{ id: 3, title: "リスト、タプル、辞書、セット" },
12-
{ id: 4, title: "制御構文と関数" },
13-
{ id: 5, title: "モジュールとパッケージ" },
14-
{ id: 6, title: "オブジェクト指向プログラミング" },
9+
{ id: 1, slug: "0-intro", title: "環境構築と基本思想" },
10+
{ id: 2, slug: "1-basics", title: "基本構文とデータ型" },
11+
{ id: 3, slug: "2-collections", title: "リスト、タプル、辞書、セット" },
12+
{ id: 4, slug: "3-control-functions", title: "制御構文と関数" },
13+
{ id: 5, slug: "4-modules", title: "モジュールとパッケージ" },
14+
{ id: 6, slug: "5-oop", title: "オブジェクト指向プログラミング" },
1515
{
1616
id: 7,
17+
slug: "6-file-io",
1718
title: "ファイルの入出力とコンテキストマネージャ",
1819
},
19-
{ id: 8, title: "例外処理" },
20-
{ id: 9, title: "ジェネレータとデコレータ" },
20+
{ id: 8, slug: "7-exceptions", title: "例外処理" },
21+
{ id: 9, slug: "8-generators-decorators", title: "ジェネレータとデコレータ" },
2122
],
2223
},
2324
{
2425
id: "ruby",
2526
lang: "Ruby",
2627
description: "hoge",
2728
pages: [
28-
{ id: 1, title: "rubyの世界へようこそ" },
29-
{ id: 2, title: "基本構文とデータ型" },
30-
{ id: 3, title: "制御構造とメソッド定義" },
31-
{ id: 4, title: "すべてがオブジェクト" },
32-
{ id: 5, title: "コレクション (Array, Hash, Range)" },
33-
{ id: 6, title: "ブロックとイテレータ" },
34-
{ id: 7, title: "クラスとオブジェクト" },
35-
{ id: 8, title: "モジュールとMix-in" },
36-
{ id: 9, title: "Proc, Lambda, クロージャ" },
37-
{ id: 10, title: "標準ライブラリの活用" },
38-
{ id: 11, title: "テスト文化入門" },
39-
{ id: 12, title: "メタプログラミング入門" },
29+
{ id: 1, slug: "0-intro", title: "rubyの世界へようこそ" },
30+
{ id: 2, slug: "1-basics", title: "基本構文とデータ型" },
31+
{ id: 3, slug: "2-control-methods", title: "制御構造とメソッド定義" },
32+
{ id: 4, slug: "3-everything-object", title: "すべてがオブジェクト" },
33+
{ id: 5, slug: "4-collections", title: "コレクション (Array, Hash, Range)" },
34+
{ id: 6, slug: "5-blocks-iterators", title: "ブロックとイテレータ" },
35+
{ id: 7, slug: "6-classes", title: "クラスとオブジェクト" },
36+
{ id: 8, slug: "7-modules", title: "モジュールとMix-in" },
37+
{ id: 9, slug: "8-proc-lambda", title: "Proc, Lambda, クロージャ" },
38+
{ id: 10, slug: "9-stdlib", title: "標準ライブラリの活用" },
39+
{ id: 11, slug: "10-testing", title: "テスト文化入門" },
40+
{ id: 12, slug: "11-metaprogramming", title: "メタプログラミング入門" },
4041
],
4142
},
4243
{
4344
id: "javascript",
4445
lang: "JavaScript",
4546
description: "hoge",
4647
pages: [
47-
{ id: 1, title: "JavaScriptへようこそ" },
48-
{ id: 2, title: "基本構文とデータ型" },
49-
{ id: 3, title: "制御構文" },
50-
{ id: 4, title: "関数とクロージャ" },
51-
{ id: 5, title: "'this'の正体" },
52-
{ id: 6, title: "オブジェクトとプロトタイプ" },
53-
{ id: 7, title: "クラス構文" },
54-
{ id: 8, title: "配列とイテレーション" },
55-
{ id: 9, title: "非同期処理①: Promise" },
56-
{ id: 10, title: "非同期処理②: Async/Await" },
48+
{ id: 1, slug: "0-intro", title: "JavaScriptへようこそ" },
49+
{ id: 2, slug: "1-basics", title: "基本構文とデータ型" },
50+
{ id: 3, slug: "2-control", title: "制御構文" },
51+
{ id: 4, slug: "3-functions-closures", title: "関数とクロージャ" },
52+
{ id: 5, slug: "4-this", title: "'this'の正体" },
53+
{ id: 6, slug: "5-objects-prototype", title: "オブジェクトとプロトタイプ" },
54+
{ id: 7, slug: "6-classes", title: "クラス構文" },
55+
{ id: 8, slug: "7-arrays", title: "配列とイテレーション" },
56+
{ id: 9, slug: "8-promise", title: "非同期処理①: Promise" },
57+
{ id: 10, slug: "9-async-await", title: "非同期処理②: Async/Await" },
5758
],
5859
},
5960
{
6061
id: "typescript",
6162
lang: "TypeScript",
6263
description: "にゃー",
6364
pages: [
64-
{ id: 1, title: "TypeScriptへようこそ" },
65-
{ id: 2, title: "基本的な型と型推論" },
66-
{ id: 3, title: "オブジェクト、インターフェース、型エイリアス" },
67-
{ id: 4, title: "関数の型定義" },
68-
{ id: 5, title: "型を組み合わせる" },
69-
{ id: 6, title: "ジェネリクス" },
70-
{ id: 7, title: "クラスとアクセス修飾子" },
71-
{ id: 8, title: "非同期処理とユーティリティ型" },
65+
{ id: 1, slug: "0-intro", title: "TypeScriptへようこそ" },
66+
{ id: 2, slug: "1-types", title: "基本的な型と型推論" },
67+
{ id: 3, slug: "2-objects-interfaces", title: "オブジェクト、インターフェース、型エイリアス" },
68+
{ id: 4, slug: "3-function-types", title: "関数の型定義" },
69+
{ id: 5, slug: "4-combining-types", title: "型を組み合わせる" },
70+
{ id: 6, slug: "5-generics", title: "ジェネリクス" },
71+
{ id: 7, slug: "6-classes", title: "クラスとアクセス修飾子" },
72+
{ id: 8, slug: "7-async-utilities", title: "非同期処理とユーティリティ型" },
7273
],
7374
},
7475
{
7576
id: "cpp",
7677
lang: "C++",
7778
description: "C++の基本から高度な機能までを学べるチュートリアル",
7879
pages: [
79-
{ id: 1, title: "C++の世界へようこそ" },
80-
{ id: 2, title: "型システムと制御構造" },
81-
{ id: 3, title: "データ集合とモダンな操作" },
82-
{ id: 4, title: "ポインタとメモリ管理" },
83-
{ id: 5, title: "関数と参照渡し" },
84-
{ id: 6, title: "プロジェクトの分割とビルド" },
85-
{ id: 7, title: "クラスの基礎" },
86-
{ id: 8, title: "クラスを使いこなす" },
87-
{ id: 9, title: "継承とポリモーフィズム" },
88-
{ id: 10, title: "テンプレート" },
89-
{ id: 11, title: "STL ①:コンテナ" },
90-
{ id: 12, title: "STL ②:アルゴリズムとラムダ式" },
91-
{ id: 13, title: "RAIIとスマートポインタ" },
80+
{ id: 1, slug: "0-intro", title: "C++の世界へようこそ" },
81+
{ id: 2, slug: "1-types-control", title: "型システムと制御構造" },
82+
{ id: 3, slug: "2-data-containers", title: "データ集合とモダンな操作" },
83+
{ id: 4, slug: "3-pointers", title: "ポインタとメモリ管理" },
84+
{ id: 5, slug: "4-functions", title: "関数と参照渡し" },
85+
{ id: 6, slug: "5-project-build", title: "プロジェクトの分割とビルド" },
86+
{ id: 7, slug: "6-classes-basics", title: "クラスの基礎" },
87+
{ id: 8, slug: "7-classes-advanced", title: "クラスを使いこなす" },
88+
{ id: 9, slug: "8-inheritance", title: "継承とポリモーフィズム" },
89+
{ id: 10, slug: "9-templates", title: "テンプレート" },
90+
{ id: 11, slug: "10-stl-containers", title: "STL ①:コンテナ" },
91+
{ id: 12, slug: "11-stl-algorithms", title: "STL ②:アルゴリズムとラムダ式" },
92+
{ id: 13, slug: "12-raii-smart-ptrs", title: "RAIIとスマートポインタ" },
9293
],
9394
},
9495
{
9596
id: "rust",
9697
lang: "Rust",
9798
description: "a",
9899
pages: [
99-
{ id: 1, title: "Rustの世界へようこそ" },
100-
{ id: 2, title: "基本構文と「不変性」" },
101-
{ id: 3, title: "関数と制御フロー" },
102-
{ id: 4, title: "所有権" },
103-
{ id: 5, title: "借用とスライス" },
104-
{ id: 6, title: "構造体とメソッド構文" },
105-
{ id: 7, title: "列挙型とパターンマッチ" },
106-
{ id: 8, title: "モジュールシステムとパッケージ管理" },
107-
{ id: 9, title: "コレクションと文字列" },
108-
{ id: 10, title: "エラーハンドリング" },
109-
{ id: 11, title: "ジェネリクスとトレイト" },
110-
{ id: 12, title: "ライフタイム" },
100+
{ id: 1, slug: "0-intro", title: "Rustの世界へようこそ" },
101+
{ id: 2, slug: "1-basics", title: "基本構文と「不変性」" },
102+
{ id: 3, slug: "2-functions-control", title: "関数と制御フロー" },
103+
{ id: 4, slug: "3-ownership", title: "所有権" },
104+
{ id: 5, slug: "4-borrowing-slices", title: "借用とスライス" },
105+
{ id: 6, slug: "5-structs-methods", title: "構造体とメソッド構文" },
106+
{ id: 7, slug: "6-enums-pattern", title: "列挙型とパターンマッチ" },
107+
{ id: 8, slug: "7-modules", title: "モジュールシステムとパッケージ管理" },
108+
{ id: 9, slug: "8-collections-strings", title: "コレクションと文字列" },
109+
{ id: 10, slug: "9-error-handling", title: "エラーハンドリング" },
110+
{ id: 11, slug: "10-generics-traits", title: "ジェネリクスとトレイト" },
111+
{ id: 12, slug: "11-lifetimes", title: "ライフタイム" },
111112
],
112113
},
113114
] as const;

0 commit comments

Comments
 (0)