Skip to content
This repository was archived by the owner on Jan 24, 2025. It is now read-only.

Commit bb5bb74

Browse files
committed
feat: nextjs api
1 parent 52ac699 commit bb5bb74

26 files changed

+3417
-723
lines changed

.eslintrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ typings/
5050
# Optional npm cache directory
5151
.npm
5252

53+
# Next.js build output
54+
.next
55+
5356
# Optional eslint cache
5457
.eslintcache
5558

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
"arrowParens": "avoid",
88
"endOfLine": "auto",
99
"proseWrap": "always"
10-
}
10+
}

generic.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type Without<T, K> = Pick<T, Exclude<keyof T, K>>;
2+
3+
type Option<T> = Some<T> | None;
4+
5+
type Entries<T> = {
6+
[K in keyof T]: [K, T[K]];
7+
}[keyof T][];
8+
9+
type SimpleNotFound = { notFound: true };

next-env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.

next.config.cjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/** @type {import('next').NextConfig} */
2+
3+
const { withContentlayer } = require("next-contentlayer");
4+
5+
module.exports = withContentlayer({
6+
reactStrictMode: true,
7+
eslint: {
8+
// Warning: This allows production builds to successfully complete even if
9+
// your project has ESLint errors.
10+
// ignoreDuringBuilds: true,
11+
},
12+
compiler: {
13+
styledComponents: true,
14+
},
15+
swcMinify: true,
16+
// webpack5: true,
17+
webpack: config => {
18+
config.resolve.fallback = { fs: false, path: false };
19+
20+
return config;
21+
},
22+
images: {
23+
remotePatterns: [
24+
{
25+
protocol: "https",
26+
hostname: "**",
27+
},
28+
],
29+
},
30+
});

package.json

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,29 @@
1010
"scripts": {
1111
"runner": "npx ts-node -r tsconfig-paths/register",
1212
"contentlayer:build": "npx contentlayer build --clearCache",
13-
"test": "yarn contentlayer:build"
13+
"test": "yarn contentlayer:build",
14+
"dev": "yarn contentlayer:build && next dev",
15+
"build": "yarn contentlayer:build && next build",
16+
"start": "next start",
17+
"lint": "next lint"
18+
},
19+
"dependencies": {
20+
"@types/node": "20.4.2",
21+
"@types/react": "18.2.15",
22+
"@types/react-dom": "18.2.7",
23+
"autoprefixer": "10.4.14",
24+
"eslint": "8.45.0",
25+
"eslint-config-next": "13.4.10",
26+
"next": "13.4.10",
27+
"next-contentlayer": "^0.3.4",
28+
"postcss": "8.4.26",
29+
"react": "18.2.0",
30+
"react-dom": "18.2.0",
31+
"tailwindcss": "3.3.3",
32+
"typescript": "5.1.6"
1433
},
1534
"devDependencies": {
1635
"contentlayer": "0.3.0",
17-
"eslint": "^8.38.0",
18-
"gray-matter": "^4.0.3",
19-
"ts-node": "^10.9.1",
20-
"tsconfig-paths": "^4.2.0",
21-
"typescript": "^5.0.4"
36+
"prettier": "^3.0.0"
2237
}
2338
}

postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

public/favicon.ico

15 KB
Binary file not shown.

public/robots.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# *
2+
User-agent: *
3+
Disallow: /api/
4+
Disallow: /auth/
5+
Disallow: *

src/pages/_app.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import '@/styles/globals.css'
2+
import type { AppProps } from 'next/app'
3+
4+
export default function App({ Component, pageProps }: AppProps) {
5+
return <Component {...pageProps} />
6+
}

src/pages/_document.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Html, Head, Main, NextScript } from "next/document";
2+
3+
export default function Document() {
4+
return (
5+
<Html lang="en">
6+
<Head />
7+
<body>
8+
<Main />
9+
<NextScript />
10+
</body>
11+
</Html>
12+
);
13+
}

src/pages/api/[[...index]].ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
4+
return res.status(403).send("Unauthorized");
5+
}

src/pages/api/content/[[...slug]].ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* api route to retrieve a single piece of content,
3+
* based on the provided url `slug`
4+
*/
5+
6+
import { SimpleRecordGroupName } from "@/types";
7+
import { computeNavItem } from "@/utils/navItem";
8+
import {
9+
allDeveloperGuides,
10+
allDeveloperResources,
11+
allSolanaDocs,
12+
} from "contentlayer/generated";
13+
import type { NextApiRequest, NextApiResponse } from "next";
14+
15+
export default function handler(
16+
req: NextApiRequest,
17+
res: NextApiResponse<SimpleNotFound | any>,
18+
) {
19+
// get the content record group
20+
const slug = req.query?.slug || [];
21+
22+
if (!slug || !Array.isArray(slug) || slug.length <= 0)
23+
return res.status(404).json({ notFound: true });
24+
25+
const group = slug[0] as SimpleRecordGroupName;
26+
27+
// retrieve the correct group's records by its simple group name
28+
const records = ((group: SimpleRecordGroupName) => {
29+
switch (group) {
30+
case "docs":
31+
return allSolanaDocs;
32+
case "guides":
33+
return allDeveloperGuides;
34+
case "resources":
35+
return allDeveloperResources;
36+
}
37+
})(group);
38+
39+
if (!records) return res.status(404).json({ notFound: true });
40+
41+
// define the formatted href value to search for
42+
const href = `/${slug.join("/")}`;
43+
44+
// init the record to be returned
45+
let record;
46+
47+
// locate the correct record requested (via the url param)
48+
for (let i = 0; i < records.length; i++) {
49+
// @ts-ignore
50+
const navItem = computeNavItem(records[i]);
51+
52+
// only care about the requested record
53+
if (navItem.href != href) continue;
54+
55+
// set the requested record's data (weaving in the computed nav item data)
56+
record = Object.assign(navItem, records[i]);
57+
58+
/**
59+
* todo: support next/prev type records
60+
* note: this will likely require processing the nav records?
61+
*/
62+
63+
// break out of the loop and stop processing
64+
break;
65+
}
66+
67+
if (!record) return res.status(404).json({ notFound: true });
68+
69+
// remove the html formatted content (since it is undesired data to send over the wire)
70+
// @ts-ignore
71+
record.body = record.body.raw.trim();
72+
73+
// todo: preprocess the body content? (if desired in the future)
74+
75+
// todo: support sending related content records back to the client
76+
77+
// finally, return the json formatted listing of NavItems
78+
return res.status(200).json(record);
79+
}

src/pages/api/nav/[group].ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* api route to generate the nav item listing for
3+
* each supported content record `group`
4+
*/
5+
6+
import type { NextApiRequest, NextApiResponse } from "next";
7+
import { NavItem, SimpleRecordGroupName } from "@/types";
8+
import { generateNavItemListing } from "@/utils/navItem";
9+
import {
10+
allDeveloperGuides,
11+
allDeveloperResources,
12+
allSolanaDocs,
13+
} from "contentlayer/generated";
14+
15+
export default function handler(
16+
req: NextApiRequest,
17+
res: NextApiResponse<SimpleNotFound | NavItem[]>,
18+
) {
19+
// get the content record group
20+
const group = req.query?.group?.toString() as SimpleRecordGroupName;
21+
if (!group) return res.status(404).json({ notFound: true });
22+
23+
// retrieve the correct group's records by its simple group name
24+
const records = ((group: SimpleRecordGroupName) => {
25+
switch (group) {
26+
case "docs":
27+
return allSolanaDocs;
28+
case "guides":
29+
return allDeveloperGuides;
30+
// case "resources":
31+
// return allDeveloperResources;
32+
}
33+
})(group);
34+
35+
if (!records) return res.status(404).json({ notFound: true });
36+
37+
const navItems = generateNavItemListing(records);
38+
39+
// finally, return the json formatted listing of NavItems
40+
return res.status(200).json(navItems);
41+
}

src/pages/api/paths/[group].ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* api route to generate a path listing for
3+
* each supported content record `group`
4+
*/
5+
6+
import type { NextApiRequest, NextApiResponse } from "next";
7+
import { NavItem, SimpleRecordGroupName } from "@/types";
8+
import { computeNavItem, shouldIgnoreRecord } from "@/utils/navItem";
9+
import {
10+
allDeveloperGuides,
11+
allDeveloperResources,
12+
allSolanaDocs,
13+
} from "contentlayer/generated";
14+
15+
export default function handler(
16+
req: NextApiRequest,
17+
res: NextApiResponse<SimpleNotFound | NavItem[]>,
18+
) {
19+
// get the content record group
20+
const group = req.query?.group?.toString() as SimpleRecordGroupName;
21+
if (!group) return res.status(404).json({ notFound: true });
22+
23+
// retrieve the correct group's records by its simple group name
24+
const records = ((group: SimpleRecordGroupName) => {
25+
switch (group) {
26+
case "docs":
27+
return allSolanaDocs;
28+
case "guides":
29+
return allDeveloperGuides;
30+
case "resources":
31+
return allDeveloperResources;
32+
}
33+
})(group);
34+
35+
if (!records) return res.status(404).json({ notFound: true });
36+
37+
// init the listing response
38+
const listing: Array<NavItem> = [];
39+
40+
/**
41+
* todo: assorted things
42+
* - better support for external links
43+
*/
44+
45+
// compute the path data to return
46+
records.map(record => {
47+
if (shouldIgnoreRecord({ fileName: record._raw.sourceFileName })) return;
48+
49+
// @ts-ignore
50+
const navItem = computeNavItem(record);
51+
52+
if (!navItem.href || !!record.isExternal) return;
53+
54+
listing.push(navItem);
55+
});
56+
57+
// finally, return the json formatted listing
58+
return res.status(200).json(listing);
59+
}

src/pages/api/records/[group].ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* api route to generate a listing of records for a given `group`
3+
*/
4+
5+
import type { NextApiRequest, NextApiResponse } from "next";
6+
import { NavItem, SimpleRecordGroupName } from "@/types";
7+
import { computeNavItem, shouldIgnoreRecord } from "@/utils/navItem";
8+
import {
9+
allDeveloperGuides,
10+
allDeveloperResources,
11+
allSolanaDocs,
12+
} from "contentlayer/generated";
13+
14+
export default function handler(
15+
req: NextApiRequest,
16+
res: NextApiResponse<SimpleNotFound | NavItem[]>,
17+
) {
18+
// get the content record group
19+
const group = req.query?.group?.toString() as SimpleRecordGroupName;
20+
if (!group) return res.status(404).json({ notFound: true });
21+
22+
// retrieve the correct group's records by its simple group name
23+
const records = ((group: SimpleRecordGroupName) => {
24+
switch (group) {
25+
case "docs":
26+
return allSolanaDocs;
27+
case "guides":
28+
return allDeveloperGuides;
29+
case "resources":
30+
return allDeveloperResources;
31+
}
32+
})(group);
33+
34+
if (!records) return res.status(404).json({ notFound: true });
35+
36+
const listing: Array<any> = [];
37+
38+
// compute the listing data to return
39+
records.map(record => {
40+
if (shouldIgnoreRecord({ fileName: record._raw.sourceFileName })) return;
41+
42+
// @ts-ignore
43+
const navItem = computeNavItem(record);
44+
45+
if (!navItem.href) return;
46+
47+
// @ts-ignore
48+
record = Object.assign(navItem, record);
49+
50+
const attributesToDelete = ["_id", "_raw", "body", "type"];
51+
52+
if (!record.featured)
53+
attributesToDelete.push("featured", "featuredPriority");
54+
55+
// remove any undesired content from the response
56+
// @ts-ignore
57+
attributesToDelete.forEach(e => delete record[e]);
58+
59+
listing.push(record);
60+
});
61+
62+
// todo: add pagination support?
63+
64+
// finally, return the json formatted listing
65+
return res.status(200).json(listing);
66+
}

0 commit comments

Comments
 (0)