Home

Exploring Next.js: A Journey into Static Site Generation

I decided to recreate static generation similar Next.js. Next.js provides a seamless way to pre-render pages at build time. It generates static HTML files for each route, which are served to users with minimal server-side processing. The key components of this process include:

  • Static HTML Files: Generated for each page based on the provided data.
  • Assets: Such as images, CSS, and JavaScript files, which are referenced by these HTML files.
  • Routing: Defines how pages are organized and accessed.

An Approach to Recreating Static Generation

  1. Generating the dist Folder The dist folder is where all generated static files reside. To mimic Next.js static generation, I manually created this folder and populated it with the necessary files based on the routing structure of the application.

    Page Routing: I defined the routing structure by analyzing the page components and their corresponding routes. Each route was associated with an HTML file in the dist folder. For instance, the route /about would correspond to an about.html file.

    HTML Generation: I manually created static HTML files for each route. These files included the pre-rendered content and the necessary links to CSS and JavaScript files.

  2. Copying Resources To ensure that all resources are correctly available in the static build, I needed to carefully copy and organize images, CSS, and JavaScript files.

    Images: I collected all static images used across the pages and placed them in an images directory within dist. Each HTML file was updated to reference the correct path to these images.

    CSS Files: CSS files were placed in a styles directory within dist. I ensured that each HTML file linked to the appropriate CSS files to maintain the visual styling of the pages.

    JavaScript Files: Any JavaScript required for interactive elements was also included in the dist folder. I made sure that the HTML files referenced these scripts correctly.

The snippet of router returns proper resources

app.get("*", async (req, res) => {
  const bundle = distBundles[req.originalUrl];
  const page = distPages[req.originalUrl];

  if (page) {
    return res.sendFile(path.join(process.cwd(), "dist/pages", page));
  }

  if (bundle) {
    return res.sendFile(path.join(process.cwd(), "dist/pages", bundle));
  }

  if (req.originalUrl.includes("/globals.css")) {
    return res.sendFile(path.join(process.cwd(), "dist/globals.css"));
  }

  if (req.originalUrl.includes("/resources/")) {
    return res.sendFile(path.join(process.cwd(), "dist", req.originalUrl));
  }
});

This is a starting point of the Node.js app

app.listen(port, async () => {
  createDistFolder();
  copyFolderFromTo("src/resources", "dist/resources");
  copyFileFromFolderToFolder(`src/globals.css`, `dist/globals.css`);

  pages = traverseFolder("src/pages", []);

  for (let index = 0; index < pages.length; index++) {
    const page = pages[index];

    if (page.includes("layout.tsx")) {
      continue;
    }

    let tsxPage: Record<string, any> = {};

    if (page.includes("page.tsx")) {
      tsxPage = await import(`./pages/${page}`);
    }

    if (bracketsRegex.test(page)) {
      const pageIds = tsxPage.getStaticPageIds() || [];
      for (const pageId of pageIds) {
        const genPagePath = page
          .replace(bracketsRegex, pageId)
          .replace("/page.tsx", "");

        createFolderIfNotExist(`src/pages/${genPagePath}`);

        copyFileFromFolderToFolder(
          `src/pages/${page}`,
          `src/pages/${genPagePath}/page.tsx`
        );

        await createStaticPage(
          `${genPagePath}/page.tsx`,
          tsxPage,
          pages,
          pageId
        );

        removeFolderAndItsContent(`src/pages/${genPagePath}`);
      }
    } else {
      await createStaticPage(page, tsxPage, pages);
    }
  }

  console.log(`Server is running on http//localhost:${port}`);
});

So basically it recreates a structure from pages folders, build every page, fill it with appropriate data e.g. images or js. This is a page generation

const createStaticPage = async (
  page: string,
  tsxPage: Record<string, any>,
  pages: string[],
  pageId?: string
) => {
  const pathLength = page.split("/").length - 1;
  let cssPath = "";
  for (let i = 0; i < pathLength; i++) {
    cssPath += "../";
  }
  cssPath += "globals.css";

  let Layout: any = null;
  const Page = tsxPage.default;
  const fullPath = createDistFolders(page);
  const pathWithoutExtension = fullPath
    .replace("/", "")
    .replace("/page.tsx", "");
  const layoutPath = findLayout(`${pathWithoutExtension}/layout.tsx`, pages);

  if (layoutPath) {
    const tsxLayout = await import(`./pages/${layoutPath}`);
    Layout = tsxLayout.default;
  }

  // create static and dynamic page content
  let staticPage: string = "";
  let dynamicPage = Layout ? "<Layout><Page /></Layout>" : "<Page />";
  let data: Record<string, any> | null = null;

  if (tsxPage.getStaticProps) {
    data = await tsxPage.getStaticProps(pageId);
    staticPage = Layout
      ? renderToString(
          <Layout>
            <Page {...data} />
          </Layout>
        )
      : renderToString(<Page {...data} />);
  } else {
    staticPage = Layout
      ? renderToString(
          <Layout>
            <Page />
          </Layout>
        )
      : renderToString(<Page />);
  }

  if (data) {
    dynamicPage = Layout
      ? "<Layout><Page {..." + JSON.stringify(data) + "} /></Layout>"
      : "<Page {..." + JSON.stringify(data) + "} />";
  }

  // copyFileFromFolderToFolder(`src/globals.css`, `dist/${cssPath}`);
  createReactPageEntryPoint(fullPath, dynamicPage, layoutPath);

  // create js bundle for dynamic page
  const bundleName = `bundle.js`;
  buildPageJsBundle(fullPath, bundleName);

  distPages[pathWithoutExtension || "/"] = `${pathWithoutExtension}/page.html`;

  distBundles[`${pathWithoutExtension}/${bundleName}`] =
    `${pathWithoutExtension}/${bundleName}`;

  const outputHtml = staticTemplate(
    staticPage,
    `${pathWithoutExtension}/${bundleName}`,
    cssPath
  );

  if (typeof outputHtml === "string") {
    createHtmlPage(fullPath, outputHtml);
  }

  // delete helper page after build app.tsx
  deleteReactPageEntryPoint();
};

I've never published this project, it was done for the investigation purposes