Skip to content
Snippets Groups Projects
Select Git revision
  • eread/make-linting-docker-image-also-a-site-build-docker-image
  • main default protected
  • sselhorn-main-patch-67701
  • release-17-9
  • 17.9 protected
  • 172-extract-translatable-strings-from-templates-vue
  • janis-add-pages-parallel-deployments-to-nav
  • 261-search-links-on-the-results-page-cannot-be-opened-in-a-new-tab
  • esahlani-add-new-solution-docs-to-nav
  • adjust-col-widths
  • release-vers
  • axil-deploy-prior-17-9
  • 183-prepare-project-for-launch-content-and-domain-updates
  • launch-checklist
  • release-17-8
  • eread/use-gum-and-log-for-debug-output
  • pages-test
  • support-asciidoctor
  • eread/run-lychee-against-built-site
  • elastic-faux-phrase-query
  • alpha
21 results

pages_not_in_nav.cjs

pages_not_in_nav.cjs 4.88 KiB
#!/usr/bin/env node

/**
 * @file pages_not_in_nav.js
 * Generates a report of pages which are not included in navigation.yaml.
 */

const fs = require("fs");
const path = require("path");
const glob = require("glob");
const yaml = require("js-yaml");

// Configuration
const CONFIG = {
  excludePatterns: [
    /^architecture\//,
    /^legal\//,
    /^drawers\//,
    /^operator\/adr\//,
    /^charts\/development\//,
    /^development\//,
    /^omnibus\/development\//,
    /^user\/application_security\/dast\/browser\/checks\/.+/,
    /^user\/application_security\/api_security_testing\/checks\/.+/,
  ],
  skipReasons: [
    { condition: "isRedirect", message: "redirected" },
    { condition: "isDeprecated", message: "deprecated" },
    { condition: "isIgnored", message: "ignored" },
  ],
  colors: {
    info: "\x1b[32m",
    warn: "\x1b[33m",
    error: "\x1b[31m",
    reset: "\x1b[0m",
    italics: "\u001b[3m",
  },
};

// Utility functions
const utils = {
  shouldExclude: (eachPath) =>
    CONFIG.excludePatterns.some((pattern) => pattern.test(eachPath)),

  getRelativePath: (filename, source, productConfig) => {
    const absoluteProjectRoot = path.resolve(process.cwd());
    const absoluteCloneDir = path.resolve(
      absoluteProjectRoot,
      productConfig.products[source].clone_dir,
    );

    const absoluteDocsDir = path.join(
      absoluteCloneDir,
      productConfig.products[source].docs_dir,
    );
    return path.relative(absoluteDocsDir, filename);
  },

  getPageData: (filename) => {
    const safeFilename = path.resolve(filename);
    if (!fs.existsSync(safeFilename)) {
      throw new Error("File does not exist");
    }
    const contents = fs.readFileSync(safeFilename, "utf-8");
    const title = contents
      .split("\n")
      .filter((line) => line.startsWith("title: "))
      .toString()
      .toLowerCase();
    return {
      filename: safeFilename,
      isRedirect: contents.includes("redirect_to"),
      isDeprecated:
        title.includes("(deprecated)") || title.includes("(removed)"),
      isIgnored: contents.includes("ignore_in_report"),
    };
  },

  log: (type, message) =>
    // eslint-disable-next-line no-console
    console[type](
      `${CONFIG.colors[type]}${type.toUpperCase()}:${CONFIG.colors.reset} ${message}`,
    ),
};

// Load files and output the report
async function main() {
  const projectRoot = process.cwd();
  const PRODUCTS_YAML_PATH = path.join(projectRoot, "data", "products.yaml");
  const NAVIGATION_YAML_PATH = path.join(
    projectRoot,
    "data",
    "navigation.yaml",
  );

  const productConfig = yaml.load(fs.readFileSync(PRODUCTS_YAML_PATH, "utf8"), {
    schema: yaml.FAILSAFE_SCHEMA,
  });
  const navYaml = yaml.load(fs.readFileSync(NAVIGATION_YAML_PATH, "utf8"), {
    schema: yaml.FAILSAFE_SCHEMA,
  });
  const nav = JSON.stringify(navYaml);

  for (const source in productConfig.products) {
    // Filter unwanted properties from the prototype chain (Eslint)
    if (productConfig.products[source]) {
      const product = productConfig.products[source];
      const absoluteProjectRoot = path.resolve(process.cwd());
      const absoluteCloneDir = path.resolve(
        absoluteProjectRoot,
        product.clone_dir,
      );
      const absoluteDocsDir = path.join(absoluteCloneDir, product.docs_dir);
      const globPattern = path.join(absoluteDocsDir, "**", "*.md");

      glob.sync(globPattern).forEach((filename) => {
        try {
          const pageData = utils.getPageData(filename);

          // Skip the page if it meets a specified skip reason
          for (const { condition, message } of CONFIG.skipReasons) {
            if (pageData[condition]) {
              if (process.env.VERBOSE) {
                utils.log("info", `skipping ${message} page: ${filename}.`);
              }
              return;
            }
          }

          // Convert the markdown filepath into a string that matches the URL path on the website.
          let relativePath = utils
            .getRelativePath(filename, source, productConfig)
            .replace("_index.md", "")
            .replace(".md", "/");

          // Non-GitLab projects include their project name in the path
          if (source !== "gitlab") {
            relativePath = `${source}/${relativePath}`;
          }
          if (
            !nav.includes(relativePath) &&
            !utils.shouldExclude(relativePath)
          ) {
            utils.log(
              "warn",
              `page at ${CONFIG.colors.italics}https://docs.gitlab.com/${relativePath}${CONFIG.colors.reset} is missing from global navigation!`,
            );
          }
        } catch (error) {
          utils.log(
            "error",
            `skipping '${filename}' because of error: '${error}'\nFix '${filename}' and try again.`,
          );
        }
      });
    }
  }
}

// Run the script
main().catch((error) => {
  utils.log("error", `An unexpected error occurred: ${error}`);
  process.exit(1);
});