diff --git a/src/components/pages/Sitemap.astro b/src/components/pages/Sitemap.astro index db75c63cd4..a7cb130d87 100644 --- a/src/components/pages/Sitemap.astro +++ b/src/components/pages/Sitemap.astro @@ -1,90 +1,157 @@ --- -import { - sitemapSectionsJa as ja, - sitemapSectionsEn as en, - type SitemapSection, -} from "@data/sitemapSections"; +import en from "@data/nav/en.yml"; +import ja from "@data/nav/ja.yml"; import { basename } from "path"; -import type { AstroInstance, MDXInstance, MarkdownInstance } from "astro"; +import type { MDXInstance, MarkdownInstance } from "astro"; import Slugger from "github-slugger"; -import Layout, { Frontmatter } from "@layouts/Layout.astro"; +import Layout, { type Frontmatter } from "@layouts/Layout.astro"; import type { Lang } from "@components/types"; +import type { Navigation } from "@data/schemas/nav"; +import Markdown from "@components/utils/Markdown.astro"; +import { unified } from "unified"; +import remarkParse from "remark-parse"; +import { toHast } from "mdast-util-to-hast"; +import { toText } from "hast-util-to-text"; interface Props { lang: Lang; } -interface AstroInstanceExt extends AstroInstance { - title: string; - sitemap?: boolean; -} - -type PageInstance = - | MarkdownInstance - | MDXInstance - | AstroInstanceExt; +const { lang } = Astro.props; +const navigations: Navigation[] = { ja, en }[lang]; -interface Page { - title: string; - url: string; +interface SectionNode { + name: string; + prefixes: string[]; + children: SectionNode[]; } -interface Section extends SitemapSection { - slug: string; - links: Page[]; +const parser = unified().use(remarkParse); +function text(content: string) { + const mdast = parser.parse(content); + const hast = toHast(mdast); + return toText(hast); } -type HeadingType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; +function findChild(node: SectionNode, prefix: string): SectionNode | undefined { + if (node.prefixes.includes(prefix)) return node; + for (const child of node.children) { + const found = findChild(child, prefix); + if (found) return found; + } +} -const { lang } = Astro.props; +const root: SectionNode = { + name: "", + prefixes: [], + children: [], +}; +for (const nav of navigations) { + let parent = root; + if (nav.sitemap?.section) { + parent = { + name: text(nav.name), + prefixes: [], + children: [], + }; + root.children.push(parent); + } + for (const content of nav.contents) { + if (!nav.sitemap && !content.sitemap) continue; + const sitemap = { ...nav.sitemap, ...content.sitemap }; -let pages = (await Astro.glob("/src/pages/**/*.{astro,md,mdx}")) - .filter((page): page is PageInstance & { url: string } => { - if (!page.url) return false; - if (basename(page.url).startsWith("_")) return false; - if ("frontmatter" in page) { - const { layout, sitemap, redirect_to } = page.frontmatter; - if (layout === false || sitemap === false || redirect_to) return false; + let parentWithOverride = parent; + if (sitemap.parent) { + const overridingParent = findChild(root, sitemap.parent); + if (!overridingParent) { + throw new Error(`Parent not found: ${sitemap.parent}`); + } + parentWithOverride = overridingParent; + } + if (sitemap.section) { + const section = { + name: text(content.name), + prefixes: [content.url], + children: [], + }; + parentWithOverride.children.push(section); } else { - const { sitemap } = page; - if (sitemap === false) return false; + parentWithOverride.prefixes.push(content.url); } - return page.url.startsWith("/en/") === (lang === "en"); - }) - .map((page) => { - const { title } = "frontmatter" in page ? page.frontmatter : page; - return { title, url: page.url }; - }); + } +} +root.children.push({ + name: { ja: "その他", en: "Others" }[lang], + prefixes: [{ ja: "/", en: "/en/" }[lang]], + children: [], +}); + +type Depth = 1 | 2 | 3 | 4 | 5 | 6; +function nextDepth(depth: Depth) { + if (depth >= 6) throw new Error("Depth too deep"); + return (depth + 1) as Depth; +} +interface Entry { + title: string; + url: string; +} +interface Section { + name: string; + slug: string; + depth: Depth; + prefixes: string[]; + entries: Entry[]; +} -// https://stackoverflow.com/questions/979256/sorting-an-array-of-objects-by-property-values#comment48111034_979289 -// @ts-ignore -pages.sort((a, b) => (a.url > b.url) - (a.url < b.url)); +function resolveSections( + node: SectionNode, + slugger = new Slugger(), + depth: Depth = 1, +) { + const sections: Section[] = [ + { + name: node.name, + slug: slugger.slug(node.name), + depth, + prefixes: node.prefixes, + entries: [], + }, + ]; + for (const child of node.children) { + const childSections = resolveSections(child, slugger, nextDepth(depth)); + sections.push(...childSections); + } + return sections; +} +const sections = resolveSections(root); +sections.shift(); // remove root -const slugger = new Slugger(); +type Page = MarkdownInstance | MDXInstance; +const pages = await Astro.glob("/src/pages/**/*.{md,mdx}"); -const sections = { ja, en }[lang].map
((section) => { - let links: Page[] = []; - section.patterns?.forEach((pattern) => { - pages = pages.filter((page) => { - let cond = false; - cond ||= pattern.test(page.url); - if (section.negativePatterns) { - cond &&= !section.negativePatterns.some((pattern) => - pattern.test(page.url) - ); - } - if (cond) { - links.push(page); - } - return !cond; - }); - }); - return { - ...section, - slug: slugger.slug(section.name), - links, - }; -}); +const indexSectionByPrefixes = sections.flatMap((section) => + section.prefixes.map((prefix) => ({ prefix, section })), +); +// longest prefix first +indexSectionByPrefixes.sort((a, b) => b.prefix.length - a.prefix.length); +for (const page of pages) { + if (!page.url) continue; + if (basename(page.url).startsWith("_")) continue; + const { layout, sitemap, redirect_to } = page.frontmatter; + if (layout === false || sitemap === false || redirect_to) continue; + if (page.url.startsWith("/en/") !== (lang === "en")) continue; + + const { section } = indexSectionByPrefixes.find( + (item) => + page.url!.startsWith(item.prefix) || page.url + "/" === item.prefix, + )!; // the "Others" section should catch + section.entries.push({ title: page.frontmatter.title, url: page.url }); +} +for (const section of sections) { + // https://stackoverflow.com/questions/979256/sorting-an-array-of-objects-by-property-values#comment48111034_979289 + // @ts-ignore + section.entries.sort((a, b) => (a.url > b.url) - (a.url < b.url)); +} const meta = { file: `src/pages${{ en: "/en", ja: "" }[lang]}/sitemap.astro`, @@ -102,7 +169,7 @@ const meta = { ...meta, }} headings={sections.map((section) => ({ - text: section.name, + text: text(section.name), slug: section.slug, depth: section.depth, }))} @@ -110,15 +177,17 @@ const meta = { > { sections.map((section) => { - const Heading: HeadingType = `h${section.depth}`; + const Heading = `h${section.depth}` as const; return ( <> - {section.name} + + +
    - {section.links.map((link) => { + {section.entries.map(({ url, title }) => { return (
  • - {link.title} + {title}
  • ); })} diff --git a/src/data/nav/en.yml b/src/data/nav/en.yml index e7f14c381a..87ab88966b 100644 --- a/src/data/nav/en.yml +++ b/src/data/nav/en.yml @@ -6,9 +6,13 @@ - name: "For New Students" url: "/en/oc/" + sitemap: + section: true - name: "For Faculty Members" url: "/en/faculty_members/" + sitemap: + section: true - name: "For Staff Members" url: "/en/staff_members/" @@ -20,6 +24,8 @@ url: "/en/support/" - name: "ICT Systems at UTokyo" + sitemap: + section: true contents: - name: "UTokyo Account" @@ -57,9 +63,13 @@ - name: "UTokyo Azure" url: "/en/research_computing/utokyo_azure/" + sitemap: + parent: "/en/research_computing/" - name: "Full List" url: "/en/systems/" + sitemap: + section: false # - # name: "List of all systems" # url: "/en/systems/" @@ -69,6 +79,8 @@ - name: "Effective Use of Online Resources" url: "/en/online/" + sitemap: + section: true - name: "Search Online Resources by Tool" url: "/en/online/tools" @@ -78,12 +90,21 @@ - name: "Copyright Handling on Creating Materials" url: "/en/articles/copyright/" + - + name: "Articles" + url: "/en/articles/" + hidden: true + sitemap: + section: false + parent: "/en/online/" - name: "Guides / Events" contents: - name: "Notice" url: "/en/notice/" + sitemap: + section: true - name: "Information Security Portal Site" url: "https://univtokyo.sharepoint.com/sites/Security/SitePages/en/Home.aspx" @@ -99,6 +120,8 @@ - name: "Orientations / Seminars" url: "/en/events/" + sitemap: + section: true # - # name: "Community of Online Education Practices" # url: "/en/events/2020-luncheon/" diff --git a/src/data/nav/ja.yml b/src/data/nav/ja.yml index b57bdc0685..8074455770 100644 --- a/src/data/nav/ja.yml +++ b/src/data/nav/ja.yml @@ -6,9 +6,13 @@ - name: "大学生活に必要な情報システムの準備について(新入生向け)" url: "/oc/" + sitemap: + section: true - name: "東京大学における情報システムの準備について(教員向け)" url: "/faculty_members/" + sitemap: + section: true - name: "東京大学における情報システムの準備について(職員向け)" url: "/staff_members/" @@ -20,6 +24,8 @@ url: "/support/" - name: "東京大学のシステム" + sitemap: + section: true contents: - name: "UTokyo Account" @@ -56,16 +62,22 @@ url: "/research_computing/" - name: "UTokyo Azure" - url: "/research_computing/utokyo_azure/" + url: "/research_computing/utokyo_azure/" + sitemap: + parent: "/research_computing/" - name: "一覧" url: "/systems/" + sitemap: + section: false - name: "オンラインの活用" contents: - name: "オンラインを活用するために" url: "/online/" + sitemap: + section: true - name: "使えるツールから探す" url: "/online/tools" @@ -81,12 +93,28 @@ - name: "資料作成における著作権" url: "/articles/copyright/" + - + name: "グッドプラクティスの共有" + url: "/good-practice/" + hidden: true + sitemap: + section: true + parent: "/online/" + - + name: 記事 + url: "/articles/" + hidden: true + sitemap: + section: false + parent: "/online/" - name: "各種案内・イベント等" contents: - name: "お知らせ" url: "/notice/" + sitemap: + section: true - name: "情報セキュリティポータルサイト" url: "https://univtokyo.sharepoint.com/sites/Security/" @@ -102,9 +130,14 @@ - name: "説明会等" url: "/events/" + sitemap: + section: true - name: "オンライン授業情報交換会" url: "/events/luncheon/" + sitemap: + section: true + parent: "/events/" - name: "サポート" contents: diff --git a/src/data/schemas/nav.d.ts b/src/data/schemas/nav.d.ts index b18e09e6e8..ade3459860 100644 --- a/src/data/schemas/nav.d.ts +++ b/src/data/schemas/nav.d.ts @@ -1,7 +1,16 @@ export interface Navigation { name: string; + sitemap?: { + section: boolean; + parent?: never; + }; contents: { name: string; url: string; + hidden?: boolean; + sitemap?: { + section?: boolean; + parent?: string; + }; }[]; } diff --git a/src/data/schemas/nav.json b/src/data/schemas/nav.json index a2d127b83f..f91e174e1f 100644 --- a/src/data/schemas/nav.json +++ b/src/data/schemas/nav.json @@ -9,6 +9,17 @@ "type": "string", "description": "ナビゲーションのまとまりのタイトルです." }, + "sitemap": { + "type": "object", + "description": "`/sitemap/` の生成に関する設定です.存在する場合はこの名前の見出しが生成され,`contents` の全ての項目の親となります.", + "properties": { + "section": { + "type": "boolean", + "description": "見出しに現れることを宣言します." + } + }, + "required": ["section"] + }, "contents": { "type": "array", "description": "そのまとまりのナビゲーションの中身です.", @@ -23,6 +34,23 @@ "url": { "type": "string", "description": "ナビゲーションのリンク先です.`/`から始めてください." + }, + "sitemap": { + "type": "object", + "properties": { + "section": { + "type": "boolean", + "description": "見出しに現れるかどうかを指定します.`false` の場合,親の見出しの中に直接入ります." + }, + "parent": { + "type": "string", + "description": "親の見出しがどれかを指定します." + } + } + }, + "hidden": { + "type": "boolean", + "description": "ヘッダーとフッターには表示しない場合に指定します." } }, "required": ["name", "url"], diff --git a/src/data/sitemapSections.ts b/src/data/sitemapSections.ts deleted file mode 100644 index ee4b9616e9..0000000000 --- a/src/data/sitemapSections.ts +++ /dev/null @@ -1,204 +0,0 @@ -export interface SitemapSection { - patterns: RegExp[]; - negativePatterns?: RegExp[]; - depth: 1 | 2 | 3 | 4 | 5 | 6; - name: string; -} - -export const sitemapSectionsJa: SitemapSection[] = [ - { - patterns: [/^\/oc($|\/)/], - depth: 2, - name: "オンライン授業全般(学生向け)", - }, - { - patterns: [/^\/faculty_members($|\/)/], - depth: 2, - name: "オンライン授業全般(教員向け)", - }, - { - patterns: [/^\/systems($|\/)/], - depth: 2, - name: "東京大学のシステム", - }, - { - patterns: [/^\/utokyo_account($|\/)/], - depth: 3, - name: "UTokyo Account", - }, - { - patterns: [/^\/utas$/], - depth: 3, - name: "UTAS", - }, - { - patterns: [/^\/utol($|\/)/], - depth: 3, - name: "UTOL", - }, - { - patterns: [/^\/zoom($|\/)/], - depth: 3, - name: "Zoom", - }, - { - patterns: [/^\/webex($|\/)/], - depth: 3, - name: "Webex", - }, - { - patterns: [/^\/google($|\/)/], - depth: 3, - name: "ECCSクラウドメール (Google Workspace)", - }, - { - patterns: [/^\/microsoft($|\/)/], - depth: 3, - name: "UTokyo Microsoft License", - }, - { - patterns: [/^\/utokyo_wifi($|\/)/], - depth: 3, - name: "UTokyo Wi-Fi", - }, - { - patterns: [/^\/slack($|\/)/], - depth: 3, - name: "UTokyo Slack", - }, - { - patterns: [/^\/slido($|\/)/], - depth: 3, - name: "Slido", - }, - { - patterns: [/^\/utokyo_vpn($|\/)/], - depth: 3, - name: "UTokyo VPN", - }, - { - patterns: [/^\/online($|\/)/, /^\/articles\//], - depth: 2, - name: "オンラインを活用するために", - }, - { - patterns: [/^\/good-practice($|\/)/], - depth: 3, - name: "グッドプラクティスの共有", - }, - { - patterns: [/^\/notice($|\/)/], - depth: 2, - name: "お知らせ", - }, - { - patterns: [/^\/events($|\/)/], - negativePatterns: [/\/events\/luncheon($|\/)/], - depth: 2, - name: "イベント・説明会等", - }, - { - patterns: [/^\/events\/luncheon($|\/)/], - depth: 3, - name: "オンライン授業情報交換会", - }, - { - patterns: [/.*/], - depth: 2, - name: "その他", - }, -]; - -export const sitemapSectionsEn: SitemapSection[] = [ - { - patterns: [/^\/en\/oc($|\/)/], - depth: 2, - name: "Online Classes (for students)", - }, - { - patterns: [/^\/en\/faculty_members($|\/)/], - depth: 2, - name: "Online Classes (for faculty members)", - }, - { - patterns: [/^\/en\/systems($|\/)/], - depth: 2, - name: "ICT systems", - }, - { - patterns: [/^\/en\/utokyo_account($|\/)/], - depth: 3, - name: "UTokyo Account", - }, - { - patterns: [/^\/en\/utas$/], - depth: 3, - name: "UTAS", - }, - { - patterns: [/^\/en\/utol($|\/)/], - depth: 3, - name: "UTOL", - }, - { - patterns: [/^\/en\/zoom($|\/)/], - depth: 3, - name: "Zoom", - }, - { - patterns: [/^\/en\/webex($|\/)/], - depth: 3, - name: "Webex", - }, - { - patterns: [/^\/en\/google($|\/)/], - depth: 3, - name: "ECCS Cloud Email", - }, - { - patterns: [/^\/en\/microsoft($|\/)/], - depth: 3, - name: "UTokyo Microsoft License", - }, - { - patterns: [/^\/en\/utokyo_wifi($|\/)/], - depth: 3, - name: "UTokyo Wi-Fi", - }, - { - patterns: [/^\/en\/slack($|\/)/], - depth: 3, - name: "UTokyo Slack", - }, - { - patterns: [/^\/en\/slido($|\/)/], - depth: 3, - name: "Slido", - }, - { - patterns: [/^\/en\/utokyo_vpn($|\/)/], - depth: 3, - name: "UTokyo VPN", - }, - { - patterns: [/^\/en\/online($|\/)/, /^\/articles\//], - depth: 2, - name: "Teaching Excellence", - }, - { - patterns: [/^\/en\/notice($|\/)/], - depth: 2, - name: "Notice", - }, - { - patterns: [/^\/en\/events($|\/)/], - negativePatterns: [/\/events\/luncheon($|\/)/], - depth: 2, - name: "Orientations / Seminars", - }, - { - patterns: [/.*/], - depth: 2, - name: "Others", - }, -]; diff --git a/src/layouts/Footer/Navigation.astro b/src/layouts/Footer/Navigation.astro index 84b125b6e9..3d42836294 100644 --- a/src/layouts/Footer/Navigation.astro +++ b/src/layouts/Footer/Navigation.astro @@ -23,13 +23,15 @@ const data: Navigation[] = {
  • {row.name}
      - {row.contents.map(({ name, url }) => ( -
    • - - - -
    • - ))} + {row.contents + .filter((content) => !content.hidden) + .map(({ name, url }) => ( +
    • + + + +
    • + ))}
  • )) diff --git a/src/layouts/Header/Navigation.astro b/src/layouts/Header/Navigation.astro index a594d82bea..6f72770cce 100644 --- a/src/layouts/Header/Navigation.astro +++ b/src/layouts/Header/Navigation.astro @@ -23,13 +23,15 @@ const data: Navigation[] = {
  • {row.name}
      - {row.contents.map(({ name, url }) => ( -
    • - - - -
    • - ))} + {row.contents + .filter((content) => !content.hidden) + .map(({ name, url }) => ( +
    • + + + +
    • + ))}
  • ))