Skip to content

Commit 91fd883

Browse files
CopilotTechQuery
andauthored
[refactor] replace Git submodule with GitHub content API for Policy Wiki pages (#29)
Co-authored-by: TechQuery <[email protected]>
1 parent bda2d96 commit 91fd883

File tree

13 files changed

+509
-339
lines changed

13 files changed

+509
-339
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/node_modules
55
/.pnp
66
.pnp.js
7+
package-lock.json
78

89
# testing
910
/coverage

components/Git/Issue/Card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const IssueCard: FC<IssueCardProps> = ({
5858
</Card.Header>
5959
<Card.Body
6060
as="article"
61-
dangerouslySetInnerHTML={{ __html: marked(body || '') }}
61+
dangerouslySetInnerHTML={{ __html: marked(body || '') as string }}
6262
/>
6363
<Card.Footer className="d-flex justify-content-between align-items-center">
6464
{user && <Nameplate name={user.name || ''} avatar={user.avatar_url} />}

models/Wiki.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Content, ContentModel } from 'mobx-github';
2+
import { DataObject } from 'mobx-restful';
3+
4+
import './Base';
5+
6+
export interface XContent extends Content {
7+
meta?: DataObject;
8+
children?: XContent[];
9+
}
10+
11+
export const policyContentStore = new ContentModel('fpsig', 'open-source-policy');

models/configuration.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ export const LARK_API_HOST = `${API_Host}/api/Lark/`;
2020

2121
export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy';
2222

23-
export const GithubToken =
24-
(globalThis.document && parseCookie().token) || process.env.GITHUB_TOKEN;
23+
export const GithubToken = (globalThis.document && parseCookie().token) || process.env.GH_PAT;
2524

2625
export const LarkAppMeta = {
2726
host: process.env.NEXT_PUBLIC_LARK_API_HOST,

package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,32 @@
1515
"@koa/router": "^14.0.0",
1616
"@mdx-js/loader": "^3.1.0",
1717
"@mdx-js/react": "^3.1.0",
18-
"@next/mdx": "^15.5.0",
18+
"@next/mdx": "^15.5.2",
1919
"core-js": "^3.45.1",
2020
"file-type": "^21.0.0",
2121
"idea-react": "^2.0.0-rc.13",
2222
"koa": "^3.0.1",
2323
"koajax": "^3.1.2",
2424
"license-filter": "^0.2.5",
25-
"marked": "^16.2.0",
25+
"marked": "^16.2.1",
2626
"mime": "^4.0.7",
2727
"mobx": "^6.13.7",
28-
"mobx-github": "^0.3.11",
28+
"mobx-github": "^0.4.0",
2929
"mobx-i18n": "^0.7.1",
3030
"mobx-lark": "^2.4.0",
3131
"mobx-react": "^9.2.0",
3232
"mobx-react-helper": "^0.5.1",
3333
"mobx-restful": "^2.1.0",
3434
"mobx-restful-table": "^2.5.3",
35-
"next": "^15.5.0",
35+
"next": "^15.5.2",
3636
"next-pwa": "^5.6.0",
3737
"next-ssr-middleware": "^1.0.2",
3838
"react": "^19.1.1",
3939
"react-bootstrap": "^2.10.10",
4040
"react-dom": "^19.1.1",
4141
"react-typed-component": "^1.0.6",
4242
"undici": "^7.15.0",
43-
"web-utility": "^4.5.1",
43+
"web-utility": "^4.5.3",
4444
"yaml": "^2.8.1"
4545
},
4646
"devDependencies": {
@@ -49,18 +49,18 @@
4949
"@babel/preset-react": "^7.27.1",
5050
"@cspell/eslint-plugin": "^9.2.0",
5151
"@eslint/js": "^9.34.0",
52-
"@next/eslint-plugin-next": "^15.5.0",
52+
"@next/eslint-plugin-next": "^15.5.2",
5353
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
5454
"@stylistic/eslint-plugin": "^5.2.3",
5555
"@types/eslint-config-prettier": "^6.11.3",
5656
"@types/koa": "^3.0.0",
5757
"@types/koa__router": "^12.0.4",
5858
"@types/next-pwa": "^5.6.9",
59-
"@types/node": "^22.17.2",
60-
"@types/react": "^19.1.11",
61-
"@types/react-dom": "^19.1.7",
59+
"@types/node": "^22.18.0",
60+
"@types/react": "^19.1.12",
61+
"@types/react-dom": "^19.1.9",
6262
"eslint": "^9.34.0",
63-
"eslint-config-next": "^15.5.0",
63+
"eslint-config-next": "^15.5.2",
6464
"eslint-config-prettier": "^10.1.8",
6565
"eslint-plugin-react": "^7.37.5",
6666
"eslint-plugin-simple-import-sort": "^12.1.1",
@@ -73,9 +73,9 @@
7373
"next-with-less": "^3.0.1",
7474
"prettier": "^3.6.2",
7575
"prettier-plugin-css-order": "^2.1.2",
76-
"sass": "^1.90.0",
76+
"sass": "^1.91.0",
7777
"typescript": "~5.9.2",
78-
"typescript-eslint": "^8.40.0"
78+
"typescript-eslint": "^8.41.0"
7979
},
8080
"resolutions": {
8181
"next": "$next"

pages/_document.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export default class CustomDocument extends Document<CustomDocumentProps> {
4040
rel="stylesheet"
4141
href="https://unpkg.com/[email protected]/font/bootstrap-icons.css"
4242
/>
43+
<link
44+
rel="stylesheet"
45+
href="https://unpkg.com/[email protected]/github-markdown.css"
46+
/>
4347
</Head>
4448

4549
<body>

pages/api/core.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,28 @@ export interface ArticleMeta {
5858
subs: ArticleMeta[];
5959
}
6060

61-
const MDX_pattern = /\.mdx?$/;
62-
63-
export async function splitFrontMatter(path: string) {
64-
const { readFile } = await import('fs/promises');
65-
66-
const file = await readFile(path, 'utf-8');
61+
export const MD_pattern = /\.(md|markdown)$/i,
62+
MDX_pattern = /\.mdx?$/i;
6763

64+
export function splitFrontMatter(raw: string) {
6865
const [, frontMatter, markdown] =
69-
file.trim().match(/^---[\r\n]([\s\S]+?[\r\n])---[\r\n]([\s\S]*)/) || [];
66+
raw.trim().match(/^---[\r\n]([\s\S]+?[\r\n])---[\r\n]([\s\S]*)/) || [];
7067

71-
if (!frontMatter) return { markdown: file };
68+
if (!frontMatter) return { markdown: raw };
7269

7370
try {
7471
const meta = parse(frontMatter) as DataObject;
7572

7673
return { markdown, meta };
7774
} catch (error) {
78-
console.error(`Error reading front matter for ${path}:`, error);
75+
console.error(`Error parsing Front Matter:`, error);
7976

8077
return { markdown };
8178
}
8279
}
8380

8481
export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerator<ArticleMeta> {
85-
const { readdir } = await import('fs/promises');
82+
const { readdir, readFile } = await import('fs/promises');
8683

8784
const list = await readdir(prefix + path, { withFileTypes: true });
8885

@@ -99,7 +96,9 @@ export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerato
9996
if (node.isFile() && isMDX) {
10097
const article: ArticleMeta = { name, path, subs: [] };
10198

102-
const { meta } = await splitFrontMatter(`${node.path}/${node.name}`);
99+
const file = await readFile(`${node.path}/${node.name}`, 'utf-8');
100+
101+
const { meta } = splitFrontMatter(file);
103102

104103
if (meta) article.meta = meta;
105104

pages/wiki/[...slug].tsx

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,142 @@
11
import { marked } from 'marked';
2-
import { DataObject } from 'mobx-restful';
3-
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
2+
import { GetStaticPaths, GetStaticProps } from 'next';
43
import { ParsedUrlQuery } from 'querystring';
4+
import { FC } from 'react';
5+
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';
6+
import { decodeBase64 } from 'web-utility';
57

6-
import { pageListOf, splitFrontMatter, traverseTree } from '../api/core';
8+
import { PageHead } from '../../components/Layout/PageHead';
9+
import { policyContentStore, XContent } from '../../models/Wiki';
10+
import { splitFrontMatter } from '../api/core';
711

812
interface WikiPageParams extends ParsedUrlQuery {
913
slug: string[];
1014
}
1115

1216
export const getStaticPaths: GetStaticPaths<WikiPageParams> = async () => {
13-
const tree = await Array.fromAsync(pageListOf('wiki', 'public/'));
14-
const list = tree.map(root => [...traverseTree(root, 'subs')]).flat();
15-
const paths = list
16-
.map(({ path }) => path && { params: { slug: path.split('/') } })
17-
.filter(Boolean) as { params: WikiPageParams }[];
17+
const nodes = await policyContentStore.getAll();
18+
19+
const paths = nodes
20+
.filter(({ type }) => type === 'file')
21+
.map(({ path }) => ({ params: { slug: path.split('/') } }));
1822

1923
return { paths, fallback: 'blocking' };
2024
};
2125

22-
interface WikiPageProps {
23-
meta?: DataObject;
24-
markup: string;
25-
}
26-
27-
export const getStaticProps: GetStaticProps<WikiPageProps, WikiPageParams> = async ({ params }) => {
26+
export const getStaticProps: GetStaticProps<XContent, WikiPageParams> = async ({ params }) => {
2827
const { slug } = params!;
29-
// https://github.com/vercel/next.js/issues/12851
30-
if (slug[0] !== 'wiki') slug.unshift('wiki');
3128

32-
const { meta, markdown } = await splitFrontMatter(`public/${slug.join('/')}.md`);
29+
const node = await policyContentStore.getOne(slug.join('/'));
30+
31+
const { meta, markdown } = splitFrontMatter(decodeBase64(node.content!));
3332

3433
const markup = marked(markdown) as string;
3534

36-
return { props: JSON.parse(JSON.stringify({ meta, markup })) };
35+
return {
36+
props: JSON.parse(JSON.stringify({ ...node, content: markup, meta })),
37+
revalidate: 300, // Revalidate every 5 minutes
38+
};
3739
};
3840

39-
const WikiPage: NextPage<WikiPageProps> = ({ meta, markup }) => (
40-
<>
41-
{meta && (
42-
<blockquote>
43-
<a target="_blank" href={meta.url} rel="noreferrer">
44-
{meta.url}
45-
</a>
46-
</blockquote>
47-
)}
48-
<article dangerouslySetInnerHTML={{ __html: markup }} />
49-
</>
41+
const WikiPage: FC<XContent> = ({ name, path, parent_path, content, meta }) => (
42+
<Container className="py-4">
43+
<PageHead title={name} />
44+
45+
<Breadcrumb className="mb-4">
46+
<Breadcrumb.Item href="/wiki">Wiki</Breadcrumb.Item>
47+
48+
{parent_path?.split('/').map((segment, index, array) => {
49+
const breadcrumbPath = array.slice(0, index + 1).join('/');
50+
51+
return (
52+
<Breadcrumb.Item key={breadcrumbPath} href={`/wiki/${breadcrumbPath}`}>
53+
{segment}
54+
</Breadcrumb.Item>
55+
);
56+
})}
57+
<Breadcrumb.Item active>{name}</Breadcrumb.Item>
58+
</Breadcrumb>
59+
60+
<article>
61+
<header className="mb-4">
62+
<h1>{name}</h1>
63+
64+
{meta && (
65+
<div className="d-flex flex-wrap align-items-center gap-3 mb-3">
66+
<ul className="mb-0">
67+
{meta['主题分类'] && (
68+
<li>
69+
<Badge bg="primary">{meta['主题分类']}</Badge>
70+
</li>
71+
)}
72+
{meta['发文机构'] && (
73+
<li>
74+
<Badge bg="secondary">{meta['发文机构']}</Badge>
75+
</li>
76+
)}
77+
{meta['有效性'] && (
78+
<li>
79+
<Badge bg={meta['有效性'] === '现行有效' ? 'success' : 'warning'}>
80+
{meta['有效性']}
81+
</Badge>
82+
</li>
83+
)}
84+
</ul>
85+
</div>
86+
)}
87+
88+
<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
89+
<div>
90+
{meta?.['成文日期'] && <span>成文日期: {meta['成文日期']}</span>}
91+
{meta?.['发布日期'] && meta['发布日期'] !== meta['成文日期'] && (
92+
<span className="ms-3">发布日期: {meta['发布日期']}</span>
93+
)}
94+
</div>
95+
96+
<div className="d-flex gap-2">
97+
<Button
98+
variant="outline-primary"
99+
size="sm"
100+
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
101+
target="_blank"
102+
rel="noopener noreferrer"
103+
>
104+
在 GitHub 编辑
105+
</Button>
106+
{meta?.url && (
107+
<Button
108+
variant="outline-secondary"
109+
size="sm"
110+
href={meta.url}
111+
target="_blank"
112+
rel="noopener noreferrer"
113+
>
114+
查看原文
115+
</Button>
116+
)}
117+
</div>
118+
</div>
119+
</header>
120+
121+
<div dangerouslySetInnerHTML={{ __html: content || '' }} className="markdown-body" />
122+
</article>
123+
124+
<footer className="mt-5 pt-4 border-top">
125+
<div className="text-center">
126+
<p className="text-muted">
127+
这是一个基于 GitHub 仓库的政策文档页面。
128+
<a
129+
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
130+
target="_blank"
131+
rel="noopener noreferrer"
132+
className="ms-2"
133+
>
134+
在 GitHub 上查看或编辑此内容
135+
</a>
136+
</p>
137+
</div>
138+
</footer>
139+
</Container>
50140
);
141+
51142
export default WikiPage;

0 commit comments

Comments
 (0)