Browse Source

Merge pull request #5539 from mhilbrunner/redirect-all-the-things

ReadTheDocs redirect automation.
Max Hilbrunner 3 years ago
parent
commit
2c88e0b548

+ 4 - 0
.gitignore

@@ -1,3 +1,7 @@
+*.csv
+!redirects.csv
+.env
+
 _build/
 env/
 __pycache__

+ 47 - 0
_tools/redirects/README.md

@@ -0,0 +1,47 @@
+# ReadTheDocs redirect tools
+
+The scripts located in this directory help in creating and maintaining redirects on [Read the Docs](https://readthedocs.io).
+Also refer to Read the Docs [API documentation](https://docs.readthedocs.io/en/stable/api/index.html).
+
+Note that RTD redirects only apply in case of 404 errors, and to all branches and languages:
+<https://docs.readthedocs.io/en/stable/user-defined-redirects.html>.
+If this ever changes, we need to rework how we manage these (likely adding per-branch logic).
+
+`convert_git_renames_to_csv.py` creates a list of renamed files in Git to create redirects for.
+`create_redirects.py` is used to actually manage redirects on ReadTheDocs.
+
+For more information on the scripts themselves, see their help output.
+
+## Setup
+
+To install requirements: `pip3 install -r requirements.txt`.
+Git is also required and needs to be available in the `PATH`.
+To interact with the Read the Docs API, a valid API key must be set as
+`RTD_AUTH_TOKEN` (either as a environment variable or in a [.env file](https://pypi.org/project/python-dotenv/)).
+
+## Usage
+
+Lets say we recently renamed some files in the Git branch `3.4` (compared to the `stable` branch), and now we want to create redirects for these.
+For this, we would (after setting up the API token and requirements, see Setup above):
+
+> python convert_git_renames_to_csv.py stable 3.4
+
+This should output a list of the redirects to create. Lets append these to the redirects file:
+
+> python convert_git_renames_to_csv.py stable 3.4 >> redirects.csv
+
+After this, redirects for renamed files should have been appended to `redirects.csv`. You may want to double check that!
+Now lets submit these to ReadTheDocs and create redirects there:
+
+> python create_redirects.py
+
+And that should be it!
+
+The script takes care to not add duplicate redirects if the same ones already exist.
+The created redirects are also valid for all branches and languages, which works out
+as they only apply for actually missing files - when a user encounters a 404, that is.
+
+The script also only touches `page` type redirects, all other types may still be added
+and managed manually on RTD or via other means. All `page` redirects need to
+be managed with these tools however, as they will otherwise just overwrite any
+changes made elsewhere.

+ 41 - 22
_tools/redirects/convert_git_renames_to_csv.py

@@ -1,4 +1,6 @@
-"""Uses git to list files that were renamed between two revisions and converts
+#!/usr/bin/env python3
+
+"""Uses Git to list files that were renamed between two revisions and converts
 that to a CSV table.
 
 Use it to prepare and double-check data for create_redirects.py.
@@ -9,32 +11,40 @@ import argparse
 import csv
 import sys
 
-try:
-    subprocess.check_output(["git", "--version"])
-except subprocess.CalledProcessError:
-    print("Git not found. It's required to run this program.")
-
 
 def parse_command_line_args():
     parser = argparse.ArgumentParser(
-        description="Uses git to list files that were renamed between two revisions and "
+        description="Uses Git to list files that were renamed between two revisions and "
         "converts that to a CSV table. Use it to prepare and double-check data for create_redirects.py."
     )
     parser.add_argument(
         "revision1",
         type=str,
-        help="Start revision to get renamed files from.",
+        help="Start revision to get renamed files from (old).",
     )
     parser.add_argument(
         "revision2",
         type=str,
-        help="End revision to get renamed files from.",
+        help="End revision to get renamed files from (new).",
     )
     parser.add_argument("-f", "--output-file", type=str, help="Path to the output file")
     return parser.parse_args()
 
 
+def dict_item_to_str(item):
+    s = ""
+    for key in item:
+        s += item[key]
+    return s
+
+
 def main():
+    try:
+        subprocess.check_output(["git", "--version"])
+    except subprocess.CalledProcessError:
+        print("Git not found. It's required to run this program.")
+        exit(1)
+
     args = parse_command_line_args()
     assert args.revision1 != args.revision2, "Revisions must be different."
     for revision in [args.revision1, args.revision2]:
@@ -49,7 +59,7 @@ def main():
         except subprocess.CalledProcessError:
             print(
                 f"Revision {revision} not found in this repository. "
-                "Please make sure that both revisions exist locally in your git repository."
+                "Please make sure that both revisions exist locally in your Git repository."
             )
             exit(1)
 
@@ -68,25 +78,34 @@ def main():
         .decode("utf-8")
         .split("\n")
     )
-    renamed_documents = [f for f in renamed_files if f.endswith(".rst")]
+    renamed_documents = [f for f in renamed_files if f.lower().endswith(".rst")]
 
     csv_data: list[dict] = []
-    branch = args.revision2
+
     for document in renamed_documents:
         _, source, destination = document.split("\t")
+        source = source.replace(".rst", ".html")
+        destination = destination.replace(".rst", ".html")
+        if not source.startswith("/"):
+            source = "/" + source
+        if not destination.startswith("/"):
+            destination = "/" + destination
         csv_data.append(
-            {"source": source, "destination": destination, "branch": branch}
+            {"source": source, "destination": destination}
         )
 
-    if args.output_file:
-        with open(args.output_file, "w") as f:
-            writer = csv.DictWriter(f, fieldnames=csv_data[0].keys()).writerows(
-                csv_data
-            )
-            writer.writeheader()
-            writer.writerows(csv_data)
-    else:
-        writer = csv.DictWriter(sys.stdout, fieldnames=csv_data[0].keys())
+    if len(csv_data) < 1:
+        print("No renames found for", args.revision1, "->", args.revision2)
+        return
+
+    csv_data.sort(key=dict_item_to_str)
+
+    out = args.output_file
+    if not out:
+        out = sys.stdout.fileno()
+
+    with open(out, "w", encoding="utf-8", newline="") as f:
+        writer = csv.DictWriter(f, fieldnames=csv_data[0].keys())
         writer.writeheader()
         writer.writerows(csv_data)
 

+ 284 - 71
_tools/redirects/create_redirects.py

@@ -1,111 +1,324 @@
-"""Create page redirects for a specific branch of the docs.
+#!/usr/bin/env python3
 
-Loads data from a CSV file with three columns: source, destination, branch
-
-Where the source and destination are paths to RST files in the repository.
-
-Pre-requisites:
-
-- You need the dotenv Python module installed. We use this to let you store your
-  API auth token privately.
-  
-  You can install it by running: pip3 install -r requirements.txt
+"""Manages page redirects for the Godot documentation on ReadTheDocs. (https://docs.godotengine.org)
+Note that RTD redirects only apply in case of 404 errors, and to all branches and languages:
+https://docs.readthedocs.io/en/stable/user-defined-redirects.html.
+If this ever changes, we need to rework how we manage these (likely adding per-branch logic).
 
 How to use:
+- Install requirements: pip3 install -r requirements.txt
+- Store your API token in RTD_API_TOKEN environment variable or
+  a .env file (the latter requires the package dotenv)
+- Generate new redirects from two git revisions using convert_git_renames_to_csv.py
+- Run this script
 
-- Generate a CSV file from two git revisions using convert_git_renames_to_csv.py
-- Store your API token in a .env variable in this directory like so:
-  RTD_API_TOKEN=your_token_here
-- Run this script, passing it the path to your generated CSV file as an
-  argument.
+Example:
+  python convert_git_renames_to_csv.py stable 3.4 >> redirects.csv
+  python create_redirects.py
 
-The script directly creates redirects using the CSV data. It does not check if a
-redirect already exist or if it's correct.
+This would add all files that were renamed in 3.4 from stable to redirects.csv,
+and then create the redirects on RTD accordingly.
+Care is taken to not add redirects that already exist on RTD.
 """
 
 import argparse
 import csv
-import json
 import os
+import time
 
-import dotenv
+import requests
 from requests.models import default_hooks
+from requests.adapters import HTTPAdapter
+from requests.packages.urllib3.util.retry import Retry
 
-try:
-    import requests
-except ImportError:
-    print(
-        "Required third-party module `requests` not found. "
-        "Please install it with `pip install requests` (or `pip3 install requests` on Linux)."
-    )
-
-
-dotenv.load_dotenv()
-RTD_AUTH_TOKEN: str = os.environ.get("RTD_AUTH_TOKEN", "")
-if RTD_AUTH_TOKEN == "":
-    print("Missing auth token in .env file or .env file not found. Aborting.")
-    exit(1)
-
-REDIRECT_URL = "https://readthedocs.org/api/v3/projects/pip/redirects/"
-REQUEST_HEADERS = {"Authorization": f"token {RTD_AUTH_TOKEN}"}
-
+RTD_AUTH_TOKEN = ""
+REQUEST_HEADERS = ""
+REDIRECT_URL = "https://readthedocs.org/api/v3/projects/godot/redirects/"
+USER_AGENT = "Godot RTD Redirects on Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36"
+DEFAULT_PAGINATED_SIZE = 1024
+API_SLEEP_TIME = 0.2 # Seconds.
+REDIRECT_SUFFIXES = [".html", "/"]
+TIMEOUT_SECONDS = 5
+HTTP = None
 
 def parse_command_line_args():
-    parser = argparse.ArgumentParser(
-        description="Create page redirects for a specific branch of the docs."
-    )
+    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
     parser.add_argument(
-        "csv_file",
+        "-f",
+        "--file",
+        metavar="file",
+        default="redirects.csv",
         type=str,
-        help="Path to a CSV file with three columns: source, destination, branch.",
+        help="Path to a CSV file used to keep a list of redirects, containing two columns: source and destination.",
+    )
+    parser.add_argument(
+        "--delete",
+        action="store_true",
+        help="Deletes all currently setup 'page' and 'exact' redirects on ReadTheDocs.",
     )
-    # add dry-run argument
     parser.add_argument(
-        "-d",
         "--dry-run",
         action="store_true",
-        help="Run the program and output information without side effects.",
+        help="Safe mode: Run the program and output information without any calls to the ReadTheDocs API.",
+    )
+    parser.add_argument(
+        "--dump",
+        action="store_true",
+        help="Only dumps or deletes (if --delete) existing RTD redirects, skips submission.",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        help="Enables verbose output.",
     )
     return parser.parse_args()
 
 
-def make_redirect(source, destination, branch, args):
-    # Currently, the program only works for the EN version of the docs
-    trimmed_source = source.replace(".rst", "")
-    trimmed_destination = destination.replace(".rst", "")
+def make_redirect(source, destination, args, retry=0):
+    json_data = {"from_url": source, "to_url": destination, "type": "page"}
+    headers = REQUEST_HEADERS
+
+    if args.verbose:
+        print("POST " + REDIRECT_URL, headers, json_data)
 
-    source_slug = f"/en/{branch}/{trimmed_source}"
-    destination_slug = f"/en/{branch}/{trimmed_destination}"
-    json_data = {"from_url": source_slug, "to_url": destination_slug, "type": "page"}
     if args.dry_run:
-        print(f"{source_slug} -> {destination_slug}")
+        print(f"Created redirect {source} -> {destination} (DRY RUN)")
+        return
+
+    response = HTTP.post(
+        REDIRECT_URL,
+        json=json_data,
+        headers=headers,
+        timeout=TIMEOUT_SECONDS
+    )
+
+    if response.status_code == 201:
+        print(f"Created redirect {source} -> {destination}")
+    elif response.status_code == 429 and retry<5:
+        retry += 1
+        time.sleep(retry*retry)
+        make_redirect(source, destination, args, retry)
+        return
     else:
-        response = requests.post(
-            REDIRECT_URL,
-            json=json.dumps(json_data),
+        print(
+            f"Failed to create redirect {source} -> {destination}. "
+            f"Status code: {response.status_code}"
+        )
+        exit(1)
+
+
+def sleep():
+    time.sleep(API_SLEEP_TIME)
+
+
+def id(from_url, to_url):
+    return from_url + " -> " + to_url
+
+
+def get_paginated(url, parameters={"limit": DEFAULT_PAGINATED_SIZE}):
+    entries = []
+    count = -1
+    while True:
+        data = HTTP.get(
+            url,
             headers=REQUEST_HEADERS,
+            params=parameters,
+            timeout=TIMEOUT_SECONDS
         )
-        if response.status_code == 201:
-            print(f"Created redirect {source_slug} -> {destination_slug}")
+        if data.status_code != 200:
+            if data.status_code == 401:
+                print("Access denied, check RTD API key in RTD_AUTH_TOKEN!")
+            print("Error accessing RTD API: " + url + ": " + str(data.status_code))
+            exit(1)
         else:
+            json = data.json()
+            if json["count"] and count < 0:
+                count = json["count"]
+            entries.extend(json["results"])
+            next = json["next"]
+            if next and len(next) > 0 and next != url:
+                url = next
+                sleep()
+                continue
+        if count > 0 and len(entries) != count:
             print(
-                f"Failed to create redirect {source_slug} -> {destination_slug}. "
-                f"Status code: {response.status_code}"
+                "Mismatch getting paginated content from " + url + ": " +
+                "expected " + str(count) + " items, got " + str(len(entries)))
+            exit(1)
+        return entries
+
+
+def delete_redirect(id):
+    url = REDIRECT_URL + str(id)
+    data = HTTP.delete(url, headers=REQUEST_HEADERS, timeout=TIMEOUT_SECONDS)
+    if data.status_code != 204:
+        print("Error deleting redirect with ID", id, "- code:", data.status_code)
+        exit(1)
+    else:
+        print("Deleted redirect", id, "on RTD.")
+
+
+def get_existing_redirects(delete=False):
+    redirs = get_paginated(REDIRECT_URL)
+    existing = []
+    for redir in redirs:
+        if redir["type"] != "page":
+            print(
+                "Ignoring redirect (only type 'page' is handled): #" +
+                str(redir["pk"]) + " " + id(redir["from_url"], redir["to_url"]) +
+                " on ReadTheDocs is '" + redir["type"] + "'. "
             )
+            continue
+        if delete:
+            delete_redirect(redir["pk"])
+            sleep()
+        else:
+            existing.append([redir["from_url"], redir["to_url"]])
+    return existing
+
+
+def set_auth(token):
+    global RTD_AUTH_TOKEN
+    RTD_AUTH_TOKEN = token
+    global REQUEST_HEADERS
+    REQUEST_HEADERS = {"Authorization": f"token {RTD_AUTH_TOKEN}", "User-Agent": USER_AGENT}
+
+
+def load_auth():
+    try:
+        import dotenv
+        dotenv.load_dotenv()
+    except:
+        print("Failed to load dotenv. If you want to use .env files, install the dotenv.")
+    token = os.environ.get("RTD_AUTH_TOKEN", "")
+    if len(token) < 1:
+        print("Missing auth token in RTD_AUTH_TOKEN env var or .env file not found. Aborting.")
+        exit(1)
+    set_auth(token)
+
+
+def has_suffix(s, suffixes):
+    for suffix in suffixes:
+        if s.endswith(suffix):
+            return True
+    return False
+
+
+def is_valid_redirect_url(url):
+    if len(url) < len("/a"):
+        return False
+
+    if not has_suffix(url.lower(), REDIRECT_SUFFIXES):
+        return False
+
+    return True
+
+
+def redirect_to_str(item):
+    return id(item[0], item[1])
 
 
 def main():
     args = parse_command_line_args()
-    redirect_data = []
-    with open(args.csv_file, "r") as f:
-        redirect_data = list(csv.DictReader(f))
-    assert redirect_data[0].keys() == {
-        "source",
-        "destination",
-        "branch",
-    }, "CSV file must have those three columns: source, destination, branch."
-    for row in redirect_data:
-        make_redirect(row["source"], row["destination"], row["branch"], args)
+
+    if not args.dry_run:
+        load_auth()
+
+        retry_strategy = Retry(
+            total=3,
+            status_forcelist=[429, 500, 502, 503, 504],
+            backoff_factor=2,
+            method_whitelist=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
+        )
+        adapter = HTTPAdapter(max_retries=retry_strategy)
+        global HTTP
+        HTTP = requests.Session()
+        HTTP.mount("https://", adapter)
+        HTTP.mount("http://", adapter)
+
+    to_add = []
+    redirects_file = []
+    with open(args.file, "r", encoding="utf-8") as f:
+        redirects_file = list(csv.DictReader(f))
+        if len(redirects_file) > 0:
+            assert redirects_file[0].keys() == {
+                "source",
+                "destination",
+            }, "CSV file must have a header and two columns: source, destination."
+
+        for row in redirects_file:
+            to_add.append([row["source"], row["destination"]])
+        print("Loaded", len(redirects_file), "redirects from", args.file + ".")
+
+    existing = []
+    if not args.dry_run:
+        existing = get_existing_redirects(args.delete)
+    print("Loaded", len(existing), "existing redirects from RTD.")
+
+    print("Total redirects:", str(len(to_add)) +
+          " (+" + str(len(existing)), "existing.)")
+
+    redirects = []
+    added = {}
+    for redirect in to_add:
+        if len(redirect) != 2:
+            print("Invalid redirect:", redirect, "- expected 2 elements, got:", len(redirect))
+            continue
+
+        if redirect[0] == redirect[1]:
+            print("Invalid redirect:", redirect, "- redirects to itself!")
+            continue
+
+        if not is_valid_redirect_url(redirect[0]) or not is_valid_redirect_url(redirect[1]):
+            print("Invalid redirect:", redirect, "- invalid URL!")
+            continue
+
+        if not redirect[0].startswith("/") or not redirect[1].startswith("/"):
+            print("Invalid redirect:", redirect, "- invalid URL: should start with slash!")
+            continue
+
+        redirect_id = id(redirect[0], redirect[1])
+        if redirect_id in added:
+            # Duplicate; skip.
+            continue
+
+        reverse_id = id(redirect[1], redirect[0])
+        if reverse_id in added:
+            # Duplicate; skip.
+            continue
+
+        added[redirect_id] = True
+        redirects.append(redirect)
+
+    redirects.sort(key=redirect_to_str)
+
+    with open(args.file, "w", encoding="utf-8", newline="") as f:
+        writer = csv.writer(f)
+        writer.writerows([["source", "destination"]])
+        writer.writerows(redirects)
+
+    existing_ids = {}
+    for e in existing:
+        existing_ids[id(e[0], e[1])] = True
+
+    if not args.dump:
+        print("Creating redirects.")
+        for redirect in redirects:
+            if not id(redirect[0], redirect[1]) in existing_ids:
+                make_redirect(redirect[0], redirect[1], args)
+
+            if not id(redirect[1], redirect[0]) in existing_ids:
+                make_redirect(redirect[1], redirect[0], args)
+
+            if not args.dry_run:
+                sleep()
+
+    print("Finished creating", len(redirects), "redirects.")
+    print("(" + str(2*len(redirects)) + " including reverse redirects.)")
+
+    if args.dry_run:
+        print("THIS WAS A DRY RUN, NOTHING WAS SUBMITTED TO READTHEDOCS!")
 
 
 if __name__ == "__main__":

+ 390 - 0
_tools/redirects/redirects.csv

@@ -0,0 +1,390 @@
+source,destination
+/classes/_classes.html,/classes/
+/classes/class_bulletphysicsserver.html,/classes/class_gltfdocument.html
+/community/tutorials/3d/mesh_generation_with_heightmap_and_shaders.html,/tutorials/3d/mesh_generation_with_heightmap_and_shaders.html
+/community/tutorials/gdnative/gdnative-c-example.html,/tutorials/plugins/gdnative/gdnative-c-example.html
+/community/tutorials/gdnative/index.html,/tutorials/plugins/gdnative/index.html
+/community/tutorials/vr/index.html,/tutorials/vr/index.html
+/community/tutorials/vr/vr_primer.html,/tutorials/vr/vr_primer.html
+/content/3d/making_trees.html,/tutorials/content/making_trees.html
+/contributing/_contributing.html,/community/contributing/
+/contributing/bug_triage_guidelines.html,/community/contributing/bug_triage_guidelines.html
+/contributing/doc_and_l10n_guidelines.html,/community/contributing/doc_and_l10n_guidelines.html
+/contributing/updating_the_class_reference.html,/community/contributing/updating_the_class_reference.html
+/development/consoles/consoles.html,/tutorials/platform/consoles.html
+/development/plugins/import_plugins.html,/tutorials/plugins/editor/import_plugins.html
+/development/plugins/index.html,/tutorials/plugins/editor/index.html
+/development/plugins/making_plugins.html,/tutorials/plugins/editor/making_plugins.html
+/getting_started/editor/command_line_tutorial.html,/tutorials/editor/command_line_tutorial.html
+/getting_started/editor/default_key_mapping.html,/tutorials/editor/default_key_mapping.html
+/getting_started/editor/external_editor.html,/tutorials/editor/external_editor.html
+/getting_started/editor/using_the_web_editor.html,/tutorials/editor/using_the_web_editor.html
+/getting_started/scripting/c_sharp/c_sharp_basics.html,/tutorials/scripting/c_sharp/c_sharp_basics.html
+/getting_started/scripting/c_sharp/c_sharp_differences.html,/tutorials/scripting/c_sharp/c_sharp_differences.html
+/getting_started/scripting/c_sharp/c_sharp_features.html,/tutorials/scripting/c_sharp/c_sharp_features.html
+/getting_started/scripting/c_sharp/c_sharp_style_guide.html,/tutorials/scripting/c_sharp/c_sharp_style_guide.html
+/getting_started/scripting/c_sharp/index.html,/tutorials/scripting/c_sharp/index.html
+/getting_started/scripting/creating_script_templates.html,/tutorials/scripting/creating_script_templates.html
+/getting_started/scripting/cross_language_scripting.html,/tutorials/scripting/cross_language_scripting.html
+/getting_started/scripting/gdscript/gdscript_advanced.html,/tutorials/scripting/gdscript/gdscript_advanced.html
+/getting_started/scripting/gdscript/gdscript_basics.html,/tutorials/scripting/gdscript/gdscript_basics.html
+/getting_started/scripting/gdscript/gdscript_exports.html,/tutorials/scripting/gdscript/gdscript_exports.html
+/getting_started/scripting/gdscript/gdscript_format_string.html,/tutorials/scripting/gdscript/gdscript_format_string.html
+/getting_started/scripting/gdscript/gdscript_styleguide.html,/tutorials/scripting/gdscript/gdscript_styleguide.html
+/getting_started/scripting/gdscript/index.html,/tutorials/scripting/gdscript/index.html
+/getting_started/scripting/gdscript/static_typing.html,/tutorials/scripting/gdscript/static_typing.html
+/getting_started/scripting/gdscript/warning_system.html,/tutorials/scripting/gdscript/warning_system.html
+/getting_started/scripting/visual_script/custom_visualscript_nodes.html,/tutorials/scripting/visual_script/custom_visualscript_nodes.html
+/getting_started/scripting/visual_script/getting_started.html,/tutorials/scripting/visual_script/getting_started.html
+/getting_started/scripting/visual_script/index.html,/tutorials/scripting/visual_script/index.html
+/getting_started/scripting/visual_script/nodes_purposes.html,/tutorials/scripting/visual_script/nodes_purposes.html
+/getting_started/scripting/visual_script/what_is_visual_scripting.html,/tutorials/scripting/visual_script/what_is_visual_scripting.html
+/getting_started/step_by_step/exporting.html,/tutorials/export/exporting_basics.html
+/getting_started/step_by_step/filesystem.html,/tutorials/scripting/filesystem.html
+/getting_started/step_by_step/godot_design_philosophy.html,/getting_started/introduction/godot_design_philosophy.html
+/getting_started/step_by_step/resources.html,/tutorials/scripting/resources.html
+/getting_started/step_by_step/scene_tree.html,/tutorials/scripting/scene_tree.html
+/getting_started/step_by_step/singletons_autoload.html,/tutorials/scripting/singletons_autoload.html
+/getting_started/workflow/assets/escn_exporter/animation.html,/tutorials/assets_pipeline/escn_exporter/animation.html
+/getting_started/workflow/assets/escn_exporter/index.html,/tutorials/assets_pipeline/escn_exporter/index.html
+/getting_started/workflow/assets/escn_exporter/lights.html,/tutorials/assets_pipeline/escn_exporter/lights.html
+/getting_started/workflow/assets/escn_exporter/material.html,/tutorials/assets_pipeline/escn_exporter/material.html
+/getting_started/workflow/assets/escn_exporter/mesh.html,/tutorials/assets_pipeline/escn_exporter/mesh.html
+/getting_started/workflow/assets/escn_exporter/physics.html,/tutorials/assets_pipeline/escn_exporter/physics.html
+/getting_started/workflow/assets/escn_exporter/skeleton.html,/tutorials/assets_pipeline/escn_exporter/skeleton.html
+/getting_started/workflow/assets/import_process.html,/tutorials/assets_pipeline/import_process.html
+/getting_started/workflow/assets/importing_audio_samples.html,/tutorials/assets_pipeline/importing_audio_samples.html
+/getting_started/workflow/assets/importing_images.html,/tutorials/assets_pipeline/importing_images.html
+/getting_started/workflow/assets/importing_scenes.html,/tutorials/assets_pipeline/importing_scenes.html
+/getting_started/workflow/assets/importing_translations.html,/tutorials/assets_pipeline/importing_translations.html
+/getting_started/workflow/assets/index.html,/tutorials/assets_pipeline/index.html
+/getting_started/workflow/best_practices/autoloads_versus_internal_nodes.html,/tutorials/best_practices/autoloads_versus_internal_nodes.html
+/getting_started/workflow/best_practices/data_preferences.html,/tutorials/best_practices/data_preferences.html
+/getting_started/workflow/best_practices/godot_interfaces.html,/tutorials/best_practices/godot_interfaces.html
+/getting_started/workflow/best_practices/godot_notifications.html,/tutorials/best_practices/godot_notifications.html
+/getting_started/workflow/best_practices/index.html,/tutorials/best_practices/index.html
+/getting_started/workflow/best_practices/introduction_best_practices.html,/tutorials/best_practices/introduction_best_practices.html
+/getting_started/workflow/best_practices/logic_preferences.html,/tutorials/best_practices/logic_preferences.html
+/getting_started/workflow/best_practices/node_alternatives.html,/tutorials/best_practices/node_alternatives.html
+/getting_started/workflow/best_practices/scene_organization.html,/tutorials/best_practices/scene_organization.html
+/getting_started/workflow/best_practices/scenes_versus_scripts.html,/tutorials/best_practices/scenes_versus_scripts.html
+/getting_started/workflow/best_practices/what_are_godot_classes.html,/tutorials/best_practices/what_are_godot_classes.html
+/getting_started/workflow/export/android_custom_build.html,/tutorials/export/android_custom_build.html
+/getting_started/workflow/export/changing_application_icon_for_windows.html,/tutorials/export/changing_application_icon_for_windows.html
+/getting_started/workflow/export/exporting_for_android.html,/tutorials/export/exporting_for_android.html
+/getting_started/workflow/export/exporting_for_dedicated_servers.html,/tutorials/export/exporting_for_dedicated_servers.html
+/getting_started/workflow/export/exporting_for_ios.html,/tutorials/export/exporting_for_ios.html
+/getting_started/workflow/export/exporting_for_uwp.html,/tutorials/export/exporting_for_uwp.html
+/getting_started/workflow/export/exporting_for_web.html,/tutorials/export/exporting_for_web.html
+/getting_started/workflow/export/exporting_pcks.html,/tutorials/export/exporting_pcks.html
+/getting_started/workflow/export/exporting_projects.html,/tutorials/export/exporting_projects.html
+/getting_started/workflow/export/feature_tags.html,/tutorials/export/feature_tags.html
+/getting_started/workflow/export/index.html,/tutorials/export/index.html
+/getting_started/workflow/export/one-click_deploy.html,/tutorials/export/one-click_deploy.html
+/getting_started/workflow/project_setup/project_organization.html,/tutorials/best_practices/project_organization.html
+/getting_started/workflow/project_setup/version_control_systems.html,/tutorials/best_practices/version_control_systems.html
+/learning/editor/2d_and_3d_keybindings.html,/getting_started/editor/2d_and_3d_keybindings.html
+/learning/editor/command_line_tutorial.html,/getting_started/editor/command_line_tutorial.html
+/learning/editor/index.html,/getting_started/editor/index.html
+/learning/editor/unity_to_godot.html,/getting_started/editor/unity_to_godot.html
+/learning/features/2d/2d_transforms.html,/tutorials/2d/2d_transforms.html
+/learning/features/2d/canvas_layers.html,/tutorials/2d/canvas_layers.html
+/learning/features/2d/custom_drawing_in_2d.html,/tutorials/2d/custom_drawing_in_2d.html
+/learning/features/2d/index.html,/tutorials/2d/index.html
+/learning/features/2d/particle_systems_2d.html,/tutorials/2d/particle_systems_2d.html
+/learning/features/2d/using_tilemaps.html,/tutorials/2d/using_tilemaps.html
+/learning/features/3d/3d_performance_and_limitations.html,/tutorials/3d/3d_performance_and_limitations.html
+/learning/features/3d/baked_lightmaps.html,/tutorials/3d/baked_lightmaps.html
+/learning/features/3d/environment_and_post_processing.html,/tutorials/3d/environment_and_post_processing.html
+/learning/features/3d/gi_probes.html,/tutorials/3d/gi_probes.html
+/learning/features/3d/high_dynamic_range.html,/tutorials/3d/high_dynamic_range.html
+/learning/features/3d/index.html,/tutorials/3d/index.html
+/learning/features/3d/introduction_to_3d.html,/tutorials/3d/introduction_to_3d.html
+/learning/features/3d/lights_and_shadows.html,/tutorials/3d/lights_and_shadows.html
+/learning/features/3d/reflection_probes.html,/tutorials/3d/reflection_probes.html
+/learning/features/3d/spatial_material.html,/tutorials/3d/spatial_material.html
+/learning/features/3d/using_gridmaps.html,/tutorials/3d/using_gridmaps.html
+/learning/features/animation/cutout_animation.html,/tutorials/animation/cutout_animation.html
+/learning/features/animation/index.html,/tutorials/animation/index.html
+/learning/features/animation/introduction_2d.html,/tutorials/animation/introduction_2d.html
+/learning/features/assetlib/index.html,/tutorials/assetlib/index.html
+/learning/features/assetlib/uploading_to_assetlib.html,/tutorials/assetlib/uploading_to_assetlib.html
+/learning/features/assetlib/using_assetlib.html,/tutorials/assetlib/using_assetlib.html
+/learning/features/assetlib/what_is_assetlib.html,/tutorials/assetlib/what_is_assetlib.html
+/learning/features/audio/audio_buses.html,/tutorials/audio/audio_buses.html
+/learning/features/audio/audio_streams.html,/tutorials/audio/audio_streams.html
+/learning/features/audio/index.html,/tutorials/audio/index.html
+/learning/features/gui/bbcode_in_richtextlabel.html,/tutorials/gui/bbcode_in_richtextlabel.html
+/learning/features/gui/custom_gui_controls.html,/tutorials/gui/custom_gui_controls.html
+/learning/features/gui/gui_skinning.html,/tutorials/gui/gui_skinning.html
+/learning/features/gui/index.html,/tutorials/gui/index.html
+/learning/features/gui/size_and_anchors.html,/tutorials/gui/size_and_anchors.html
+/learning/features/inputs/index.html,/tutorials/inputs/index.html
+/learning/features/inputs/inputevent.html,/tutorials/inputs/inputevent.html
+/learning/features/inputs/mouse_and_input_coordinates.html,/tutorials/inputs/mouse_and_input_coordinates.html
+/learning/features/math/index.html,/tutorials/math/index.html
+/learning/features/math/matrices_and_transforms.html,/tutorials/math/matrices_and_transforms.html
+/learning/features/math/vector_math.html,/tutorials/math/vector_math.html
+/learning/features/math/vectors_advanced.html,/tutorials/math/vectors_advanced.html
+/learning/features/misc/background_loading.html,/tutorials/misc/background_loading.html
+/learning/features/misc/binary_serialization_api.html,/tutorials/misc/binary_serialization_api.html
+/learning/features/misc/data_paths.html,/tutorials/misc/data_paths.html
+/learning/features/misc/encrypting_save_games.html,/tutorials/misc/encrypting_save_games.html
+/learning/features/misc/handling_quit_requests.html,/tutorials/misc/handling_quit_requests.html
+/learning/features/misc/index.html,/tutorials/misc/index.html
+/learning/features/misc/internationalizing_games.html,/tutorials/misc/internationalizing_games.html
+/learning/features/misc/locales.html,/tutorials/misc/locales.html
+/learning/features/misc/pausing_games.html,/tutorials/misc/pausing_games.html
+/learning/features/misc/saving_games.html,/tutorials/misc/saving_games.html
+/learning/features/networking/high_level_multiplayer.html,/tutorials/networking/high_level_multiplayer.html
+/learning/features/networking/http_client_class.html,/tutorials/networking/http_client_class.html
+/learning/features/networking/index.html,/tutorials/networking/index.html
+/learning/features/networking/ssl_certificates.html,/tutorials/networking/ssl_certificates.html
+/learning/features/physics/index.html,/tutorials/physics/index.html
+/learning/features/physics/kinematic_character_2d.html,/tutorials/physics/kinematic_character_2d.html
+/learning/features/physics/physics_introduction.html,/tutorials/physics/physics_introduction.html
+/learning/features/physics/ray-casting.html,/tutorials/physics/ray-casting.html
+/learning/features/platform/android_in_app_purchases.html,/tutorials/platform/android_in_app_purchases.html
+/learning/features/platform/index.html,/tutorials/platform/index.html
+/learning/features/platform/services_for_ios.html,/tutorials/platform/services_for_ios.html
+/learning/features/shading/index.html,/tutorials/shading/index.html
+/learning/features/shading/screen-reading_shaders.html,/tutorials/shading/screen-reading_shaders.html
+/learning/features/shading/shader_materials.html,/tutorials/shading/shader_materials.html
+/learning/features/shading/shading_language.html,/tutorials/shading/shading_language.html
+/learning/features/viewports/index.html,/tutorials/viewports/index.html
+/learning/features/viewports/multiple_resolutions.html,/tutorials/viewports/multiple_resolutions.html
+/learning/features/viewports/viewports.html,/tutorials/viewports/viewports.html
+/learning/scripting/c_sharp/c_sharp_basics.html,/getting_started/scripting/c_sharp/c_sharp_basics.html
+/learning/scripting/c_sharp/c_sharp_differences.html,/getting_started/scripting/c_sharp/c_sharp_differences.html
+/learning/scripting/c_sharp/c_sharp_features.html,/getting_started/scripting/c_sharp/c_sharp_features.html
+/learning/scripting/c_sharp/index.html,/getting_started/scripting/c_sharp/index.html
+/learning/scripting/gdscript/gdscript_advanced.html,/getting_started/scripting/gdscript/gdscript_advanced.html
+/learning/scripting/gdscript/gdscript_basics.html,/getting_started/scripting/gdscript/gdscript_basics.html
+/learning/scripting/gdscript/gdscript_format_string.html,/getting_started/scripting/gdscript/gdscript_format_string.html
+/learning/scripting/gdscript/gdscript_styleguide.html,/getting_started/scripting/gdscript/gdscript_styleguide.html
+/learning/scripting/gdscript/index.html,/getting_started/scripting/gdscript/index.html
+/learning/scripting/index.html,/getting_started/scripting/index.html
+/learning/scripting/visual_script/getting_started.html,/getting_started/scripting/visual_script/getting_started.html
+/learning/scripting/visual_script/index.html,/getting_started/scripting/visual_script/index.html
+/learning/scripting/visual_script/nodes_purposes.html,/getting_started/scripting/visual_script/nodes_purposes.html
+/learning/scripting/visual_script/what_is_visual_scripting.html,/getting_started/scripting/visual_script/what_is_visual_scripting.html
+/learning/step_by_step/animations.html,/getting_started/step_by_step/animations.html
+/learning/step_by_step/filesystem.html,/getting_started/step_by_step/filesystem.html
+/learning/step_by_step/godot_design_philosophy.html,/getting_started/step_by_step/godot_design_philosophy.html
+/learning/step_by_step/index.html,/getting_started/step_by_step/index.html
+/learning/step_by_step/instancing.html,/getting_started/step_by_step/instancing.html
+/learning/step_by_step/instancing_continued.html,/getting_started/step_by_step/instancing_continued.html
+/learning/step_by_step/intro_to_the_editor_interface.html,/getting_started/step_by_step/intro_to_the_editor_interface.html
+/learning/step_by_step/resources.html,/getting_started/step_by_step/resources.html
+/learning/step_by_step/scene_tree.html,/getting_started/step_by_step/scene_tree.html
+/learning/step_by_step/scenes_and_nodes.html,/getting_started/step_by_step/scenes_and_nodes.html
+/learning/step_by_step/scripting.html,/getting_started/step_by_step/scripting.html
+/learning/step_by_step/scripting_continued.html,/getting_started/step_by_step/scripting_continued.html
+/learning/step_by_step/singletons_autoload.html,/getting_started/step_by_step/singletons_autoload.html
+/learning/step_by_step/splash_screen.html,/getting_started/step_by_step/splash_screen.html
+/learning/step_by_step/ui_code_a_life_bar.html,/getting_started/step_by_step/ui_code_a_life_bar.html
+/learning/step_by_step/ui_game_user_interface.html,/getting_started/step_by_step/ui_game_user_interface.html
+/learning/step_by_step/ui_introduction_to_the_ui_system.html,/getting_started/step_by_step/ui_introduction_to_the_ui_system.html
+/learning/step_by_step/ui_main_menu.html,/getting_started/step_by_step/ui_main_menu.html
+/learning/step_by_step/your_first_game.html,/getting_started/step_by_step/your_first_game.html
+/learning/workflow/assets/import_process.html,/getting_started/workflow/assets/import_process.html
+/learning/workflow/assets/importing_audio_samples.html,/getting_started/workflow/assets/importing_audio_samples.html
+/learning/workflow/assets/importing_images.html,/getting_started/workflow/assets/importing_images.html
+/learning/workflow/assets/importing_scenes.html,/getting_started/workflow/assets/importing_scenes.html
+/learning/workflow/assets/importing_translations.html,/getting_started/workflow/assets/importing_translations.html
+/learning/workflow/assets/index.html,/getting_started/workflow/assets/index.html
+/learning/workflow/export/customizing_html5_shell.html,/getting_started/workflow/export/customizing_html5_shell.html
+/learning/workflow/export/exporting_for_android.html,/getting_started/workflow/export/exporting_for_android.html
+/learning/workflow/export/exporting_for_ios.html,/getting_started/workflow/export/exporting_for_ios.html
+/learning/workflow/export/exporting_for_pc.html,/getting_started/workflow/export/exporting_for_pc.html
+/learning/workflow/export/exporting_for_uwp.html,/getting_started/workflow/export/exporting_for_uwp.html
+/learning/workflow/export/exporting_for_web.html,/getting_started/workflow/export/exporting_for_web.html
+/learning/workflow/export/exporting_projects.html,/getting_started/workflow/export/exporting_projects.html
+/learning/workflow/export/feature_tags.html,/getting_started/workflow/export/feature_tags.html
+/learning/workflow/export/index.html,/getting_started/workflow/export/index.html
+/learning/workflow/export/one-click_deploy.html,/getting_started/workflow/export/one-click_deploy.html
+/learning/workflow/index.html,/getting_started/workflow/index.html
+/learning/workflow/project_setup/index.html,/getting_started/workflow/project_setup/index.html
+/learning/workflow/project_setup/project_organization.html,/getting_started/workflow/project_setup/project_organization.html
+/reference/2d_and_3d_keybindings.html,/learning/editor/2d_and_3d_keybindings.html
+/reference/_compiling.html,/development/compiling/
+/reference/_developing.html,/development/cpp/
+/reference/android_in_app_purchases.html,/learning/features/platform/android_in_app_purchases.html
+/reference/batch_building_templates.html,/development/compiling/batch_building_templates.html
+/reference/bbcode_in_richtextlabel.html,/learning/features/gui/bbcode_in_richtextlabel.html
+/reference/binary_serialization_api.html,/learning/features/misc/binary_serialization_api.html
+/reference/command_line_tutorial.html,/learning/editor/command_line_tutorial.html
+/reference/compiling_for_android.html,/development/compiling/compiling_for_android.html
+/reference/compiling_for_ios.html,/development/compiling/compiling_for_ios.html
+/reference/compiling_for_osx.html,/development/compiling/compiling_for_osx.html
+/reference/compiling_for_uwp.html,/development/compiling/compiling_for_uwp.html
+/reference/compiling_for_web.html,/development/compiling/compiling_for_web.html
+/reference/compiling_for_windows.html,/development/compiling/compiling_for_windows.html
+/reference/compiling_for_x11.html,/development/compiling/compiling_for_x11.html
+/reference/configuring_an_ide.html,/development/cpp/configuring_an_ide.html
+/reference/core_types.html,/development/cpp/core_types.html
+/reference/creating_android_modules.html,/development/cpp/creating_android_modules.html
+/reference/cross-compiling_for_ios_on_linux.html,/development/compiling/cross-compiling_for_ios_on_linux.html
+/reference/custom_modules_in_c++.html,/development/cpp/custom_modules_in_cpp.html
+/reference/faq.html,/about/faq.html
+/reference/gdscript.html,/learning/scripting/gdscript/gdscript_basics.html
+/reference/gdscript_more_efficiently.html,/learning/scripting/gdscript/gdscript_advanced.html
+/reference/gdscript_printf.html,/learning/scripting/gdscript/gdscript_format_string.html
+/reference/inheritance_class_tree.html,/development/cpp/inheritance_class_tree.html
+/reference/introduction_to_godot_development.html,/development/cpp/introduction_to_godot_development.html
+/reference/introduction_to_the_buildsystem.html,/development/compiling/introduction_to_the_buildsystem.html
+/reference/locales.html,/learning/features/misc/locales.html
+/reference/object_class.html,/development/cpp/object_class.html
+/reference/packaging_godot.html,/development/compiling/packaging_godot.html
+/reference/services_for_ios.html,/learning/features/platform/services_for_ios.html
+/reference/shading_language.html,/learning/features/shading/shading_language.html
+/reference/unity_to_godot.html,/learning/editor/unity_to_godot.html
+/reference/variant_class.html,/development/cpp/variant_class.html
+/tutorials/2d/_2d.html,/learning/features/2d/
+/tutorials/2d/_2d_gui.html,/learning/features/gui/
+/tutorials/2d/_2d_physics.html,/learning/features/physics/
+/tutorials/2d/custom_gui_controls.html,/learning/features/gui/custom_gui_controls.html
+/tutorials/2d/cutout_animation.html,/learning/features/animation/cutout_animation.html
+/tutorials/2d/gui_skinning.html,/learning/features/gui/gui_skinning.html
+/tutorials/2d/kinematic_character_2d.html,/learning/features/physics/kinematic_character_2d.html
+/tutorials/2d/physics_introduction.html,/learning/features/physics/physics_introduction.html
+/tutorials/2d/screen-reading_shaders.html,/learning/features/shading/screen-reading_shaders.html
+/tutorials/2d/size_and_anchors.html,/learning/features/gui/size_and_anchors.html
+/tutorials/2d/viewport_and_canvas_transforms.html,/learning/features/2d/2d_transforms.html
+/tutorials/3d/_3d.html,/learning/features/3d/
+/tutorials/3d/_3d_physics.html,/learning/features/physics/
+/tutorials/3d/fixed_materials.html,/learning/features/3d/fixed_materials.html
+/tutorials/3d/importing_3d_meshes.html,/learning/features/3d/importing_3d_meshes.html
+/tutorials/3d/importing_3d_scenes.html,/learning/features/3d/importing_3d_scenes.html
+/tutorials/3d/inverse_kinematics.html,/community/tutorials/3d/inverse_kinematics.html
+/tutorials/3d/lighting.html,/learning/features/lighting/lighting.html
+/tutorials/3d/materials.html,/learning/features/3d/materials.html
+/tutorials/3d/shader_materials.html,/learning/features/shading/shader_materials.html
+/tutorials/3d/shadow_mapping.html,/learning/features/lighting/shadow_mapping.html
+/tutorials/3d/vertex_animation/animating_thousands_of_fish.html,/tutorials/performance/vertex_animation/animating_thousands_of_fish.html
+/tutorials/3d/vertex_animation/controlling_thousands_of_fish.html,/tutorials/performance/vertex_animation/controlling_thousands_of_fish.html
+/tutorials/3d/vertex_animation/index.html,/tutorials/performance/vertex_animation/index.html
+/tutorials/3d/working_with_3d_skeletons.html,/community/tutorials/3d/working_with_3d_skeletons.html
+/tutorials/_math.html,/learning/features/math/
+/tutorials/_misc_tutorials.html,/learning/features/misc/
+/tutorials/_networking.html,/learning/features/networking/
+/tutorials/_plugins.html,/development/plugins/
+/tutorials/_shaders.html,/learning/features/shading/
+/tutorials/asset_pipeline/_asset_pipeline.html,/learning/workflow/
+/tutorials/asset_pipeline/_export.html,/learning/workflow/export/
+/tutorials/asset_pipeline/_import.html,/learning/workflow/assets/
+/tutorials/asset_pipeline/exporting_for_android.html,/learning/workflow/export/exporting_for_android.html
+/tutorials/asset_pipeline/exporting_for_ios.html,/learning/workflow/export/exporting_for_ios.html
+/tutorials/asset_pipeline/exporting_for_pc.html,/learning/workflow/export/exporting_for_pc.html
+/tutorials/asset_pipeline/exporting_for_uwp.html,/learning/workflow/export/exporting_for_uwp.html
+/tutorials/asset_pipeline/exporting_for_web.html,/learning/workflow/export/exporting_for_web.html
+/tutorials/asset_pipeline/exporting_images.html,/learning/workflow/assets/exporting_images.html
+/tutorials/asset_pipeline/exporting_projects.html,/learning/workflow/export/exporting_projects.html
+/tutorials/asset_pipeline/import_process.html,/learning/workflow/assets/import_process.html
+/tutorials/asset_pipeline/importing_audio_samples.html,/learning/workflow/assets/importing_audio_samples.html
+/tutorials/asset_pipeline/importing_fonts.html,/learning/workflow/assets/importing_fonts.html
+/tutorials/asset_pipeline/importing_textures.html,/learning/workflow/assets/importing_textures.html
+/tutorials/asset_pipeline/importing_translations.html,/learning/workflow/assets/importing_translations.html
+/tutorials/asset_pipeline/managing_image_files.html,/learning/workflow/assets/managing_image_files.html
+/tutorials/asset_pipeline/one-click_deploy.html,/learning/workflow/export/one-click_deploy.html
+/tutorials/assetlib/index.html,/community/asset_library/index.html
+/tutorials/assetlib/uploading_to_assetlib.html,/community/asset_library/uploading_to_assetlib.html
+/tutorials/assetlib/using_assetlib.html,/community/asset_library/using_assetlib.html
+/tutorials/assetlib/what_is_assetlib.html,/community/asset_library/what_is_assetlib.html
+/tutorials/content/making_trees.html,/tutorials/shaders/making_trees.html
+/tutorials/content/procedural_geometry/arraymesh.html,/tutorials/3d/procedural_geometry/arraymesh.html
+/tutorials/content/procedural_geometry/immediategeometry.html,/tutorials/3d/procedural_geometry/immediategeometry.html
+/tutorials/content/procedural_geometry/index.html,/tutorials/3d/procedural_geometry/index.html
+/tutorials/content/procedural_geometry/meshdatatool.html,/tutorials/3d/procedural_geometry/meshdatatool.html
+/tutorials/content/procedural_geometry/surfacetool.html,/tutorials/3d/procedural_geometry/surfacetool.html
+/tutorials/debug/debugger_panel.html,/tutorials/scripting/debug/debugger_panel.html
+/tutorials/debug/index.html,/tutorials/scripting/debug/index.html
+/tutorials/debug/overview_of_debugging_tools.html,/tutorials/scripting/debug/overview_of_debugging_tools.html
+/tutorials/engine/background_loading.html,/learning/features/misc/background_loading.html
+/tutorials/engine/data_paths.html,/learning/features/misc/data_paths.html
+/tutorials/engine/encrypting_save_games.html,/learning/features/misc/encrypting_save_games.html
+/tutorials/engine/handling_quit_requests.html,/learning/features/misc/handling_quit_requests.html
+/tutorials/engine/inputevent.html,/learning/features/inputs/inputevent.html
+/tutorials/engine/internationalizing_games.html,/learning/features/misc/internationalizing_games.html
+/tutorials/engine/mouse_and_input_coordinates.html,/learning/features/inputs/mouse_and_input_coordinates.html
+/tutorials/engine/multiple_resolutions.html,/learning/features/viewports/multiple_resolutions.html
+/tutorials/engine/pausing_games.html,/learning/features/misc/pausing_games.html
+/tutorials/engine/project_organization.html,/learning/workflow/project_setup/project_organization.html
+/tutorials/engine/saving_games.html,/learning/features/misc/saving_games.html
+/tutorials/engine/viewports.html,/learning/features/viewports/viewports.html
+/tutorials/gui/bbcode_in_richtextlabel.html,/tutorials/ui/bbcode_in_richtextlabel.html
+/tutorials/gui/control_node_gallery.html,/tutorials/ui/control_node_gallery.html
+/tutorials/gui/custom_gui_controls.html,/tutorials/ui/custom_gui_controls.html
+/tutorials/gui/gui_containers.html,/tutorials/ui/gui_containers.html
+/tutorials/gui/size_and_anchors.html,/tutorials/ui/size_and_anchors.html
+/tutorials/high_level_multiplayer.html,/learning/features/networking/high_level_multiplayer.html
+/tutorials/http_client_class.html,/learning/features/networking/http_client_class.html
+/tutorials/legal/complying_with_licenses.html,/about/complying_with_licenses.html
+/tutorials/making_plugins.html,/development/plugins/making_plugins.html
+/tutorials/matrices_and_transforms.html,/learning/features/math/matrices_and_transforms.html
+/tutorials/mesh_generation_with_heightmap_and_shaders.html,/community/tutorials/3d/mesh_generation_with_heightmap_and_shaders.html
+/tutorials/misc/background_loading.html,/tutorials/io/background_loading.html
+/tutorials/misc/binary_serialization_api.html,/tutorials/io/binary_serialization_api.html
+/tutorials/misc/change_scenes_manually.html,/tutorials/scripting/change_scenes_manually.html
+/tutorials/misc/data_paths.html,/tutorials/io/data_paths.html
+/tutorials/misc/encrypting_save_games.html,/tutorials/io/encrypting_save_games.html
+/tutorials/misc/gles2_gles3_differences.html,/tutorials/rendering/gles2_gles3_differences.html
+/tutorials/misc/handling_quit_requests.html,/tutorials/inputs/handling_quit_requests.html
+/tutorials/misc/instancing_with_signals.html,/tutorials/scripting/instancing_with_signals.html
+/tutorials/misc/internationalizing_games.html,/tutorials/i18n/internationalizing_games.html
+/tutorials/misc/jitter_stutter.html,/tutorials/rendering/jitter_stutter.html
+/tutorials/misc/locales.html,/tutorials/i18n/locales.html
+/tutorials/misc/pausing_games.html,/tutorials/scripting/pausing_games.html
+/tutorials/misc/running_code_in_the_editor.html,/tutorials/plugins/running_code_in_the_editor.html
+/tutorials/misc/saving_games.html,/tutorials/io/saving_games.html
+/tutorials/optimization/batching.html,/tutorials/performance/batching.html
+/tutorials/optimization/cpu_optimization.html,/tutorials/performance/cpu_optimization.html
+/tutorials/optimization/general_optimization.html,/tutorials/performance/general_optimization.html
+/tutorials/optimization/gpu_optimization.html,/tutorials/performance/gpu_optimization.html
+/tutorials/optimization/index.html,/tutorials/performance/index.html
+/tutorials/optimization/optimizing_3d_performance.html,/tutorials/performance/optimizing_3d_performance.html
+/tutorials/optimization/using_multimesh.html,/tutorials/performance/using_multimesh.html
+/tutorials/optimization/using_servers.html,/tutorials/performance/using_servers.html
+/tutorials/platform/android_in_app_purchases.html,/tutorials/platform/android/android_in_app_purchases.html
+/tutorials/plugins/android/android_plugin.html,/tutorials/platform/android/android_plugin.html
+/tutorials/plugins/android/index.html,/tutorials/platform/android/index.html
+/tutorials/plugins/gdnative/gdnative-c-example.html,/tutorials/scripting/gdnative/gdnative_c_example.html
+/tutorials/plugins/gdnative/gdnative-cpp-example.html,/tutorials/scripting/gdnative/gdnative_cpp_example.html
+/tutorials/plugins/gdnative/index.html,/tutorials/scripting/gdnative/index.html
+/tutorials/ray-casting.html,/learning/features/physics/ray-casting.html
+/tutorials/shading/advanced_postprocessing.html,/tutorials/shaders/advanced_postprocessing.html
+/tutorials/shading/godot_shader_language_style_guide.html,/tutorials/shaders/shaders_style_guide.html
+/tutorials/shading/index.html,/tutorials/shaders/index.html
+/tutorials/shading/migrating_to_godot_shader_language.html,/tutorials/shaders/converting_glsl_to_godot_shaders.html
+/tutorials/shading/screen-reading_shaders.html,/tutorials/shaders/screen-reading_shaders.html
+/tutorials/shading/shader_materials.html,/tutorials/shaders/shader_materials.html
+/tutorials/shading/shading_reference/canvas_item_shader.html,/tutorials/shaders/shader_reference/canvas_item_shader.html
+/tutorials/shading/shading_reference/index.html,/tutorials/shaders/shader_reference/index.html
+/tutorials/shading/shading_reference/particle_shader.html,/tutorials/shaders/shader_reference/particle_shader.html
+/tutorials/shading/shading_reference/shading_language.html,/tutorials/shaders/shader_reference/shading_language.html
+/tutorials/shading/shading_reference/spatial_shader.html,/tutorials/shaders/shader_reference/spatial_shader.html
+/tutorials/shading/visual_shaders.html,/tutorials/shaders/visual_shaders.html
+/tutorials/shading/your_first_shader/index.html,/tutorials/shaders/your_first_shader/index.html
+/tutorials/shading/your_first_shader/your_second_spatial_shader.html,/tutorials/shaders/your_first_shader/your_second_3d_shader.html
+/tutorials/ssl_certificates.html,/learning/features/networking/ssl_certificates.html
+/tutorials/step_by_step/_step_by_step.html,/learning/step_by_step/
+/tutorials/step_by_step/animations.html,/learning/step_by_step/animations.html
+/tutorials/step_by_step/filesystem.html,/learning/step_by_step/filesystem.html
+/tutorials/step_by_step/gui_tutorial.html,/learning/step_by_step/gui_tutorial.html
+/tutorials/step_by_step/instancing.html,/learning/step_by_step/instancing.html
+/tutorials/step_by_step/instancing_continued.html,/learning/step_by_step/instancing_continued.html
+/tutorials/step_by_step/resources.html,/learning/step_by_step/resources.html
+/tutorials/step_by_step/scene_tree.html,/learning/step_by_step/scene_tree.html
+/tutorials/step_by_step/scenes_and_nodes.html,/learning/step_by_step/scenes_and_nodes.html
+/tutorials/step_by_step/scripting.html,/learning/step_by_step/scripting.html
+/tutorials/step_by_step/scripting_continued.html,/learning/step_by_step/scripting_continued.html
+/tutorials/step_by_step/simple_2d_game.html,/learning/step_by_step/simple_2d_game.html
+/tutorials/step_by_step/singletons_autoload.html,/learning/step_by_step/singletons_autoload.html
+/tutorials/step_by_step/splash_screen.html,/learning/step_by_step/splash_screen.html
+/tutorials/threads/thread_safe_apis.html,/tutorials/performance/threads/thread_safe_apis.html
+/tutorials/threads/using_multiple_threads.html,/tutorials/performance/threads/using_multiple_threads.html
+/tutorials/vector_math.html,/learning/features/math/vector_math.html
+/tutorials/viewports/custom_postprocessing.html,/tutorials/shaders/custom_postprocessing.html
+/tutorials/viewports/multiple_resolutions.html,/tutorials/rendering/multiple_resolutions.html
+/tutorials/viewports/using_viewport_as_texture.html,/tutorials/shaders/using_viewport_as_texture.html
+/tutorials/viewports/viewports.html,/tutorials/rendering/viewports.html