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

Commit

Permalink
feat: nextjs api
Browse files Browse the repository at this point in the history
  • Loading branch information
nickfrosty committed Jul 18, 2023
1 parent 52ac699 commit bb5bb74
Show file tree
Hide file tree
Showing 26 changed files with 3,417 additions and 723 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ typings/
# Optional npm cache directory
.npm

# Next.js build output
.next

# Optional eslint cache
.eslintcache

Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
"arrowParens": "avoid",
"endOfLine": "auto",
"proseWrap": "always"
}
}
9 changes: 9 additions & 0 deletions generic.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type Without<T, K> = Pick<T, Exclude<keyof T, K>>;

type Option<T> = Some<T> | None;

type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];

type SimpleNotFound = { notFound: true };
5 changes: 5 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
30 changes: 30 additions & 0 deletions next.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** @type {import('next').NextConfig} */

const { withContentlayer } = require("next-contentlayer");

module.exports = withContentlayer({
reactStrictMode: true,
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
// ignoreDuringBuilds: true,
},
compiler: {
styledComponents: true,
},
swcMinify: true,
// webpack5: true,
webpack: config => {
config.resolve.fallback = { fs: false, path: false };

return config;
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
});
27 changes: 21 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,29 @@
"scripts": {
"runner": "npx ts-node -r tsconfig-paths/register",
"contentlayer:build": "npx contentlayer build --clearCache",
"test": "yarn contentlayer:build"
"test": "yarn contentlayer:build",
"dev": "yarn contentlayer:build && next dev",
"build": "yarn contentlayer:build && next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "20.4.2",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"eslint": "8.45.0",
"eslint-config-next": "13.4.10",
"next": "13.4.10",
"next-contentlayer": "^0.3.4",
"postcss": "8.4.26",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.1.6"
},
"devDependencies": {
"contentlayer": "0.3.0",
"eslint": "^8.38.0",
"gray-matter": "^4.0.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.0.4"
"prettier": "^3.0.0"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file added public/favicon.ico
Binary file not shown.
5 changes: 5 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# *
User-agent: *
Disallow: /api/
Disallow: /auth/
Disallow: *
6 changes: 6 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
13 changes: 13 additions & 0 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
5 changes: 5 additions & 0 deletions src/pages/api/[[...index]].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(403).send("Unauthorized");
}
79 changes: 79 additions & 0 deletions src/pages/api/content/[[...slug]].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* api route to retrieve a single piece of content,
* based on the provided url `slug`
*/

import { SimpleRecordGroupName } from "@/types";
import { computeNavItem } from "@/utils/navItem";
import {
allDeveloperGuides,
allDeveloperResources,
allSolanaDocs,
} from "contentlayer/generated";
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(
req: NextApiRequest,
res: NextApiResponse<SimpleNotFound | any>,
) {
// get the content record group
const slug = req.query?.slug || [];

if (!slug || !Array.isArray(slug) || slug.length <= 0)
return res.status(404).json({ notFound: true });

const group = slug[0] as SimpleRecordGroupName;

// retrieve the correct group's records by its simple group name
const records = ((group: SimpleRecordGroupName) => {
switch (group) {
case "docs":
return allSolanaDocs;
case "guides":
return allDeveloperGuides;
case "resources":
return allDeveloperResources;
}
})(group);

if (!records) return res.status(404).json({ notFound: true });

// define the formatted href value to search for
const href = `/${slug.join("/")}`;

// init the record to be returned
let record;

// locate the correct record requested (via the url param)
for (let i = 0; i < records.length; i++) {
// @ts-ignore
const navItem = computeNavItem(records[i]);

// only care about the requested record
if (navItem.href != href) continue;

// set the requested record's data (weaving in the computed nav item data)
record = Object.assign(navItem, records[i]);

/**
* todo: support next/prev type records
* note: this will likely require processing the nav records?
*/

// break out of the loop and stop processing
break;
}

if (!record) return res.status(404).json({ notFound: true });

// remove the html formatted content (since it is undesired data to send over the wire)
// @ts-ignore
record.body = record.body.raw.trim();

// todo: preprocess the body content? (if desired in the future)

// todo: support sending related content records back to the client

// finally, return the json formatted listing of NavItems
return res.status(200).json(record);
}
41 changes: 41 additions & 0 deletions src/pages/api/nav/[group].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* api route to generate the nav item listing for
* each supported content record `group`
*/

import type { NextApiRequest, NextApiResponse } from "next";
import { NavItem, SimpleRecordGroupName } from "@/types";
import { generateNavItemListing } from "@/utils/navItem";
import {
allDeveloperGuides,
allDeveloperResources,
allSolanaDocs,
} from "contentlayer/generated";

export default function handler(
req: NextApiRequest,
res: NextApiResponse<SimpleNotFound | NavItem[]>,
) {
// get the content record group
const group = req.query?.group?.toString() as SimpleRecordGroupName;
if (!group) return res.status(404).json({ notFound: true });

// retrieve the correct group's records by its simple group name
const records = ((group: SimpleRecordGroupName) => {
switch (group) {
case "docs":
return allSolanaDocs;
case "guides":
return allDeveloperGuides;
// case "resources":
// return allDeveloperResources;
}
})(group);

if (!records) return res.status(404).json({ notFound: true });

const navItems = generateNavItemListing(records);

// finally, return the json formatted listing of NavItems
return res.status(200).json(navItems);
}
59 changes: 59 additions & 0 deletions src/pages/api/paths/[group].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* api route to generate a path listing for
* each supported content record `group`
*/

import type { NextApiRequest, NextApiResponse } from "next";
import { NavItem, SimpleRecordGroupName } from "@/types";
import { computeNavItem, shouldIgnoreRecord } from "@/utils/navItem";
import {
allDeveloperGuides,
allDeveloperResources,
allSolanaDocs,
} from "contentlayer/generated";

export default function handler(
req: NextApiRequest,
res: NextApiResponse<SimpleNotFound | NavItem[]>,
) {
// get the content record group
const group = req.query?.group?.toString() as SimpleRecordGroupName;
if (!group) return res.status(404).json({ notFound: true });

// retrieve the correct group's records by its simple group name
const records = ((group: SimpleRecordGroupName) => {
switch (group) {
case "docs":
return allSolanaDocs;
case "guides":
return allDeveloperGuides;
case "resources":
return allDeveloperResources;
}
})(group);

if (!records) return res.status(404).json({ notFound: true });

// init the listing response
const listing: Array<NavItem> = [];

/**
* todo: assorted things
* - better support for external links
*/

// compute the path data to return
records.map(record => {
if (shouldIgnoreRecord({ fileName: record._raw.sourceFileName })) return;

// @ts-ignore
const navItem = computeNavItem(record);

if (!navItem.href || !!record.isExternal) return;

listing.push(navItem);
});

// finally, return the json formatted listing
return res.status(200).json(listing);
}
66 changes: 66 additions & 0 deletions src/pages/api/records/[group].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* api route to generate a listing of records for a given `group`
*/

import type { NextApiRequest, NextApiResponse } from "next";
import { NavItem, SimpleRecordGroupName } from "@/types";
import { computeNavItem, shouldIgnoreRecord } from "@/utils/navItem";
import {
allDeveloperGuides,
allDeveloperResources,
allSolanaDocs,
} from "contentlayer/generated";

export default function handler(
req: NextApiRequest,
res: NextApiResponse<SimpleNotFound | NavItem[]>,
) {
// get the content record group
const group = req.query?.group?.toString() as SimpleRecordGroupName;
if (!group) return res.status(404).json({ notFound: true });

// retrieve the correct group's records by its simple group name
const records = ((group: SimpleRecordGroupName) => {
switch (group) {
case "docs":
return allSolanaDocs;
case "guides":
return allDeveloperGuides;
case "resources":
return allDeveloperResources;
}
})(group);

if (!records) return res.status(404).json({ notFound: true });

const listing: Array<any> = [];

// compute the listing data to return
records.map(record => {
if (shouldIgnoreRecord({ fileName: record._raw.sourceFileName })) return;

// @ts-ignore
const navItem = computeNavItem(record);

if (!navItem.href) return;

// @ts-ignore
record = Object.assign(navItem, record);

const attributesToDelete = ["_id", "_raw", "body", "type"];

if (!record.featured)
attributesToDelete.push("featured", "featuredPriority");

// remove any undesired content from the response
// @ts-ignore
attributesToDelete.forEach(e => delete record[e]);

listing.push(record);
});

// todo: add pagination support?

// finally, return the json formatted listing
return res.status(200).json(listing);
}
Loading

0 comments on commit bb5bb74

Please sign in to comment.