Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ jobs:
../bin/compile build /tmp
mv build/bin/nginx ../bin/
- run:
name: Require redirects file to be generated
command: test -f config/nginx-redirects.conf
name: Require redirects file to be generated with content
command: |
./bin/assert-nginx-redirects.sh
- run:
name: Verify all files are compressed
command: ./bin/assert-compressed.sh
Expand Down
29 changes: 29 additions & 0 deletions bin/assert-nginx-redirects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash

#
# A utility script to assert that the nginx redirects configuration file exists and is valid
#
# Usage: assert-nginx-redirects.sh
#

echo "Validating redirect file..."

# Check file exists
if [ ! -f config/nginx-redirects.conf ]; then
echo "ERROR: config/nginx-redirects.conf does not exist"
exit 1
fi
echo "✓ File exists"

# Check file is not empty
if [ ! -s config/nginx-redirects.conf ]; then
echo "ERROR: config/nginx-redirects.conf is empty (0 bytes)"
ls -lah config/nginx-redirects.conf
exit 1
fi

# Count redirects (lines ending with semicolon)
REDIRECT_COUNT=$(grep -c ';$' config/nginx-redirects.conf || echo "0")
echo "✓ Found ${REDIRECT_COUNT} redirects"

echo "✓ Validation passed: ${REDIRECT_COUNT} redirects in config/nginx-redirects.conf"
20 changes: 16 additions & 4 deletions data/createPages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import { createLanguagePageVariants } from './createPageVariants';
import { LATEST_ABLY_API_VERSION_STRING } from '../transform/constants';
import { createContentMenuDataFromPage } from './createContentMenuDataFromPage';
import { DEFAULT_LANGUAGE } from './constants';
import { writeRedirectToConfigFile } from './writeRedirectToConfigFile';
import { writeRedirectToConfigFile, getRedirectCount } from './writeRedirectToConfigFile';
import { siteMetadata } from '../../gatsby-config';
import { GatsbyNode } from 'gatsby';
import { GatsbyNode, Reporter } from 'gatsby';
import { examples, DEFAULT_EXAMPLE_LANGUAGES } from '../../src/data/examples/';
import { Example } from '../../src/data/examples/types';

const writeRedirect = writeRedirectToConfigFile('config/nginx-redirects.conf');
const documentTemplate = path.resolve(`src/templates/document.tsx`);
const apiReferenceTemplate = path.resolve(`src/templates/apiReference.tsx`);
const examplesTemplate = path.resolve(`src/templates/examples.tsx`);
Expand Down Expand Up @@ -101,7 +100,11 @@ interface MdxRedirectsQueryResult {
};
}

export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions: { createPage, createRedirect } }) => {
export const createPages: GatsbyNode['createPages'] = async ({
graphql,
actions: { createPage, createRedirect },
reporter,
}) => {
/**
* It's not ideal to have:
* * the reusable function `documentCreator` defined inline like this
Expand All @@ -111,6 +114,9 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
* and testable function.
*/

// Initialize redirect writer with reporter
const writeRedirect = writeRedirectToConfigFile('config/nginx-redirects.conf', reporter);

// DOCUMENT TEMPLATE
const documentResult = await graphql<DocumentQueryResult>(`
query {
Expand Down Expand Up @@ -245,6 +251,8 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
// We need to be prefix aware just like Gatsby's internals so it works
// with nginx redirects
writeRedirect(redirectFrom, pagePath);
} else {
reporter.info(`[REDIRECTS] Skipping hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`);
}

createRedirect({
Expand Down Expand Up @@ -341,6 +349,8 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
// We need to be prefix aware just like Gatsby's internals so it works
// with nginx redirects
writeRedirect(redirectFrom, toPath);
} else {
reporter.info(`[REDIRECTS] Skipping MDX hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`);
}

createRedirect({
Expand All @@ -360,4 +370,6 @@ export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions:
...examples.map(exampleCreator),
...mdxRedirectsResult.data.allMdx.nodes.map(mdxRedirectCreator),
]);

reporter.info(`[REDIRECTS] Completed writing ${getRedirectCount()} redirects`);
};
40 changes: 37 additions & 3 deletions data/createPages/writeRedirectToConfigFile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
import * as fs from 'fs';
import { Reporter } from 'gatsby';

let redirectCount = 0;
let isInitialized = false;

export const writeRedirectToConfigFile = (filePath: string, reporter?: Reporter) => {
// Detect re-initialization
if (isInitialized) {
reporter?.warn(`[REDIRECTS] WARNING: writeRedirectToConfigFile called multiple times!`);
return (from: string, to: string) => {
reporter?.warn(`[REDIRECTS] Skipping redirect write due to re-initialization: ${from} -> ${to}`);
};
}

reporter?.info(`[REDIRECTS] Initializing redirect file at ${filePath}`);

try {
fs.writeFileSync(filePath, '');
isInitialized = true;
} catch (error) {
reporter?.error(`[REDIRECTS] Failed to initialize redirect file: ${error}`);
throw error;
}

export const writeRedirectToConfigFile = (filePath: string) => {
fs.writeFileSync(filePath, '');
return (from: string, to: string) => {
fs.appendFileSync(filePath, createRedirectForConfigFile(from, to));
try {
fs.appendFileSync(filePath, createRedirectForConfigFile(from, to));
redirectCount++;
} catch (error) {
reporter?.error(`[REDIRECTS] Failed to write redirect ${from} -> ${to}: ${error}`);
throw error;
}
};
};

export const getRedirectCount = (): number => redirectCount;

export const resetRedirectCount = (): void => {
redirectCount = 0;
isInitialized = false;
};

const createRedirectForConfigFile = (from: string, to: string): string => `${from} ${to};\n`;
30 changes: 29 additions & 1 deletion data/onPostBuild/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
import { GatsbyNode } from 'gatsby';
import { GatsbyNode, Reporter } from 'gatsby';
import { onPostBuild as llmstxt } from './llmstxt';
import { onPostBuild as compressAssets } from './compressAssets';
import { validateRedirectFile, REDIRECT_FILE_PATH } from '../utils/validateRedirectFile';

const validateRedirects = async (reporter: Reporter): Promise<void> => {
reporter.info(`[REDIRECTS] Validating redirect file...`);

const result = validateRedirectFile({
minRedirects: 10, // arbitrarily small, just enough to get a sense of if things are healthy
validateFormat: true,
});

// Report errors
if (result.errors.length > 0) {
result.errors.forEach((error) => reporter.error(`[REDIRECTS] ${error}`));
reporter.panicOnBuild(
`CRITICAL: ${REDIRECT_FILE_PATH} validation failed. This will cause all redirects to fail in production!`,
);
return;
}

// Report warnings
result.warnings.forEach((warning) => reporter.warn(`[REDIRECTS] ${warning}`));

// Report success
reporter.info(`[REDIRECTS] ✓ Validation passed: ${result.lineCount} redirects found`);
};

export const onPostBuild: GatsbyNode['onPostBuild'] = async (args) => {
// Validate redirects first - fail fast if there's an issue
await validateRedirects(args.reporter);

// Run all onPostBuild functions in sequence
await llmstxt(args);
await compressAssets(args);
Expand Down
85 changes: 85 additions & 0 deletions data/utils/validateRedirectFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as fs from 'fs';

export const REDIRECT_FILE_PATH = 'config/nginx-redirects.conf';

export interface RedirectValidationResult {
exists: boolean;
lineCount: number;
isValid: boolean;
errors: string[];
warnings: string[];
}

/**
* Validates the nginx redirects configuration file
* @param options Optional configuration for validation
* @returns Validation result with status and any errors/warnings
*/
export const validateRedirectFile = (options?: {
minRedirects?: number;
validateFormat?: boolean;
}): RedirectValidationResult => {
const { minRedirects = 0, validateFormat = true } = options ?? {};

const result: RedirectValidationResult = {
exists: false,
lineCount: 0,
isValid: true,
errors: [],
warnings: [],
};

// Check if file exists
if (!fs.existsSync(REDIRECT_FILE_PATH)) {
result.errors.push(`${REDIRECT_FILE_PATH} does not exist`);
result.isValid = false;
return result;
}
result.exists = true;

// Read and count non-empty lines
const content = fs.readFileSync(REDIRECT_FILE_PATH, 'utf-8');
const lines = content.trim().split('\n').filter((line) => line.length > 0);
result.lineCount = lines.length;

// Check if file has content
if (result.lineCount === 0) {
result.errors.push(`${REDIRECT_FILE_PATH} is empty (no redirect rules found)`);
result.isValid = false;
return result;
}

// Check minimum redirect count
if (minRedirects > 0 && result.lineCount < minRedirects) {
result.warnings.push(
`Found only ${result.lineCount} redirects, expected at least ${minRedirects}. ` +
`This may indicate an issue with redirect generation.`,
);
}

// Validate redirect format if requested
if (validateFormat) {
const invalidLines = lines.filter((line) => !line.match(/^\/[^\s]+ \/[^\s]+;$/));

if (invalidLines.length > 0) {
result.warnings.push(
`Found ${invalidLines.length} lines with invalid redirect format. ` +
`Expected format: "/from /to;" - First few: ${invalidLines.slice(0, 3).map((l) => `"${l}"`).join(', ')}`,
);
}
}

return result;
};

/**
* Counts the number of redirect lines written so far
* Used for in-process validation during build
*/
export const countRedirectLines = (): number => {
if (!fs.existsSync(REDIRECT_FILE_PATH)) {
return 0;
}
const content = fs.readFileSync(REDIRECT_FILE_PATH, 'utf-8');
return content.trim().split('\n').filter((line) => line.length > 0).length;
};