Skip to content

Commit e543e28

Browse files
authored
feat: remove unused static pages (#193)
* feat: remove unused static pages * feat: changelog path * docs: add changelogs
1 parent 3d11eae commit e543e28

9 files changed

+386
-278
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ADR: Unify Static Path Resolution for Markdown Content
2+
3+
**Date:** 2025-05-28
4+
5+
**Status:** Accepted
6+
7+
**Context:**
8+
9+
The previous system for handling static page generation involved disparate logic for resolving direct file paths, aliases, and redirects. This was spread across `getStaticPaths` and `getStaticProps` in `src/pages/[...slug].tsx`, and various helper functions in `scripts/common.ts` and `src/lib/content/paths.ts`. This led to:
10+
11+
- Increased complexity in understanding how a URL maps to a content file.
12+
- Potential inconsistencies in path resolution.
13+
- Difficulty in maintaining and extending the system.
14+
- Generation of potentially unused static pages if not carefully managed.
15+
16+
**Decision:**
17+
18+
We decided to refactor the static path resolution mechanism to centralize and unify the logic. The core of this decision is the introduction of a new function, `getStaticJSONPaths` (located in `src/lib/content/paths.ts`).
19+
20+
This function will be responsible for:
21+
22+
1. Aggregating all valid content paths from markdown files (excluding specific directories like `contributor` and `tags`).
23+
2. Incorporating reversed alias mappings (where the alias target becomes the key and the alias path becomes the value).
24+
3. Applying redirect rules, ensuring they don't conflict with aliases and correctly point to canonical content paths.
25+
4. Providing a single, comprehensive `Record<string, string>` where keys are browser-accessible paths (e.g., `/my-alias/page`) and values are the corresponding canonical content file paths (e.g., `/actual-folder/actual-page`).
26+
27+
The `getStaticPaths` and `getStaticProps` functions in `src/pages/[...slug].tsx` will be simplified to consume the output of `getStaticJSONPaths` directly. This makes them solely responsible for using this mapping to generate static pages and fetch content, rather than performing complex resolution logic themselves.
28+
29+
Helper functions in `scripts/common.ts` for generating Nginx redirects and other path mappings will also be refactored to align with this unified approach, ensuring consistency between server-side redirects and application-level path resolution.
30+
31+
**Consequences:**
32+
33+
- **Pros:**
34+
- **Simplified Logic:** Path resolution logic is now centralized, making it easier to understand, debug, and maintain.
35+
- **Improved Consistency:** Ensures that all parts of the system (static generation, client-side navigation, server-side redirects) use the same source of truth for path mapping.
36+
- **Reduced Redundancy:** Eliminates duplicate path resolution logic.
37+
- **Leaner Builds:** By relying on a definitive list of resolvable paths, we implicitly avoid generating unused static pages.
38+
- **Easier Extensibility:** Adding new types of path mappings or modifying existing ones becomes more straightforward by targeting the `getStaticJSONPaths` function.
39+
- **Cons:**
40+
- The `getStaticJSONPaths` function becomes a critical piece of infrastructure; any bugs here will have a wide impact.
41+
- Initial refactoring requires careful testing to ensure all edge cases for aliases and redirects are handled correctly.
42+
43+
**Rationale:**
44+
45+
The previous approach was becoming increasingly difficult to manage as the number of aliases, redirects, and content files grew. A unified system provides a more robust and scalable foundation for handling content paths. This change prioritizes maintainability and predictability in how URLs are resolved to content.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Refactor: Static Page Generation and Path Resolution
2+
3+
This update introduces a significant refactoring of the system that handles static page generation, aliases, and redirects. The primary goal was to simplify logic, improve maintainability, and ensure consistent path resolution across the application.
4+
5+
**Key Changes & Improvements:**
6+
7+
- **Unified Path Resolution:** Introduced a new centralized function, `getStaticJSONPaths` (in `src/lib/content/paths.ts`). This function now serves as the single source of truth for mapping browser-accessible URLs to their corresponding canonical content file paths. It intelligently combines direct markdown file paths, reversed alias mappings, and filtered redirect rules.
8+
- **Simplified Page Generation Logic:** The `getStaticPaths` and `getStaticProps` functions within `src/pages/[...slug].tsx` have been substantially simplified. They now rely entirely on `getStaticJSONPaths` to determine valid static paths and to resolve requested URLs to the correct content files. This reduces complexity and potential inconsistencies.
9+
- **Removal of Unused Static Pages:** As a direct result of the more precise path resolution provided by `getStaticJSONPaths`, pages that are not explicitly defined or resolvable through this new system are no longer generated. This helps in keeping the build lean and focused on relevant content.
10+
- **Robust Redirect and Alias Handling:** The logic for generating Nginx redirects (`getNginxRedirects`) and other redirect mappings (`getRedirectsNotToAliases` in `scripts/common.ts`) has been refactored. This ensures that redirects are handled more robustly, especially in relation to aliases, preventing conflicts and ensuring predictable behavior.
11+
- **Updated Changelog URL:** The navigation link for the Changelog in the sidebar (`src/components/layout/Sidebar.tsx`) has been updated from `/updates/changelog` to `/changelog` for a cleaner URL structure.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Specification: Unified Static Path Resolution System
2+
3+
**Version:** 1.0
4+
**Date:** 2025-05-28
5+
6+
## 1. Introduction
7+
8+
This document outlines the specification for a refactored system to handle static page generation, URL aliasing, and redirects. The primary objective is to create a unified, maintainable, and predictable mechanism for mapping browser-accessible URLs to their corresponding content files.
9+
10+
## 2. Goals
11+
12+
- Centralize path resolution logic.
13+
- Simplify the `getStaticPaths` and `getStaticProps` implementations in Next.js.
14+
- Ensure consistent handling of direct file paths, aliases, and redirects.
15+
- Reduce redundancy in path management code.
16+
- Implicitly prevent the generation of unused static pages by relying on a definitive set of resolvable paths.
17+
18+
## 3. System Architecture
19+
20+
The core component of the new system is the `getStaticJSONPaths` function, located in `src/lib/content/paths.ts`.
21+
22+
### 3.1. `getStaticJSONPaths` Function
23+
24+
- **Purpose:** To generate a comprehensive mapping of all valid browser-accessible paths to their canonical content file paths.
25+
- **Output:** `Promise<Record<string, string>>`
26+
- Keys: Normalized browser-accessible paths (e.g., `/my-blog-post`, `/alias/to/content`). Paths will be slash-prefixed and have no trailing slash.
27+
- Values: Normalized canonical content file paths relative to the content root (e.g., `/blog/my-post`, `/actual/folder/content-file`). Paths will be slash-prefixed and have no trailing slash.
28+
- **Logic:**
29+
1. **Fetch Raw Data:**
30+
- Read `redirects.json` to get redirect rules (`getRedirectsNotToAliases` from `scripts/common.ts` will be used to pre-filter redirects that might conflict with or are superseded by aliases).
31+
- Read `aliases.json` to get alias rules (`getReversedAliasPaths` from `scripts/common.ts` will be used to get a mapping of `target -> alias`).
32+
- Scan the `public/content` directory to get all markdown/MDX file paths (`getAllMarkdownFiles` from `scripts/common.ts`), excluding specified directories like `contributor/` and `tags/`.
33+
2. **Path Normalization:** All paths (keys and values) will be normalized:
34+
- Ensure they start with a `/`.
35+
- Ensure they do not end with a `/` (unless it's the root path `/`).
36+
3. **Processing Order & Precedence:**
37+
- **Markdown Files:** Direct markdown file paths form the base set of canonical content.
38+
- **Aliases:** Alias paths (keys from the reversed alias map) will map to their corresponding original content paths (values from the reversed alias map). These effectively create alternative URLs for existing content.
39+
- **Redirects:** Redirect source paths will map to their target paths. If a redirect target is an alias, it should resolve to the alias's canonical content path. Redirects should not overwrite existing direct markdown paths or alias paths if the source of the redirect is already a valid content path or alias.
40+
4. **Output Generation:** Combine these sources into a single record, ensuring that aliases and redirects correctly point to the final canonical content path.
41+
42+
### 3.2. Next.js Page Generation (`src/pages/[...slug].tsx`)
43+
44+
- **`getStaticPaths`:**
45+
- Will call `getStaticJSONPaths()` once.
46+
- The keys of the returned record will be transformed into the `paths` array required by Next.js.
47+
- **`getStaticProps`:**
48+
- Will call `getStaticJSONPaths()` once.
49+
- The `slug` from `params` will be used to look up the canonical content file path from the record returned by `getStaticJSONPaths`.
50+
- If the requested path is not found as a key in the map, it implies a 404 (though Next.js handles this if not in `getStaticPaths`).
51+
- The resolved canonical path will be used to fetch the markdown content.
52+
53+
### 3.3. Scripting (`scripts/common.ts`)
54+
55+
- **`getNginxRedirects`:** This function will be updated to use `getStaticJSONPaths` or a similar underlying filtered redirect list to ensure Nginx redirect rules are consistent with the application's path resolution.
56+
- **`getRedirectsNotToAliases`:** This utility will help in pre-filtering redirects before they are consumed by `getStaticJSONPaths` to avoid conflicts where a redirect source might also be an alias target.
57+
- **`getReversedAliasPaths`:** This utility provides the `target -> alias` mapping crucial for `getStaticJSONPaths`.
58+
59+
## 4. Path Resolution Examples
60+
61+
Assume `public/content/blog/my-article.md` exists.
62+
Assume `aliases.json`: `{ "/latest-post": "/blog/my-article" }`
63+
Assume `redirects.json`: `{ "/old-post-url": "/latest-post" }`
64+
65+
`getStaticJSONPaths` would produce (among others):
66+
67+
- `"/blog/my-article": "/blog/my-article"`
68+
- `"/latest-post": "/blog/my-article"` (from reversed alias)
69+
- `"/old-post-url": "/blog/my-article"` (redirect resolves through alias to canonical)
70+
71+
## 5. UI Changes
72+
73+
- The changelog link in `src/components/layout/Sidebar.tsx` will be updated from `/updates/changelog` to `/changelog` to reflect a cleaner URL structure, which should be a path resolvable by the new system.
74+
75+
## 6. Non-Goals
76+
77+
- Dynamic server-side redirects beyond what Nginx handles.
78+
- Client-side redirect logic (Next.js handles this based on `getStaticPaths`).
79+
80+
## 7. Future Considerations
81+
82+
- Performance of `getStaticJSONPaths` if the number of files, aliases, or redirects becomes extremely large.
83+
- More sophisticated conflict resolution strategies for paths if needed.

scripts/common.ts

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import fs from 'fs/promises';
22
import path from 'path';
3-
import { exec } from 'child_process';
43

54
const CONTENT_DIR = path.join(process.cwd(), 'public/content');
65
const REDIRECTS_JSON_PATH = path.join(CONTENT_DIR, 'redirects.json');
76
const ALIAS_JSON_PATH = path.join(CONTENT_DIR, 'aliases.json');
87

9-
function removingTrailingSlash(path: string): string {
8+
export function removingTrailingSlash(path: string): string {
109
return path.replace(/\/$/, '');
1110
}
1211

@@ -35,20 +34,6 @@ export async function getJSONFileContent(
3534
}
3635
}
3736

38-
export async function execPromise(command: string): Promise<void> {
39-
return new Promise((resolve, reject) => {
40-
const child = exec(command, { cwd: process.cwd() }, (error) => {
41-
if (error) {
42-
reject(error);
43-
} else {
44-
resolve();
45-
}
46-
});
47-
child.stdout?.pipe(process.stdout);
48-
child.stderr?.pipe(process.stderr);
49-
});
50-
}
51-
5237
/**
5338
* Gets all markdown and MDX files recursively from a directory
5439
* @param dir The directory to search in
@@ -174,51 +159,127 @@ export async function getAliasPaths(): Promise<Record<string, string>> {
174159
return Object.assign({}, nestedAliasPaths, aliases);
175160
}
176161

177-
function getIsSelfReferential(entryURL: string, targetURL: string): boolean {
178-
// Check if the entry URL is a self-referential alias
179-
return normalizePathWithSlash(entryURL) === normalizePathWithSlash(targetURL);
180-
}
181-
182-
function getNestedRelatedAliases(
183-
aliases: Record<string, string>,
184-
): Record<string, string> {
185-
const nestedAliases: Record<string, string> = {};
186-
for (const [aliasKey, aliasValue] of Object.entries(aliases)) {
187-
// Split the alias key into segments
188-
const segments = aliasKey.split('/').filter(Boolean);
189-
// Create a new key for each segment
190-
for (let i = 1; i <= segments.length; i++) {
191-
const newKey = segments.slice(i).join('/');
192-
if (!newKey) {
193-
continue; // Skip empty keys
194-
}
195-
const isSelfReferential = getIsSelfReferential(newKey, aliasValue);
196-
if (isSelfReferential) {
197-
break;
198-
}
199-
if (!nestedAliases[newKey]) {
200-
nestedAliases[newKey] = aliasValue;
201-
}
202-
}
203-
// Add the original alias if it doesn't already exist
204-
if (!nestedAliases[aliasKey]) {
205-
nestedAliases[aliasKey] = aliasValue;
206-
}
207-
}
208-
return nestedAliases;
209-
}
210-
211162
export async function getReversedAliasPaths(): Promise<Record<string, string>> {
212163
const aliases = await getAliasPaths();
213164

214165
// Reverse the keys and values
215166
const reversedAliases = Object.fromEntries(
216167
Object.entries(aliases).map(([key, value]) => [value, key]),
217168
);
218-
return getNestedRelatedAliases(reversedAliases);
169+
return reversedAliases;
219170
}
220171

221172
export async function getRedirects(): Promise<Record<string, string>> {
222173
const redirects = await getJSONFileContent(REDIRECTS_JSON_PATH);
223174
return redirects;
224175
}
176+
177+
async function filterRedirectsLogic(
178+
processRedirect: (
179+
normalizedRedirectKey: string,
180+
normalizedRedirectValue: string,
181+
aliasesEntries: [string, string][],
182+
markdownPaths: string[],
183+
filteredRedirects: Record<string, string>,
184+
) => void,
185+
): Promise<Record<string, string>> {
186+
const redirects = await getRedirects();
187+
const aliases = await getReversedAliasPaths();
188+
const allMarkdownFiles = await getAllMarkdownFiles(CONTENT_DIR);
189+
const aliasesEntries = Object.entries(aliases);
190+
191+
const markdownPaths = allMarkdownFiles
192+
.filter(
193+
slugArray =>
194+
!slugArray[0]?.toLowerCase()?.startsWith('contributor') &&
195+
!slugArray[0]?.toLowerCase()?.startsWith('tags'),
196+
)
197+
.map(slugArray => slugArray.join('/'));
198+
199+
const filteredRedirects: Record<string, string> = {};
200+
201+
for (const [redirectKey, redirectValue] of Object.entries(redirects)) {
202+
const normalizedRedirectKey = normalizePathWithSlash(redirectKey);
203+
const normalizedRedirectValue = normalizePathWithSlash(redirectValue);
204+
205+
const isMatchedAlias = aliasesEntries.some(([aliasKey, aliasVal]) => {
206+
const normalizedAliasKey = normalizePathWithSlash(aliasKey);
207+
const normalizedAliasValue = normalizePathWithSlash(aliasVal);
208+
const isMatchedReversedValues =
209+
normalizedRedirectValue === normalizedAliasKey ||
210+
normalizedRedirectKey === normalizedAliasValue;
211+
if (isMatchedReversedValues) {
212+
return true;
213+
}
214+
return normalizedRedirectKey === normalizedAliasKey;
215+
});
216+
217+
if (!isMatchedAlias) {
218+
processRedirect(
219+
normalizedRedirectKey,
220+
normalizedRedirectValue,
221+
aliasesEntries,
222+
markdownPaths,
223+
filteredRedirects,
224+
);
225+
}
226+
}
227+
return filteredRedirects;
228+
}
229+
230+
export async function getNginxRedirects(): Promise<Record<string, string>> {
231+
return filterRedirectsLogic(
232+
(
233+
normalizedRedirectKey,
234+
normalizedRedirectValue,
235+
aliasesEntries,
236+
markdownPaths,
237+
filteredRedirects,
238+
) => {
239+
const aliasRedirectValue = aliasesEntries.find(([aliasKey]) => {
240+
const normalizedAliasKey = normalizePathWithSlash(aliasKey);
241+
return normalizedRedirectValue === normalizedAliasKey;
242+
});
243+
244+
if (aliasRedirectValue) {
245+
filteredRedirects[normalizedRedirectKey] = aliasRedirectValue[1];
246+
return;
247+
}
248+
249+
const isMatchedMdPath = markdownPaths.find(
250+
mdPath => normalizePathWithSlash(mdPath) === normalizedRedirectValue,
251+
);
252+
if (isMatchedMdPath) {
253+
filteredRedirects[normalizedRedirectKey] = isMatchedMdPath;
254+
}
255+
},
256+
);
257+
}
258+
259+
export async function getRedirectsNotToAliases(): Promise<
260+
Record<string, string>
261+
> {
262+
return filterRedirectsLogic(
263+
(
264+
normalizedRedirectKey,
265+
normalizedRedirectValue,
266+
aliasesEntries,
267+
markdownPaths,
268+
filteredRedirects,
269+
) => {
270+
const aliasRedirectValue = aliasesEntries.find(([aliasKey]) => {
271+
const normalizedAliasKey = normalizePathWithSlash(aliasKey);
272+
return normalizedRedirectValue === normalizedAliasKey;
273+
});
274+
275+
if (!aliasRedirectValue) {
276+
const isMatchedMdPath = markdownPaths.find(
277+
mdPath => normalizePathWithSlash(mdPath) === normalizedRedirectValue,
278+
);
279+
if (!isMatchedMdPath) {
280+
filteredRedirects[normalizedRedirectKey] = normalizedRedirectValue;
281+
}
282+
}
283+
},
284+
);
285+
}

scripts/generate-nginx-redirect-map.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import fs from 'fs/promises';
22
import path from 'path';
33
import {
4-
getReversedAliasPaths,
4+
getAllMarkdownFiles,
55
getJSONFileContent,
6+
getNginxRedirects,
7+
getReversedAliasPaths,
68
normalizePathWithSlash,
7-
getAllMarkdownFiles,
89
} from './common.js';
910

1011
const CONTENT_DIR = path.join(process.cwd(), 'public/content');
@@ -59,12 +60,13 @@ async function getValidShortenPaths(alias: Record<string, string> = {}) {
5960

6061
async function generateNginxRedirectMap() {
6162
const alias = await getReversedAliasPaths();
63+
const redirects = await getNginxRedirects();
6264
const shortenRedirects = await getValidShortenPaths(alias);
6365

6466
let mapContent = 'map $request_uri $redirect_target {\n';
6567
mapContent += ' default 0;\n';
6668

67-
const paths = [alias, shortenRedirects];
69+
const paths = [alias, redirects, shortenRedirects];
6870
// Flatten the array of objects into a single array of objects
6971
const flattenedPaths = paths.reduce<Record<string, string>>((acc, obj) => {
7072
const mapEntries = Object.entries(obj).map<Record<string, string>>(

src/components/layout/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const navLinks = [
2424
},
2525
{ title: 'Earn', url: '/earn', Icon: MemoIcons.earn },
2626
{ title: 'Hiring', url: '/careers', Icon: MemoIcons.careers },
27-
{ title: 'Changelog', url: '/updates/changelog', Icon: MemoIcons.updates },
27+
{ title: 'Changelog', url: '/changelog', Icon: MemoIcons.updates },
2828
{ title: 'OGIFs', url: '/updates/ogif', Icon: MemoIcons.ogif },
2929
];
3030

0 commit comments

Comments
 (0)