Przeglądaj źródła

Build a website. (#4189)

Demo site: https://jonmeow.carbon-lang.dev/

I'm trying to keep work under the `/website` subdirectory so that the
misc files don't interfere with unrelated views of the repository. The
`prebuild.py` script does some work to move things around and add
frontmatter, helping the jekyll generation.

I'm using the "just-the-docs" theme because I think it's a decent match
for what we want, and getting jekyll up and running with it wasn't too
difficult. Note #1526 proposed using Docusaurus; I started out there,
but was having trouble getting it working with newer versions. The
plugins in particular I got stuck trying to make work, which sent me
looking for options that we could have working with less customization.
I do lean towards jekyll though, because it's what GH uses so hopefully
we can get a more consistent experience.

Having a website has been approved for a while under #1492, but hasn't
been a priority. I'm mainly doing this because I want to just be able to
point people to carbon-lang.dev and have easy links that way.

---------

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Jon Ross-Perkins 1 rok temu
rodzic
commit
b6396e97f8

+ 1 - 0
.codespell_ignore

@@ -16,4 +16,5 @@ inout
 parameteras
 pullrequest
 rightt
+rouge
 statics

+ 41 - 0
.github/workflows/gh_pages_ci.yaml

@@ -0,0 +1,41 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+name: GitHub Pages CI
+
+on:
+  pull_request:
+
+# Cancel previous workflows on the PR when there are multiple fast commits.
+# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
+jobs:
+  # Build job
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Checkout
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - name: Prebuild actions
+        run: ./website/prebuild.py
+      - name: Setup Ruby
+        uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
+        with:
+          # Runs 'bundle install' and caches installed gems automatically.
+          bundler-cache: true
+          # Increment this number if you need to re-download cached gems.
+          cache-version: 0
+      - name: Build with Jekyll
+        run: bundle exec jekyll build

+ 76 - 0
.github/workflows/gh_pages_deploy.yaml

@@ -0,0 +1,76 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+name: GitHub Pages deploy
+
+on:
+  # Runs on pushes targeting the default branch.
+  push:
+    branches: ['trunk']
+
+  # Allows you to run this workflow manually from the Actions tab.
+  workflow_dispatch:
+
+# Cancel previous workflows on the PR when there are multiple fast commits.
+# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
+  cancel-in-progress: true
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages.
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Checkout
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - name: Prebuild actions
+        run: ./website/prebuild.py
+      - name: Setup Pages
+        uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
+      - name: Setup Ruby
+        uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
+        with:
+          # Runs 'bundle install' and caches installed gems automatically.
+          bundler-cache: true
+          # Increment this number if you need to re-download cached gems.
+          cache-version: 0
+      - name: Build with Jekyll
+        env:
+          JEKYLL_ENV: production
+        run: |
+          bundle exec jekyll build --verbose \
+            --source ./ \
+            --destination ./_site \
+            --baseurl "${{ steps.pages.outputs.base_path }}"
+      - name: Upload artifact
+        # Automatically uploads an artifact from the './_site' directory by
+        # default.
+        uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
+
+  deploy:
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    runs-on: ubuntu-latest
+    needs: build
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5

+ 3 - 1
.pre-commit-config.yaml

@@ -180,7 +180,7 @@ repos:
             Exceptions. See /LICENSE for license information.
             SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
           - --custom_format
-          - '\.(carbon|c|json|proto|ypp)(\.tmpl)?$'
+          - '\.(carbon|c|json|proto|scss|ypp)(\.tmpl)?$'
           - ''
           - '// '
           - ''
@@ -216,6 +216,8 @@ repos:
               compile_flags.txt|
               github_tools/requirements.txt|
               third_party/.*|
+              website/.ruby-version|
+              website/Gemfile.lock|
               .*\.def|
               .*\.svg|
               .*/fuzzer_corpus/.*|

+ 3 - 3
SECURITY.md

@@ -1,11 +1,11 @@
+# Security policy
+
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 
-# Security policy
-
 It's important to us that the Carbon Language provides a secure implementation.
 Thank you for taking the time to report vulnerabilities.
 
@@ -13,7 +13,7 @@ The Carbon Language is still an
 [experimental project](/README.md#project-status), so please be careful if using
 it in security-sensitive environments.
 
-# Reporting a vulnerability
+## Reporting a vulnerability
 
 Please use
 <https://github.com/carbon-language/carbon-lang/security/advisories/new> to

+ 9 - 0
docs/images/snippets.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 ## Images
 
 Images are managed in
@@ -138,3 +143,7 @@ auto main() -> int {
   return 0;
 }
 ```
+
+<!--
+{% endraw %}
+-->

+ 9 - 0
explorer/README.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 `explorer` is an implementation of Carbon whose primary purpose is to act as a
 clear specification of the language. As an extension of that goal, it can also
 be used as a platform for prototyping and validating changes to the language.
@@ -334,3 +339,7 @@ information and provide visual separation for different sections.
 - - - - -  Sub Heading - - - - -
 --------------------------------
 ```
+
+<!--
+{% endraw %}
+-->

+ 2 - 0
explorer/ast/README.md

@@ -1,3 +1,5 @@
+# Explorer AST
+
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.

+ 2 - 0
explorer/syntax/README.md

@@ -1,3 +1,5 @@
+# Explorer Syntax
+
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.

+ 9 - 0
testing/README.md

@@ -0,0 +1,9 @@
+# Testing
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+Testing-specific libraries.

+ 9 - 0
testing/file_test/README.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 ## BUILD
 
 A typical BUILD target will look like:
@@ -156,3 +161,7 @@ Supported comment markers are:
     Tips like this are added by autoupdate, for example providing commands to
     run the test directly. Tips have no impact on validation; the marker informs
     autoupdate that it can update or remove them as needed.
+
+<!--
+{% endraw %}
+-->

+ 1 - 0
website/.ruby-version

@@ -0,0 +1 @@
+3.3

+ 12 - 0
website/404.md

@@ -0,0 +1,12 @@
+---
+title: 404
+nav_exclude: true
+---
+
+# 404: File not found
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->

+ 14 - 0
website/Gemfile

@@ -0,0 +1,14 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+source 'https://rubygems.org'
+
+gem "jekyll", "4.3.3"
+gem "just-the-docs", "0.8.2"
+
+group :jekyll_plugins do
+  gem "jekyll-default-layout", "0.1.5"
+  gem "jekyll-readme-index", "0.3.0"
+  gem "jekyll-relative-links", "0.7.0"
+end

+ 100 - 0
website/Gemfile.lock

@@ -0,0 +1,100 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.8.7)
+      public_suffix (>= 2.0.2, < 7.0)
+    bigdecimal (3.1.8)
+    colorator (1.1.0)
+    concurrent-ruby (1.3.4)
+    em-websocket (0.5.3)
+      eventmachine (>= 0.12.9)
+      http_parser.rb (~> 0)
+    eventmachine (1.2.7)
+    ffi (1.17.0)
+    forwardable-extended (2.6.0)
+    google-protobuf (4.27.3-arm64-darwin)
+      bigdecimal
+      rake (>= 13)
+    google-protobuf (4.27.3-x86_64-linux)
+      bigdecimal
+      rake (>= 13)
+    http_parser.rb (0.8.0)
+    i18n (1.14.5)
+      concurrent-ruby (~> 1.0)
+    jekyll (4.3.3)
+      addressable (~> 2.4)
+      colorator (~> 1.0)
+      em-websocket (~> 0.5)
+      i18n (~> 1.0)
+      jekyll-sass-converter (>= 2.0, < 4.0)
+      jekyll-watch (~> 2.0)
+      kramdown (~> 2.3, >= 2.3.1)
+      kramdown-parser-gfm (~> 1.0)
+      liquid (~> 4.0)
+      mercenary (>= 0.3.6, < 0.5)
+      pathutil (~> 0.9)
+      rouge (>= 3.0, < 5.0)
+      safe_yaml (~> 1.0)
+      terminal-table (>= 1.8, < 4.0)
+      webrick (~> 1.7)
+    jekyll-default-layout (0.1.5)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-include-cache (0.2.1)
+      jekyll (>= 3.7, < 5.0)
+    jekyll-readme-index (0.3.0)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-relative-links (0.7.0)
+      jekyll (>= 3.3, < 5.0)
+    jekyll-sass-converter (3.0.0)
+      sass-embedded (~> 1.54)
+    jekyll-seo-tag (2.8.0)
+      jekyll (>= 3.8, < 5.0)
+    jekyll-watch (2.2.1)
+      listen (~> 3.0)
+    just-the-docs (0.8.2)
+      jekyll (>= 3.8.5)
+      jekyll-include-cache
+      jekyll-seo-tag (>= 2.0)
+      rake (>= 12.3.1)
+    kramdown (2.4.0)
+      rexml
+    kramdown-parser-gfm (1.1.0)
+      kramdown (~> 2.0)
+    liquid (4.0.4)
+    listen (3.9.0)
+      rb-fsevent (~> 0.10, >= 0.10.3)
+      rb-inotify (~> 0.9, >= 0.9.10)
+    mercenary (0.4.0)
+    pathutil (0.16.2)
+      forwardable-extended (~> 2.6)
+    public_suffix (6.0.1)
+    rake (13.2.1)
+    rb-fsevent (0.11.2)
+    rb-inotify (0.11.1)
+      ffi (~> 1.0)
+    rexml (3.3.5)
+      strscan
+    rouge (4.3.0)
+    safe_yaml (1.0.5)
+    sass-embedded (1.77.8)
+      google-protobuf (~> 4.26)
+      rake (>= 13)
+    strscan (3.1.0)
+    terminal-table (3.0.2)
+      unicode-display_width (>= 1.1.1, < 3)
+    unicode-display_width (2.5.0)
+    webrick (1.8.1)
+
+PLATFORMS
+  arm64-darwin
+  x86_64-linux
+
+DEPENDENCIES
+  jekyll (= 4.3.3)
+  jekyll-default-layout (= 0.1.5)
+  jekyll-readme-index (= 0.3.0)
+  jekyll-relative-links (= 0.7.0)
+  just-the-docs (= 0.8.2)
+
+BUNDLED WITH
+   2.5.17

+ 59 - 0
website/README.md

@@ -0,0 +1,59 @@
+# Documentation website
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!-- toc -->
+
+## Table of contents
+
+-   [Overview](#overview)
+-   [Build process](#build-process)
+-   [Development tips](#development-tips)
+-   [Domain configuration](#domain-configuration)
+
+<!-- tocstop -->
+
+## Overview
+
+Carbon's main website is the GitHub project page. Carbon remains too early and
+experimental to have a full-fledged website.
+
+This directories contains infrastructure for building the convenience
+documentation website, which should be at <https://docs.carbon-lang.dev>.
+
+## Build process
+
+Website generation is done by
+[gh_pages_deploy.yaml](/.github/workflows/gh_pages_deploy.yaml). It runs
+`prebuild.py`, which prepares files for website generation, then builds the
+website using Jekyll, configured through `_config.yml`.
+
+## Development tips
+
+[rbenv](https://github.com/rbenv/rbenv) can be used to set up Ruby and `bundle`.
+
+To run a server, run `bundle exec jekyll serve`. See
+[Jekyll docs](https://jekyllrb.com/docs/usage/) for more commands.
+
+To update the `Gemfile.lock` after `Gemfile` changes, run `bundle update`.
+
+## Domain configuration
+
+The
+[custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site)
+is configured in three places:
+
+-   GitHub organization
+    [verified domains](https://github.com/organizations/carbon-language/settings/pages)
+-   GitHub repository
+    [custom domain](https://github.com/carbon-language/carbon-lang/settings/pages)
+-   Google Cloud DNS
+    -   This is visible with `dig docs.carbon-lang.dev`
+
+Note all of these require admin permissions to modify. For sharing test pages, a
+GitHub user and repository can be used, pushing to `<username>.github.io` (or
+getting a custom DNS setup).

+ 25 - 0
website/_config.yml

@@ -0,0 +1,25 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+title: Carbon Language documentation
+description: An experimental successor to C++
+
+# https://just-the-docs.github.io/just-the-docs/
+theme: just-the-docs
+
+favicon_ico: '/favicon.png'
+
+aux_links:
+  Discord: https://discord.gg/ZjVdShJDAs
+  GitHub: https://github.com/carbon-language/carbon-lang
+
+mermaid:
+  # Version of mermaid library
+  # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/
+  version: '10.9.1'
+
+# We add frontmatter to files, but still want README.md -> index.md.
+readme_index:
+  remove_originals: true
+  with_frontmatter: true

+ 33 - 0
website/_includes/footer_custom.html

@@ -0,0 +1,33 @@
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!--
+Used from:
+https://github.com/just-the-docs/just-the-docs/blob/main/_includes/components/footer.html
+-->
+<footer>
+  <p><a href="#top" id="back-to-top">Back to top</a></p>
+
+  <p>
+    <a
+      href="https://github.com/carbon-language/carbon-lang/blob/trunk/{{ page.path }}"
+      id="edit-this-page"
+      >View on GitHub</a
+    >
+  </p>
+
+  <p class="text-small">
+    Part of the Carbon Language project, licensed under the
+    <a href="https://github.com/carbon-language/carbon-lang/blob/trunk/LICENSE"
+      >Apache License v2.0 with LLVM Exceptions</a
+    >.
+  </p>
+
+  <p class="text-small">
+    This site is hosted on GitHub Pages and uses
+    <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>.
+  </p>
+</footer>

+ 12 - 0
website/_includes/nav_footer_custom.html

@@ -0,0 +1,12 @@
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!--
+Used from:
+https://github.com/just-the-docs/just-the-docs/blob/main/_includes/components/sidebar.html
+
+This just removes the nav footer; we use `footer_custom.html` instead.
+-->

+ 47 - 0
website/_sass/custom/custom.scss

@@ -0,0 +1,47 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This is modifying the builtin scss, particularly:
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/layout.scss
+
+.site-nav {
+  // The navbar will typically overflow, so keep a scrollbar so that the zippies
+  // don't move on open/close.
+  overflow-y: scroll;
+}
+
+.side-bar {
+  @include mq(lg) {
+    // Allow left alignment instead of centering.
+    width: #{$nav-width};
+  }
+
+  & + .main {
+    @include mq(lg) {
+      // Allow left alignment instead of centering.
+      margin-left: #{$nav-width};
+    }
+  }
+}
+
+.site-footer {
+  color: #57606a;
+}
+
+// Comments in code.
+.highlight .c {
+  color: #57606a;
+}
+
+// Misc syntax in code.
+.highlight .nb,
+.highlight .nt,
+.highlight .nv {
+  color: #a36000;
+}
+
+// Strings in code.
+.highlight .s2 {
+  color: #3f7f3f;
+}

+ 14 - 0
website/_sass/custom/setup.scss

@@ -0,0 +1,14 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This is modifying the builtin scss, particularly:
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/color_schemes/light.scss
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/support/_variables.scss
+
+// Prefer wider content.
+$nav-width: 20rem;
+$content-width: 60rem;
+
+$code-background-color: #f6f8fa;
+$body-text-color: #1f2328;

BIN
website/favicon.png


+ 9 - 0
website/implementation.md

@@ -0,0 +1,9 @@
+# Implementation
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+These documents cover components of Carbon's reference implementation.

+ 198 - 0
website/prebuild.py

@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+"""Updates files in preparation for a jekyll build.
+
+Used from .github/workflows/gh_pages.yaml. This updates the file and directory
+structure prior to the jekyll build.
+"""
+
+import dataclasses
+import os
+from pathlib import Path
+import re
+from typing import Optional
+
+__copyright__ = """
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""
+
+
+@dataclasses.dataclass
+class ChildDir:
+    """Tracks whether a child directory has grandchildren."""
+
+    title: str
+    has_grandchildren: bool = False
+
+
+def get_title(f: Path, content: str) -> str:
+    """Returns a file's title according to markdown.
+
+    Replacements are for YAML compatibility in `add_frontmatter`.
+    """
+    m = re.search("^# (.*)$", content, re.MULTILINE)
+    assert m, f
+    title = m[1]
+    title = title.replace("\\", "\\\\")
+    title = title.replace('"', '\\"')
+    title = re.sub("`([^`]+)`", r"<code>\1</code>", title)
+    return title
+
+
+def add_frontmatter(
+    f: Path,
+    orig_content: str,
+    titles: list[str],
+    nav_order: Optional[int],
+    has_children: bool,
+) -> None:
+    """Adds frontmatter to a file."""
+    content = "---\n"
+
+    assert len(titles) <= 3
+    content += f'title: "{titles[-1]}"\n'
+    if len(titles) >= 2:
+        content += f'parent: "{titles[-2]}"\n'
+    if len(titles) == 3:
+        content += f'grand_parent: "{titles[-3]}"\n'
+
+    if nav_order is not None:
+        content += f"nav_order: {nav_order}\n"
+    if has_children:
+        # has_toc is disabling the list of child files in a separate table of
+        # contents.
+        content += "has_children: true\nhas_toc: false\n"
+    content += "---\n\n" + orig_content
+    f.write_text(content)
+
+
+def label_subdir(
+    subdir_str: str,
+    top_nav_order: int,
+    parent_title: Optional[str] = None,
+    grandchild_dirs: bool = False,
+) -> None:
+    """Automatically adds child information to a subdirectory's markdown files.
+
+    This is in support of navigation.
+    """
+    assert not (parent_title and grandchild_dirs)
+
+    subdir = Path(subdir_str)
+    readme = subdir / "README.md"
+    readme_content = readme.read_text()
+
+    readme_title = get_title(readme, readme_content)
+    readme_titles = [readme_title]
+    if parent_title:
+        readme_titles.insert(0, parent_title)
+    children = [x for x in subdir.glob("**/*.md") if x != readme]
+    add_frontmatter(
+        readme, readme_content, readme_titles, top_nav_order, bool(children)
+    )
+
+    if grandchild_dirs:
+        # When adding grandchildren, we cluster files by child directory.
+        child_dirs = {}
+        for child in children:
+            if child.name == "README.md":
+                title = get_title(child, child.read_text())
+                child_dirs[child.parent] = ChildDir(title)
+        for child in children:
+            if child.name != "README.md" and child.parent in child_dirs:
+                child_dirs[child.parent].has_grandchildren = True
+
+    for child in children:
+        child_content = child.read_text()
+        child_title = get_title(child, child_content)
+        child_nav_order = None
+
+        if subdir_str == "proposals":
+            # Use proposal numbers as part of the title and ordering.
+            m = re.match(r"p(\d+).md", child.name)
+            # Skip files that aren't proposals.
+            if not m:
+                continue
+            child_title = f"#{m[1]}: {child_title}"
+            child_nav_order = int(m[1])
+
+        titles = [readme_title, child_title]
+        has_children = False
+        if parent_title:
+            titles.insert(0, parent_title)
+        elif grandchild_dirs and child.parent in child_dirs:
+            if child.name == "README.md":
+                has_children = child_dirs[child.parent].has_grandchildren
+            else:
+                dir_title = child_dirs[child.parent].title
+                titles.insert(1, dir_title)
+
+        add_frontmatter(
+            child, child_content, titles, child_nav_order, has_children
+        )
+
+
+def label_root_file(
+    name: str, title: str, top_nav_order: int, has_children: bool = False
+) -> None:
+    """Adds frontmatter to a root file, like CONTRIBUTING.md."""
+    f = Path(name)
+    add_frontmatter(f, f.read_text(), [title], top_nav_order, has_children)
+
+
+def main() -> None:
+    # Ensure this runs from the repo root.
+    os.chdir(Path(__file__).parent.parent)
+
+    # bazel-execroot interferes with jekyll because it's a broken symlink.
+    Path("bazel-execroot").unlink()
+
+    # The external symlink is created by scripts/create_compdb.py, and can
+    # interfere with local execution.
+    external = Path("external")
+    if external.exists():
+        external.unlink()
+
+    # Move files to the repo root.
+    for f in Path("website").iterdir():
+        if f.name == "README.md":
+            continue
+        f.rename(f.name)
+
+    # Use an object for a reference.
+    nav_order = [0]
+
+    # Returns an incrementing value for ordering.
+    def next(nav_order: list[int]) -> int:
+        nav_order[0] += 1
+        return nav_order[0]
+
+    label_root_file("README.md", "Home", next(nav_order))
+    label_root_file("CONTRIBUTING.md", "Contributing", next(nav_order))
+    label_subdir("docs/design", next(nav_order), grandchild_dirs=True)
+    label_subdir("docs/guides", next(nav_order))
+    label_subdir("docs/project", next(nav_order), grandchild_dirs=True)
+    label_subdir("docs/spec", next(nav_order))
+    # Provide a small file to cluster implementation-related directories.
+    label_root_file(
+        "implementation.md",
+        "Implementation",
+        next(nav_order),
+        has_children=True,
+    )
+    label_subdir("utils", next(nav_order))
+    label_subdir("proposals", next(nav_order))
+    label_root_file("CODE_OF_CONDUCT.md", "Code of conduct", next(nav_order))
+    label_root_file("SECURITY.md", "Security policy", next(nav_order))
+
+    # Reset the order for the implementation children.
+    nav_order[0] = 0
+    label_subdir("toolchain", next(nav_order), parent_title="Implementation")
+    label_subdir("explorer", next(nav_order), parent_title="Implementation")
+    label_subdir("testing", next(nav_order), parent_title="Implementation")
+
+
+if __name__ == "__main__":
+    main()