diff --git a/content/actions/learn-github-actions/migrating-from-circleci-to-github-actions.md b/content/actions/learn-github-actions/migrating-from-circleci-to-github-actions.md index cbb113098844..f32c255333ba 100644 --- a/content/actions/learn-github-actions/migrating-from-circleci-to-github-actions.md +++ b/content/actions/learn-github-actions/migrating-from-circleci-to-github-actions.md @@ -443,7 +443,9 @@ jobs: path: vendor/bundle key: administrate-${{ matrix.image }}-${{ hashFiles('Gemfile.lock') }} - name: Install postgres headers - run: sudo apt-get install libpq-dev + run: | + sudo apt-get update + sudo apt-get install libpq-dev - name: Install dependencies run: bundle install --path vendor/bundle - name: Setup environment configuration diff --git a/content/actions/using-github-hosted-runners/about-github-hosted-runners.md b/content/actions/using-github-hosted-runners/about-github-hosted-runners.md index e31cad845da0..6eb8734b54c5 100644 --- a/content/actions/using-github-hosted-runners/about-github-hosted-runners.md +++ b/content/actions/using-github-hosted-runners/about-github-hosted-runners.md @@ -93,6 +93,10 @@ We recommend using actions to interact with the software installed on runners. T If there is a tool that you'd like to request, please open an issue at [actions/virtual-environments](https://github.com/actions/virtual-environments). This repository also contains announcements about all major software updates on runners. +#### Installing additional software + +You can install additional software on {% data variables.product.prodname_dotcom %}-hosted runners. For more information, see "[Customizing GitHub-hosted runners](/actions/using-github-hosted-runners/customizing-github-hosted-runners)". + ### IP addresses {% note %} diff --git a/content/actions/using-github-hosted-runners/customizing-github-hosted-runners.md b/content/actions/using-github-hosted-runners/customizing-github-hosted-runners.md new file mode 100644 index 000000000000..789f759f891d --- /dev/null +++ b/content/actions/using-github-hosted-runners/customizing-github-hosted-runners.md @@ -0,0 +1,92 @@ +--- +title: Customizing GitHub-hosted runners +intro: >- + You can install additional software on GitHub-hosted runners as a + part of your workflow. +product: '{% data reusables.gated-features.actions %}' +versions: + free-pro-team: '*' + enterprise-server: '>=2.22' +type: tutorial +topics: + - Workflows +--- + +{% data reusables.actions.enterprise-github-hosted-runners %} + +If you require additional software packages on {% data variables.product.prodname_dotcom %}-hosted runners, you can create a job that installs the packages as part of your workflow. + +To see which packages are already installed by default, see "[Preinstalled software](/actions/using-github-hosted-runners/about-github-hosted-runners#preinstalled-software)." + +This guide demonstrates how to create a job that installs additional software on a {% data variables.product.prodname_dotcom %}-hosted runner. + +### Installing software on Ubuntu runners + +The following example demonstrates how to install an `apt` package as part of a job. + +{% raw %} +```yaml +name: Build on Ubuntu +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - name: Install jq tool + run: | + sudo apt-get update + sudo apt-get install jq +``` +{% endraw %} + +{% note %} + +**Note:** Always run `sudo apt-get update` before installing a package. In case the `apt` index is stale, this command fetches and re-indexes any available packages, which helps prevent package installation failures. + +{% endnote %} + +### Installing software on macOS runners + +The following example demonstrates how to install Brew packages and casks as part of a job. + +{% raw %} +```yaml +name: Build on macOS +on: push + +jobs: + build: + runs-on: macos-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - name: Install GitHub CLI + run: | + brew update + brew install gh + - name: Install Microsoft Edge + run: | + brew update + brew install --cask microsoft-edge +``` +{% endraw %} + +### Installing software on Windows runners + +The following example demonstrates how to use [Chocolatey](https://community.chocolatey.org/packages) to install the {% data variables.product.prodname_dotcom %} CLI as part of a job. + +{% raw %} +```yaml +name: Build on Windows +on: push +jobs: + build: + runs-on: windows-latest + steps: + - run: choco install gh + - run: gh version +``` +{% endraw %} diff --git a/content/actions/using-github-hosted-runners/index.md b/content/actions/using-github-hosted-runners/index.md index 681269a25393..fb91ee9e6418 100644 --- a/content/actions/using-github-hosted-runners/index.md +++ b/content/actions/using-github-hosted-runners/index.md @@ -11,6 +11,7 @@ versions: {% data reusables.actions.enterprise-github-hosted-runners %} {% link_in_list /about-github-hosted-runners %} +{% link_in_list /customizing-github-hosted-runners %} {% link_in_list /about-ae-hosted-runners %} {% link_in_list /adding-ae-hosted-runners %} {% link_in_list /using-ae-hosted-runners-in-a-workflow %} diff --git a/content/admin/github-actions/using-the-latest-version-of-the-official-bundled-actions.md b/content/admin/github-actions/using-the-latest-version-of-the-official-bundled-actions.md index ad09801a476d..704a7e097578 100644 --- a/content/admin/github-actions/using-the-latest-version-of-the-official-bundled-actions.md +++ b/content/admin/github-actions/using-the-latest-version-of-the-official-bundled-actions.md @@ -26,12 +26,19 @@ You can use {% data variables.product.prodname_github_connect %} to allow {% dat Once {% data variables.product.prodname_github_connect %} is configured, you can use the latest version of an action by deleting its local repository in the `actions` organization on your instance. For example, if your enterprise instance is using the `actions/checkout@v1` action, and you need to use `actions/checkout@v2` which isn't available on your enterprise instance, perform the following steps to be able to use the latest `checkout` action from {% data variables.product.prodname_dotcom_the_website %}: -1. To get the required access to delete the `checkout` repository, use the `ghe-org-admin-promote` command to promote a user to be an owner of the bundled `actions` organization. For more information, see "[Accessing the administrative shell (SSH)](/admin/configuration/accessing-the-administrative-shell-ssh)" and "[`ghe-org-admin-promote`](/admin/configuration/command-line-utilities#ghe-org-admin-promote)." For example: +1. By default, site administrators are not owners of the bundled actions organization. To get the required access to delete the `checkout` repository, use the `ghe-org-admin-promote` command to promote a user to be an owner of the bundled `actions` organization. For more information, see "[Accessing the administrative shell (SSH)](/admin/configuration/accessing-the-administrative-shell-ssh)" and "[`ghe-org-admin-promote`](/admin/configuration/command-line-utilities#ghe-org-admin-promote)." For example: ```shell - ghe-org-admin-promote -u USERNAME -o actions + $ ghe-org-admin-promote -u octocat -o actions + Do you want to give organization admin privileges for actions to octocat? (y/N) y + Making octocat an admin of actions + --> Adding octocat as an admin of actions + --> octocat is now an admin of the actions organization + --> Done. ``` 1. On your {% data variables.product.product_name %} instance, delete the `checkout` repository within the `actions` organization. For information on how to delete a repository, see "[Deleting a repository ](/github/administering-a-repository/deleting-a-repository)." +1. It is recommended that you leave the `actions` organization once you no longer require administrative access. For more information, see "[Removing yourself from an organization +](/github/setting-up-and-managing-your-github-user-account/removing-yourself-from-an-organization)." 1. Configure your workflow's YAML to use `actions/checkout@v2`. 1. Each time your workflow runs, the runner will use the `v2` version of `actions/checkout` from {% data variables.product.prodname_dotcom_the_website %}. diff --git a/content/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account.md b/content/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account.md index 23fb2d23e715..7a1376f8f781 100644 --- a/content/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account.md +++ b/content/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account.md @@ -87,6 +87,7 @@ After adding a new SSH key to your {% data variables.product.product_name %} acc If your SSH public key file has a different name than the example code, modify the filename to match your current setup. When copying your key, don't add any newlines or whitespace. ```shell + $ sudo apt-get update $ sudo apt-get install xclip # Downloads and installs xclip. If you don't have `apt-get`, you might need to use another installer (like `yum`) diff --git a/content/github/authenticating-to-github/using-ssh-over-the-https-port.md b/content/github/authenticating-to-github/using-ssh-over-the-https-port.md index 3acbcec2750d..d3b7eb7dc9f8 100644 --- a/content/github/authenticating-to-github/using-ssh-over-the-https-port.md +++ b/content/github/authenticating-to-github/using-ssh-over-the-https-port.md @@ -11,7 +11,7 @@ topics: {% tip %} -**GitHub Enterprise users**: Accessing GitHub Enterprise via SSH over the HTTPS port is currently not supported. +**{% data variables.product.prodname_ghe_server %} users**: Accessing {% data variables.product.prodname_ghe_server %} via SSH over the HTTPS port is currently not supported. {% endtip %} diff --git a/data/reusables/secret-scanning/partner-secret-list-private-repo.md b/data/reusables/secret-scanning/partner-secret-list-private-repo.md index 69cad3e6633b..030bdefa4d25 100644 --- a/data/reusables/secret-scanning/partner-secret-list-private-repo.md +++ b/data/reusables/secret-scanning/partner-secret-list-private-repo.md @@ -32,6 +32,7 @@ Dropbox | Dropbox Short Lived Access Token | dropbox_short_lived_access_token Dynatrace | Dynatrace Access Token | dynatrace_access_token Dynatrace | Dynatrace Internal Token | dynatrace_internal_token Facebook | Facebook Access Token | facebook_access_token +Fastly | Fastly API Token | fastly_api_token Finicity | Finicity App Key | finicity_app_key Frame.io | Frame.io JSON Web Token | frameio_jwt Frame.io| Frame.io Developer Token | frameio_developer_token @@ -54,6 +55,7 @@ Mailchimp | Mailchimp API Key | mailchimp_api_key Mailgun | Mailgun API Key | mailgun_api_key npm | npm Access Token | npm_access_token NuGet | NuGet API Key | nuget_api_key +OpenAI | OpenAI API Key | openai_api_key Palantir | Palantir JSON Web Token | palantir_jwt Postman | Postman API Key | postman_api_key Proctorio | Proctorio Consumer Key | proctorio_consumer_key diff --git a/data/reusables/secret-scanning/partner-secret-list-public-repo.md b/data/reusables/secret-scanning/partner-secret-list-public-repo.md index a3926d6d72cc..a13fab6009b7 100644 --- a/data/reusables/secret-scanning/partner-secret-list-public-repo.md +++ b/data/reusables/secret-scanning/partner-secret-list-public-repo.md @@ -47,6 +47,7 @@ Mailgun | Mailgun API Key MessageBird | MessageBird API Key npm | npm Access Token NuGet | NuGet API Key +OpenAI | OpenAI API Key Palantir | Palantir JSON Web Token Plivo | Plivo Auth Token Postman | Postman API Key diff --git a/feature-flags.json b/feature-flags.json index 64c69f19d789..5a00273700b9 100644 --- a/feature-flags.json +++ b/feature-flags.json @@ -1,4 +1,5 @@ { "FEATURE_TEST_TRUE": true, - "FEATURE_TEST_FALSE": false + "FEATURE_TEST_FALSE": false, + "FEATURE_NEW_SITETREE": false } diff --git a/includes/article.html b/includes/article.html index ba58d0f82263..d4b408324025 100644 --- a/includes/article.html +++ b/includes/article.html @@ -68,6 +68,14 @@

diff --git a/includes/breadcrumbs.html b/includes/breadcrumbs.html index 2a08fecb2288..57c6ab0caa8f 100644 --- a/includes/breadcrumbs.html +++ b/includes/breadcrumbs.html @@ -1,3 +1,16 @@ +{% if FEATURE_NEW_SITETREE %} + +{% else %} +{% endif %} diff --git a/includes/category-articles-list.html b/includes/category-articles-list.html new file mode 100644 index 000000000000..27e2cdd7f7c1 --- /dev/null +++ b/includes/category-articles-list.html @@ -0,0 +1,34 @@ +{% for categoryPage in currentProductTree.childPages %} +{% if categoryPage.href == currentPath %}{% assign currentCategory = categoryPage %}{% endif %} +{% endfor %} + +{% if currentCategory.page.shortTitle and currentCategory.page.shortTitle != '' %}{% assign currentCategoryTitle = currentCategory.page.shortTitle %}{% else %}{% assign currentCategoryTitle = currentCategory.page.title %}{% endif %} + +{% assign maxArticles = 10 %} + +
+

{{ currentCategoryTitle }} docs

+ +
+ {% for childPage in currentCategory.childPages %} + {% unless childPage.page.hidden %} +
+

{{ childPage.page.title }}

+
    + {% for grandchildPage in childPage.childPages %} +
  • + + {{ grandchildPage.page.title }} + +
  • + {% endfor %} + {% assign numArticles = childPage.childPages | obj_size %} + {% if numArticles > maxArticles %} + + {% endif %} +
+
+ {% endunless %} + {% endfor %} +
+
diff --git a/includes/generic-toc-items.html b/includes/generic-toc-items.html new file mode 100644 index 000000000000..0c2def85be8e --- /dev/null +++ b/includes/generic-toc-items.html @@ -0,0 +1,12 @@ +{% if tocItems %} + +{% for tocItem in tocItems %} + +{% assign title = tocItem.title %} +{% assign fullPath = tocItem.fullPath %} +{% assign intro = tocItem.intro %} +{% include liquid-tags/link-with-intro %} + +{% endfor %} + +{% endif %} diff --git a/includes/generic-toc-list.html b/includes/generic-toc-list.html new file mode 100644 index 000000000000..8cdd55c9c730 --- /dev/null +++ b/includes/generic-toc-list.html @@ -0,0 +1,24 @@ +{% if tocItems %} + +{% endif %} diff --git a/includes/product-articles-list.html b/includes/product-articles-list.html new file mode 100644 index 000000000000..de0bec335691 --- /dev/null +++ b/includes/product-articles-list.html @@ -0,0 +1,46 @@ +{% assign maxArticles = 10 %} + +{% if currentProductTree.page.shortTitle and currentProductTree.page.shortTitle != '' %}{% assign productTitle = currentProductTree.page.shortTitle %}{% else %}{% assign productTitle = currentProductTree.page.title %}{% endif %} + +
+

All {{ productTitle }} docs

+ +
+ {% for childPage in currentProductTree.childPages %} + {% if childPage.page.documentType == "article" %}{% assign standaloneCategory = true %}{% else %}{% assign standaloneCategory = false %}{% endif %} + {% unless standaloneCategory %} +
+

{{ childPage.page.title }}

+ + {% if childPage.childPages and childPage.childPages[0].page.documentType == "mapTopic" %} +
    + {% for grandchildPage in childPage.childPages %} + {% unless grandchildPage.page.hidden %} + {% assign numArticles = childPage.childPages | obj_size %} +
  • + + {{ grandchildPage.page.title }} + +
  • + {% if numArticles > maxArticles %} + + {% endif %} + {% endunless %} + {% endfor %} +
+ {% else %} +
    + {% assign numArticles = childPage.childPages | obj_size %} + {% for grandchildPage in childPage.childPages %} +
  • {{ grandchildPage.page.title }}
  • + {% endfor %} +
+ {% if numArticles > maxArticles %} + + {% endif %} + {% endif %} +
+ {% endunless %} + {% endfor %} +
+
diff --git a/includes/sidebar-product.html b/includes/sidebar-product.html new file mode 100644 index 000000000000..5b5b6b78c11b --- /dev/null +++ b/includes/sidebar-product.html @@ -0,0 +1,74 @@ + + +{% include all-products-link %} + +{% unless currentProductTree.page.hidden %} + +{% if currentProductTree.renderedShortTitle %}{% assign productTitle = currentProductTree.renderedShortTitle %}{% else %}{% assign productTitle = currentProductTree.renderedFullTitle %}{% endif %} + + +
  • + +
  • + +{% endunless %} diff --git a/includes/sidebar.html b/includes/sidebar.html index 74f1c94cc518..470a26208fe9 100644 --- a/includes/sidebar.html +++ b/includes/sidebar.html @@ -15,7 +15,11 @@ {% else %} {% endif %} diff --git a/layouts/product-landing.html b/layouts/product-landing.html index 618ec963ef1b..db20278143de 100644 --- a/layouts/product-landing.html +++ b/layouts/product-landing.html @@ -150,11 +150,22 @@

    Guides

    {% endif %}
    + {% if FEATURE_NEW_SITETREE %} + {% if page.documentType == "category" %} + {% include category-articles-list %} + {% endif %} + {% if page.documentType == "product" %} + {% include product-articles-list %} + {% endif %} + {% endif %} + + {% unless FEATURE_NEW_SITETREE %} {% if currentCategory %} {% include all-articles-category %} {% else %} {% include all-articles-product %} {% endif %} + {% endunless %}
    {% include support-section %} diff --git a/lib/create-tree.js b/lib/create-tree.js index 8649b8283eab..fe89e52eb41b 100644 --- a/lib/create-tree.js +++ b/lib/create-tree.js @@ -20,7 +20,7 @@ module.exports = async function createTree (originalPath, langObj) { const localizedBasePath = path.posix.join(__dirname, '..', langObj.dir, 'content') // Initialize the Page! This is where the file reads happen. - let page = await Page.init({ + const page = await Page.init({ basePath: localizedBasePath, relativePath, languageCode: langObj.code @@ -29,17 +29,10 @@ module.exports = async function createTree (originalPath, langObj) { if (!page) { // Do not throw an error if Early Access is not available. if (relativePath.startsWith('early-access')) return - // If a translated path doesn't exist, fall back to the English so there is parity between - // the English tree and the translated trees. - if (langObj.code !== 'en') { - page = await Page.init({ - basePath: basePath, - relativePath, - languageCode: langObj.code - }) - } - - if (!page) throw Error(`Cannot initialize page for ${filepath}`) + // Do not throw an error if translated page is not available. + if (langObj.code !== 'en') return + + throw Error(`Cannot initialize page for ${filepath} in ${langObj.code}`) } // Create the root tree object on the first run, and create children recursively. diff --git a/lib/redirects/add-redirect-to-frontmatter.js b/lib/redirects/add-redirect-to-frontmatter.js index 3e2d26a4c980..d392eed5ecc7 100644 --- a/lib/redirects/add-redirect-to-frontmatter.js +++ b/lib/redirects/add-redirect-to-frontmatter.js @@ -1,7 +1,7 @@ // add a new redirect string to redirect_from frontmatter module.exports = function addRedirectToFrontmatter (redirectFromData, newRedirectString) { - if (Array.isArray(redirectFromData)) { + if (Array.isArray(redirectFromData) && !redirectFromData.includes(newRedirectString)) { redirectFromData.push(newRedirectString) } else if (typeof redirectFromData === 'string') { redirectFromData = [redirectFromData] diff --git a/middleware/categories-for-support.js b/middleware/categories-for-support.js new file mode 100644 index 000000000000..b577d012b533 --- /dev/null +++ b/middleware/categories-for-support.js @@ -0,0 +1,55 @@ +const path = require('path') +const renderOpts = { textOnly: true, encodeEntities: true } + +// This middleware exposes a list of all categories and child articles at /categories.json. +// GitHub Support uses this for internal ZenDesk search functionality. +module.exports = async function categoriesForSupport (req, res, next) { + const englishSiteTree = req.context.siteTree.en + + const allCategories = [] + + await Promise.all(Object.keys(englishSiteTree).map(async (version) => { + await Promise.all(englishSiteTree[version].childPages.map(async (productPage) => { + if (productPage.page.relativePath.startsWith('early-access')) return + if (!productPage.childPages) return + + await Promise.all(productPage.childPages.map(async (categoryPage) => { + // We can't get the rendered titles from middleware/render-tree-titles + // here because that middleware only runs on the current version, and this + // middleware processes all versions. + const name = categoryPage.page.title.includes('{') + ? await categoryPage.page.renderProp('title', req.context, renderOpts) + : categoryPage.page.title + + allCategories.push({ + name, + published_articles: await findArticlesPerCategory(categoryPage, [], req.context) + }) + })) + })) + })) + + return res.json(allCategories) +} + +async function findArticlesPerCategory (currentPage, articlesArray, context) { + if (currentPage.page.documentType === 'article') { + const title = currentPage.page.title.includes('{') + ? await currentPage.page.renderProp('title', context, renderOpts) + : currentPage.page.title + + articlesArray.push({ + title, + slug: path.basename(currentPage.href) + }) + } + + if (!currentPage.childPages) return articlesArray + + // Run recursively to find any articles deeper in the tree. + await Promise.all(currentPage.childPages.map(async (childPage) => { + await findArticlesPerCategory(childPage, articlesArray, context) + })) + + return articlesArray +} diff --git a/middleware/context.js b/middleware/context.js index 30081f2ec11d..ec9c94c85338 100644 --- a/middleware/context.js +++ b/middleware/context.js @@ -10,7 +10,7 @@ const { getPathWithoutLanguage } = require('../lib/path-utils') const productNames = require('../lib/product-names') -const warmServer = require('../lib/warm-server') +const warmServer = process.env.FEATURE_NEW_SITETREE ? require('../lib/warm-server2') : require('../lib/warm-server') const featureFlags = Object.keys(require('../feature-flags')) const builtAssets = require('../lib/built-asset-urls') const searchVersions = require('../lib/search/versions') @@ -27,7 +27,7 @@ module.exports = async function contextualize (req, res, next) { // make feature flag environment variables accessible in layouts req.context.process = { env: {} } featureFlags.forEach(featureFlagName => { - req.context.process.env[featureFlagName] = process.env[featureFlagName] + req.context[featureFlagName] = process.env[featureFlagName] }) // define each context property explicitly for code-search friendliness diff --git a/middleware/contextualizers/breadcrumbs.js b/middleware/contextualizers/breadcrumbs.js new file mode 100644 index 000000000000..2eccfd0e41f0 --- /dev/null +++ b/middleware/contextualizers/breadcrumbs.js @@ -0,0 +1,38 @@ +module.exports = async function breadcrumbs (req, res, next) { + if (!req.context.page) return next() + if (req.context.page.hidden) return next() + + req.context.breadcrumbs = [] + + // Return an empty array on the landing page. + if (req.context.page.documentType === 'homepage') { + return next() + } + + const currentSiteTree = req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] + + await createBreadcrumb( + // Array of child pages on the root, i.e., the product level. + currentSiteTree.childPages, + req.context + ) + + return next() +} + +async function createBreadcrumb (pageArray, context) { + // Find each page in the siteTree's array of child pages that starts with the requested path. + const childPage = pageArray.find(page => context.currentPath.startsWith(page.href)) + + context.breadcrumbs.push({ + documentType: childPage.page.documentType, + href: childPage.href, + title: childPage.renderedShortTitle || childPage.renderedFullTitle + }) + + // Recursively loop through the siteTree and create each breadcrumb, until we reach the + // point where the current siteTree page is the same as the requested page. Then stop. + if (childPage.childPages && context.currentPath !== childPage.href) { + createBreadcrumb(childPage.childPages, context) + } +} diff --git a/middleware/contextualizers/current-product-tree.js b/middleware/contextualizers/current-product-tree.js new file mode 100644 index 000000000000..92d0cb03d628 --- /dev/null +++ b/middleware/contextualizers/current-product-tree.js @@ -0,0 +1,10 @@ +module.exports = function currentProductTree (req, res, next) { + if (!req.context.page) return next() + if (req.context.page.documentType === 'homepage') return next() + + const currentSiteTree = req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] + + req.context.currentProductTree = currentSiteTree.childPages.find(page => req.context.currentPath.startsWith(page.href)) + + return next() +} diff --git a/middleware/contextualizers/early-access-breadcrumbs.js b/middleware/contextualizers/early-access-breadcrumbs.js new file mode 100644 index 000000000000..21f1a0aee8d9 --- /dev/null +++ b/middleware/contextualizers/early-access-breadcrumbs.js @@ -0,0 +1,55 @@ +module.exports = async function breadcrumbs (req, res, next) { + if (!req.context.page) return next() + if (!req.context.page.hidden) return next() + + req.context.breadcrumbs = [] + + // Return an empty array on the landing page. + if (req.context.page.documentType === 'homepage') { + return next() + } + + const earlyAccessProduct = req.context.siteTree[req.language][req.context.currentVersion].childPages.find(childPage => childPage.page.relativePath === 'early-access/index.md') + if (!earlyAccessProduct) return next() + + // Create initial landing page breadcrumb + req.context.breadcrumbs.push({ + documentType: earlyAccessProduct.page.documentType, + href: '', + title: earlyAccessProduct.page.title + }) + + // If this is the Early Access landing page, return now + if (req.context.currentPath === earlyAccessProduct.href) { + return next() + } + + // Otherwise, create breadcrumbs + await createBreadcrumb( + earlyAccessProduct.childPages, + req.context + ) + + return next() +} + +async function createBreadcrumb (pageArray, context) { + // Find each page in the siteTree's array of child pages that starts with the requested path. + const childPage = pageArray.find(page => context.currentPath.startsWith(page.href)) + + // Gray out product breadcrumb links and `Articles` categories + const hideHref = childPage.page.documentType === 'product' || + (childPage.page.documentType === 'category' && childPage.page.relativePath.endsWith('/articles/index.md')) + + context.breadcrumbs.push({ + documentType: childPage.page.documentType, + href: hideHref ? '' : childPage.href, + title: await childPage.page.renderTitle(context, { textOnly: true, encodeEntities: true }) + }) + + // Recursively loop through the siteTree and create each breadcrumb, until we reach the + // point where the current siteTree page is the same as the requested page. Then stop. + if (childPage.childPages && context.currentPath !== childPage.href) { + createBreadcrumb(childPage.childPages, context) + } +} diff --git a/middleware/contextualizers/early-access-links.js b/middleware/contextualizers/early-access-links.js index f09c68f1d9d9..b30090a0731a 100644 --- a/middleware/contextualizers/early-access-links.js +++ b/middleware/contextualizers/early-access-links.js @@ -7,7 +7,7 @@ module.exports = function earlyAccessContext (req, res, next) { // Get a list of all hidden pages per version const earlyAccessPageLinks = uniq(Object.values(req.context.pages) - .filter(page => page.hidden && page.relativePath.startsWith('early-access') && page.relativePath !== 'early-access/index.md') + .filter(page => page.hidden && page.relativePath.startsWith('early-access') && !page.relativePath.endsWith('index.md')) .map(page => page.permalinks) .flat()) // Get links for the current version diff --git a/middleware/contextualizers/generic-toc.js b/middleware/contextualizers/generic-toc.js new file mode 100644 index 000000000000..9ebcee7d089c --- /dev/null +++ b/middleware/contextualizers/generic-toc.js @@ -0,0 +1,59 @@ +const { sortBy } = require('lodash') + +module.exports = async function genericToc (req, res, next) { + if (!req.context.page) return next() + if (req.context.page.hidden) return next() + if (req.context.currentLayoutName !== 'default') return next() + if (req.context.page.documentType === 'homepage' || req.context.page.documentType === 'article') return next() + + const currentSiteTree = req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] + + // Find the array of child pages that start with the requested path. + const currentPageInSiteTree = findPageInSiteTree(currentSiteTree.childPages, req.context.currentPath) + + req.context.tocItems = sortBy( + await getUnsortedTocItems(currentPageInSiteTree.childPages, req.context), + // Sort by the ordered array of `children` in the frontmatter. + currentPageInSiteTree.page.children + ) + + return next() +} + +// Recursively loop through the siteTree until we reach the point where the +// current siteTree page is the same as the requested page. Then stop. +function findPageInSiteTree (pageArray, currentPath) { + const childPage = pageArray.find(page => currentPath.startsWith(page.href)) + + if (childPage.href === currentPath) { + return childPage + } + + return findPageInSiteTree(childPage.childPages, currentPath) +} + +async function getUnsortedTocItems (pageArray, context) { + return Promise.all(pageArray.map(async (childPage) => { + // return an empty string if it's a hidden link on a non-hidden page (hidden links on hidden pages are OK) + if (childPage.page.hidden && !context.page.hidden) { + return '' + } + + const fullPath = childPage.href + // Titles are already rendered by middleware/contextualizers/render-tree-titles.js. + const title = childPage.renderedFullTitle + const intro = await childPage.page.renderProp('intro', context, { unwrap: true }) + + if (!childPage.childPages) { + return { fullPath, title, intro } + } + + const childTocItems = sortBy( + await getUnsortedTocItems(childPage.childPages, context), + // Sort by the ordered array of `children` in the frontmatter. + childPage.page.children + ) + + return { fullPath, title, intro, childTocItems } + })) +} diff --git a/middleware/contextualizers/layout.js b/middleware/contextualizers/layout.js index 54c40540a459..7a6051c2eb4f 100644 --- a/middleware/contextualizers/layout.js +++ b/middleware/contextualizers/layout.js @@ -3,20 +3,18 @@ const layouts = require('../../lib/layouts') module.exports = function layoutContext (req, res, next) { if (!req.context.page) return next() - let layoutName - - if (req.context.page.layout) { + const layoutOptsByType = { // Layouts can be specified with a `layout` frontmatter value. // Any invalid layout values will be caught by frontmatter schema validation. - layoutName = req.context.page.layout + string: req.context.page.layout, // A `layout: false` value means use no layout. - } else if (req.context.page.layout === false) { - layoutName = '' - // If undefined, use either the default layout or the generic-toc layout. - } else if (req.context.page.layout === undefined) { - layoutName = 'default' + boolean: '', + // For all other files (like articles and the homepage), use the `default` layout. + undefined: 'default' } + const layoutName = layoutOptsByType[typeof (req.context.page.layout)] + // Attach to the context object req.context.currentLayoutName = layoutName req.context.currentLayout = layouts[layoutName] diff --git a/middleware/contextualizers/render-tree-titles.js b/middleware/contextualizers/render-tree-titles.js new file mode 100644 index 000000000000..0d71ae50ddb6 --- /dev/null +++ b/middleware/contextualizers/render-tree-titles.js @@ -0,0 +1,33 @@ +const { sortBy } = require('lodash') +const renderOpts = { textOnly: true, encodeEntities: true } + +module.exports = async function renderTreeTitles (req, res, next) { + if (!req.context.page) return next() + if (req.context.page.documentType === 'homepage') return next() + + await renderLiquidInTitles(req.context.siteTree[req.context.currentLanguage][req.context.currentVersion], req.context) + + return next() +} + +// Add new props to each siteTree page here... +async function renderLiquidInTitles (pageInTree, context) { + // We _only_ need to render the titles and shortTitles that contain Liquid. + pageInTree.renderedFullTitle = pageInTree.page.title.includes('{') + ? await pageInTree.page.renderProp('title', context, renderOpts) + : pageInTree.page.title + + if (pageInTree.page.shortTitle) { + pageInTree.renderedShortTitle = pageInTree.page.shortTitle.includes('{') + ? await pageInTree.page.renderProp('shortTitle', context, renderOpts) + : pageInTree.page.shortTitle + } + + if (!pageInTree.childPages) return + + pageInTree.page.childPages = sortBy( + await Promise.all(pageInTree.childPages.map(async (childPage) => await renderLiquidInTitles(childPage, context))), + // Sort by the ordered array of `children` in the frontmatter. + pageInTree.page.children + ) +} diff --git a/middleware/index.js b/middleware/index.js index e6cacac9a7cb..cb70213a790f 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -97,7 +97,12 @@ module.exports = function (app) { app.use(asyncMiddleware(instrument('./archived-enterprise-versions'))) app.use(instrument('./robots')) app.use(/(\/.*)?\/early-access$/, instrument('./contextualizers/early-access-links')) - app.use('/categories.json', asyncMiddleware(instrument('./categories-for-support-team'))) + if (!process.env.FEATURE_NEW_SITETREE) { + app.use('/categories.json', asyncMiddleware(instrument('./categories-for-support-team'))) + } + if (process.env.FEATURE_NEW_SITETREE) { + app.use('/categories.json', asyncMiddleware(instrument('./categories-for-support'))) + } app.use(instrument('./loaderio-verification')) app.get('/_500', asyncMiddleware(instrument('./trigger-error'))) @@ -111,8 +116,20 @@ module.exports = function (app) { app.use(instrument('./contextualizers/webhooks')) app.use(asyncMiddleware(instrument('./contextualizers/whats-new-changelog'))) app.use(instrument('./contextualizers/layout')) - app.use(asyncMiddleware(instrument('./breadcrumbs'))) - app.use(asyncMiddleware(instrument('./early-access-breadcrumbs'))) + + if (!process.env.FEATURE_NEW_SITETREE) { + app.use(asyncMiddleware(instrument('./breadcrumbs'))) + app.use(asyncMiddleware(instrument('./early-access-breadcrumbs'))) + } + + if (process.env.FEATURE_NEW_SITETREE) { + app.use(asyncMiddleware(instrument('./contextualizers/render-tree-titles'))) + app.use(instrument('./contextualizers/current-product-tree')) + app.use(asyncMiddleware(instrument('./contextualizers/generic-toc'))) + app.use(asyncMiddleware(instrument('./contextualizers/breadcrumbs'))) + app.use(asyncMiddleware(instrument('./contextualizers/early-access-breadcrumbs'))) + } + app.use(asyncMiddleware(instrument('./enterprise-server-releases'))) app.use(asyncMiddleware(instrument('./dev-toc'))) app.use(asyncMiddleware(instrument('./featured-links'))) diff --git a/script/content-migrations/add-early-access-tocs.js b/script/content-migrations/add-early-access-tocs.js new file mode 100755 index 000000000000..34d936f50e86 --- /dev/null +++ b/script/content-migrations/add-early-access-tocs.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') +const readFrontmatter = require('../../lib/read-frontmatter') +const earlyAccessDir = path.posix.join(process.cwd(), 'content', 'early-access') +const { sentenceCase } = require('change-case') + +updateOrCreateToc(earlyAccessDir) + +console.log('Updated Early Access TOCs!') + +function updateOrCreateToc (directory) { + const children = fs.readdirSync(directory) + .filter(subpath => !subpath.endsWith('index.md')) + + if (!children.length) return + + const tocFile = path.posix.join(directory, 'index.md') + + let content, data + + if (fs.existsSync(tocFile)) { + const matter = readFrontmatter(fs.readFileSync(tocFile, 'utf8')) + content = matter.content + data = matter.data + } else { + content = '' + data = { + title: sentenceCase(path.basename(directory)), + versions: '*', + hidden: true + } + } + + data.children = children.map(child => `/${child.replace('.md', '')}`) + const newContents = readFrontmatter.stringify(content, data, { lineWidth: 10000 }) + fs.writeFileSync(tocFile, newContents) + + children.forEach(child => { + if (child.endsWith('.md')) return + updateOrCreateToc(path.posix.join(directory, child)) + }) +} diff --git a/script/content-migrations/remove-map-topics.js b/script/content-migrations/remove-map-topics.js index 7da04a8e4dac..ec24064da777 100755 --- a/script/content-migrations/remove-map-topics.js +++ b/script/content-migrations/remove-map-topics.js @@ -3,6 +3,7 @@ const fs = require('fs') const path = require('path') const walk = require('walk-sync') +const stripHtmlComments = require('strip-html-comments') const languages = require('../../lib/languages') const frontmatter = require('../../lib/read-frontmatter') const addRedirectToFrontmatter = require('../../lib/redirects/add-redirect-to-frontmatter') @@ -26,6 +27,10 @@ const categoryIndexFiles = fullDirectoryPaths.map(fullDirectoryPath => walk(full categoryIndexFiles.forEach(categoryIndexFile => { let categoryIndexContent = fs.readFileSync(categoryIndexFile, 'utf8') + if (categoryIndexFile.endsWith('github/getting-started-with-github/index.md')) { + categoryIndexContent = stripHtmlComments(categoryIndexContent.replace(/\n$/m.test(line)) + + newContent = newLinesArray.join('\n') + // Index files should no longer have body content, so we write an empty string - fs.writeFileSync(indexFile, frontmatter.stringify('', data, { lineWidth: 10000 })) + fs.writeFileSync(indexFile, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) }) function getLinks (linkItemArray) {