mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-30 16:37:56 +02:00
Merge branch 'main' into ozaki/execa
This commit is contained in:
commit
6c4a1d7891
147 changed files with 5409 additions and 981 deletions
|
@ -32,6 +32,8 @@
|
||||||
"website/_dogfooding/_pages tests/diagrams.mdx",
|
"website/_dogfooding/_pages tests/diagrams.mdx",
|
||||||
"*.xyz",
|
"*.xyz",
|
||||||
"*.docx",
|
"*.docx",
|
||||||
|
"*.xsl",
|
||||||
|
"*.xslt",
|
||||||
"*.gitignore",
|
"*.gitignore",
|
||||||
"versioned_docs",
|
"versioned_docs",
|
||||||
"*.min.*",
|
"*.min.*",
|
||||||
|
|
35
.github/workflows/continuous-releases.yml
vendored
Normal file
35
.github/workflows/continuous-releases.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: Continuous Releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- docusaurus-v**
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Continuous Releases
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: Installation
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: yarn build:packages
|
||||||
|
|
||||||
|
- name: Initialize fresh templates
|
||||||
|
run: |
|
||||||
|
yarn create-docusaurus template/docusaurus-classic-js classic --javascript -p npm
|
||||||
|
yarn create-docusaurus template/docusaurus-classic-ts classic --typescript -p npm
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
run: npx pkg-pr-new publish './packages/*' --template './template/*' --compact --comment=off
|
|
@ -5,6 +5,9 @@ build
|
||||||
coverage
|
coverage
|
||||||
.docusaurus
|
.docusaurus
|
||||||
|
|
||||||
|
.svg
|
||||||
|
*.svg
|
||||||
|
|
||||||
jest/vendor
|
jest/vendor
|
||||||
|
|
||||||
packages/lqip-loader/lib/
|
packages/lqip-loader/lib/
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@crowdin/cli": "^3.13.0",
|
"@crowdin/cli": "^3.13.0",
|
||||||
|
"@prettier/plugin-xml": "^2.2.0",
|
||||||
"@swc/core": "1.2.197",
|
"@swc/core": "1.2.197",
|
||||||
"@swc/jest": "^0.2.26",
|
"@swc/jest": "^0.2.26",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
@ -105,7 +106,7 @@
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"lockfile-lint": "^4.14.0",
|
"lockfile-lint": "^4.14.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.8",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
|
|
|
@ -42,6 +42,10 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
showReadingTime: true,
|
showReadingTime: true,
|
||||||
|
feedOptions: {
|
||||||
|
type: ['rss', 'atom'],
|
||||||
|
xslt: true,
|
||||||
|
},
|
||||||
// Please change this to your repo.
|
// Please change this to your repo.
|
||||||
// Remove this to remove the "edit this page" links.
|
// Remove this to remove the "edit this page" links.
|
||||||
editUrl:
|
editUrl:
|
||||||
|
|
|
@ -48,6 +48,10 @@ const config = {
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
showReadingTime: true,
|
showReadingTime: true,
|
||||||
|
feedOptions: {
|
||||||
|
type: ['rss', 'atom'],
|
||||||
|
xslt: true,
|
||||||
|
},
|
||||||
// Please change this to your repo.
|
// Please change this to your repo.
|
||||||
// Remove this to remove the "edit this page" links.
|
// Remove this to remove the "edit this page" links.
|
||||||
editUrl:
|
editUrl:
|
||||||
|
|
75
packages/docusaurus-plugin-content-blog/assets/atom.css
Normal file
75
packages/docusaurus-plugin-content-blog/assets/atom.css
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1.6rem 2.4rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .rss-icon {
|
||||||
|
height: 3.2rem;
|
||||||
|
width: 3.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-description {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-date {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #797b7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: #434349;
|
||||||
|
}
|
92
packages/docusaurus-plugin-content-blog/assets/atom.xsl
Normal file
92
packages/docusaurus-plugin-content-blog/assets/atom.xsl
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xsl:stylesheet
|
||||||
|
version="3.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Atom Feed | <xsl:value-of
|
||||||
|
select="atom:feed/atom:title"
|
||||||
|
/></title>
|
||||||
|
<link rel="stylesheet" href="atom.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an Atom feed</strong>. Subscribe by copying the URL
|
||||||
|
from the address bar into your newsreader. Visit
|
||||||
|
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free.
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill: #f78422"
|
||||||
|
width="455.731"
|
||||||
|
height="455.731"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #ffffff"
|
||||||
|
cx="109.833"
|
||||||
|
cy="346.26"
|
||||||
|
r="46.088"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<xsl:value-of select="atom:feed/atom:title" />
|
||||||
|
</h1>
|
||||||
|
<p class="blog-description">
|
||||||
|
<xsl:value-of select="atom:feed/atom:subtitle" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="blog-posts">
|
||||||
|
<xsl:for-each select="atom:feed/atom:entry">
|
||||||
|
<div class="blog-post">
|
||||||
|
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
|
||||||
|
select="atom:title"
|
||||||
|
/></a></h3>
|
||||||
|
<div class="blog-post-date">
|
||||||
|
Published on <xsl:value-of
|
||||||
|
select="substring(atom:updated,0,11)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="blog-post-description">
|
||||||
|
<xsl:value-of select="atom:summary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
75
packages/docusaurus-plugin-content-blog/assets/rss.css
Normal file
75
packages/docusaurus-plugin-content-blog/assets/rss.css
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1.6rem 2.4rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .rss-icon {
|
||||||
|
height: 3.2rem;
|
||||||
|
width: 3.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-description {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-date {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #797b7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: #434349;
|
||||||
|
}
|
86
packages/docusaurus-plugin-content-blog/assets/rss.xsl
Normal file
86
packages/docusaurus-plugin-content-blog/assets/rss.xsl
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xsl:stylesheet
|
||||||
|
version="3.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
|
||||||
|
<link rel="stylesheet" href="rss.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an RSS feed</strong>. Subscribe by copying the URL
|
||||||
|
from the address bar into your newsreader. Visit
|
||||||
|
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free.
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill: #f78422"
|
||||||
|
width="455.731"
|
||||||
|
height="455.731"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #ffffff"
|
||||||
|
cx="109.833"
|
||||||
|
cy="346.26"
|
||||||
|
r="46.088"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<xsl:value-of select="rss/channel/title" />
|
||||||
|
</h1>
|
||||||
|
<p class="blog-description">
|
||||||
|
<xsl:value-of select="rss/channel/description" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="blog-posts">
|
||||||
|
<xsl:for-each select="rss/channel/item">
|
||||||
|
<div class="blog-post">
|
||||||
|
<h3><a href="{link}"><xsl:value-of select="title" /></a></h3>
|
||||||
|
<div class="blog-post-date">
|
||||||
|
Published on <xsl:value-of select="substring(pubDate,0,17)" />
|
||||||
|
</div>
|
||||||
|
<div class="blog-post-description">
|
||||||
|
<xsl:value-of select="description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
|
@ -59,6 +59,7 @@
|
||||||
"node": ">=18.0"
|
"node": ">=18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@total-typescript/shoehorn": "^0.1.2"
|
"@total-typescript/shoehorn": "^0.1.2",
|
||||||
|
"tree-node-cli": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,3 +7,4 @@ slorber:
|
||||||
twitter: sebastienlorber
|
twitter: sebastienlorber
|
||||||
x: https://x.com/sebastienlorber
|
x: https://x.com/sebastienlorber
|
||||||
github: slorber
|
github: slorber
|
||||||
|
page: true
|
||||||
|
|
76
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css
generated
Normal file
76
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.css
generated
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: #0d1137;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 1.5 rem;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 3rem 0;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #e52165;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rss-icon {
|
||||||
|
height: 3.8rem;
|
||||||
|
width: 3.8rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-7 {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2:not(:first-child) {
|
||||||
|
margin-top: 5.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
65
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl
generated
Normal file
65
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-atom.xsl
generated
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Atom Feed | <xsl:value-of select="atom:feed/atom:title" /></title>
|
||||||
|
<link rel="stylesheet" href="custom-atom.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an Atom feed</strong>. Subscribe by copying the URL from the address
|
||||||
|
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free. </div>
|
||||||
|
<h1 class="flex items-start">
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
|
||||||
|
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
Custom Atom Feed Preview </h1>
|
||||||
|
<h2>
|
||||||
|
<xsl:value-of select="atom:feed/atom:title" />
|
||||||
|
</h2>
|
||||||
|
<p>Description: <xsl:value-of select="atom:feed/atom:subtitle" /></p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="postsList">
|
||||||
|
<xsl:for-each select="atom:feed/atom:entry">
|
||||||
|
<div class="pb-7">
|
||||||
|
<a href="{atom:link[@rel='alternate']/@href}">
|
||||||
|
<xsl:value-of select="atom:title" />
|
||||||
|
</a>
|
||||||
|
<div class="text-2 text-offset"> Published on <xsl:value-of
|
||||||
|
select="substring(atom:updated, 0, 17)" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2 text-offset italic">
|
||||||
|
<xsl:value-of select="atom:summary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
</xsl:stylesheet>
|
76
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css
generated
Normal file
76
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.css
generated
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: #0d1137;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 1.5 rem;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 3rem 0;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #e52165;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rss-icon {
|
||||||
|
height: 3.8rem;
|
||||||
|
width: 3.8rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-7 {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2:not(:first-child) {
|
||||||
|
margin-top: 5.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
66
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl
generated
Normal file
66
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/custom-rss.xsl
generated
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
|
||||||
|
<link rel="stylesheet" href="custom-rss.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an RSS feed</strong>. Subscribe by copying the URL from the address
|
||||||
|
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free. </div>
|
||||||
|
<h1 class="flex items-start">
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
|
||||||
|
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
Custom RSS Feed Preview </h1>
|
||||||
|
<h2>
|
||||||
|
<xsl:value-of select="rss/channel/title" />
|
||||||
|
</h2>
|
||||||
|
<p>Description: <xsl:value-of select="rss/channel/description" /></p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="postsList">
|
||||||
|
<xsl:for-each select="rss/channel/item">
|
||||||
|
<div class="pb-7">
|
||||||
|
<a href="{link}">
|
||||||
|
<xsl:value-of select="title" />
|
||||||
|
</a>
|
||||||
|
<div class="text-2 text-offset"> Published on <xsl:value-of
|
||||||
|
select="substring(pubDate,0,17)" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2 text-offset italic">
|
||||||
|
<xsl:value-of
|
||||||
|
select="description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
</xsl:stylesheet>
|
75
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css
generated
Normal file
75
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.css
generated
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1.6rem 2.4rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .rss-icon {
|
||||||
|
height: 3.2rem;
|
||||||
|
width: 3.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-description {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-date {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #797b7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: #434349;
|
||||||
|
}
|
92
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl
generated
Normal file
92
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl
generated
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xsl:stylesheet
|
||||||
|
version="3.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Atom Feed | <xsl:value-of
|
||||||
|
select="atom:feed/atom:title"
|
||||||
|
/></title>
|
||||||
|
<link rel="stylesheet" href="atom.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an Atom feed</strong>. Subscribe by copying the URL
|
||||||
|
from the address bar into your newsreader. Visit
|
||||||
|
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free.
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill: #f78422"
|
||||||
|
width="455.731"
|
||||||
|
height="455.731"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #ffffff"
|
||||||
|
cx="109.833"
|
||||||
|
cy="346.26"
|
||||||
|
r="46.088"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<xsl:value-of select="atom:feed/atom:title" />
|
||||||
|
</h1>
|
||||||
|
<p class="blog-description">
|
||||||
|
<xsl:value-of select="atom:feed/atom:subtitle" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="blog-posts">
|
||||||
|
<xsl:for-each select="atom:feed/atom:entry">
|
||||||
|
<div class="blog-post">
|
||||||
|
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
|
||||||
|
select="atom:title"
|
||||||
|
/></a></h3>
|
||||||
|
<div class="blog-post-date">
|
||||||
|
Published on <xsl:value-of
|
||||||
|
select="substring(atom:updated,0,11)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="blog-post-description">
|
||||||
|
<xsl:value-of select="atom:summary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: #0d1137;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 1.5 rem;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 3rem 0;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #e52165;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rss-icon {
|
||||||
|
height: 3.8rem;
|
||||||
|
width: 3.8rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-7 {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2:not(:first-child) {
|
||||||
|
margin-top: 5.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Atom Feed | <xsl:value-of select="atom:feed/atom:title" /></title>
|
||||||
|
<link rel="stylesheet" href="custom-atom.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an Atom feed</strong>. Subscribe by copying the URL from the address
|
||||||
|
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free. </div>
|
||||||
|
<h1 class="flex items-start">
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
|
||||||
|
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
Custom Atom Feed Preview </h1>
|
||||||
|
<h2>
|
||||||
|
<xsl:value-of select="atom:feed/atom:title" />
|
||||||
|
</h2>
|
||||||
|
<p>Description: <xsl:value-of select="atom:feed/atom:subtitle" /></p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="postsList">
|
||||||
|
<xsl:for-each select="atom:feed/atom:entry">
|
||||||
|
<div class="pb-7">
|
||||||
|
<a href="{atom:link[@rel='alternate']/@href}">
|
||||||
|
<xsl:value-of select="atom:title" />
|
||||||
|
</a>
|
||||||
|
<div class="text-2 text-offset"> Published on <xsl:value-of
|
||||||
|
select="substring(atom:updated, 0, 17)" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2 text-offset italic">
|
||||||
|
<xsl:value-of select="atom:summary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
</xsl:stylesheet>
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: #0d1137;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 1.5 rem;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 3rem 0;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #e52165;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rss-icon {
|
||||||
|
height: 3.8rem;
|
||||||
|
width: 3.8rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-7 {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2:not(:first-child) {
|
||||||
|
margin-top: 5.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
|
||||||
|
<link rel="stylesheet" href="custom-rss.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an RSS feed</strong>. Subscribe by copying the URL from the address
|
||||||
|
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free. </div>
|
||||||
|
<h1 class="flex items-start">
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
|
||||||
|
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
Custom RSS Feed Preview </h1>
|
||||||
|
<h2>
|
||||||
|
<xsl:value-of select="rss/channel/title" />
|
||||||
|
</h2>
|
||||||
|
<p>Description: <xsl:value-of select="rss/channel/description" /></p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="postsList">
|
||||||
|
<xsl:for-each select="rss/channel/item">
|
||||||
|
<div class="pb-7">
|
||||||
|
<a href="{link}">
|
||||||
|
<xsl:value-of select="title" />
|
||||||
|
</a>
|
||||||
|
<div class="text-2 text-offset"> Published on <xsl:value-of
|
||||||
|
select="substring(pubDate,0,17)" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2 text-offset italic">
|
||||||
|
<xsl:value-of
|
||||||
|
select="description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
</xsl:stylesheet>
|
75
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css
generated
Normal file
75
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.css
generated
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 800px;
|
||||||
|
/* stylelint-disable-next-line font-family-name-quotes */
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: block;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1.6rem 2.4rem;
|
||||||
|
border: 1px solid dodgerblue;
|
||||||
|
border-left-width: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #005aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-wrap: balance;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 .rss-icon {
|
||||||
|
height: 3.2rem;
|
||||||
|
width: 3.2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-description {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-date {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #797b7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: #434349;
|
||||||
|
}
|
86
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl
generated
Normal file
86
packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/rss.xsl
generated
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xsl:stylesheet
|
||||||
|
version="3.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
|
||||||
|
<link rel="stylesheet" href="rss.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="description">
|
||||||
|
<div class="info">
|
||||||
|
<strong>This is an RSS feed</strong>. Subscribe by copying the URL
|
||||||
|
from the address bar into your newsreader. Visit
|
||||||
|
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
|
||||||
|
and get started. It’s free.
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<div class="rss-icon">
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 455.731 455.731"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="fill: #f78422"
|
||||||
|
width="455.731"
|
||||||
|
height="455.731"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
|
||||||
|
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
|
||||||
|
C391.986,303.103,357.971,220.923,296.208,159.16z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style="fill: #ffffff"
|
||||||
|
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
|
||||||
|
C282.429,270.196,184.507,172.273,64.143,172.273z"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #ffffff"
|
||||||
|
cx="109.833"
|
||||||
|
cy="346.26"
|
||||||
|
r="46.088"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<xsl:value-of select="rss/channel/title" />
|
||||||
|
</h1>
|
||||||
|
<p class="blog-description">
|
||||||
|
<xsl:value-of select="rss/channel/description" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="blog-posts">
|
||||||
|
<xsl:for-each select="rss/channel/item">
|
||||||
|
<div class="blog-post">
|
||||||
|
<h3><a href="{link}"><xsl:value-of select="title" /></a></h3>
|
||||||
|
<div class="blog-post-date">
|
||||||
|
Published on <xsl:value-of select="substring(pubDate,0,17)" />
|
||||||
|
</div>
|
||||||
|
<div class="blog-post-description">
|
||||||
|
<xsl:value-of select="description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
|
@ -2,3 +2,5 @@ slorber:
|
||||||
name: Sébastien Lorber (translated)
|
name: Sébastien Lorber (translated)
|
||||||
title: Docusaurus maintainer (translated)
|
title: Docusaurus maintainer (translated)
|
||||||
email: lorber.sebastien@gmail.com
|
email: lorber.sebastien@gmail.com
|
||||||
|
page:
|
||||||
|
permalink: "/slorber-custom-permalink-localized"
|
||||||
|
|
|
@ -25,6 +25,25 @@ exports[`paginateBlogPosts generates a single page 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`paginateBlogPosts generates pages - 0 blog post 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"items": [],
|
||||||
|
"metadata": {
|
||||||
|
"blogDescription": "Blog Description",
|
||||||
|
"blogTitle": "Blog Title",
|
||||||
|
"nextPage": undefined,
|
||||||
|
"page": 1,
|
||||||
|
"permalink": "/blog",
|
||||||
|
"postsPerPage": 2,
|
||||||
|
"previousPage": undefined,
|
||||||
|
"totalCount": 0,
|
||||||
|
"totalPages": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`paginateBlogPosts generates pages 1`] = `
|
exports[`paginateBlogPosts generates pages 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -121,7 +121,9 @@ exports[`blog plugin process blog posts load content 2`] = `
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"imageURL": undefined,
|
"imageURL": undefined,
|
||||||
|
"key": null,
|
||||||
"name": "Sébastien Lorber",
|
"name": "Sébastien Lorber",
|
||||||
|
"page": null,
|
||||||
"title": "Docusaurus maintainer",
|
"title": "Docusaurus maintainer",
|
||||||
"url": "https://sebastienlorber.com",
|
"url": "https://sebastienlorber.com",
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`validateOptions throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`;
|
exports[`validateOptions feed throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`;
|
||||||
|
|
||||||
exports[`validateOptions throws Error in case of invalid options 1`] = `""postsPerPage" must be greater than or equal to 1"`;
|
exports[`validateOptions throws Error in case of invalid options 1`] = `""postsPerPage" must be greater than or equal to 1"`;
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'path';
|
import {fromPartial, type PartialDeep} from '@total-typescript/shoehorn';
|
||||||
import {
|
import {getBlogPostAuthors, groupBlogPostsByAuthorKey} from '../authors';
|
||||||
type AuthorsMap,
|
import type {AuthorsMap, BlogPost} from '@docusaurus/plugin-content-blog';
|
||||||
getAuthorsMap,
|
|
||||||
getBlogPostAuthors,
|
function post(partial: PartialDeep<BlogPost>): BlogPost {
|
||||||
validateAuthorsMap,
|
return fromPartial(partial);
|
||||||
} from '../authors';
|
}
|
||||||
|
|
||||||
describe('getBlogPostAuthors', () => {
|
describe('getBlogPostAuthors', () => {
|
||||||
it('can read no authors', () => {
|
it('can read no authors', () => {
|
||||||
|
@ -42,7 +42,15 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{name: 'Sébastien Lorber'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
imageURL: undefined,
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
|
title: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -51,7 +59,15 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{title: 'maintainer'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
title: 'maintainer',
|
||||||
|
imageURL: undefined,
|
||||||
|
key: null,
|
||||||
|
name: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -60,7 +76,14 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{imageURL: 'https://github.com/slorber.png'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
key: null,
|
||||||
|
name: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -69,7 +92,14 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{imageURL: '/img/slorber.png'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
imageURL: '/img/slorber.png',
|
||||||
|
key: null,
|
||||||
|
name: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -78,7 +108,15 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/baseURL',
|
baseUrl: '/baseURL',
|
||||||
}),
|
}),
|
||||||
).toEqual([{imageURL: '/baseURL/img/slorber.png'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
imageURL: '/baseURL/img/slorber.png',
|
||||||
|
|
||||||
|
key: null,
|
||||||
|
name: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -99,6 +137,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
title: 'maintainer1',
|
title: 'maintainer1',
|
||||||
imageURL: 'https://github.com/slorber1.png',
|
imageURL: 'https://github.com/slorber1.png',
|
||||||
url: 'https://github.com/slorber1',
|
url: 'https://github.com/slorber1',
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -109,10 +149,19 @@ describe('getBlogPostAuthors', () => {
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
authors: 'slorber',
|
authors: 'slorber',
|
||||||
},
|
},
|
||||||
authorsMap: {slorber: {name: 'Sébastien Lorber'}},
|
authorsMap: {
|
||||||
|
slorber: {name: 'Sébastien Lorber', key: 'slorber', page: null},
|
||||||
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{key: 'slorber', name: 'Sébastien Lorber'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
key: 'slorber',
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
imageURL: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
getBlogPostAuthors({
|
getBlogPostAuthors({
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -122,6 +171,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
slorber: {
|
slorber: {
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: 'https://github.com/slorber.png',
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
key: 'slorber',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
|
@ -131,6 +182,7 @@ describe('getBlogPostAuthors', () => {
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: 'https://github.com/slorber.png',
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
|
@ -142,6 +194,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
slorber: {
|
slorber: {
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: '/img/slorber.png',
|
imageURL: '/img/slorber.png',
|
||||||
|
key: 'slorber',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
|
@ -151,6 +205,7 @@ describe('getBlogPostAuthors', () => {
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: '/img/slorber.png',
|
imageURL: '/img/slorber.png',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
|
@ -162,6 +217,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
slorber: {
|
slorber: {
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: '/img/slorber.png',
|
imageURL: '/img/slorber.png',
|
||||||
|
key: 'slorber',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseUrl: '/baseUrl',
|
baseUrl: '/baseUrl',
|
||||||
|
@ -171,6 +228,7 @@ describe('getBlogPostAuthors', () => {
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
imageURL: '/baseUrl/img/slorber.png',
|
imageURL: '/baseUrl/img/slorber.png',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -182,14 +240,31 @@ describe('getBlogPostAuthors', () => {
|
||||||
authors: ['slorber', 'yangshun'],
|
authors: ['slorber', 'yangshun'],
|
||||||
},
|
},
|
||||||
authorsMap: {
|
authorsMap: {
|
||||||
slorber: {name: 'Sébastien Lorber', title: 'maintainer'},
|
slorber: {
|
||||||
yangshun: {name: 'Yangshun Tay'},
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
key: 'slorber',
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
yangshun: {name: 'Yangshun Tay', key: 'yangshun', page: null},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{key: 'slorber', name: 'Sébastien Lorber', title: 'maintainer'},
|
{
|
||||||
{key: 'yangshun', name: 'Yangshun Tay'},
|
key: 'slorber',
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
imageURL: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'yangshun',
|
||||||
|
name: 'Yangshun Tay',
|
||||||
|
imageURL: undefined,
|
||||||
|
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,7 +277,15 @@ describe('getBlogPostAuthors', () => {
|
||||||
authorsMap: undefined,
|
authorsMap: undefined,
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([{name: 'Sébastien Lorber', title: 'maintainer'}]);
|
).toEqual([
|
||||||
|
{
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
imageURL: undefined,
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can read authors Author[]', () => {
|
it('can read authors Author[]', () => {
|
||||||
|
@ -218,8 +301,14 @@ describe('getBlogPostAuthors', () => {
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{name: 'Sébastien Lorber', title: 'maintainer'},
|
{
|
||||||
{name: 'Yangshun Tay'},
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
imageURL: undefined,
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
{name: 'Yangshun Tay', imageURL: undefined, key: null, page: null},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -238,66 +327,38 @@ describe('getBlogPostAuthors', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
authorsMap: {
|
authorsMap: {
|
||||||
slorber: {name: 'Sébastien Lorber', title: 'maintainer'},
|
slorber: {
|
||||||
yangshun: {name: 'Yangshun Tay', title: 'Yangshun title original'},
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
key: 'slorber',
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
|
yangshun: {
|
||||||
|
name: 'Yangshun Tay',
|
||||||
|
title: 'Yangshun title original',
|
||||||
|
key: 'yangshun',
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{key: 'slorber', name: 'Sébastien Lorber', title: 'maintainer'},
|
{
|
||||||
|
key: 'slorber',
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
imageURL: undefined,
|
||||||
|
page: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'yangshun',
|
key: 'yangshun',
|
||||||
name: 'Yangshun Tay',
|
name: 'Yangshun Tay',
|
||||||
title: 'Yangshun title local override',
|
title: 'Yangshun title local override',
|
||||||
extra: 42,
|
extra: 42,
|
||||||
|
imageURL: undefined,
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{name: 'Alexey'},
|
{name: 'Alexey', imageURL: undefined, key: null, page: null},
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can normalize inline authors', () => {
|
|
||||||
expect(
|
|
||||||
getBlogPostAuthors({
|
|
||||||
frontMatter: {
|
|
||||||
authors: [
|
|
||||||
{
|
|
||||||
name: 'Seb1',
|
|
||||||
socials: {
|
|
||||||
x: 'https://x.com/sebastienlorber',
|
|
||||||
twitter: 'sebastienlorber',
|
|
||||||
github: 'slorber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Seb2',
|
|
||||||
socials: {
|
|
||||||
x: 'sebastienlorber',
|
|
||||||
twitter: 'https://twitter.com/sebastienlorber',
|
|
||||||
github: 'https://github.com/slorber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
authorsMap: {},
|
|
||||||
baseUrl: '/',
|
|
||||||
}),
|
|
||||||
).toEqual([
|
|
||||||
{
|
|
||||||
name: 'Seb1',
|
|
||||||
socials: {
|
|
||||||
x: 'https://x.com/sebastienlorber',
|
|
||||||
twitter: 'https://twitter.com/sebastienlorber',
|
|
||||||
github: 'https://github.com/slorber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Seb2',
|
|
||||||
socials: {
|
|
||||||
x: 'https://x.com/sebastienlorber',
|
|
||||||
twitter: 'https://twitter.com/sebastienlorber',
|
|
||||||
github: 'https://github.com/slorber',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -339,8 +400,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
authorsMap: {
|
authorsMap: {
|
||||||
yangshun: {name: 'Yangshun Tay'},
|
yangshun: {name: 'Yangshun Tay', key: 'yangshun', page: null},
|
||||||
jmarcey: {name: 'Joel Marcey'},
|
jmarcey: {name: 'Joel Marcey', key: 'jmarcey', page: null},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
|
@ -360,8 +421,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
authorsMap: {
|
authorsMap: {
|
||||||
yangshun: {name: 'Yangshun Tay'},
|
yangshun: {name: 'Yangshun Tay', key: 'yangshun', page: null},
|
||||||
jmarcey: {name: 'Joel Marcey'},
|
jmarcey: {name: 'Joel Marcey', key: 'jmarcey', page: null},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
|
@ -381,8 +442,8 @@ describe('getBlogPostAuthors', () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
authorsMap: {
|
authorsMap: {
|
||||||
yangshun: {name: 'Yangshun Tay'},
|
yangshun: {name: 'Yangshun Tay', key: 'yangshun', page: null},
|
||||||
jmarcey: {name: 'Joel Marcey'},
|
jmarcey: {name: 'Joel Marcey', key: 'jmarcey', page: null},
|
||||||
},
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
|
@ -415,7 +476,9 @@ describe('getBlogPostAuthors', () => {
|
||||||
authors: [{key: 'slorber'}],
|
authors: [{key: 'slorber'}],
|
||||||
author_title: 'legacy title',
|
author_title: 'legacy title',
|
||||||
},
|
},
|
||||||
authorsMap: {slorber: {name: 'Sébastien Lorber'}},
|
authorsMap: {
|
||||||
|
slorber: {name: 'Sébastien Lorber', key: 'slorber', page: null},
|
||||||
|
},
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
}),
|
}),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
@ -425,241 +488,37 @@ describe('getBlogPostAuthors', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAuthorsMap', () => {
|
describe('groupBlogPostsByAuthorKey', () => {
|
||||||
const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles');
|
const authorsMap: AuthorsMap = fromPartial({
|
||||||
const contentPaths = {
|
ozaki: {},
|
||||||
contentPathLocalized: fixturesDir,
|
slorber: {},
|
||||||
contentPath: fixturesDir,
|
keyWithNoPost: {},
|
||||||
};
|
|
||||||
|
|
||||||
it('getAuthorsMap can read yml file', async () => {
|
|
||||||
await expect(
|
|
||||||
getAuthorsMap({
|
|
||||||
contentPaths,
|
|
||||||
authorsMapPath: 'authors.yml',
|
|
||||||
}),
|
|
||||||
).resolves.toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAuthorsMap can read json file', async () => {
|
it('can group blog posts', () => {
|
||||||
await expect(
|
const post1 = post({metadata: {authors: [{key: 'ozaki'}]}});
|
||||||
getAuthorsMap({
|
const post2 = post({
|
||||||
contentPaths,
|
metadata: {authors: [{key: 'slorber'}, {key: 'ozaki'}]},
|
||||||
authorsMapPath: 'authors.json',
|
});
|
||||||
}),
|
const post3 = post({metadata: {authors: [{key: 'slorber'}]}});
|
||||||
).resolves.toBeDefined();
|
const post4 = post({
|
||||||
|
metadata: {authors: [{name: 'Inline author 1'}, {key: 'slorber'}]},
|
||||||
|
});
|
||||||
|
const post5 = post({
|
||||||
|
metadata: {authors: [{name: 'Inline author 2'}]},
|
||||||
|
});
|
||||||
|
const post6 = post({
|
||||||
|
metadata: {authors: [{key: 'unknownKey'}]},
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAuthorsMap can return undefined if yaml file not found', async () => {
|
const blogPosts = [post1, post2, post3, post4, post5, post6];
|
||||||
await expect(
|
|
||||||
getAuthorsMap({
|
|
||||||
contentPaths,
|
|
||||||
authorsMapPath: 'authors_does_not_exist.yml',
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAuthorsMap returns normalized', () => {
|
expect(groupBlogPostsByAuthorKey({authorsMap, blogPosts})).toEqual({
|
||||||
it('socials', async () => {
|
ozaki: [post1, post2],
|
||||||
const authorsMap = await getAuthorsMap({
|
slorber: [post2, post3, post4],
|
||||||
contentPaths,
|
keyWithNoPost: [],
|
||||||
authorsMapPath: 'authors.yml',
|
// We don't care about this edge case, it doesn't happen in practice
|
||||||
});
|
unknownKey: undefined,
|
||||||
expect(authorsMap.slorber.socials).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"stackoverflow": "https://stackoverflow.com/users/82609",
|
|
||||||
"twitter": "https://twitter.com/sebastienlorber",
|
|
||||||
"x": "https://x.com/sebastienlorber",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
expect(authorsMap.JMarcey.socials).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"stackoverflow": "https://stackoverflow.com/users/102705/Joel-Marcey",
|
|
||||||
"twitter": "https://twitter.com/JoelMarcey",
|
|
||||||
"x": "https://x.com/JoelMarcey",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateAuthorsMap', () => {
|
|
||||||
it('accept valid authors map', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
slorber: {
|
|
||||||
name: 'Sébastien Lorber',
|
|
||||||
title: 'maintainer',
|
|
||||||
url: 'https://sebastienlorber.com',
|
|
||||||
imageURL: 'https://github.com/slorber.png',
|
|
||||||
},
|
|
||||||
yangshun: {
|
|
||||||
name: 'Yangshun Tay',
|
|
||||||
imageURL: 'https://github.com/yangshun.png',
|
|
||||||
randomField: 42,
|
|
||||||
},
|
|
||||||
jmarcey: {
|
|
||||||
name: 'Joel',
|
|
||||||
title: 'creator of Docusaurus',
|
|
||||||
hello: new Date(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rename snake case image_url to camelCase imageURL', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
slorber: {
|
|
||||||
name: 'Sébastien Lorber',
|
|
||||||
image_url: 'https://github.com/slorber.png',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateAuthorsMap(authorsMap)).toEqual({
|
|
||||||
slorber: {
|
|
||||||
name: 'Sébastien Lorber',
|
|
||||||
imageURL: 'https://github.com/slorber.png',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accept author with only image', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
slorber: {
|
|
||||||
imageURL: 'https://github.com/slorber.png',
|
|
||||||
url: 'https://github.com/slorber',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject author without name or image', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
slorber: {
|
|
||||||
title: 'foo',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap(authorsMap),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""slorber" must contain at least one of [name, imageURL]"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject undefined author', () => {
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap({
|
|
||||||
slorber: undefined,
|
|
||||||
}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""slorber" cannot be undefined. It should be an author object containing properties like name, title, and imageURL."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject null author', () => {
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap({
|
|
||||||
slorber: null,
|
|
||||||
}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject array author', () => {
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap({slorber: []}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject array content', () => {
|
|
||||||
expect(() => validateAuthorsMap([])).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"The authors map file should contain an object where each entry contains an author key and the corresponding author's data."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject flat author', () => {
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap({name: 'Sébastien'}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""name" should be an author object containing properties like name, title, and imageURL."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reject non-map author', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
// @ts-expect-error: for tests
|
|
||||||
slorber: [],
|
|
||||||
};
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap(authorsMap),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authors socials', () => {
|
|
||||||
it('valid known author map socials', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
ozaki: {
|
|
||||||
name: 'ozaki',
|
|
||||||
socials: {
|
|
||||||
twitter: 'ozakione',
|
|
||||||
github: 'ozakione',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throw socials that are not strings', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
ozaki: {
|
|
||||||
name: 'ozaki',
|
|
||||||
socials: {
|
|
||||||
// @ts-expect-error: for tests
|
|
||||||
twitter: 42,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap(authorsMap),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""ozaki.socials.twitter" must be a string"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throw socials that are objects', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
ozaki: {
|
|
||||||
name: 'ozaki',
|
|
||||||
socials: {
|
|
||||||
// @ts-expect-error: for tests
|
|
||||||
twitter: {link: 'ozakione'},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
validateAuthorsMap(authorsMap),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`""ozaki.socials.twitter" must be a string"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('valid unknown author map socials', () => {
|
|
||||||
const authorsMap: AuthorsMap = {
|
|
||||||
ozaki: {
|
|
||||||
name: 'ozaki',
|
|
||||||
socials: {
|
|
||||||
random: 'ozakione',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
type AuthorsMapInput,
|
||||||
|
checkAuthorsMapPermalinkCollisions,
|
||||||
|
getAuthorsMap,
|
||||||
|
validateAuthorsMap,
|
||||||
|
validateAuthorsMapInput,
|
||||||
|
} from '../authorsMap';
|
||||||
|
import type {AuthorsMap} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
describe('checkAuthorsMapPermalinkCollisions', () => {
|
||||||
|
it('do not throw when permalinks are unique', () => {
|
||||||
|
const authors: AuthorsMap = {
|
||||||
|
author1: {
|
||||||
|
name: 'author1',
|
||||||
|
key: 'author1',
|
||||||
|
page: {
|
||||||
|
permalink: '/author1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
author2: {
|
||||||
|
name: 'author2',
|
||||||
|
key: 'author2',
|
||||||
|
page: {
|
||||||
|
permalink: '/author2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
checkAuthorsMapPermalinkCollisions(authors);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw when permalinks collide', () => {
|
||||||
|
const authors: AuthorsMap = {
|
||||||
|
author1: {
|
||||||
|
name: 'author1',
|
||||||
|
key: 'author1',
|
||||||
|
page: {
|
||||||
|
permalink: '/author1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
author2: {
|
||||||
|
name: 'author1',
|
||||||
|
key: 'author1',
|
||||||
|
page: {
|
||||||
|
permalink: '/author1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
checkAuthorsMapPermalinkCollisions(authors);
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"The following permalinks are duplicated:
|
||||||
|
Permalink: /author1
|
||||||
|
Authors: author1, author1"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAuthorsMap', () => {
|
||||||
|
const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles');
|
||||||
|
const contentPaths = {
|
||||||
|
contentPathLocalized: fixturesDir,
|
||||||
|
contentPath: fixturesDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('getAuthorsMap can read yml file', async () => {
|
||||||
|
await expect(
|
||||||
|
getAuthorsMap({
|
||||||
|
contentPaths,
|
||||||
|
authorsMapPath: 'authors.yml',
|
||||||
|
authorsBaseRoutePath: '/authors',
|
||||||
|
}),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAuthorsMap can read json file', async () => {
|
||||||
|
await expect(
|
||||||
|
getAuthorsMap({
|
||||||
|
contentPaths,
|
||||||
|
authorsMapPath: 'authors.json',
|
||||||
|
authorsBaseRoutePath: '/authors',
|
||||||
|
}),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAuthorsMap can return undefined if yaml file not found', async () => {
|
||||||
|
await expect(
|
||||||
|
getAuthorsMap({
|
||||||
|
contentPaths,
|
||||||
|
authorsMapPath: 'authors_does_not_exist.yml',
|
||||||
|
authorsBaseRoutePath: '/authors',
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateAuthorsMapInput', () => {
|
||||||
|
it('accept valid authors map', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
slorber: {
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
title: 'maintainer',
|
||||||
|
url: 'https://sebastienlorber.com',
|
||||||
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
key: 'slorber',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
yangshun: {
|
||||||
|
name: 'Yangshun Tay',
|
||||||
|
imageURL: 'https://github.com/yangshun.png',
|
||||||
|
randomField: 42,
|
||||||
|
key: 'yangshun',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
jmarcey: {
|
||||||
|
name: 'Joel',
|
||||||
|
title: 'creator of Docusaurus',
|
||||||
|
hello: new Date(),
|
||||||
|
key: 'jmarcey',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateAuthorsMapInput(authorsMap)).toEqual(authorsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rename snake case image_url to camelCase imageURL', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
slorber: {
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
image_url: 'https://github.com/slorber.png',
|
||||||
|
key: 'slorber',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateAuthorsMapInput(authorsMap)).toEqual({
|
||||||
|
slorber: {
|
||||||
|
name: 'Sébastien Lorber',
|
||||||
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
page: false,
|
||||||
|
key: 'slorber',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept author with only image', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
slorber: {
|
||||||
|
imageURL: 'https://github.com/slorber.png',
|
||||||
|
url: 'https://github.com/slorber',
|
||||||
|
key: 'slorber',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateAuthorsMapInput(authorsMap)).toEqual(authorsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject author without name or image', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
slorber: {
|
||||||
|
title: 'foo',
|
||||||
|
key: 'slorber',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput(authorsMap),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""slorber" must contain at least one of [name, imageURL]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject undefined author', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput({
|
||||||
|
slorber: undefined,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""slorber" cannot be undefined. It should be an author object containing properties like name, title, and imageURL."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject null author', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput({
|
||||||
|
slorber: null,
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject array author', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput({slorber: []}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject array content', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput([]),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"The authors map file should contain an object where each entry contains an author key and the corresponding author's data."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject flat author', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput({name: 'Sébastien'}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""name" should be an author object containing properties like name, title, and imageURL."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject non-map author', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
// @ts-expect-error: intentionally invalid
|
||||||
|
slorber: [],
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMapInput(authorsMap),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""slorber" should be an author object containing properties like name, title, and imageURL."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authors socials', () => {
|
||||||
|
it('valid known author map socials', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
ozaki: {
|
||||||
|
name: 'ozaki',
|
||||||
|
socials: {
|
||||||
|
twitter: 'ozakione',
|
||||||
|
github: 'ozakione',
|
||||||
|
},
|
||||||
|
key: 'ozaki',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw socials that are not strings', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
ozaki: {
|
||||||
|
name: 'ozaki',
|
||||||
|
socials: {
|
||||||
|
// @ts-expect-error: for tests
|
||||||
|
twitter: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMap(authorsMap),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""ozaki.socials.twitter" must be a string"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw socials that are objects', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
ozaki: {
|
||||||
|
name: 'ozaki',
|
||||||
|
socials: {
|
||||||
|
// @ts-expect-error: for tests
|
||||||
|
twitter: {link: 'ozakione'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
validateAuthorsMap(authorsMap),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""ozaki.socials.twitter" must be a string"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('valid unknown author map socials', () => {
|
||||||
|
const authorsMap: AuthorsMapInput = {
|
||||||
|
ozaki: {
|
||||||
|
name: 'ozaki',
|
||||||
|
socials: {
|
||||||
|
random: 'ozakione',
|
||||||
|
},
|
||||||
|
key: 'ozaki',
|
||||||
|
page: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,7 +5,6 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
|
||||||
import {reportDuplicateAuthors, reportInlineAuthors} from '../authorsProblems';
|
import {reportDuplicateAuthors, reportInlineAuthors} from '../authorsProblems';
|
||||||
import type {Author} from '@docusaurus/plugin-content-blog';
|
import type {Author} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
@ -23,9 +22,13 @@ describe('duplicate authors', () => {
|
||||||
const authors: Author[] = [
|
const authors: Author[] = [
|
||||||
{
|
{
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -42,11 +45,13 @@ describe('duplicate authors', () => {
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber 1',
|
name: 'Sébastien Lorber 1',
|
||||||
title: 'some title',
|
title: 'some title',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber 2',
|
name: 'Sébastien Lorber 2',
|
||||||
imageURL: '/slorber.png',
|
imageURL: '/slorber.png',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ describe('duplicate authors', () => {
|
||||||
}),
|
}),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Duplicate blog post authors were found in blog post "doc.md" front matter:
|
"Duplicate blog post authors were found in blog post "doc.md" front matter:
|
||||||
- {"key":"slorber","name":"Sébastien Lorber 2","imageURL":"/slorber.png"}"
|
- {"key":"slorber","name":"Sébastien Lorber 2","imageURL":"/slorber.png","page":null}"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -91,10 +96,12 @@ describe('inline authors', () => {
|
||||||
{
|
{
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ozaki',
|
key: 'ozaki',
|
||||||
name: 'Clément Couriol',
|
name: 'Clément Couriol',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -110,13 +117,15 @@ describe('inline authors', () => {
|
||||||
{
|
{
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber',
|
name: 'Sébastien Lorber',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{name: 'Inline author 1'},
|
{name: 'Inline author 1', page: null, key: null},
|
||||||
{
|
{
|
||||||
key: 'ozaki',
|
key: 'ozaki',
|
||||||
name: 'Clément Couriol',
|
name: 'Clément Couriol',
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{imageURL: '/inline-author2.png'},
|
{imageURL: '/inline-author2.png', page: null, key: null},
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
@ -125,8 +134,8 @@ describe('inline authors', () => {
|
||||||
}),
|
}),
|
||||||
).toThrowErrorMatchingInlineSnapshot(`
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Some blog authors used in "doc.md" are not defined in "authors.yml":
|
"Some blog authors used in "doc.md" are not defined in "authors.yml":
|
||||||
- {"name":"Inline author 1"}
|
- {"name":"Inline author 1","page":null,"key":null}
|
||||||
- {"imageURL":"/inline-author2.png"}
|
- {"imageURL":"/inline-author2.png","page":null,"key":null}
|
||||||
|
|
||||||
Note that we recommend to declare authors once in a "authors.yml" file and reference them by key in blog posts front matter to avoid author info duplication.
|
Note that we recommend to declare authors once in a "authors.yml" file and reference them by key in blog posts front matter to avoid author info duplication.
|
||||||
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
||||||
|
@ -134,45 +143,4 @@ describe('inline authors', () => {
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('warn inline authors', () => {
|
|
||||||
const authors: Author[] = [
|
|
||||||
{
|
|
||||||
key: 'slorber',
|
|
||||||
name: 'Sébastien Lorber',
|
|
||||||
},
|
|
||||||
{name: 'Inline author 1'},
|
|
||||||
{
|
|
||||||
key: 'ozaki',
|
|
||||||
name: 'Clément Couriol',
|
|
||||||
},
|
|
||||||
{imageURL: '/inline-author2.png'},
|
|
||||||
];
|
|
||||||
|
|
||||||
const consoleMock = jest
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
testReport({
|
|
||||||
authors,
|
|
||||||
options: {
|
|
||||||
onInlineAuthors: 'warn',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).not.toThrow();
|
|
||||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(consoleMock.mock.calls[0]).toMatchInlineSnapshot(`
|
|
||||||
[
|
|
||||||
"[WARNING] Some blog authors used in "doc.md" are not defined in "authors.yml":
|
|
||||||
- {"name":"Inline author 1"}
|
|
||||||
- {"imageURL":"/inline-author2.png"}
|
|
||||||
|
|
||||||
Note that we recommend to declare authors once in a "authors.yml" file and reference them by key in blog posts front matter to avoid author info duplication.
|
|
||||||
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
|
||||||
More info at https://docusaurus.io/docs/blog
|
|
||||||
",
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,6 +54,24 @@ describe('paginateBlogPosts', () => {
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('generates pages - 0 blog post', () => {
|
||||||
|
const pages = paginateBlogPosts({
|
||||||
|
blogPosts: [],
|
||||||
|
basePageUrl: '/blog',
|
||||||
|
blogTitle: 'Blog Title',
|
||||||
|
blogDescription: 'Blog Description',
|
||||||
|
postsPerPageOption: 2,
|
||||||
|
pageBasePath: 'page',
|
||||||
|
});
|
||||||
|
// As part ot https://github.com/facebook/docusaurus/pull/10216
|
||||||
|
// it was decided that authors with "page: true" that haven't written any
|
||||||
|
// blog posts yet should still have a dedicated author page
|
||||||
|
// For this purpose, we generate an empty first page
|
||||||
|
expect(pages).toHaveLength(1);
|
||||||
|
expect(pages[0]!.items).toHaveLength(0);
|
||||||
|
expect(pages).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('generates pages at blog root', () => {
|
it('generates pages at blog root', () => {
|
||||||
expect(
|
expect(
|
||||||
paginateBlogPosts({
|
paginateBlogPosts({
|
||||||
|
|
|
@ -10,12 +10,15 @@ import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
|
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
|
||||||
import {fromPartial} from '@total-typescript/shoehorn';
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
import {DEFAULT_OPTIONS} from '../options';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
|
import tree from 'tree-node-cli';
|
||||||
|
import {DEFAULT_OPTIONS, validateOptions} from '../options';
|
||||||
import {generateBlogPosts} from '../blogUtils';
|
import {generateBlogPosts} from '../blogUtils';
|
||||||
import {createBlogFeedFiles} from '../feed';
|
import {createBlogFeedFiles} from '../feed';
|
||||||
import type {LoadContext, I18n} from '@docusaurus/types';
|
import {getAuthorsMap} from '../authorsMap';
|
||||||
|
import type {LoadContext, I18n, Validate} from '@docusaurus/types';
|
||||||
import type {BlogContentPaths} from '../types';
|
import type {BlogContentPaths} from '../types';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
|
import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
const DefaultI18N: I18n = {
|
const DefaultI18N: I18n = {
|
||||||
currentLocale: 'en',
|
currentLocale: 'en',
|
||||||
|
@ -49,12 +52,28 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths {
|
||||||
|
|
||||||
async function testGenerateFeeds(
|
async function testGenerateFeeds(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
optionsInput: Options,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const options = validateOptions({
|
||||||
|
validate: normalizePluginOptions as Validate<
|
||||||
|
Options | undefined,
|
||||||
|
PluginOptions
|
||||||
|
>,
|
||||||
|
options: optionsInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentPaths = getBlogContentPaths(context.siteDir);
|
||||||
|
const authorsMap = await getAuthorsMap({
|
||||||
|
contentPaths,
|
||||||
|
authorsMapPath: options.authorsMapPath,
|
||||||
|
authorsBaseRoutePath: '/authors',
|
||||||
|
});
|
||||||
|
|
||||||
const blogPosts = await generateBlogPosts(
|
const blogPosts = await generateBlogPosts(
|
||||||
getBlogContentPaths(context.siteDir),
|
contentPaths,
|
||||||
context,
|
context,
|
||||||
options,
|
options,
|
||||||
|
authorsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
await createBlogFeedFiles({
|
await createBlogFeedFiles({
|
||||||
|
@ -63,10 +82,11 @@ async function testGenerateFeeds(
|
||||||
siteConfig: context.siteConfig,
|
siteConfig: context.siteConfig,
|
||||||
outDir: context.outDir,
|
outDir: context.outDir,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
contentPaths,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
describe.each(['atom', 'rss', 'json'] as const)('%s', (feedType) => {
|
||||||
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
|
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
|
||||||
|
|
||||||
it('does not get generated without posts', async () => {
|
it('does not get generated without posts', async () => {
|
||||||
|
@ -96,13 +116,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
feedOptions: {
|
feedOptions: {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
|
xslt: {atom: null, rss: null},
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
onInlineTags: 'ignore',
|
onInlineTags: 'ignore',
|
||||||
onInlineAuthors: 'ignore',
|
onInlineAuthors: 'ignore',
|
||||||
} as PluginOptions,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fsMock).toHaveBeenCalledTimes(0);
|
expect(fsMock).toHaveBeenCalledTimes(0);
|
||||||
|
@ -139,13 +160,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
feedOptions: {
|
feedOptions: {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
|
xslt: {atom: null, rss: null},
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
onInlineTags: 'ignore',
|
onInlineTags: 'ignore',
|
||||||
onInlineAuthors: 'ignore',
|
onInlineAuthors: 'ignore',
|
||||||
} as PluginOptions,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -194,13 +216,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
xslt: {atom: null, rss: null},
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
onInlineTags: 'ignore',
|
onInlineTags: 'ignore',
|
||||||
onInlineAuthors: 'ignore',
|
onInlineAuthors: 'ignore',
|
||||||
} as PluginOptions,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -240,13 +263,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
limit: 2,
|
limit: 2,
|
||||||
|
xslt: {atom: null, rss: null},
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
onInlineTags: 'ignore',
|
onInlineTags: 'ignore',
|
||||||
onInlineAuthors: 'ignore',
|
onInlineAuthors: 'ignore',
|
||||||
} as PluginOptions,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -286,13 +310,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
feedOptions: {
|
feedOptions: {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
|
xslt: {atom: null, rss: null},
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
onInlineTags: 'ignore',
|
onInlineTags: 'ignore',
|
||||||
onInlineAuthors: 'ignore',
|
onInlineAuthors: 'ignore',
|
||||||
} as PluginOptions,
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -300,4 +325,99 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
fsMock.mockClear();
|
fsMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has xslt files for feed', async () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
|
const outDir = path.join(siteDir, 'build-snap');
|
||||||
|
const siteConfig = {
|
||||||
|
title: 'Hello',
|
||||||
|
baseUrl: '/myBaseUrl/',
|
||||||
|
url: 'https://docusaurus.io',
|
||||||
|
favicon: 'image/favicon.ico',
|
||||||
|
markdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
|
// copied the output to the fixture...
|
||||||
|
await testGenerateFeeds(
|
||||||
|
fromPartial({
|
||||||
|
siteDir,
|
||||||
|
siteConfig,
|
||||||
|
i18n: DefaultI18N,
|
||||||
|
outDir,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
path: 'blog',
|
||||||
|
routeBasePath: 'blog',
|
||||||
|
tagsBasePath: 'tags',
|
||||||
|
authorsMapPath: 'authors.yml',
|
||||||
|
include: DEFAULT_OPTIONS.include,
|
||||||
|
exclude: DEFAULT_OPTIONS.exclude,
|
||||||
|
feedOptions: {
|
||||||
|
type: [feedType],
|
||||||
|
copyright: 'Copyright',
|
||||||
|
xslt: true,
|
||||||
|
},
|
||||||
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
|
defaultReadingTime({content}),
|
||||||
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
|
onInlineTags: 'ignore',
|
||||||
|
onInlineAuthors: 'ignore',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree');
|
||||||
|
|
||||||
|
expect(fsMock.mock.calls).toMatchSnapshot();
|
||||||
|
fsMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has custom xslt files for feed', async () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
|
const outDir = path.join(siteDir, 'build-snap');
|
||||||
|
const siteConfig = {
|
||||||
|
title: 'Hello',
|
||||||
|
baseUrl: '/myBaseUrl/',
|
||||||
|
url: 'https://docusaurus.io',
|
||||||
|
favicon: 'image/favicon.ico',
|
||||||
|
markdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
|
// copied the output to the fixture...
|
||||||
|
await testGenerateFeeds(
|
||||||
|
fromPartial({
|
||||||
|
siteDir,
|
||||||
|
siteConfig,
|
||||||
|
i18n: DefaultI18N,
|
||||||
|
outDir,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
path: 'blog',
|
||||||
|
routeBasePath: 'blog',
|
||||||
|
tagsBasePath: 'tags',
|
||||||
|
authorsMapPath: 'authors.yml',
|
||||||
|
include: DEFAULT_OPTIONS.include,
|
||||||
|
exclude: DEFAULT_OPTIONS.exclude,
|
||||||
|
feedOptions: {
|
||||||
|
type: [feedType],
|
||||||
|
copyright: 'Copyright',
|
||||||
|
xslt: {
|
||||||
|
rss: 'custom-rss.xsl',
|
||||||
|
atom: 'custom-atom.xsl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readingTime: ({content, defaultReadingTime}) =>
|
||||||
|
defaultReadingTime({content}),
|
||||||
|
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||||
|
onInlineTags: 'ignore',
|
||||||
|
onInlineAuthors: 'ignore',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree');
|
||||||
|
|
||||||
|
expect(fsMock.mock.calls).toMatchSnapshot();
|
||||||
|
fsMock.mockClear();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -220,12 +220,17 @@ describe('blog plugin', () => {
|
||||||
authors: [
|
authors: [
|
||||||
{
|
{
|
||||||
name: 'Yangshun Tay (translated)',
|
name: 'Yangshun Tay (translated)',
|
||||||
|
imageURL: undefined,
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: 'lorber.sebastien@gmail.com',
|
email: 'lorber.sebastien@gmail.com',
|
||||||
key: 'slorber',
|
key: 'slorber',
|
||||||
name: 'Sébastien Lorber (translated)',
|
name: 'Sébastien Lorber (translated)',
|
||||||
title: 'Docusaurus maintainer (translated)',
|
title: 'Docusaurus maintainer (translated)',
|
||||||
|
imageURL: undefined,
|
||||||
|
page: {permalink: '/blog/authors/slorber-custom-permalink-localized'},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
date: new Date('2018-12-14'),
|
date: new Date('2018-12-14'),
|
||||||
|
@ -319,6 +324,8 @@ describe('blog plugin', () => {
|
||||||
title: 'Docusaurus maintainer',
|
title: 'Docusaurus maintainer',
|
||||||
url: 'https://sebastienlorber.com',
|
url: 'https://sebastienlorber.com',
|
||||||
imageURL: undefined,
|
imageURL: undefined,
|
||||||
|
page: null,
|
||||||
|
key: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
prevItem: undefined,
|
prevItem: undefined,
|
||||||
|
|
|
@ -6,8 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
import {validateOptions, DEFAULT_OPTIONS} from '../options';
|
import {validateOptions, DEFAULT_OPTIONS, XSLTBuiltInPaths} from '../options';
|
||||||
import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog';
|
import type {
|
||||||
|
Options,
|
||||||
|
PluginOptions,
|
||||||
|
UserFeedOptions,
|
||||||
|
} from '@docusaurus/plugin-content-blog';
|
||||||
import type {Validate} from '@docusaurus/types';
|
import type {Validate} from '@docusaurus/types';
|
||||||
|
|
||||||
function testValidate(options?: Options) {
|
function testValidate(options?: Options) {
|
||||||
|
@ -38,7 +42,10 @@ describe('validateOptions', () => {
|
||||||
it('accepts correctly defined user options', () => {
|
it('accepts correctly defined user options', () => {
|
||||||
const userOptions: Options = {
|
const userOptions: Options = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
feedOptions: {type: 'rss' as const, title: 'myTitle'},
|
feedOptions: {
|
||||||
|
type: 'rss' as const,
|
||||||
|
title: 'myTitle',
|
||||||
|
},
|
||||||
path: 'not_blog',
|
path: 'not_blog',
|
||||||
routeBasePath: '/myBlog',
|
routeBasePath: '/myBlog',
|
||||||
postsPerPage: 5,
|
postsPerPage: 5,
|
||||||
|
@ -48,7 +55,13 @@ describe('validateOptions', () => {
|
||||||
};
|
};
|
||||||
expect(testValidate(userOptions)).toEqual({
|
expect(testValidate(userOptions)).toEqual({
|
||||||
...userOptions,
|
...userOptions,
|
||||||
feedOptions: {type: ['rss'], title: 'myTitle', copyright: '', limit: 20},
|
feedOptions: {
|
||||||
|
type: ['rss'],
|
||||||
|
title: 'myTitle',
|
||||||
|
copyright: '',
|
||||||
|
limit: 20,
|
||||||
|
xslt: {rss: null, atom: null},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,6 +92,7 @@ describe('validateOptions', () => {
|
||||||
).toThrowErrorMatchingSnapshot();
|
).toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('feed', () => {
|
||||||
it('throws Error in case of invalid feed type', () => {
|
it('throws Error in case of invalid feed type', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
testValidate({
|
testValidate({
|
||||||
|
@ -97,18 +111,27 @@ describe('validateOptions', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
feedOptions: {type: ['rss', 'atom', 'json'], copyright: '', limit: 20},
|
feedOptions: {
|
||||||
|
type: ['rss', 'atom', 'json'],
|
||||||
|
copyright: '',
|
||||||
|
limit: 20,
|
||||||
|
xslt: {rss: null, atom: null},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts null type and return same', () => {
|
it('accepts null feed type and return same', () => {
|
||||||
expect(
|
expect(
|
||||||
testValidate({
|
testValidate({
|
||||||
feedOptions: {type: null},
|
feedOptions: {type: null},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
feedOptions: {type: null, limit: 20},
|
feedOptions: {
|
||||||
|
type: null,
|
||||||
|
limit: 20,
|
||||||
|
xslt: {rss: null, atom: null},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -132,10 +155,122 @@ describe('validateOptions', () => {
|
||||||
title: 'title',
|
title: 'title',
|
||||||
copyright: '',
|
copyright: '',
|
||||||
limit: 20,
|
limit: 20,
|
||||||
|
xslt: {rss: null, atom: null},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('feed xslt', () => {
|
||||||
|
function testXSLT(xslt: UserFeedOptions['xslt']) {
|
||||||
|
return testValidate({feedOptions: {xslt}}).feedOptions.xslt;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts xslt: true', () => {
|
||||||
|
expect(testXSLT(true)).toEqual({
|
||||||
|
rss: XSLTBuiltInPaths.rss,
|
||||||
|
atom: XSLTBuiltInPaths.atom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: false', () => {
|
||||||
|
expect(testXSLT(false)).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: null', () => {
|
||||||
|
expect(testXSLT(null)).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: undefined', () => {
|
||||||
|
expect(testXSLT(undefined)).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: true}', () => {
|
||||||
|
expect(testXSLT({rss: true})).toEqual({
|
||||||
|
rss: XSLTBuiltInPaths.rss,
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {atom: true}', () => {
|
||||||
|
expect(testXSLT({atom: true})).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: XSLTBuiltInPaths.atom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: true, atom: true}', () => {
|
||||||
|
expect(testXSLT({rss: true, atom: true})).toEqual({
|
||||||
|
rss: XSLTBuiltInPaths.rss,
|
||||||
|
atom: XSLTBuiltInPaths.atom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: "custom-path"}', () => {
|
||||||
|
expect(testXSLT({rss: 'custom-path'})).toEqual({
|
||||||
|
rss: 'custom-path',
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: true, atom: "custom-path"}', () => {
|
||||||
|
expect(testXSLT({rss: true, atom: 'custom-path'})).toEqual({
|
||||||
|
rss: XSLTBuiltInPaths.rss,
|
||||||
|
atom: 'custom-path',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: null, atom: true}', () => {
|
||||||
|
expect(testXSLT({rss: null, atom: true})).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: XSLTBuiltInPaths.atom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts xslt: {rss: false, atom: null}', () => {
|
||||||
|
expect(testXSLT({rss: false, atom: null})).toEqual({
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects xslt: 42', () => {
|
||||||
|
// @ts-expect-error: bad type
|
||||||
|
expect(() => testXSLT(42)).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""feedOptions.xslt" must be one of [object, boolean]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('rejects xslt: []', () => {
|
||||||
|
// @ts-expect-error: bad type
|
||||||
|
expect(() => testXSLT([])).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""feedOptions.xslt" must be one of [object, boolean]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects xslt: {rss: 42}', () => {
|
||||||
|
// @ts-expect-error: bad type
|
||||||
|
expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""feedOptions.xslt.rss" must be one of [string, boolean]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects xslt: {rss: []}', () => {
|
||||||
|
// @ts-expect-error: bad type
|
||||||
|
expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""feedOptions.xslt.rss" must be one of [string, boolean]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts 0 sidebar count', () => {
|
it('accepts 0 sidebar count', () => {
|
||||||
const userOptions = {blogSidebarCount: 0};
|
const userOptions = {blogSidebarCount: 0};
|
||||||
expect(testValidate(userOptions)).toEqual({
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
|
|
@ -5,83 +5,16 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {getDataFileData, normalizeUrl} from '@docusaurus/utils';
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
|
||||||
import {AuthorSocialsSchema, normalizeSocials} from './authorsSocials';
|
|
||||||
import type {BlogContentPaths} from './types';
|
|
||||||
import type {
|
import type {
|
||||||
Author,
|
Author,
|
||||||
|
AuthorsMap,
|
||||||
|
BlogPost,
|
||||||
BlogPostFrontMatter,
|
BlogPostFrontMatter,
|
||||||
BlogPostFrontMatterAuthor,
|
BlogPostFrontMatterAuthor,
|
||||||
BlogPostFrontMatterAuthors,
|
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
export type AuthorsMap = {[authorKey: string]: Author};
|
|
||||||
|
|
||||||
const AuthorsMapSchema = Joi.object<AuthorsMap>()
|
|
||||||
.pattern(
|
|
||||||
Joi.string(),
|
|
||||||
Joi.object<Author>({
|
|
||||||
name: Joi.string(),
|
|
||||||
url: URISchema,
|
|
||||||
imageURL: URISchema,
|
|
||||||
title: Joi.string(),
|
|
||||||
email: Joi.string(),
|
|
||||||
socials: AuthorSocialsSchema,
|
|
||||||
})
|
|
||||||
.rename('image_url', 'imageURL')
|
|
||||||
.or('name', 'imageURL')
|
|
||||||
.unknown()
|
|
||||||
.required()
|
|
||||||
.messages({
|
|
||||||
'object.base':
|
|
||||||
'{#label} should be an author object containing properties like name, title, and imageURL.',
|
|
||||||
'any.required':
|
|
||||||
'{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.messages({
|
|
||||||
'object.base':
|
|
||||||
"The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
|
|
||||||
});
|
|
||||||
|
|
||||||
export function validateAuthorsMap(content: unknown): AuthorsMap {
|
|
||||||
const {error, value} = AuthorsMapSchema.validate(content);
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAuthor(author: Author): Author {
|
|
||||||
return {
|
|
||||||
...author,
|
|
||||||
socials: author.socials ? normalizeSocials(author.socials) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAuthorsMap(authorsMap: AuthorsMap): AuthorsMap {
|
|
||||||
return _.mapValues(authorsMap, normalizeAuthor);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAuthorsMap(params: {
|
|
||||||
authorsMapPath: string;
|
|
||||||
contentPaths: BlogContentPaths;
|
|
||||||
}): Promise<AuthorsMap | undefined> {
|
|
||||||
const authorsMap = await getDataFileData(
|
|
||||||
{
|
|
||||||
filePath: params.authorsMapPath,
|
|
||||||
contentPaths: params.contentPaths,
|
|
||||||
fileType: 'authors map',
|
|
||||||
},
|
|
||||||
// TODO annoying to test: tightly coupled FS reads + validation...
|
|
||||||
validateAuthorsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
return authorsMap ? normalizeAuthorsMap(authorsMap) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthorsParam = {
|
type AuthorsParam = {
|
||||||
frontMatter: BlogPostFrontMatter;
|
frontMatter: BlogPostFrontMatter;
|
||||||
authorsMap: AuthorsMap | undefined;
|
authorsMap: AuthorsMap | undefined;
|
||||||
|
@ -102,6 +35,7 @@ function normalizeImageUrl({
|
||||||
|
|
||||||
// Legacy v1/early-v2 front matter fields
|
// Legacy v1/early-v2 front matter fields
|
||||||
// We may want to deprecate those in favor of using only frontMatter.authors
|
// We may want to deprecate those in favor of using only frontMatter.authors
|
||||||
|
// TODO Docusaurus v4: remove this legacy front matter
|
||||||
function getFrontMatterAuthorLegacy({
|
function getFrontMatterAuthorLegacy({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
|
@ -123,38 +57,41 @@ function getFrontMatterAuthorLegacy({
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
imageURL,
|
imageURL,
|
||||||
|
// legacy front matter authors do not have an author key/page
|
||||||
|
key: null,
|
||||||
|
page: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFrontMatterAuthors(
|
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
|
||||||
frontMatterAuthors: BlogPostFrontMatterAuthors = [],
|
const {authorsMap, frontMatter, baseUrl} = params;
|
||||||
): BlogPostFrontMatterAuthor[] {
|
return normalizeFrontMatterAuthors().map(toAuthor);
|
||||||
function normalizeFrontMatterAuthor(
|
|
||||||
authorInput: string | Author,
|
function normalizeFrontMatterAuthors(): BlogPostFrontMatterAuthor[] {
|
||||||
|
if (frontMatter.authors === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAuthor(
|
||||||
|
authorInput: string | BlogPostFrontMatterAuthor,
|
||||||
): BlogPostFrontMatterAuthor {
|
): BlogPostFrontMatterAuthor {
|
||||||
if (typeof authorInput === 'string') {
|
if (typeof authorInput === 'string') {
|
||||||
// Technically, we could allow users to provide an author's name here, but
|
// We could allow users to provide an author's name here, but we only
|
||||||
// we only support keys, otherwise, a typo in a key would fallback to
|
// support keys, otherwise, a typo in a key would fall back to
|
||||||
// becoming a name and may end up unnoticed
|
// becoming a name and may end up unnoticed
|
||||||
return {key: authorInput};
|
return {key: authorInput};
|
||||||
}
|
}
|
||||||
return authorInput;
|
return authorInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.isArray(frontMatterAuthors)
|
return Array.isArray(frontMatter.authors)
|
||||||
? frontMatterAuthors.map(normalizeFrontMatterAuthor)
|
? frontMatter.authors.map(normalizeAuthor)
|
||||||
: [normalizeFrontMatterAuthor(frontMatterAuthors)];
|
: [normalizeAuthor(frontMatter.authors)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
|
|
||||||
const {authorsMap} = params;
|
|
||||||
const frontMatterAuthors = normalizeFrontMatterAuthors(
|
|
||||||
params.frontMatter.authors,
|
|
||||||
);
|
|
||||||
|
|
||||||
function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
|
function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
|
||||||
if (key) {
|
if (key) {
|
||||||
if (!authorsMap || Object.keys(authorsMap).length === 0) {
|
if (!authorsMap || Object.keys(authorsMap).length === 0) {
|
||||||
|
@ -175,36 +112,29 @@ ${Object.keys(authorsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
|
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
|
||||||
return normalizeAuthor({
|
const author = {
|
||||||
// Author def from authorsMap can be locally overridden by front matter
|
// Author def from authorsMap can be locally overridden by front matter
|
||||||
...getAuthorsMapAuthor(frontMatterAuthor.key),
|
...getAuthorsMapAuthor(frontMatterAuthor.key),
|
||||||
...frontMatterAuthor,
|
...frontMatterAuthor,
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return frontMatterAuthors.map(toAuthor);
|
return {
|
||||||
}
|
|
||||||
|
|
||||||
function fixAuthorImageBaseURL(
|
|
||||||
authors: Author[],
|
|
||||||
{baseUrl}: {baseUrl: string},
|
|
||||||
) {
|
|
||||||
return authors.map((author) => ({
|
|
||||||
...author,
|
...author,
|
||||||
|
key: author.key ?? null,
|
||||||
|
page: author.page ?? null,
|
||||||
imageURL: normalizeImageUrl({imageURL: author.imageURL, baseUrl}),
|
imageURL: normalizeImageUrl({imageURL: author.imageURL, baseUrl}),
|
||||||
}));
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBlogPostAuthors(params: AuthorsParam): Author[] {
|
export function getBlogPostAuthors(params: AuthorsParam): Author[] {
|
||||||
const authorLegacy = getFrontMatterAuthorLegacy(params);
|
const authorLegacy = getFrontMatterAuthorLegacy(params);
|
||||||
const authors = getFrontMatterAuthors(params);
|
const authors = getFrontMatterAuthors(params);
|
||||||
|
|
||||||
const updatedAuthors = fixAuthorImageBaseURL(authors, params);
|
|
||||||
|
|
||||||
if (authorLegacy) {
|
if (authorLegacy) {
|
||||||
// Technically, we could allow mixing legacy/authors front matter, but do we
|
// Technically, we could allow mixing legacy/authors front matter, but do we
|
||||||
// really want to?
|
// really want to?
|
||||||
if (updatedAuthors.length > 0) {
|
if (authors.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`To declare blog post authors, use the 'authors' front matter in priority.
|
`To declare blog post authors, use the 'authors' front matter in priority.
|
||||||
Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`,
|
Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`,
|
||||||
|
@ -213,5 +143,21 @@ Don't mix 'authors' with other existing 'author_*' front matter. Choose one or t
|
||||||
return [authorLegacy];
|
return [authorLegacy];
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedAuthors;
|
return authors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group blog posts by author key
|
||||||
|
* Blog posts with only inline authors are ignored
|
||||||
|
*/
|
||||||
|
export function groupBlogPostsByAuthorKey({
|
||||||
|
blogPosts,
|
||||||
|
authorsMap,
|
||||||
|
}: {
|
||||||
|
blogPosts: BlogPost[];
|
||||||
|
authorsMap: AuthorsMap | undefined;
|
||||||
|
}): Record<string, BlogPost[]> {
|
||||||
|
return _.mapValues(authorsMap, (author, key) =>
|
||||||
|
blogPosts.filter((p) => p.metadata.authors.some((a) => a.key === key)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
171
packages/docusaurus-plugin-content-blog/src/authorsMap.ts
Normal file
171
packages/docusaurus-plugin-content-blog/src/authorsMap.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {readDataFile, normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
||||||
|
import {AuthorSocialsSchema, normalizeSocials} from './authorsSocials';
|
||||||
|
import type {BlogContentPaths} from './types';
|
||||||
|
import type {
|
||||||
|
Author,
|
||||||
|
AuthorAttributes,
|
||||||
|
AuthorPage,
|
||||||
|
AuthorsMap,
|
||||||
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
type AuthorInput = AuthorAttributes & {
|
||||||
|
page?: boolean | AuthorPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthorsMapInput = {[authorKey: string]: AuthorInput};
|
||||||
|
|
||||||
|
const AuthorPageSchema = Joi.object<AuthorPage>({
|
||||||
|
permalink: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AuthorsMapInputSchema = Joi.object<AuthorsMapInput>()
|
||||||
|
.pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string(),
|
||||||
|
url: URISchema,
|
||||||
|
imageURL: URISchema,
|
||||||
|
title: Joi.string(),
|
||||||
|
email: Joi.string(),
|
||||||
|
page: Joi.alternatives(Joi.bool(), AuthorPageSchema),
|
||||||
|
socials: AuthorSocialsSchema,
|
||||||
|
description: Joi.string(),
|
||||||
|
})
|
||||||
|
.rename('image_url', 'imageURL')
|
||||||
|
.or('name', 'imageURL')
|
||||||
|
.unknown()
|
||||||
|
.required()
|
||||||
|
.messages({
|
||||||
|
'object.base':
|
||||||
|
'{#label} should be an author object containing properties like name, title, and imageURL.',
|
||||||
|
'any.required':
|
||||||
|
'{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.messages({
|
||||||
|
'object.base':
|
||||||
|
"The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function checkAuthorsMapPermalinkCollisions(
|
||||||
|
authorsMap: AuthorsMap | undefined,
|
||||||
|
): void {
|
||||||
|
if (!authorsMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permalinkCounts = _(authorsMap)
|
||||||
|
// Filter to keep only authors with a page
|
||||||
|
.pickBy((author) => !!author.page)
|
||||||
|
// Group authors by their permalink
|
||||||
|
.groupBy((author) => author.page?.permalink)
|
||||||
|
// Filter to keep only permalinks with more than one author
|
||||||
|
.pickBy((authors) => authors.length > 1)
|
||||||
|
// Transform the object into an array of [permalink, authors] pairs
|
||||||
|
.toPairs()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
if (permalinkCounts.length > 0) {
|
||||||
|
const errorMessage = permalinkCounts
|
||||||
|
.map(
|
||||||
|
([permalink, authors]) =>
|
||||||
|
`Permalink: ${permalink}\nAuthors: ${authors
|
||||||
|
.map((author) => author.name || 'Unknown')
|
||||||
|
.join(', ')}`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`The following permalinks are duplicated:\n${errorMessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAuthor({
|
||||||
|
authorsBaseRoutePath,
|
||||||
|
authorKey,
|
||||||
|
author,
|
||||||
|
}: {
|
||||||
|
authorsBaseRoutePath: string;
|
||||||
|
authorKey: string;
|
||||||
|
author: AuthorInput;
|
||||||
|
}): Author & {key: string} {
|
||||||
|
function getAuthorPage(): AuthorPage | null {
|
||||||
|
if (!author.page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const slug =
|
||||||
|
author.page === true ? _.kebabCase(authorKey) : author.page.permalink;
|
||||||
|
return {
|
||||||
|
permalink: normalizeUrl([authorsBaseRoutePath, slug]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...author,
|
||||||
|
key: authorKey,
|
||||||
|
page: getAuthorPage(),
|
||||||
|
socials: author.socials ? normalizeSocials(author.socials) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAuthorsMap({
|
||||||
|
authorsBaseRoutePath,
|
||||||
|
authorsMapInput,
|
||||||
|
}: {
|
||||||
|
authorsBaseRoutePath: string;
|
||||||
|
authorsMapInput: AuthorsMapInput;
|
||||||
|
}): AuthorsMap {
|
||||||
|
return _.mapValues(authorsMapInput, (author, authorKey) => {
|
||||||
|
return normalizeAuthor({authorsBaseRoutePath, authorKey, author});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateAuthorsMapInput(content: unknown): AuthorsMapInput {
|
||||||
|
const {error, value} = AuthorsMapInputSchema.validate(content);
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthorsMapInput(params: {
|
||||||
|
authorsMapPath: string;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
|
}): Promise<AuthorsMapInput | undefined> {
|
||||||
|
const content = await readDataFile({
|
||||||
|
filePath: params.authorsMapPath,
|
||||||
|
contentPaths: params.contentPaths,
|
||||||
|
});
|
||||||
|
return content ? validateAuthorsMapInput(content) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuthorsMap(params: {
|
||||||
|
authorsMapPath: string;
|
||||||
|
authorsBaseRoutePath: string;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
|
}): Promise<AuthorsMap | undefined> {
|
||||||
|
const authorsMapInput = await getAuthorsMapInput(params);
|
||||||
|
if (!authorsMapInput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const authorsMap = normalizeAuthorsMap({authorsMapInput, ...params});
|
||||||
|
return authorsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateAuthorsMap(content: unknown): AuthorsMapInput {
|
||||||
|
const {error, value} = AuthorsMapInputSchema.validate(content);
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
|
@ -29,11 +29,12 @@ import {
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {getTagsFile} from '@docusaurus/utils-validation';
|
import {getTagsFile} from '@docusaurus/utils-validation';
|
||||||
import {validateBlogPostFrontMatter} from './frontMatter';
|
import {validateBlogPostFrontMatter} from './frontMatter';
|
||||||
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
|
import {getBlogPostAuthors} from './authors';
|
||||||
import {reportAuthorsProblems} from './authorsProblems';
|
import {reportAuthorsProblems} from './authorsProblems';
|
||||||
import type {TagsFile} from '@docusaurus/utils';
|
import type {TagsFile} from '@docusaurus/utils';
|
||||||
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
|
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
|
AuthorsMap,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
ReadingTimeFunction,
|
ReadingTimeFunction,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
|
@ -64,7 +65,7 @@ export function paginateBlogPosts({
|
||||||
const totalCount = blogPosts.length;
|
const totalCount = blogPosts.length;
|
||||||
const postsPerPage =
|
const postsPerPage =
|
||||||
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
|
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
|
||||||
const numberOfPages = Math.ceil(totalCount / postsPerPage);
|
const numberOfPages = Math.max(1, Math.ceil(totalCount / postsPerPage));
|
||||||
|
|
||||||
const pages: BlogPaginated[] = [];
|
const pages: BlogPaginated[] = [];
|
||||||
|
|
||||||
|
@ -366,6 +367,7 @@ export async function generateBlogPosts(
|
||||||
contentPaths: BlogContentPaths,
|
contentPaths: BlogContentPaths,
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
|
authorsMap?: AuthorsMap,
|
||||||
): Promise<BlogPost[]> {
|
): Promise<BlogPost[]> {
|
||||||
const {include, exclude} = options;
|
const {include, exclude} = options;
|
||||||
|
|
||||||
|
@ -378,11 +380,6 @@ export async function generateBlogPosts(
|
||||||
ignore: exclude,
|
ignore: exclude,
|
||||||
});
|
});
|
||||||
|
|
||||||
const authorsMap = await getAuthorsMap({
|
|
||||||
contentPaths,
|
|
||||||
authorsMapPath: options.authorsMapPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tagsFile = await getTagsFile({contentPaths, tags: options.tags});
|
const tagsFile = await getTagsFile({contentPaths, tags: options.tags});
|
||||||
|
|
||||||
async function doProcessBlogSourceFile(blogSourceFile: string) {
|
async function doProcessBlogSourceFile(blogSourceFile: string) {
|
||||||
|
|
|
@ -7,15 +7,20 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import logger from '@docusaurus/logger';
|
|
||||||
import {Feed, type Author as FeedAuthor} from 'feed';
|
import {Feed, type Author as FeedAuthor} from 'feed';
|
||||||
import * as srcset from 'srcset';
|
import * as srcset from 'srcset';
|
||||||
import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
|
import {
|
||||||
|
getDataFilePath,
|
||||||
|
normalizeUrl,
|
||||||
|
readOutputHTMLFile,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
import {
|
import {
|
||||||
blogPostContainerID,
|
blogPostContainerID,
|
||||||
applyTrailingSlash,
|
applyTrailingSlash,
|
||||||
} from '@docusaurus/utils-common';
|
} from '@docusaurus/utils-common';
|
||||||
import {load as cheerioLoad} from 'cheerio';
|
import {load as cheerioLoad} from 'cheerio';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
import type {BlogContentPaths} from './types';
|
||||||
import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
|
import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
FeedType,
|
FeedType,
|
||||||
|
@ -23,6 +28,8 @@ import type {
|
||||||
Author,
|
Author,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
BlogFeedItem,
|
BlogFeedItem,
|
||||||
|
FeedOptions,
|
||||||
|
FeedXSLTOptions,
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
async function generateBlogFeed({
|
async function generateBlogFeed({
|
||||||
|
@ -180,32 +187,144 @@ async function defaultCreateFeedItems({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveXsltFilePaths({
|
||||||
|
xsltFilePath,
|
||||||
|
contentPaths,
|
||||||
|
}: {
|
||||||
|
xsltFilePath: string;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
|
}) {
|
||||||
|
const xsltAbsolutePath: string = path.isAbsolute(xsltFilePath)
|
||||||
|
? xsltFilePath
|
||||||
|
: (await getDataFilePath({filePath: xsltFilePath, contentPaths})) ??
|
||||||
|
path.resolve(contentPaths.contentPath, xsltFilePath);
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(xsltAbsolutePath))) {
|
||||||
|
throw new Error(
|
||||||
|
logger.interpolate`Blog feed XSLT file not found at path=${path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
xsltAbsolutePath,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPath = path.parse(xsltAbsolutePath);
|
||||||
|
const cssAbsolutePath = path.resolve(
|
||||||
|
parsedPath.dir,
|
||||||
|
`${parsedPath.name}.css`,
|
||||||
|
);
|
||||||
|
if (!(await fs.pathExists(xsltAbsolutePath))) {
|
||||||
|
throw new Error(
|
||||||
|
logger.interpolate`Blog feed XSLT file was found at path=${path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
xsltAbsolutePath,
|
||||||
|
)}
|
||||||
|
But its expected co-located CSS file could not be found at path=${path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
cssAbsolutePath,
|
||||||
|
)}
|
||||||
|
If you want to provide a custom XSLT file, you must provide a CSS file with the exact same name.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {xsltAbsolutePath, cssAbsolutePath};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateXsltFiles({
|
||||||
|
xsltFilePath,
|
||||||
|
generatePath,
|
||||||
|
contentPaths,
|
||||||
|
}: {
|
||||||
|
xsltFilePath: string;
|
||||||
|
generatePath: string;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
|
}) {
|
||||||
|
const {xsltAbsolutePath, cssAbsolutePath} = await resolveXsltFilePaths({
|
||||||
|
xsltFilePath,
|
||||||
|
contentPaths,
|
||||||
|
});
|
||||||
|
const xsltOutputPath = path.join(
|
||||||
|
generatePath,
|
||||||
|
path.basename(xsltAbsolutePath),
|
||||||
|
);
|
||||||
|
const cssOutputPath = path.join(generatePath, path.basename(cssAbsolutePath));
|
||||||
|
await fs.copy(xsltAbsolutePath, xsltOutputPath);
|
||||||
|
await fs.copy(cssAbsolutePath, cssOutputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This modifies the XML feed content to add a relative href to the XSLT file
|
||||||
|
// Good enough for now: we probably don't need a full XML parser just for that
|
||||||
|
// See also https://darekkay.com/blog/rss-styling/
|
||||||
|
function injectXslt({
|
||||||
|
feedContent,
|
||||||
|
xsltFilePath,
|
||||||
|
}: {
|
||||||
|
feedContent: string;
|
||||||
|
xsltFilePath: string;
|
||||||
|
}) {
|
||||||
|
return feedContent.replace(
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
|
`<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="${path.basename(
|
||||||
|
xsltFilePath,
|
||||||
|
)}"?>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedConfigs: Record<
|
||||||
|
FeedType,
|
||||||
|
{
|
||||||
|
outputFileName: string;
|
||||||
|
getContent: (feed: Feed) => string;
|
||||||
|
getXsltFilePath: (xslt: FeedXSLTOptions) => string | null;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
rss: {
|
||||||
|
outputFileName: 'rss.xml',
|
||||||
|
getContent: (feed) => feed.rss2(),
|
||||||
|
getXsltFilePath: (xslt) => xslt.rss,
|
||||||
|
},
|
||||||
|
atom: {
|
||||||
|
outputFileName: 'atom.xml',
|
||||||
|
getContent: (feed) => feed.atom1(),
|
||||||
|
getXsltFilePath: (xslt) => xslt.atom,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
outputFileName: 'feed.json',
|
||||||
|
getContent: (feed) => feed.json1(),
|
||||||
|
getXsltFilePath: () => null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
async function createBlogFeedFile({
|
async function createBlogFeedFile({
|
||||||
feed,
|
feed,
|
||||||
feedType,
|
feedType,
|
||||||
generatePath,
|
generatePath,
|
||||||
|
feedOptions,
|
||||||
|
contentPaths,
|
||||||
}: {
|
}: {
|
||||||
feed: Feed;
|
feed: Feed;
|
||||||
feedType: FeedType;
|
feedType: FeedType;
|
||||||
generatePath: string;
|
generatePath: string;
|
||||||
|
feedOptions: FeedOptions;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
}) {
|
}) {
|
||||||
const [feedContent, feedPath] = (() => {
|
|
||||||
switch (feedType) {
|
|
||||||
case 'rss':
|
|
||||||
return [feed.rss2(), 'rss.xml'];
|
|
||||||
case 'json':
|
|
||||||
return [feed.json1(), 'feed.json'];
|
|
||||||
case 'atom':
|
|
||||||
return [feed.atom1(), 'atom.xml'];
|
|
||||||
default:
|
|
||||||
throw new Error(`Feed type ${feedType} not supported.`);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
try {
|
try {
|
||||||
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
|
const feedConfig = FeedConfigs[feedType];
|
||||||
|
|
||||||
|
let feedContent = feedConfig.getContent(feed);
|
||||||
|
|
||||||
|
const xsltFilePath = feedConfig.getXsltFilePath(feedOptions.xslt);
|
||||||
|
if (xsltFilePath) {
|
||||||
|
await generateXsltFiles({xsltFilePath, contentPaths, generatePath});
|
||||||
|
feedContent = injectXslt({feedContent, xsltFilePath});
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = path.join(generatePath, feedConfig.outputFileName);
|
||||||
|
await fs.outputFile(outputPath, feedContent);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Generating ${feedType} feed failed.`);
|
throw new Error(`Generating ${feedType} feed failed.`, {
|
||||||
throw err;
|
cause: err as Error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,12 +341,14 @@ export async function createBlogFeedFiles({
|
||||||
siteConfig,
|
siteConfig,
|
||||||
outDir,
|
outDir,
|
||||||
locale,
|
locale,
|
||||||
|
contentPaths,
|
||||||
}: {
|
}: {
|
||||||
blogPosts: BlogPost[];
|
blogPosts: BlogPost[];
|
||||||
options: PluginOptions;
|
options: PluginOptions;
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
outDir: string;
|
outDir: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
contentPaths: BlogContentPaths;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const blogPosts = allBlogPosts.filter(shouldBeInFeed);
|
const blogPosts = allBlogPosts.filter(shouldBeInFeed);
|
||||||
|
|
||||||
|
@ -250,6 +371,8 @@ export async function createBlogFeedFiles({
|
||||||
feed,
|
feed,
|
||||||
feedType,
|
feedType,
|
||||||
generatePath: path.join(outDir, options.routeBasePath),
|
generatePath: path.join(outDir, options.routeBasePath),
|
||||||
|
feedOptions: options.feedOptions,
|
||||||
|
contentPaths,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {translateContent, getTranslationFiles} from './translations';
|
||||||
import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
|
import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
|
||||||
|
|
||||||
import {createAllRoutes} from './routes';
|
import {createAllRoutes} from './routes';
|
||||||
|
import {checkAuthorsMapPermalinkCollisions, getAuthorsMap} from './authorsMap';
|
||||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
|
@ -160,11 +161,30 @@ export default async function pluginContentBlog(
|
||||||
blogTitle,
|
blogTitle,
|
||||||
blogSidebarTitle,
|
blogSidebarTitle,
|
||||||
pageBasePath,
|
pageBasePath,
|
||||||
|
authorsBasePath,
|
||||||
|
authorsMapPath,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
|
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
|
||||||
const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
|
const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
|
||||||
let blogPosts = await generateBlogPosts(contentPaths, context, options);
|
|
||||||
|
const authorsMap = await getAuthorsMap({
|
||||||
|
contentPaths,
|
||||||
|
authorsMapPath,
|
||||||
|
authorsBaseRoutePath: normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
authorsBasePath,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
checkAuthorsMapPermalinkCollisions(authorsMap);
|
||||||
|
|
||||||
|
let blogPosts = await generateBlogPosts(
|
||||||
|
contentPaths,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
authorsMap,
|
||||||
|
);
|
||||||
blogPosts = await applyProcessBlogPosts({
|
blogPosts = await applyProcessBlogPosts({
|
||||||
blogPosts,
|
blogPosts,
|
||||||
processBlogPosts: options.processBlogPosts,
|
processBlogPosts: options.processBlogPosts,
|
||||||
|
@ -178,6 +198,7 @@ export default async function pluginContentBlog(
|
||||||
blogListPaginated: [],
|
blogListPaginated: [],
|
||||||
blogTags: {},
|
blogTags: {},
|
||||||
blogTagsListPath,
|
blogTagsListPath,
|
||||||
|
authorsMap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +247,7 @@ export default async function pluginContentBlog(
|
||||||
blogListPaginated,
|
blogListPaginated,
|
||||||
blogTags,
|
blogTags,
|
||||||
blogTagsListPath,
|
blogTagsListPath,
|
||||||
|
authorsMap,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -366,6 +388,7 @@ export default async function pluginContentBlog(
|
||||||
outDir,
|
outDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
locale: currentLocale,
|
locale: currentLocale,
|
||||||
|
contentPaths,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
import {
|
import {
|
||||||
Joi,
|
Joi,
|
||||||
RemarkPluginsSchema,
|
RemarkPluginsSchema,
|
||||||
|
@ -19,11 +20,20 @@ import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
Options,
|
Options,
|
||||||
FeedType,
|
FeedType,
|
||||||
|
FeedXSLTOptions,
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
import type {OptionValidationContext} from '@docusaurus/types';
|
import type {OptionValidationContext} from '@docusaurus/types';
|
||||||
|
|
||||||
export const DEFAULT_OPTIONS: PluginOptions = {
|
export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
feedOptions: {type: ['rss', 'atom'], copyright: '', limit: 20},
|
feedOptions: {
|
||||||
|
type: ['rss', 'atom'],
|
||||||
|
copyright: '',
|
||||||
|
limit: 20,
|
||||||
|
xslt: {
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
beforeDefaultRehypePlugins: [],
|
beforeDefaultRehypePlugins: [],
|
||||||
beforeDefaultRemarkPlugins: [],
|
beforeDefaultRemarkPlugins: [],
|
||||||
admonitions: true,
|
admonitions: true,
|
||||||
|
@ -34,6 +44,8 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
showReadingTime: true,
|
showReadingTime: true,
|
||||||
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
|
||||||
blogTagsListComponent: '@theme/BlogTagsListPage',
|
blogTagsListComponent: '@theme/BlogTagsListPage',
|
||||||
|
blogAuthorsPostsComponent: '@theme/Blog/Pages/BlogAuthorsPostsPage',
|
||||||
|
blogAuthorsListComponent: '@theme/Blog/Pages/BlogAuthorsListPage',
|
||||||
blogPostComponent: '@theme/BlogPostPage',
|
blogPostComponent: '@theme/BlogPostPage',
|
||||||
blogListComponent: '@theme/BlogListPage',
|
blogListComponent: '@theme/BlogListPage',
|
||||||
blogArchiveComponent: '@theme/BlogArchivePage',
|
blogArchiveComponent: '@theme/BlogArchivePage',
|
||||||
|
@ -58,9 +70,98 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
processBlogPosts: async () => undefined,
|
processBlogPosts: async () => undefined,
|
||||||
onInlineTags: 'warn',
|
onInlineTags: 'warn',
|
||||||
tags: undefined,
|
tags: undefined,
|
||||||
|
authorsBasePath: 'authors',
|
||||||
onInlineAuthors: 'warn',
|
onInlineAuthors: 'warn',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const XSLTBuiltInPaths = {
|
||||||
|
rss: path.resolve(__dirname, '..', 'assets', 'rss.xsl'),
|
||||||
|
atom: path.resolve(__dirname, '..', 'assets', 'atom.xsl'),
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeXsltOption(
|
||||||
|
option: string | null | boolean,
|
||||||
|
type: 'rss' | 'atom',
|
||||||
|
): string | null {
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
if (option === true) {
|
||||||
|
return XSLTBuiltInPaths[type];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createXSLTFilePathSchema(type: 'atom' | 'rss') {
|
||||||
|
return Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.string().required(),
|
||||||
|
Joi.boolean()
|
||||||
|
.allow(null, () => undefined)
|
||||||
|
.custom((val) => normalizeXsltOption(val, type)),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedXSLTOptionsSchema = Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.object<FeedXSLTOptions>({
|
||||||
|
rss: createXSLTFilePathSchema('rss'),
|
||||||
|
atom: createXSLTFilePathSchema('atom'),
|
||||||
|
}).required(),
|
||||||
|
Joi.boolean()
|
||||||
|
.allow(null, () => undefined)
|
||||||
|
.custom((val) => ({
|
||||||
|
rss: normalizeXsltOption(val, 'rss'),
|
||||||
|
atom: normalizeXsltOption(val, 'atom'),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.custom((val) => {
|
||||||
|
if (val === null) {
|
||||||
|
return {
|
||||||
|
rss: null,
|
||||||
|
atom: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
.default(DEFAULT_OPTIONS.feedOptions.xslt);
|
||||||
|
|
||||||
|
const FeedOptionsSchema = Joi.object({
|
||||||
|
type: Joi.alternatives()
|
||||||
|
.try(
|
||||||
|
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
|
||||||
|
Joi.alternatives().conditional(
|
||||||
|
Joi.string().equal('all', 'rss', 'atom', 'json'),
|
||||||
|
{
|
||||||
|
then: Joi.custom((val: FeedType | 'all') =>
|
||||||
|
val === 'all' ? ['rss', 'atom', 'json'] : [val],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.allow(null)
|
||||||
|
.default(DEFAULT_OPTIONS.feedOptions.type),
|
||||||
|
xslt: FeedXSLTOptionsSchema,
|
||||||
|
title: Joi.string().allow(''),
|
||||||
|
description: Joi.string().allow(''),
|
||||||
|
// Only add default value when user actually wants a feed (type is not null)
|
||||||
|
copyright: Joi.when('type', {
|
||||||
|
is: Joi.any().valid(null),
|
||||||
|
then: Joi.string().optional(),
|
||||||
|
otherwise: Joi.string()
|
||||||
|
.allow('')
|
||||||
|
.default(DEFAULT_OPTIONS.feedOptions.copyright),
|
||||||
|
}),
|
||||||
|
language: Joi.string(),
|
||||||
|
createFeedItems: Joi.function(),
|
||||||
|
limit: Joi.alternatives()
|
||||||
|
.try(Joi.number(), Joi.valid(null), Joi.valid(false))
|
||||||
|
.default(DEFAULT_OPTIONS.feedOptions.limit),
|
||||||
|
}).default(DEFAULT_OPTIONS.feedOptions);
|
||||||
|
|
||||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
path: Joi.string().default(DEFAULT_OPTIONS.path),
|
||||||
archiveBasePath: Joi.string()
|
archiveBasePath: Joi.string()
|
||||||
|
@ -82,6 +183,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
blogTagsPostsComponent: Joi.string().default(
|
blogTagsPostsComponent: Joi.string().default(
|
||||||
DEFAULT_OPTIONS.blogTagsPostsComponent,
|
DEFAULT_OPTIONS.blogTagsPostsComponent,
|
||||||
),
|
),
|
||||||
|
blogAuthorsPostsComponent: Joi.string().default(
|
||||||
|
DEFAULT_OPTIONS.blogAuthorsPostsComponent,
|
||||||
|
),
|
||||||
|
blogAuthorsListComponent: Joi.string().default(
|
||||||
|
DEFAULT_OPTIONS.blogAuthorsListComponent,
|
||||||
|
),
|
||||||
blogArchiveComponent: Joi.string().default(
|
blogArchiveComponent: Joi.string().default(
|
||||||
DEFAULT_OPTIONS.blogArchiveComponent,
|
DEFAULT_OPTIONS.blogArchiveComponent,
|
||||||
),
|
),
|
||||||
|
@ -107,37 +214,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
beforeDefaultRehypePlugins: RehypePluginsSchema.default(
|
beforeDefaultRehypePlugins: RehypePluginsSchema.default(
|
||||||
DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
|
DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
|
||||||
),
|
),
|
||||||
feedOptions: Joi.object({
|
feedOptions: FeedOptionsSchema,
|
||||||
type: Joi.alternatives()
|
|
||||||
.try(
|
|
||||||
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
|
|
||||||
Joi.alternatives().conditional(
|
|
||||||
Joi.string().equal('all', 'rss', 'atom', 'json'),
|
|
||||||
{
|
|
||||||
then: Joi.custom((val: FeedType | 'all') =>
|
|
||||||
val === 'all' ? ['rss', 'atom', 'json'] : [val],
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.allow(null)
|
|
||||||
.default(DEFAULT_OPTIONS.feedOptions.type),
|
|
||||||
title: Joi.string().allow(''),
|
|
||||||
description: Joi.string().allow(''),
|
|
||||||
// Only add default value when user actually wants a feed (type is not null)
|
|
||||||
copyright: Joi.when('type', {
|
|
||||||
is: Joi.any().valid(null),
|
|
||||||
then: Joi.string().optional(),
|
|
||||||
otherwise: Joi.string()
|
|
||||||
.allow('')
|
|
||||||
.default(DEFAULT_OPTIONS.feedOptions.copyright),
|
|
||||||
}),
|
|
||||||
language: Joi.string(),
|
|
||||||
createFeedItems: Joi.function(),
|
|
||||||
limit: Joi.alternatives()
|
|
||||||
.try(Joi.number(), Joi.valid(null), Joi.valid(false))
|
|
||||||
.default(DEFAULT_OPTIONS.feedOptions.limit),
|
|
||||||
}).default(DEFAULT_OPTIONS.feedOptions),
|
|
||||||
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
|
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
|
||||||
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
|
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
|
||||||
sortPosts: Joi.string()
|
sortPosts: Joi.string()
|
||||||
|
@ -157,6 +234,9 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
.disallow('')
|
.disallow('')
|
||||||
.allow(null, false)
|
.allow(null, false)
|
||||||
.default(() => DEFAULT_OPTIONS.tags),
|
.default(() => DEFAULT_OPTIONS.tags),
|
||||||
|
authorsBasePath: Joi.string()
|
||||||
|
.default(DEFAULT_OPTIONS.authorsBasePath)
|
||||||
|
.disallow(''),
|
||||||
onInlineAuthors: Joi.string()
|
onInlineAuthors: Joi.string()
|
||||||
.equal('ignore', 'log', 'warn', 'throw')
|
.equal('ignore', 'log', 'warn', 'throw')
|
||||||
.default(DEFAULT_OPTIONS.onInlineAuthors),
|
.default(DEFAULT_OPTIONS.onInlineAuthors),
|
||||||
|
|
|
@ -22,13 +22,7 @@ declare module '@docusaurus/plugin-content-blog' {
|
||||||
|
|
||||||
export type Assets = {
|
export type Assets = {
|
||||||
/**
|
/**
|
||||||
* If `metadata.yarn workspace website typecheck
|
* If `metadata.image` is a collocated image path, this entry will be the
|
||||||
4
|
|
||||||
yarn workspace v1.22.19yarn workspace website typecheck
|
|
||||||
4
|
|
||||||
yarn workspace v1.22.19yarn workspace website typecheck
|
|
||||||
4
|
|
||||||
yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
|
||||||
* bundler-generated image path. Otherwise, it's empty, and the image URL
|
* bundler-generated image path. Otherwise, it's empty, and the image URL
|
||||||
* should be accessed through `frontMatter.image`.
|
* should be accessed through `frontMatter.image`.
|
||||||
*/
|
*/
|
||||||
|
@ -66,9 +60,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
[customAuthorSocialPlatform: string]: string;
|
[customAuthorSocialPlatform: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Author = {
|
export type AuthorAttributes = {
|
||||||
key?: string; // TODO temporary, need refactor
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `name` doesn't exist, an `imageURL` is expected.
|
* If `name` doesn't exist, an `imageURL` is expected.
|
||||||
*/
|
*/
|
||||||
|
@ -98,11 +90,45 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
*/
|
*/
|
||||||
socials?: AuthorSocials;
|
socials?: AuthorSocials;
|
||||||
/**
|
/**
|
||||||
* Unknown keys are allowed, so that we can pass custom fields to authors,
|
* Description of the author.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Unknown keys are allowed, so that we can pass custom fields to authors.
|
||||||
*/
|
*/
|
||||||
[customAuthorAttribute: string]: unknown;
|
[customAuthorAttribute: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata of the author's page, if it exists.
|
||||||
|
*/
|
||||||
|
export type AuthorPage = {permalink: string};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized author metadata.
|
||||||
|
*/
|
||||||
|
export type Author = AuthorAttributes & {
|
||||||
|
/**
|
||||||
|
* Author key, if the author was loaded from the authors map.
|
||||||
|
* `null` means the author was declared inline.
|
||||||
|
*/
|
||||||
|
key: string | null;
|
||||||
|
/**
|
||||||
|
* Metadata of the author's page.
|
||||||
|
* `null` means the author doesn't have a dedicated author page.
|
||||||
|
*/
|
||||||
|
page: AuthorPage | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Authors coming from the AuthorsMap always have a key */
|
||||||
|
export type AuthorWithKey = Author & {key: string};
|
||||||
|
|
||||||
|
/** What the authors list page should know about each author. */
|
||||||
|
export type AuthorItemProp = AuthorWithKey & {
|
||||||
|
/** Number of blog posts with this author. */
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Everything is partial/unnormalized, because front matter is always
|
* Everything is partial/unnormalized, because front matter is always
|
||||||
* preserved as-is. Default values will be applied when generating metadata
|
* preserved as-is. Default values will be applied when generating metadata
|
||||||
|
@ -194,7 +220,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
last_update?: FrontMatterLastUpdate;
|
last_update?: FrontMatterLastUpdate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlogPostFrontMatterAuthor = Author & {
|
export type BlogPostFrontMatterAuthor = AuthorAttributes & {
|
||||||
/**
|
/**
|
||||||
* Will be normalized into the `imageURL` prop.
|
* Will be normalized into the `imageURL` prop.
|
||||||
*/
|
*/
|
||||||
|
@ -289,10 +315,26 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
}) => string | undefined;
|
}) => string | undefined;
|
||||||
|
|
||||||
export type FeedType = 'rss' | 'atom' | 'json';
|
export type FeedType = 'rss' | 'atom' | 'json';
|
||||||
|
|
||||||
|
export type FeedXSLTOptions = {
|
||||||
|
/**
|
||||||
|
* RSS XSLT file path, relative to the blog content folder.
|
||||||
|
* If null, no XSLT file is used and the feed will be displayed as raw XML.
|
||||||
|
*/
|
||||||
|
rss: string | null;
|
||||||
|
/**
|
||||||
|
* Atom XSLT file path, relative to the blog content folder.
|
||||||
|
* If null, no XSLT file is used and the feed will be displayed as raw XML.
|
||||||
|
*/
|
||||||
|
atom: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalized feed options used within code.
|
* Normalized feed options used within code.
|
||||||
*/
|
*/
|
||||||
export type FeedOptions = {
|
export type FeedOptions = {
|
||||||
|
/** Enable feeds xslt stylesheets */
|
||||||
|
xslt: FeedXSLTOptions;
|
||||||
/** If `null`, no feed is generated. */
|
/** If `null`, no feed is generated. */
|
||||||
type?: FeedType[] | null;
|
type?: FeedType[] | null;
|
||||||
/** Title of generated feed. */
|
/** Title of generated feed. */
|
||||||
|
@ -427,6 +469,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
blogTagsListComponent: string;
|
blogTagsListComponent: string;
|
||||||
/** Root component of the "posts containing tag" page. */
|
/** Root component of the "posts containing tag" page. */
|
||||||
blogTagsPostsComponent: string;
|
blogTagsPostsComponent: string;
|
||||||
|
/** Root component of the authors list page. */
|
||||||
|
blogAuthorsListComponent: string;
|
||||||
|
/** Root component of the "posts containing author" page. */
|
||||||
|
blogAuthorsPostsComponent: string;
|
||||||
/** Root component of the blog archive page. */
|
/** Root component of the blog archive page. */
|
||||||
blogArchiveComponent: string;
|
blogArchiveComponent: string;
|
||||||
/** Blog page title for better SEO. */
|
/** Blog page title for better SEO. */
|
||||||
|
@ -471,10 +517,20 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
* (filter, modify, delete, etc...).
|
* (filter, modify, delete, etc...).
|
||||||
*/
|
*/
|
||||||
processBlogPosts: ProcessBlogPostsFn;
|
processBlogPosts: ProcessBlogPostsFn;
|
||||||
|
/* Base path for the authors page */
|
||||||
|
authorsBasePath: string;
|
||||||
/** The behavior of Docusaurus when it finds inline authors. */
|
/** The behavior of Docusaurus when it finds inline authors. */
|
||||||
onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
|
onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserFeedXSLTOptions =
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| {
|
||||||
|
rss?: string | boolean | null;
|
||||||
|
atom?: string | boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed options, as provided by user config. `type` accepts `all` as shortcut
|
* Feed options, as provided by user config. `type` accepts `all` as shortcut
|
||||||
*/
|
*/
|
||||||
|
@ -483,6 +539,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
{
|
{
|
||||||
/** Type of feed to be generated. Use `null` to disable generation. */
|
/** Type of feed to be generated. Use `null` to disable generation. */
|
||||||
type?: FeedOptions['type'] | 'all' | FeedType;
|
type?: FeedOptions['type'] | 'all' | FeedType;
|
||||||
|
/** User-provided XSLT config for feeds, un-normalized */
|
||||||
|
xslt?: UserFeedXSLTOptions;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
/**
|
/**
|
||||||
|
@ -508,17 +566,22 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
||||||
items: BlogSidebarItem[];
|
items: BlogSidebarItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthorsMap = {[authorKey: string]: AuthorWithKey};
|
||||||
|
|
||||||
export type BlogContent = {
|
export type BlogContent = {
|
||||||
blogSidebarTitle: string;
|
blogSidebarTitle: string;
|
||||||
blogPosts: BlogPost[];
|
blogPosts: BlogPost[];
|
||||||
blogListPaginated: BlogPaginated[];
|
blogListPaginated: BlogPaginated[];
|
||||||
blogTags: BlogTags;
|
blogTags: BlogTags;
|
||||||
blogTagsListPath: string;
|
blogTagsListPath: string;
|
||||||
|
authorsMap?: AuthorsMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlogMetadata = {
|
export type BlogMetadata = {
|
||||||
/** the path to the base of the blog */
|
/** the path to the base of the blog */
|
||||||
blogBasePath: string;
|
blogBasePath: string;
|
||||||
|
/** the path to the authors list page */
|
||||||
|
authorsListPath: string;
|
||||||
/** title of the overall blog */
|
/** title of the overall blog */
|
||||||
blogTitle: string;
|
blogTitle: string;
|
||||||
};
|
};
|
||||||
|
@ -679,6 +742,47 @@ declare module '@theme/BlogTagsListPage' {
|
||||||
export default function BlogTagsListPage(props: Props): JSX.Element;
|
export default function BlogTagsListPage(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/Blog/Pages/BlogAuthorsListPage' {
|
||||||
|
import type {
|
||||||
|
AuthorItemProp,
|
||||||
|
BlogSidebar,
|
||||||
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** Blog sidebar. */
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
|
/** All authors declared in this blog. */
|
||||||
|
readonly authors: AuthorItemProp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthorsListPage(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/Blog/Pages/BlogAuthorsPostsPage' {
|
||||||
|
import type {Content} from '@theme/BlogPostPage';
|
||||||
|
import type {
|
||||||
|
AuthorItemProp,
|
||||||
|
BlogSidebar,
|
||||||
|
BlogPaginatedMetadata,
|
||||||
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** Blog sidebar. */
|
||||||
|
readonly sidebar: BlogSidebar;
|
||||||
|
/** Metadata of this author. */
|
||||||
|
readonly author: AuthorItemProp;
|
||||||
|
/** Looks exactly the same as the posts list page */
|
||||||
|
readonly listMetadata: BlogPaginatedMetadata;
|
||||||
|
/**
|
||||||
|
* Array of blog posts included on this page. Every post's metadata is also
|
||||||
|
* available.
|
||||||
|
*/
|
||||||
|
readonly items: readonly {readonly content: Content}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthorsPostsPage(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogTagsPostsPage' {
|
declare module '@theme/BlogTagsPostsPage' {
|
||||||
import type {Content} from '@theme/BlogPostPage';
|
import type {Content} from '@theme/BlogPostPage';
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
import type {TagsListItem, TagModule} from '@docusaurus/utils';
|
import type {TagsListItem, TagModule} from '@docusaurus/utils';
|
||||||
import type {
|
import type {
|
||||||
|
AuthorItemProp,
|
||||||
|
AuthorWithKey,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
BlogSidebar,
|
BlogSidebar,
|
||||||
BlogTag,
|
BlogTag,
|
||||||
|
@ -40,6 +42,19 @@ export function toTagProp({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toAuthorItemProp({
|
||||||
|
author,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
author: AuthorWithKey;
|
||||||
|
count: number;
|
||||||
|
}): AuthorItemProp {
|
||||||
|
return {
|
||||||
|
...author,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function toBlogSidebarProp({
|
export function toBlogSidebarProp({
|
||||||
blogSidebarTitle,
|
blogSidebarTitle,
|
||||||
blogPosts,
|
blogPosts,
|
||||||
|
|
|
@ -11,9 +11,15 @@ import {
|
||||||
docuHash,
|
docuHash,
|
||||||
aliasedSitePathToRelativePath,
|
aliasedSitePathToRelativePath,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {shouldBeListed} from './blogUtils';
|
import {paginateBlogPosts, shouldBeListed} from './blogUtils';
|
||||||
|
|
||||||
import {toBlogSidebarProp, toTagProp, toTagsProp} from './props';
|
import {
|
||||||
|
toAuthorItemProp,
|
||||||
|
toBlogSidebarProp,
|
||||||
|
toTagProp,
|
||||||
|
toTagsProp,
|
||||||
|
} from './props';
|
||||||
|
import {groupBlogPostsByAuthorKey} from './authors';
|
||||||
import type {
|
import type {
|
||||||
PluginContentLoadedActions,
|
PluginContentLoadedActions,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
|
@ -26,6 +32,7 @@ import type {
|
||||||
BlogContent,
|
BlogContent,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
|
AuthorWithKey,
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
type CreateAllRoutesParam = {
|
type CreateAllRoutesParam = {
|
||||||
|
@ -54,11 +61,16 @@ export async function buildAllRoutes({
|
||||||
blogListComponent,
|
blogListComponent,
|
||||||
blogPostComponent,
|
blogPostComponent,
|
||||||
blogTagsListComponent,
|
blogTagsListComponent,
|
||||||
|
blogAuthorsListComponent,
|
||||||
|
blogAuthorsPostsComponent,
|
||||||
blogTagsPostsComponent,
|
blogTagsPostsComponent,
|
||||||
blogArchiveComponent,
|
blogArchiveComponent,
|
||||||
routeBasePath,
|
routeBasePath,
|
||||||
archiveBasePath,
|
archiveBasePath,
|
||||||
blogTitle,
|
blogTitle,
|
||||||
|
authorsBasePath,
|
||||||
|
postsPerPage,
|
||||||
|
blogDescription,
|
||||||
} = options;
|
} = options;
|
||||||
const pluginId = options.id!;
|
const pluginId = options.id!;
|
||||||
const {createData} = actions;
|
const {createData} = actions;
|
||||||
|
@ -68,8 +80,15 @@ export async function buildAllRoutes({
|
||||||
blogListPaginated,
|
blogListPaginated,
|
||||||
blogTags,
|
blogTags,
|
||||||
blogTagsListPath,
|
blogTagsListPath,
|
||||||
|
authorsMap,
|
||||||
} = content;
|
} = content;
|
||||||
|
|
||||||
|
const authorsListPath = normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
authorsBasePath,
|
||||||
|
]);
|
||||||
|
|
||||||
const listedBlogPosts = blogPosts.filter(shouldBeListed);
|
const listedBlogPosts = blogPosts.filter(shouldBeListed);
|
||||||
|
|
||||||
const blogPostsById = _.keyBy(blogPosts, (post) => post.id);
|
const blogPostsById = _.keyBy(blogPosts, (post) => post.id);
|
||||||
|
@ -102,6 +121,7 @@ export async function buildAllRoutes({
|
||||||
const blogMetadata: BlogMetadata = {
|
const blogMetadata: BlogMetadata = {
|
||||||
blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
|
blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
|
||||||
blogTitle,
|
blogTitle,
|
||||||
|
authorsListPath,
|
||||||
};
|
};
|
||||||
const modulePath = await createData(
|
const modulePath = await createData(
|
||||||
`blogMetadata-${pluginId}.json`,
|
`blogMetadata-${pluginId}.json`,
|
||||||
|
@ -249,10 +269,85 @@ export async function buildAllRoutes({
|
||||||
return [tagsListRoute, ...tagsPaginatedRoutes];
|
return [tagsListRoute, ...tagsPaginatedRoutes];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAuthorsRoutes(): RouteConfig[] {
|
||||||
|
if (authorsMap === undefined || Object.keys(authorsMap).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogPostsByAuthorKey = groupBlogPostsByAuthorKey({
|
||||||
|
authorsMap,
|
||||||
|
blogPosts,
|
||||||
|
});
|
||||||
|
const authors = Object.values(authorsMap);
|
||||||
|
|
||||||
|
return [
|
||||||
|
createAuthorListRoute(),
|
||||||
|
...authors.flatMap(createAuthorPaginatedRoute),
|
||||||
|
];
|
||||||
|
|
||||||
|
function createAuthorListRoute(): RouteConfig {
|
||||||
|
return {
|
||||||
|
path: authorsListPath,
|
||||||
|
component: blogAuthorsListComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
authors: authors.map((author) =>
|
||||||
|
toAuthorItemProp({
|
||||||
|
author,
|
||||||
|
count: blogPostsByAuthorKey[author.key]?.length ?? 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
blogMetadata: blogMetadataModulePath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAuthorPaginatedRoute(author: AuthorWithKey): RouteConfig[] {
|
||||||
|
const authorBlogPosts = blogPostsByAuthorKey[author.key] ?? [];
|
||||||
|
if (!author.page) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = paginateBlogPosts({
|
||||||
|
blogPosts: authorBlogPosts,
|
||||||
|
basePageUrl: author.page.permalink,
|
||||||
|
blogDescription,
|
||||||
|
blogTitle,
|
||||||
|
pageBasePath: authorsBasePath,
|
||||||
|
postsPerPageOption: postsPerPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pages.map(({metadata, items}) => {
|
||||||
|
return {
|
||||||
|
path: metadata.permalink,
|
||||||
|
component: blogAuthorsPostsComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
items: blogPostItemsModule(items),
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
author: toAuthorItemProp({author, count: authorBlogPosts.length}),
|
||||||
|
listMetadata: metadata,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
blogMetadata: blogMetadataModulePath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...createBlogPostRoutes(),
|
...createBlogPostRoutes(),
|
||||||
...createBlogPostsPaginatedRoutes(),
|
...createBlogPostsPaginatedRoutes(),
|
||||||
...createTagsRoutes(),
|
...createTagsRoutes(),
|
||||||
...createArchiveRoute(),
|
...createArchiveRoute(),
|
||||||
|
...createAuthorsRoutes(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,6 +217,7 @@ exports[`DefaultSidebarItemsGenerator uses explicit link over the index/readme.{
|
||||||
{
|
{
|
||||||
"collapsed": undefined,
|
"collapsed": undefined,
|
||||||
"collapsible": undefined,
|
"collapsible": undefined,
|
||||||
|
"description": "Category description",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "parent/doc2",
|
"id": "parent/doc2",
|
||||||
|
|
|
@ -331,6 +331,7 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
categoriesMetadata: {
|
categoriesMetadata: {
|
||||||
Category: {
|
Category: {
|
||||||
label: 'Category label',
|
label: 'Category label',
|
||||||
|
description: 'Category description',
|
||||||
link: {
|
link: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'doc3', // Using a "local doc id" ("doc1" instead of "parent/doc1") on purpose
|
id: 'doc3', // Using a "local doc id" ("doc1" instead of "parent/doc1") on purpose
|
||||||
|
|
|
@ -282,6 +282,7 @@ describe('validateCategoryMetadataFile', () => {
|
||||||
const content: CategoryMetadataFile = {
|
const content: CategoryMetadataFile = {
|
||||||
className: 'className',
|
className: 'className',
|
||||||
label: 'Category Label',
|
label: 'Category Label',
|
||||||
|
description: 'Category Description',
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'slug',
|
slug: 'slug',
|
||||||
|
|
|
@ -249,6 +249,9 @@ Available doc IDs:
|
||||||
...(customProps !== undefined && {customProps}),
|
...(customProps !== undefined && {customProps}),
|
||||||
...(className !== undefined && {className}),
|
...(className !== undefined && {className}),
|
||||||
items,
|
items,
|
||||||
|
...(categoryMetadata?.description && {
|
||||||
|
description: categoryMetadata?.description,
|
||||||
|
}),
|
||||||
...(link && {link}),
|
...(link && {link}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ function postProcessSidebarItem(
|
||||||
.map((subItem) => postProcessSidebarItem(subItem, params))
|
.map((subItem) => postProcessSidebarItem(subItem, params))
|
||||||
.filter((v): v is SidebarItem => Boolean(v)),
|
.filter((v): v is SidebarItem => Boolean(v)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the current category doesn't have subitems, we render a normal link
|
// If the current category doesn't have subitems, we render a normal link
|
||||||
// instead.
|
// instead.
|
||||||
if (category.items.length === 0) {
|
if (category.items.length === 0) {
|
||||||
|
|
|
@ -217,6 +217,7 @@ export type PropSidebarBreadcrumbsItem =
|
||||||
export type CategoryMetadataFile = {
|
export type CategoryMetadataFile = {
|
||||||
label?: string;
|
label?: string;
|
||||||
position?: number;
|
position?: number;
|
||||||
|
description?: string;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
collapsible?: boolean;
|
collapsible?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -167,6 +167,7 @@ export function validateSidebars(sidebars: {
|
||||||
|
|
||||||
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
|
description: Joi.string(),
|
||||||
position: Joi.number(),
|
position: Joi.number(),
|
||||||
collapsed: Joi.boolean(),
|
collapsed: Joi.boolean(),
|
||||||
collapsible: Joi.boolean(),
|
collapsible: Joi.boolean(),
|
||||||
|
|
|
@ -127,6 +127,27 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
||||||
description:
|
description:
|
||||||
'The object mapping admonition type to a React component.\nUse it to add custom admonition type components, or replace existing ones.\nCan be ejected or wrapped (only manually, see our documentation).',
|
'The object mapping admonition type to a React component.\nUse it to add custom admonition type components, or replace existing ones.\nCan be ejected or wrapped (only manually, see our documentation).',
|
||||||
},
|
},
|
||||||
|
Blog: {
|
||||||
|
actions: {
|
||||||
|
// Forbidden because it's a parent folder, makes the CLI crash atm
|
||||||
|
eject: 'forbidden',
|
||||||
|
wrap: 'forbidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Blog/Components': {
|
||||||
|
actions: {
|
||||||
|
// Forbidden because it's a parent folder, makes the CLI crash atm
|
||||||
|
eject: 'forbidden',
|
||||||
|
wrap: 'forbidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Blog/Pages': {
|
||||||
|
actions: {
|
||||||
|
// Forbidden because it's a parent folder, makes the CLI crash atm
|
||||||
|
eject: 'forbidden',
|
||||||
|
wrap: 'forbidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
CodeBlock: {
|
CodeBlock: {
|
||||||
actions: {
|
actions: {
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
|
|
@ -185,6 +185,30 @@ declare module '@theme/BackToTopButton' {
|
||||||
export default function BackToTopButton(): JSX.Element;
|
export default function BackToTopButton(): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/Blog/Components/Author' {
|
||||||
|
import type {Author} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly as?: 'h1' | 'h2';
|
||||||
|
readonly author: Author;
|
||||||
|
readonly className?: string;
|
||||||
|
readonly count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthor(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/Blog/Components/Author/Socials' {
|
||||||
|
import type {Author} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly author: Author;
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthorSocials(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogListPaginator' {
|
declare module '@theme/BlogListPaginator' {
|
||||||
import type {BlogPaginatedMetadata} from '@docusaurus/plugin-content-blog';
|
import type {BlogPaginatedMetadata} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
@ -291,31 +315,6 @@ declare module '@theme/BlogPostItem/Header/Info' {
|
||||||
export default function BlogPostItemHeaderInfo(): JSX.Element;
|
export default function BlogPostItemHeaderInfo(): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogPostItem/Header/Author' {
|
|
||||||
import type {Author} from '@docusaurus/plugin-content-blog';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
readonly author: Author;
|
|
||||||
readonly singleAuthor: boolean;
|
|
||||||
readonly className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogPostItemHeaderAuthor(props: Props): JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@theme/BlogPostItem/Header/Author/Socials' {
|
|
||||||
import type {Author} from '@docusaurus/plugin-content-blog';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
readonly author: Author;
|
|
||||||
readonly className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogPostItemHeaderAuthorSocials(
|
|
||||||
props: Props,
|
|
||||||
): JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@theme/BlogPostItem/Header/Authors' {
|
declare module '@theme/BlogPostItem/Header/Authors' {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
@ -1603,7 +1602,19 @@ declare module '@theme/Tag' {
|
||||||
export default function Tag(props: Props): JSX.Element;
|
export default function Tag(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/Unlisted' {
|
declare module '@theme/ContentVisibility' {
|
||||||
|
export interface Props {
|
||||||
|
readonly metadata: {
|
||||||
|
// the visibility metadata our 3 content plugins share in common
|
||||||
|
readonly unlisted: boolean;
|
||||||
|
readonly frontMatter: {draft?: boolean; unlisted?: boolean};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContentVisibility(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/ContentVisibility/Unlisted' {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
@ -1611,6 +1622,14 @@ declare module '@theme/Unlisted' {
|
||||||
export default function Unlisted(props: Props): JSX.Element;
|
export default function Unlisted(props: Props): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/ContentVisibility/Draft' {
|
||||||
|
export interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Draft(props: Props): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/prism-include-languages' {
|
declare module '@theme/prism-include-languages' {
|
||||||
import type * as PrismNamespace from 'prismjs';
|
import type * as PrismNamespace from 'prismjs';
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {ComponentType} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import type {Props} from '@theme/BlogPostItem/Header/Author/Socials';
|
import type {Props} from '@theme/Blog/Components/Author/Socials';
|
||||||
|
|
||||||
import Twitter from '@theme/Icon/Socials/Twitter';
|
import Twitter from '@theme/Icon/Socials/Twitter';
|
||||||
import GitHub from '@theme/Icon/Socials/GitHub';
|
import GitHub from '@theme/Icon/Socials/GitHub';
|
||||||
|
@ -50,10 +50,15 @@ function SocialLink({platform, link}: {platform: string; link: string}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthorSocials({author}: {author: Props['author']}) {
|
export default function BlogAuthorSocials({
|
||||||
|
author,
|
||||||
|
}: {
|
||||||
|
author: Props['author'];
|
||||||
|
}): JSX.Element {
|
||||||
|
const entries = Object.entries(author.socials ?? {});
|
||||||
return (
|
return (
|
||||||
<div className={styles.authorSocials}>
|
<div className={styles.authorSocials}>
|
||||||
{Object.entries(author.socials ?? {}).map(([platform, linkUrl]) => {
|
{entries.map(([platform, linkUrl]) => {
|
||||||
return <SocialLink key={platform} platform={platform} link={linkUrl} />;
|
return <SocialLink key={platform} platform={platform} link={linkUrl} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
|
@ -10,7 +10,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorSocials {
|
.authorSocials {
|
||||||
margin-top: 0.2rem;
|
/*
|
||||||
|
This ensures that container takes height even if there's no social link
|
||||||
|
This keeps author names aligned even if only some have socials
|
||||||
|
*/
|
||||||
|
height: var(--docusaurus-blog-social-icon-size);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -25,7 +30,7 @@
|
||||||
height: var(--docusaurus-blog-social-icon-size);
|
height: var(--docusaurus-blog-social-icon-size);
|
||||||
width: var(--docusaurus-blog-social-icon-size);
|
width: var(--docusaurus-blog-social-icon-size);
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorSocialIcon {
|
.authorSocialIcon {
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Link, {type Props as LinkProps} from '@docusaurus/Link';
|
||||||
|
import AuthorSocials from '@theme/Blog/Components/Author/Socials';
|
||||||
|
import type {Props} from '@theme/Blog/Components/Author';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
function MaybeLink(props: LinkProps): JSX.Element {
|
||||||
|
if (props.href) {
|
||||||
|
return <Link {...props} />;
|
||||||
|
}
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorTitle({title}: {title: string}) {
|
||||||
|
return (
|
||||||
|
<small className={styles.authorTitle} title={title}>
|
||||||
|
{title}
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorName({name, as}: {name: string; as: Props['as']}) {
|
||||||
|
if (!as) {
|
||||||
|
return <span className={styles.authorName}>{name}</span>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Heading as={as} className={styles.authorName}>
|
||||||
|
{name}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorBlogPostCount({count}: {count: number}) {
|
||||||
|
return <span className={clsx(styles.authorBlogPostCount)}>{count}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: in the future we might want to have multiple "BlogAuthor" components
|
||||||
|
// Creating different display modes with the "as" prop may not be the best idea
|
||||||
|
// Explainer: https://kyleshevlin.com/prefer-multiple-compositions/
|
||||||
|
// For now, we almost use the same design for all cases, so it's good enough
|
||||||
|
export default function BlogAuthor({
|
||||||
|
as,
|
||||||
|
author,
|
||||||
|
className,
|
||||||
|
count,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const {name, title, url, imageURL, email, page} = author;
|
||||||
|
const link =
|
||||||
|
page?.permalink || url || (email && `mailto:${email}`) || undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'avatar margin-bottom--sm',
|
||||||
|
className,
|
||||||
|
styles[`author-as-${as}`],
|
||||||
|
)}>
|
||||||
|
{imageURL && (
|
||||||
|
<MaybeLink href={link} className="avatar__photo-link">
|
||||||
|
<img
|
||||||
|
className={clsx('avatar__photo', styles.authorImage)}
|
||||||
|
src={imageURL}
|
||||||
|
alt={name}
|
||||||
|
/>
|
||||||
|
</MaybeLink>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(name || title) && (
|
||||||
|
<div className={clsx('avatar__intro', styles.authorDetails)}>
|
||||||
|
<div className="avatar__name">
|
||||||
|
{name && (
|
||||||
|
<MaybeLink href={link}>
|
||||||
|
<AuthorName name={name} as={as} />
|
||||||
|
</MaybeLink>
|
||||||
|
)}
|
||||||
|
{count && <AuthorBlogPostCount count={count} />}
|
||||||
|
</div>
|
||||||
|
{!!title && <AuthorTitle title={title} />}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
We always render AuthorSocials even if there's none
|
||||||
|
This keeps other things aligned with flexbox layout
|
||||||
|
*/}
|
||||||
|
<AuthorSocials author={author} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.authorImage {
|
||||||
|
--ifm-avatar-photo-size: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h1 .authorImage {
|
||||||
|
--ifm-avatar-photo-size: 7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h2 .authorImage {
|
||||||
|
--ifm-avatar-photo-size: 5.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorName {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h1 .authorName {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h2 .authorName {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorTitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 0.8rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
line-clamp: 1;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h1 .authorTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-as-h2 .authorTitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorBlogPostCount {
|
||||||
|
background: var(--ifm-color-secondary);
|
||||||
|
color: var(--ifm-color-black);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
PageMetadata,
|
||||||
|
HtmlClassNameProvider,
|
||||||
|
ThemeClassNames,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import {translateBlogAuthorsListPageTitle} from '@docusaurus/theme-common/internal';
|
||||||
|
import BlogLayout from '@theme/BlogLayout';
|
||||||
|
import type {Props} from '@theme/Blog/Pages/BlogAuthorsListPage';
|
||||||
|
import SearchMetadata from '@theme/SearchMetadata';
|
||||||
|
import Heading from '@theme/Heading';
|
||||||
|
import Author from '@theme/Blog/Components/Author';
|
||||||
|
import type {AuthorItemProp} from '@docusaurus/plugin-content-blog';
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
function AuthorListItem({author}: {author: AuthorItemProp}) {
|
||||||
|
return (
|
||||||
|
<li className={styles.authorListItem}>
|
||||||
|
<Author as="h2" author={author} count={author.count} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorsList({authors}: {authors: Props['authors']}) {
|
||||||
|
return (
|
||||||
|
<section className={clsx('margin-vert--lg', styles.authorsListSection)}>
|
||||||
|
<ul>
|
||||||
|
{authors.map((author) => (
|
||||||
|
<AuthorListItem key={author.key} author={author} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthorsListPage({
|
||||||
|
authors,
|
||||||
|
sidebar,
|
||||||
|
}: Props): ReactNode {
|
||||||
|
const title: string = translateBlogAuthorsListPageTitle();
|
||||||
|
return (
|
||||||
|
<HtmlClassNameProvider
|
||||||
|
className={clsx(
|
||||||
|
ThemeClassNames.wrapper.blogPages,
|
||||||
|
ThemeClassNames.page.blogAuthorsListPage,
|
||||||
|
)}>
|
||||||
|
<PageMetadata title={title} />
|
||||||
|
<SearchMetadata tag="blog_authors_list" />
|
||||||
|
<BlogLayout sidebar={sidebar}>
|
||||||
|
<Heading as="h1">{title}</Heading>
|
||||||
|
<AuthorsList authors={authors} />
|
||||||
|
</BlogLayout>
|
||||||
|
</HtmlClassNameProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.authorListItem {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
PageMetadata,
|
||||||
|
HtmlClassNameProvider,
|
||||||
|
ThemeClassNames,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import {
|
||||||
|
useBlogAuthorPageTitle,
|
||||||
|
BlogAuthorsListViewAllLabel,
|
||||||
|
} from '@docusaurus/theme-common/internal';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import {useBlogMetadata} from '@docusaurus/plugin-content-blog/client';
|
||||||
|
import BlogLayout from '@theme/BlogLayout';
|
||||||
|
import BlogListPaginator from '@theme/BlogListPaginator';
|
||||||
|
import SearchMetadata from '@theme/SearchMetadata';
|
||||||
|
import type {Props} from '@theme/Blog/Pages/BlogAuthorsPostsPage';
|
||||||
|
import BlogPostItems from '@theme/BlogPostItems';
|
||||||
|
import Author from '@theme/Blog/Components/Author';
|
||||||
|
|
||||||
|
function Metadata({author}: Props): JSX.Element {
|
||||||
|
const title = useBlogAuthorPageTitle(author);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageMetadata title={title} />
|
||||||
|
<SearchMetadata tag="blog_authors_posts" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewAllAuthorsLink() {
|
||||||
|
const {authorsListPath} = useBlogMetadata();
|
||||||
|
return (
|
||||||
|
<Link href={authorsListPath}>
|
||||||
|
<BlogAuthorsListViewAllLabel />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content({author, items, sidebar, listMetadata}: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<BlogLayout sidebar={sidebar}>
|
||||||
|
<header className="margin-bottom--xl">
|
||||||
|
<Author as="h1" author={author} />
|
||||||
|
{author.description && <p>{author.description}</p>}
|
||||||
|
<ViewAllAuthorsLink />
|
||||||
|
</header>
|
||||||
|
<hr />
|
||||||
|
<BlogPostItems items={items} />
|
||||||
|
<BlogListPaginator metadata={listMetadata} />
|
||||||
|
</BlogLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogAuthorsPostsPage(props: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<HtmlClassNameProvider
|
||||||
|
className={clsx(
|
||||||
|
ThemeClassNames.wrapper.blogPages,
|
||||||
|
ThemeClassNames.page.blogAuthorsPostsPage,
|
||||||
|
)}>
|
||||||
|
<Metadata {...props} />
|
||||||
|
<Content {...props} />
|
||||||
|
</HtmlClassNameProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,62 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import Link, {type Props as LinkProps} from '@docusaurus/Link';
|
|
||||||
import AuthorSocials from '@theme/BlogPostItem/Header/Author/Socials';
|
|
||||||
|
|
||||||
import type {Props} from '@theme/BlogPostItem/Header/Author';
|
|
||||||
import styles from './styles.module.css';
|
|
||||||
|
|
||||||
function MaybeLink(props: LinkProps): JSX.Element {
|
|
||||||
if (props.href) {
|
|
||||||
return <Link {...props} />;
|
|
||||||
}
|
|
||||||
return <>{props.children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthorTitle({title}: {title: string}) {
|
|
||||||
return (
|
|
||||||
<small className={styles.authorTitle} title={title}>
|
|
||||||
{title}
|
|
||||||
</small>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogPostItemHeaderAuthor({
|
|
||||||
// singleAuthor, // may be useful in the future, or for swizzle users
|
|
||||||
author,
|
|
||||||
className,
|
|
||||||
}: Props): JSX.Element {
|
|
||||||
const {name, title, url, socials, imageURL, email} = author;
|
|
||||||
const link = url || (email && `mailto:${email}`) || undefined;
|
|
||||||
|
|
||||||
const hasSocials = socials && Object.keys(socials).length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={clsx('avatar margin-bottom--sm', className)}>
|
|
||||||
{imageURL && (
|
|
||||||
<MaybeLink href={link} className="avatar__photo-link">
|
|
||||||
<img className="avatar__photo" src={imageURL} alt={name} />
|
|
||||||
</MaybeLink>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(name || title) && (
|
|
||||||
<div className="avatar__intro">
|
|
||||||
<div className="avatar__name">
|
|
||||||
<MaybeLink href={link}>
|
|
||||||
<span className={styles.authorName}>{name}</span>
|
|
||||||
</MaybeLink>
|
|
||||||
</div>
|
|
||||||
{!!title && <AuthorTitle title={title} />}
|
|
||||||
{hasSocials && <AuthorSocials author={author} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.authorName {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authorTitle {
|
|
||||||
margin-top: 0.06rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 0.8rem;
|
|
||||||
display: -webkit-box;
|
|
||||||
overflow: hidden;
|
|
||||||
line-clamp: 1;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {useBlogPost} from '@docusaurus/plugin-content-blog/client';
|
import {useBlogPost} from '@docusaurus/plugin-content-blog/client';
|
||||||
import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author';
|
import BlogAuthor from '@theme/Blog/Components/Author';
|
||||||
import type {Props} from '@theme/BlogPostItem/Header/Authors';
|
import type {Props} from '@theme/BlogPostItem/Header/Authors';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
@ -40,8 +40,7 @@ export default function BlogPostItemHeaderAuthors({
|
||||||
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
|
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
|
||||||
)}
|
)}
|
||||||
key={idx}>
|
key={idx}>
|
||||||
<BlogPostItemHeaderAuthor
|
<BlogAuthor
|
||||||
singleAuthor={singleAuthor}
|
|
||||||
author={{
|
author={{
|
||||||
...author,
|
...author,
|
||||||
// Handle author images using relative paths
|
// Handle author images using relative paths
|
||||||
|
|
|
@ -18,8 +18,8 @@ import BlogPostPaginator from '@theme/BlogPostPaginator';
|
||||||
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
|
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
|
||||||
import BlogPostPageStructuredData from '@theme/BlogPostPage/StructuredData';
|
import BlogPostPageStructuredData from '@theme/BlogPostPage/StructuredData';
|
||||||
import TOC from '@theme/TOC';
|
import TOC from '@theme/TOC';
|
||||||
|
import ContentVisibility from '@theme/ContentVisibility';
|
||||||
import type {Props} from '@theme/BlogPostPage';
|
import type {Props} from '@theme/BlogPostPage';
|
||||||
import Unlisted from '@theme/Unlisted';
|
|
||||||
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
|
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
function BlogPostPageContent({
|
function BlogPostPageContent({
|
||||||
|
@ -30,7 +30,7 @@ function BlogPostPageContent({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const {metadata, toc} = useBlogPost();
|
const {metadata, toc} = useBlogPost();
|
||||||
const {nextItem, prevItem, frontMatter, unlisted} = metadata;
|
const {nextItem, prevItem, frontMatter} = metadata;
|
||||||
const {
|
const {
|
||||||
hide_table_of_contents: hideTableOfContents,
|
hide_table_of_contents: hideTableOfContents,
|
||||||
toc_min_heading_level: tocMinHeadingLevel,
|
toc_min_heading_level: tocMinHeadingLevel,
|
||||||
|
@ -48,7 +48,7 @@ function BlogPostPageContent({
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}>
|
}>
|
||||||
{unlisted && <Unlisted />}
|
<ContentVisibility metadata={metadata} />
|
||||||
|
|
||||||
<BlogPostItem>{children}</BlogPostItem>
|
<BlogPostItem>{children}</BlogPostItem>
|
||||||
|
|
||||||
|
|
|
@ -7,52 +7,22 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate from '@docusaurus/Translate';
|
||||||
import {
|
import {
|
||||||
PageMetadata,
|
PageMetadata,
|
||||||
HtmlClassNameProvider,
|
HtmlClassNameProvider,
|
||||||
ThemeClassNames,
|
ThemeClassNames,
|
||||||
usePluralForm,
|
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
|
import {useBlogTagsPostsPageTitle} from '@docusaurus/theme-common/internal';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import BlogLayout from '@theme/BlogLayout';
|
import BlogLayout from '@theme/BlogLayout';
|
||||||
import BlogListPaginator from '@theme/BlogListPaginator';
|
import BlogListPaginator from '@theme/BlogListPaginator';
|
||||||
import SearchMetadata from '@theme/SearchMetadata';
|
import SearchMetadata from '@theme/SearchMetadata';
|
||||||
import type {Props} from '@theme/BlogTagsPostsPage';
|
import type {Props} from '@theme/BlogTagsPostsPage';
|
||||||
import BlogPostItems from '@theme/BlogPostItems';
|
import BlogPostItems from '@theme/BlogPostItems';
|
||||||
import Unlisted from '@theme/Unlisted';
|
import Unlisted from '@theme/ContentVisibility/Unlisted';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
|
|
||||||
// Very simple pluralization: probably good enough for now
|
|
||||||
function useBlogPostsPlural() {
|
|
||||||
const {selectMessage} = usePluralForm();
|
|
||||||
return (count: number) =>
|
|
||||||
selectMessage(
|
|
||||||
count,
|
|
||||||
translate(
|
|
||||||
{
|
|
||||||
id: 'theme.blog.post.plurals',
|
|
||||||
description:
|
|
||||||
'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
|
||||||
message: 'One post|{count} posts',
|
|
||||||
},
|
|
||||||
{count},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useBlogTagsPostsPageTitle(tag: Props['tag']): string {
|
|
||||||
const blogPostsPlural = useBlogPostsPlural();
|
|
||||||
return translate(
|
|
||||||
{
|
|
||||||
id: 'theme.blog.tagTitle',
|
|
||||||
description: 'The title of the page for a blog tag',
|
|
||||||
message: '{nPosts} tagged with "{tagName}"',
|
|
||||||
},
|
|
||||||
{nPosts: blogPostsPlural(tag.count), tagName: tag.label},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element {
|
function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element {
|
||||||
const title = useBlogTagsPostsPageTitle(tag);
|
const title = useBlogTagsPostsPageTitle(tag);
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
ThemeClassNames,
|
||||||
|
DraftBannerTitle,
|
||||||
|
DraftBannerMessage,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import Admonition from '@theme/Admonition';
|
||||||
|
import type {Props} from '@theme/ContentVisibility/Draft';
|
||||||
|
|
||||||
|
export default function Draft({className}: Props): JSX.Element | null {
|
||||||
|
return (
|
||||||
|
<Admonition
|
||||||
|
type="caution"
|
||||||
|
title={<DraftBannerTitle />}
|
||||||
|
className={clsx(className, ThemeClassNames.common.draftBanner)}>
|
||||||
|
<DraftBannerMessage />
|
||||||
|
</Admonition>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import {
|
||||||
UnlistedMetadata,
|
UnlistedMetadata,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
import Admonition from '@theme/Admonition';
|
import Admonition from '@theme/Admonition';
|
||||||
import type {Props} from '@theme/Unlisted';
|
import type {Props} from '@theme/ContentVisibility/Unlisted';
|
||||||
|
|
||||||
function UnlistedBanner({className}: Props) {
|
function UnlistedBanner({className}: Props) {
|
||||||
return (
|
return (
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type {Props} from '@theme/ContentVisibility';
|
||||||
|
import Draft from '@theme/ContentVisibility/Draft';
|
||||||
|
import Unlisted from '@theme/ContentVisibility/Unlisted';
|
||||||
|
|
||||||
|
export default function ContentVisibility({
|
||||||
|
metadata,
|
||||||
|
}: Props): JSX.Element | null {
|
||||||
|
const {unlisted, frontMatter} = metadata;
|
||||||
|
// Reading draft/unlisted status from frontMatter is useful to display
|
||||||
|
// the banners in dev mode (in dev, metadata.unlisted is always false)
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/8285
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(unlisted || frontMatter.unlisted) && <Unlisted />}
|
||||||
|
{frontMatter.draft && <Draft />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
|
||||||
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
|
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
|
||||||
import DocItemContent from '@theme/DocItem/Content';
|
import DocItemContent from '@theme/DocItem/Content';
|
||||||
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
|
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
|
||||||
import Unlisted from '@theme/Unlisted';
|
import ContentVisibility from '@theme/ContentVisibility';
|
||||||
import type {Props} from '@theme/DocItem/Layout';
|
import type {Props} from '@theme/DocItem/Layout';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
@ -48,13 +48,11 @@ function useDocTOC() {
|
||||||
|
|
||||||
export default function DocItemLayout({children}: Props): JSX.Element {
|
export default function DocItemLayout({children}: Props): JSX.Element {
|
||||||
const docTOC = useDocTOC();
|
const docTOC = useDocTOC();
|
||||||
const {
|
const {metadata} = useDoc();
|
||||||
metadata: {unlisted},
|
|
||||||
} = useDoc();
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
|
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
|
||||||
{unlisted && <Unlisted />}
|
<ContentVisibility metadata={metadata} />
|
||||||
<DocVersionBanner />
|
<DocVersionBanner />
|
||||||
<div className={styles.docItemContainer}>
|
<div className={styles.docItemContainer}>
|
||||||
<article>
|
<article>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
import SearchMetadata from '@theme/SearchMetadata';
|
import SearchMetadata from '@theme/SearchMetadata';
|
||||||
import type {Props} from '@theme/DocTagDocListPage';
|
import type {Props} from '@theme/DocTagDocListPage';
|
||||||
import Unlisted from '@theme/Unlisted';
|
import Unlisted from '@theme/ContentVisibility/Unlisted';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
|
|
||||||
// Very simple pluralization: probably good enough for now
|
// Very simple pluralization: probably good enough for now
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import MDXContent from '@theme/MDXContent';
|
import MDXContent from '@theme/MDXContent';
|
||||||
import TOC from '@theme/TOC';
|
import TOC from '@theme/TOC';
|
||||||
import Unlisted from '@theme/Unlisted';
|
import ContentVisibility from '@theme/ContentVisibility';
|
||||||
import type {Props} from '@theme/MDXPage';
|
import type {Props} from '@theme/MDXPage';
|
||||||
|
|
||||||
import EditMetaRow from '@theme/EditMetaRow';
|
import EditMetaRow from '@theme/EditMetaRow';
|
||||||
|
@ -23,18 +23,15 @@ import styles from './styles.module.css';
|
||||||
|
|
||||||
export default function MDXPage(props: Props): JSX.Element {
|
export default function MDXPage(props: Props): JSX.Element {
|
||||||
const {content: MDXPageContent} = props;
|
const {content: MDXPageContent} = props;
|
||||||
|
const {metadata, assets} = MDXPageContent;
|
||||||
const {
|
const {
|
||||||
metadata: {
|
|
||||||
title,
|
title,
|
||||||
editUrl,
|
editUrl,
|
||||||
description,
|
description,
|
||||||
frontMatter,
|
frontMatter,
|
||||||
unlisted,
|
|
||||||
lastUpdatedBy,
|
lastUpdatedBy,
|
||||||
lastUpdatedAt,
|
lastUpdatedAt,
|
||||||
},
|
} = metadata;
|
||||||
assets,
|
|
||||||
} = MDXPageContent;
|
|
||||||
const {
|
const {
|
||||||
keywords,
|
keywords,
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
|
@ -60,7 +57,7 @@ export default function MDXPage(props: Props): JSX.Element {
|
||||||
<main className="container container--fluid margin-vert--lg">
|
<main className="container container--fluid margin-vert--lg">
|
||||||
<div className={clsx('row', styles.mdxPageWrapper)}>
|
<div className={clsx('row', styles.mdxPageWrapper)}>
|
||||||
<div className={clsx('col', !hideTableOfContents && 'col--8')}>
|
<div className={clsx('col', !hideTableOfContents && 'col--8')}>
|
||||||
{unlisted && <Unlisted />}
|
<ContentVisibility metadata={metadata} />
|
||||||
<article>
|
<article>
|
||||||
<MDXContent>
|
<MDXContent>
|
||||||
<MDXPageContent />
|
<MDXPageContent />
|
||||||
|
|
|
@ -123,7 +123,9 @@ export {
|
||||||
UnlistedBannerTitle,
|
UnlistedBannerTitle,
|
||||||
UnlistedBannerMessage,
|
UnlistedBannerMessage,
|
||||||
UnlistedMetadata,
|
UnlistedMetadata,
|
||||||
} from './utils/unlistedUtils';
|
DraftBannerTitle,
|
||||||
|
DraftBannerMessage,
|
||||||
|
} from './translations/contentVisibilityTranslations';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ErrorBoundaryTryAgainButton,
|
ErrorBoundaryTryAgainButton,
|
||||||
|
|
|
@ -90,3 +90,10 @@ export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
||||||
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
||||||
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
||||||
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
||||||
|
|
||||||
|
export {
|
||||||
|
useBlogTagsPostsPageTitle,
|
||||||
|
useBlogAuthorPageTitle,
|
||||||
|
translateBlogAuthorsListPageTitle,
|
||||||
|
BlogAuthorsListViewAllLabel,
|
||||||
|
} from './translations/blogTranslations';
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
|
import {usePluralForm} from '../utils/usePluralForm';
|
||||||
|
|
||||||
|
// Only used locally
|
||||||
|
function useBlogPostsPlural(): (count: number) => string {
|
||||||
|
const {selectMessage} = usePluralForm();
|
||||||
|
return (count: number) =>
|
||||||
|
selectMessage(
|
||||||
|
count,
|
||||||
|
translate(
|
||||||
|
{
|
||||||
|
id: 'theme.blog.post.plurals',
|
||||||
|
description:
|
||||||
|
'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
|
||||||
|
message: 'One post|{count} posts',
|
||||||
|
},
|
||||||
|
{count},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlogTagsPostsPageTitle(tag: {
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
|
}): string {
|
||||||
|
const blogPostsPlural = useBlogPostsPlural();
|
||||||
|
return translate(
|
||||||
|
{
|
||||||
|
id: 'theme.blog.tagTitle',
|
||||||
|
description: 'The title of the page for a blog tag',
|
||||||
|
message: '{nPosts} tagged with "{tagName}"',
|
||||||
|
},
|
||||||
|
{nPosts: blogPostsPlural(tag.count), tagName: tag.label},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlogAuthorPageTitle(author: {
|
||||||
|
key: string;
|
||||||
|
name?: string;
|
||||||
|
count: number;
|
||||||
|
}): string {
|
||||||
|
const blogPostsPlural = useBlogPostsPlural();
|
||||||
|
return translate(
|
||||||
|
{
|
||||||
|
id: 'theme.blog.author.pageTitle',
|
||||||
|
description: 'The title of the page for a blog author',
|
||||||
|
message: '{authorName} - {nPosts}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nPosts: blogPostsPlural(author.count),
|
||||||
|
authorName: author.name || author.key,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translateBlogAuthorsListPageTitle = (): string =>
|
||||||
|
translate({
|
||||||
|
id: 'theme.blog.authorsList.pageTitle',
|
||||||
|
message: 'Authors',
|
||||||
|
description: 'The title of the authors page',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function BlogAuthorsListViewAllLabel(): ReactNode {
|
||||||
|
return (
|
||||||
|
<Translate
|
||||||
|
id="theme.blog.authorsList.viewAll"
|
||||||
|
description="The label of the link targeting the blog authors page">
|
||||||
|
View All Authors
|
||||||
|
</Translate>
|
||||||
|
);
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import Head from '@docusaurus/Head';
|
||||||
export function UnlistedBannerTitle(): JSX.Element {
|
export function UnlistedBannerTitle(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Translate
|
<Translate
|
||||||
id="theme.unlistedContent.title"
|
id="theme.contentVisibility.unlistedBanner.title"
|
||||||
description="The unlisted content banner title">
|
description="The unlisted content banner title">
|
||||||
Unlisted page
|
Unlisted page
|
||||||
</Translate>
|
</Translate>
|
||||||
|
@ -22,7 +22,7 @@ export function UnlistedBannerTitle(): JSX.Element {
|
||||||
export function UnlistedBannerMessage(): JSX.Element {
|
export function UnlistedBannerMessage(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Translate
|
<Translate
|
||||||
id="theme.unlistedContent.message"
|
id="theme.contentVisibility.unlistedBanner.message"
|
||||||
description="The unlisted content banner message">
|
description="The unlisted content banner message">
|
||||||
This page is unlisted. Search engines will not index it, and only users
|
This page is unlisted. Search engines will not index it, and only users
|
||||||
having a direct link can access it.
|
having a direct link can access it.
|
||||||
|
@ -30,6 +30,8 @@ export function UnlistedBannerMessage(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Docusaurus v4 breaking change (since it's v3 public theme-common API :/)
|
||||||
|
// Move this to theme/ContentVisibility/Unlisted
|
||||||
export function UnlistedMetadata(): JSX.Element {
|
export function UnlistedMetadata(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -37,3 +39,24 @@ export function UnlistedMetadata(): JSX.Element {
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DraftBannerTitle(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Translate
|
||||||
|
id="theme.contentVisibility.draftBanner.title"
|
||||||
|
description="The draft content banner title">
|
||||||
|
Draft page
|
||||||
|
</Translate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DraftBannerMessage(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Translate
|
||||||
|
id="theme.contentVisibility.draftBanner.message"
|
||||||
|
description="The draft content banner message">
|
||||||
|
This page is a draft. It will only be visible in dev and be excluded from
|
||||||
|
the production build.
|
||||||
|
</Translate>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ export const ThemeClassNames = {
|
||||||
blogPostPage: 'blog-post-page',
|
blogPostPage: 'blog-post-page',
|
||||||
blogTagsListPage: 'blog-tags-list-page',
|
blogTagsListPage: 'blog-tags-list-page',
|
||||||
blogTagPostListPage: 'blog-tags-post-list-page',
|
blogTagPostListPage: 'blog-tags-post-list-page',
|
||||||
|
blogAuthorsListPage: 'blog-authors-list-page',
|
||||||
|
blogAuthorsPostsPage: 'blog-authors-posts-page',
|
||||||
|
|
||||||
docsDocPage: 'docs-doc-page',
|
docsDocPage: 'docs-doc-page',
|
||||||
docsTagsListPage: 'docs-tags-list-page',
|
docsTagsListPage: 'docs-tags-list-page',
|
||||||
|
@ -41,6 +43,7 @@ export const ThemeClassNames = {
|
||||||
codeBlock: 'theme-code-block',
|
codeBlock: 'theme-code-block',
|
||||||
admonition: 'theme-admonition',
|
admonition: 'theme-admonition',
|
||||||
unlistedBanner: 'theme-unlisted-banner',
|
unlistedBanner: 'theme-unlisted-banner',
|
||||||
|
draftBanner: 'theme-draft-banner',
|
||||||
|
|
||||||
admonitionType: (type: string) => `theme-admonition-${type}`,
|
admonitionType: (type: string) => `theme-admonition-${type}`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,42 +7,47 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {listTagsByLetters} from '../tagsUtils';
|
import {listTagsByLetters} from '../tagsUtils';
|
||||||
|
import type {TagsListItem} from '@docusaurus/utils';
|
||||||
|
|
||||||
describe('listTagsByLetters', () => {
|
describe('listTagsByLetters', () => {
|
||||||
type Param = Parameters<typeof listTagsByLetters>[0];
|
|
||||||
type Tag = Param[number];
|
|
||||||
type Result = ReturnType<typeof listTagsByLetters>;
|
type Result = ReturnType<typeof listTagsByLetters>;
|
||||||
|
|
||||||
it('creates letters list', () => {
|
it('creates letters list', () => {
|
||||||
const tag1: Tag = {
|
const tag1: TagsListItem = {
|
||||||
label: 'tag1',
|
label: 'tag1',
|
||||||
permalink: '/tag1',
|
permalink: '/tag1',
|
||||||
count: 1,
|
count: 1,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
const tag2: Tag = {
|
const tag2: TagsListItem = {
|
||||||
label: 'Tag2',
|
label: 'Tag2',
|
||||||
permalink: '/tag2',
|
permalink: '/tag2',
|
||||||
count: 11,
|
count: 11,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
const tagZxy: Tag = {
|
const tagZxy: TagsListItem = {
|
||||||
label: 'zxy',
|
label: 'zxy',
|
||||||
permalink: '/zxy',
|
permalink: '/zxy',
|
||||||
count: 987,
|
count: 987,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
const tagAbc: Tag = {
|
const tagAbc: TagsListItem = {
|
||||||
label: 'Abc',
|
label: 'Abc',
|
||||||
permalink: '/abc',
|
permalink: '/abc',
|
||||||
count: 123,
|
count: 123,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
const tagDef: Tag = {
|
const tagDef: TagsListItem = {
|
||||||
label: 'def',
|
label: 'def',
|
||||||
permalink: '/def',
|
permalink: '/def',
|
||||||
count: 1,
|
count: 1,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
const tagAaa: Tag = {
|
const tagAaa: TagsListItem = {
|
||||||
label: 'aaa',
|
label: 'aaa',
|
||||||
permalink: '/aaa',
|
permalink: '/aaa',
|
||||||
count: 10,
|
count: 10,
|
||||||
|
description: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedResult: Result = [
|
const expectedResult: Result = [
|
|
@ -156,6 +156,10 @@ function DocSearch({
|
||||||
|
|
||||||
const handleInput = useCallback(
|
const handleInput = useCallback(
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
|
||||||
|
// ignore browser's ctrl+f
|
||||||
|
return;
|
||||||
|
}
|
||||||
// prevents duplicate key insertion in the modal input
|
// prevents duplicate key insertion in the modal input
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setInitialQuery(event.key);
|
setInitialQuery(event.key);
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "أرشيف",
|
"theme.blog.archive.description": "أرشيف",
|
||||||
"theme.blog.archive.title": "أرشيف",
|
"theme.blog.archive.title": "أرشيف",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "التنقل في صفحة قائمة المدونة",
|
"theme.blog.paginator.navAriaLabel": "التنقل في صفحة قائمة المدونة",
|
||||||
"theme.blog.paginator.newerEntries": "إدخالات أحدث",
|
"theme.blog.paginator.newerEntries": "إدخالات أحدث",
|
||||||
"theme.blog.paginator.olderEntries": "إدخالات أقدم",
|
"theme.blog.paginator.olderEntries": "إدخالات أقدم",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "تعديل هذه الصفحة",
|
"theme.common.editThisPage": "تعديل هذه الصفحة",
|
||||||
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان {heading}",
|
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان {heading}",
|
||||||
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",
|
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "{count} مواد",
|
"theme.docs.DocCard.categoryDescription.plurals": "{count} مواد",
|
||||||
"theme.docs.breadcrumbs.home": "الرئيسية",
|
"theme.docs.breadcrumbs.home": "الرئيسية",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "التنقل التفصيلي",
|
"theme.docs.breadcrumbs.navAriaLabel": "التنقل التفصيلي",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "إصدارات",
|
"theme.navbar.mobileVersionsDropdown.label": "إصدارات",
|
||||||
"theme.tags.tagsListLabel": "الوسوم:",
|
"theme.tags.tagsListLabel": "الوسوم:",
|
||||||
"theme.tags.tagsPageLink": "عرض كل الوسوم",
|
"theme.tags.tagsPageLink": "عرض كل الوسوم",
|
||||||
"theme.tags.tagsPageTitle": "الوسوم",
|
"theme.tags.tagsPageTitle": "الوسوم"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,12 @@
|
||||||
"theme.blog.archive.description___DESCRIPTION": "The page & hero description of the blog archive page",
|
"theme.blog.archive.description___DESCRIPTION": "The page & hero description of the blog archive page",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
"theme.blog.archive.title___DESCRIPTION": "The page & hero title of the blog archive page",
|
"theme.blog.archive.title___DESCRIPTION": "The page & hero title of the blog archive page",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.author.pageTitle___DESCRIPTION": "The title of the page for a blog author",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.pageTitle___DESCRIPTION": "The title of the authors page",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
|
"theme.blog.authorsList.viewAll___DESCRIPTION": "The label of the link targeting the blog authors page",
|
||||||
"theme.blog.paginator.navAriaLabel": "Blog list page navigation",
|
"theme.blog.paginator.navAriaLabel": "Blog list page navigation",
|
||||||
"theme.blog.paginator.navAriaLabel___DESCRIPTION": "The ARIA label for the blog pagination",
|
"theme.blog.paginator.navAriaLabel___DESCRIPTION": "The ARIA label for the blog pagination",
|
||||||
"theme.blog.paginator.newerEntries": "Newer Entries",
|
"theme.blog.paginator.newerEntries": "Newer Entries",
|
||||||
|
@ -81,6 +87,14 @@
|
||||||
"theme.common.headingLinkTitle___DESCRIPTION": "Title for link to heading",
|
"theme.common.headingLinkTitle___DESCRIPTION": "Title for link to heading",
|
||||||
"theme.common.skipToMainContent": "Skip to main content",
|
"theme.common.skipToMainContent": "Skip to main content",
|
||||||
"theme.common.skipToMainContent___DESCRIPTION": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",
|
"theme.common.skipToMainContent___DESCRIPTION": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.message___DESCRIPTION": "The draft content banner message",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.draftBanner.title___DESCRIPTION": "The draft content banner title",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message___DESCRIPTION": "The unlisted content banner message",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title___DESCRIPTION": "The unlisted content banner title",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals___DESCRIPTION": "The default description for a category card in the generated index about how many items this category includes",
|
"theme.docs.DocCard.categoryDescription.plurals___DESCRIPTION": "The default description for a category card in the generated index about how many items this category includes",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
|
@ -137,9 +151,5 @@
|
||||||
"theme.tags.tagsPageLink": "View All Tags",
|
"theme.tags.tagsPageLink": "View All Tags",
|
||||||
"theme.tags.tagsPageLink___DESCRIPTION": "The label of the link targeting the tag list page",
|
"theme.tags.tagsPageLink___DESCRIPTION": "The label of the link targeting the tag list page",
|
||||||
"theme.tags.tagsPageTitle": "Tags",
|
"theme.tags.tagsPageTitle": "Tags",
|
||||||
"theme.tags.tagsPageTitle___DESCRIPTION": "The title of the tag list page",
|
"theme.tags.tagsPageTitle___DESCRIPTION": "The title of the tag list page"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.message___DESCRIPTION": "The unlisted content banner message",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page",
|
|
||||||
"theme.unlistedContent.title___DESCRIPTION": "The unlisted content banner title"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "Внимание",
|
"theme.admonition.warning": "Внимание",
|
||||||
"theme.blog.archive.description": "Архив",
|
"theme.blog.archive.description": "Архив",
|
||||||
"theme.blog.archive.title": "Архив",
|
"theme.blog.archive.title": "Архив",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Навигация в страницата със списък на блогове",
|
"theme.blog.paginator.navAriaLabel": "Навигация в страницата със списък на блогове",
|
||||||
"theme.blog.paginator.newerEntries": "По-нови записи",
|
"theme.blog.paginator.newerEntries": "По-нови записи",
|
||||||
"theme.blog.paginator.olderEntries": "По-стари записи",
|
"theme.blog.paginator.olderEntries": "По-стари записи",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Редактирай тази страница",
|
"theme.common.editThisPage": "Редактирай тази страница",
|
||||||
"theme.common.headingLinkTitle": "Директна връзка към {heading}",
|
"theme.common.headingLinkTitle": "Директна връзка към {heading}",
|
||||||
"theme.common.skipToMainContent": "Преминете към основното съдържание",
|
"theme.common.skipToMainContent": "Преминете към основното съдържание",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Тази страница е скрита. Търсачките няма да я индексират и само потребители с директна връзка имат достъп до него.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Скрита страница",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "един предмет|{count} предмета",
|
"theme.docs.DocCard.categoryDescription.plurals": "един предмет|{count} предмета",
|
||||||
"theme.docs.breadcrumbs.home": "Начална страница",
|
"theme.docs.breadcrumbs.home": "Начална страница",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Галета",
|
"theme.docs.breadcrumbs.navAriaLabel": "Галета",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Версии",
|
"theme.navbar.mobileVersionsDropdown.label": "Версии",
|
||||||
"theme.tags.tagsListLabel": "Етикети:",
|
"theme.tags.tagsListLabel": "Етикети:",
|
||||||
"theme.tags.tagsPageLink": "Вижте всички етикети",
|
"theme.tags.tagsPageLink": "Вижте всички етикети",
|
||||||
"theme.tags.tagsPageTitle": "Етикети",
|
"theme.tags.tagsPageTitle": "Етикети"
|
||||||
"theme.unlistedContent.message": "Тази страница е скрита. Търсачките няма да я индексират и само потребители с директна връзка имат достъп до него.",
|
|
||||||
"theme.unlistedContent.title": "Скрита страница"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "ব্লগ তালিকা পেজ নেভিগেশন",
|
"theme.blog.paginator.navAriaLabel": "ব্লগ তালিকা পেজ নেভিগেশন",
|
||||||
"theme.blog.paginator.newerEntries": "নতুন এন্ট্রি",
|
"theme.blog.paginator.newerEntries": "নতুন এন্ট্রি",
|
||||||
"theme.blog.paginator.olderEntries": "পুরানো এন্ট্রি",
|
"theme.blog.paginator.olderEntries": "পুরানো এন্ট্রি",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
|
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
|
||||||
"theme.common.headingLinkTitle": "{heading} এর সঙ্গে সরাসরি লিংকড",
|
"theme.common.headingLinkTitle": "{heading} এর সঙ্গে সরাসরি লিংকড",
|
||||||
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",
|
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "ট্যাগ্স:",
|
"theme.tags.tagsListLabel": "ট্যাগ্স:",
|
||||||
"theme.tags.tagsPageLink": "সমস্ত ট্যাগ্স দেখুন",
|
"theme.tags.tagsPageLink": "সমস্ত ট্যাগ্স দেখুন",
|
||||||
"theme.tags.tagsPageTitle": "ট্যাগ্স",
|
"theme.tags.tagsPageTitle": "ট্যাগ্স"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Stránkování článků na blogu",
|
"theme.blog.paginator.navAriaLabel": "Stránkování článků na blogu",
|
||||||
"theme.blog.paginator.newerEntries": "Novější záznamy",
|
"theme.blog.paginator.newerEntries": "Novější záznamy",
|
||||||
"theme.blog.paginator.olderEntries": "Starší záznamy",
|
"theme.blog.paginator.olderEntries": "Starší záznamy",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Upravit tuto stránku",
|
"theme.common.editThisPage": "Upravit tuto stránku",
|
||||||
"theme.common.headingLinkTitle": "Přímý odkaz na {heading}",
|
"theme.common.headingLinkTitle": "Přímý odkaz na {heading}",
|
||||||
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",
|
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "Tagy:",
|
"theme.tags.tagsListLabel": "Tagy:",
|
||||||
"theme.tags.tagsPageLink": "Zobrazit všechny tagy",
|
"theme.tags.tagsPageLink": "Zobrazit všechny tagy",
|
||||||
"theme.tags.tagsPageTitle": "Tagy",
|
"theme.tags.tagsPageTitle": "Tagy"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Blogoversigt navigation",
|
"theme.blog.paginator.navAriaLabel": "Blogoversigt navigation",
|
||||||
"theme.blog.paginator.newerEntries": "Nyere indslag",
|
"theme.blog.paginator.newerEntries": "Nyere indslag",
|
||||||
"theme.blog.paginator.olderEntries": "Tidligere indslag",
|
"theme.blog.paginator.olderEntries": "Tidligere indslag",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Rediger denne side",
|
"theme.common.editThisPage": "Rediger denne side",
|
||||||
"theme.common.headingLinkTitle": "Direkte link til {heading}",
|
"theme.common.headingLinkTitle": "Direkte link til {heading}",
|
||||||
"theme.common.skipToMainContent": "Hop til hovedindhold",
|
"theme.common.skipToMainContent": "Hop til hovedindhold",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "Tags:",
|
"theme.tags.tagsListLabel": "Tags:",
|
||||||
"theme.tags.tagsPageLink": "Se alle Tags",
|
"theme.tags.tagsPageLink": "Se alle Tags",
|
||||||
"theme.tags.tagsPageTitle": "Tags",
|
"theme.tags.tagsPageTitle": "Tags"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warnung",
|
"theme.admonition.warning": "warnung",
|
||||||
"theme.blog.archive.description": "Archiv",
|
"theme.blog.archive.description": "Archiv",
|
||||||
"theme.blog.archive.title": "Archiv",
|
"theme.blog.archive.title": "Archiv",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
|
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
|
||||||
"theme.blog.paginator.newerEntries": "Neuere Einträge",
|
"theme.blog.paginator.newerEntries": "Neuere Einträge",
|
||||||
"theme.blog.paginator.olderEntries": "Ältere Einträge",
|
"theme.blog.paginator.olderEntries": "Ältere Einträge",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
||||||
"theme.common.headingLinkTitle": "Direkter Link zur {heading}",
|
"theme.common.headingLinkTitle": "Direkter Link zur {heading}",
|
||||||
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 Eintrag|{count} Einträge",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 Eintrag|{count} Einträge",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versionen",
|
"theme.navbar.mobileVersionsDropdown.label": "Versionen",
|
||||||
"theme.tags.tagsListLabel": "Tags:",
|
"theme.tags.tagsListLabel": "Tags:",
|
||||||
"theme.tags.tagsPageLink": "Alle Tags anzeigen",
|
"theme.tags.tagsPageLink": "Alle Tags anzeigen",
|
||||||
"theme.tags.tagsPageTitle": "Tags",
|
"theme.tags.tagsPageTitle": "Tags"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
"theme.IdealImageMessage.error": "Error. Click para recargar",
|
"theme.IdealImageMessage.error": "Error. Click para recargar",
|
||||||
"theme.IdealImageMessage.load": "Click para recargar{sizeMessage}",
|
"theme.IdealImageMessage.load": "Click para recargar{sizeMessage}",
|
||||||
"theme.IdealImageMessage.loading": "Cargando...",
|
"theme.IdealImageMessage.loading": "Cargando...",
|
||||||
"theme.IdealImageMessage.offline": "Tu navegador está desconectado. Image no cargada"
|
"theme.IdealImageMessage.offline": "Tu navegador está desconectado. Imagen no cargada"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,26 @@
|
||||||
"theme.CodeBlock.copy": "Copiar",
|
"theme.CodeBlock.copy": "Copiar",
|
||||||
"theme.CodeBlock.copyButtonAriaLabel": "Copiar código",
|
"theme.CodeBlock.copyButtonAriaLabel": "Copiar código",
|
||||||
"theme.CodeBlock.wordWrapToggle": "Alternar ajuste de palabras",
|
"theme.CodeBlock.wordWrapToggle": "Alternar ajuste de palabras",
|
||||||
"theme.DocSidebarItem.collapseCategoryAriaLabel": "Colapsar categoría '{label}' de barra lateral",
|
"theme.DocSidebarItem.collapseCategoryAriaLabel": "Colapsar categoría '{label}' de la barra lateral",
|
||||||
"theme.DocSidebarItem.expandCategoryAriaLabel": "Ampliar la categoría '{label}' de la barra lateral",
|
"theme.DocSidebarItem.expandCategoryAriaLabel": "Ampliar la categoría '{label}' de la barra lateral",
|
||||||
"theme.ErrorPageContent.title": "Esta página ha fallado.",
|
"theme.ErrorPageContent.title": "Esta página ha fallado.",
|
||||||
"theme.ErrorPageContent.tryAgain": "Intente de nuevo",
|
"theme.ErrorPageContent.tryAgain": "Intente de nuevo",
|
||||||
"theme.NavBar.navAriaLabel": "Principal",
|
"theme.NavBar.navAriaLabel": "Principal",
|
||||||
"theme.NotFound.p1": "No pudimos encontrar lo que buscaba.",
|
"theme.NotFound.p1": "No pudimos encontrar lo que buscaba.",
|
||||||
"theme.NotFound.p2": "Comuníquese con el dueño del sitio que lo vinculó a la URL original y hágale saber que su vínculo está roto.",
|
"theme.NotFound.p2": "Comuníquese con el dueño del sitio que le proporcionó la URL original y hágale saber que su vínculo está roto.",
|
||||||
"theme.NotFound.title": "Página No Encontrada",
|
"theme.NotFound.title": "Página No Encontrada",
|
||||||
"theme.TOCCollapsible.toggleButtonLabel": "En esta página",
|
"theme.TOCCollapsible.toggleButtonLabel": "En esta página",
|
||||||
"theme.admonition.caution": "precaución",
|
"theme.admonition.caution": "precaución",
|
||||||
"theme.admonition.danger": "danger",
|
"theme.admonition.danger": "peligro",
|
||||||
"theme.admonition.info": "info",
|
"theme.admonition.info": "info",
|
||||||
"theme.admonition.note": "note",
|
"theme.admonition.note": "nota",
|
||||||
"theme.admonition.tip": "tip",
|
"theme.admonition.tip": "tip",
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "aviso",
|
||||||
"theme.blog.archive.description": "Archivo",
|
"theme.blog.archive.description": "Archivo",
|
||||||
"theme.blog.archive.title": "Archivo",
|
"theme.blog.archive.title": "Archivo",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Navegación por la página de la lista de blogs ",
|
"theme.blog.paginator.navAriaLabel": "Navegación por la página de la lista de blogs ",
|
||||||
"theme.blog.paginator.newerEntries": "Entradas más recientes",
|
"theme.blog.paginator.newerEntries": "Entradas más recientes",
|
||||||
"theme.blog.paginator.olderEntries": "Entradas más antiguas",
|
"theme.blog.paginator.olderEntries": "Entradas más antiguas",
|
||||||
|
@ -30,7 +33,7 @@
|
||||||
"theme.blog.post.paginator.olderPost": "Publicación más antigua",
|
"theme.blog.post.paginator.olderPost": "Publicación más antigua",
|
||||||
"theme.blog.post.plurals": "Una publicación|{count} publicaciones",
|
"theme.blog.post.plurals": "Una publicación|{count} publicaciones",
|
||||||
"theme.blog.post.readMore": "Leer Más",
|
"theme.blog.post.readMore": "Leer Más",
|
||||||
"theme.blog.post.readMoreLabel": "Leer más acerca {title}",
|
"theme.blog.post.readMoreLabel": "Leer más acerca de {title}",
|
||||||
"theme.blog.post.readingTime.plurals": "Lectura de un minuto|{readingTime} min de lectura",
|
"theme.blog.post.readingTime.plurals": "Lectura de un minuto|{readingTime} min de lectura",
|
||||||
"theme.blog.sidebar.navAriaLabel": "Navegación de publicaciones recientes",
|
"theme.blog.sidebar.navAriaLabel": "Navegación de publicaciones recientes",
|
||||||
"theme.blog.tagTitle": "{nPosts} etiquetados con \"{tagName}\"",
|
"theme.blog.tagTitle": "{nPosts} etiquetados con \"{tagName}\"",
|
||||||
|
@ -40,9 +43,13 @@
|
||||||
"theme.common.editThisPage": "Editar esta página",
|
"theme.common.editThisPage": "Editar esta página",
|
||||||
"theme.common.headingLinkTitle": "Enlace directo al {heading}",
|
"theme.common.headingLinkTitle": "Enlace directo al {heading}",
|
||||||
"theme.common.skipToMainContent": "Saltar al contenido principal",
|
"theme.common.skipToMainContent": "Saltar al contenido principal",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Esta página está sin clasificar. Los motores de búsqueda no la indexaran, y solo los usuarios con el enlace directo podrán acceder a esta.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Página sin clasificar",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 artículo|{count} artículos",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 artículo|{count} artículos",
|
||||||
"theme.docs.breadcrumbs.home": "Página de Inicio",
|
"theme.docs.breadcrumbs.home": "Página de Inicio",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Migas de pan",
|
||||||
"theme.docs.paginator.navAriaLabel": "Página del documento",
|
"theme.docs.paginator.navAriaLabel": "Página del documento",
|
||||||
"theme.docs.paginator.next": "Siguiente",
|
"theme.docs.paginator.next": "Siguiente",
|
||||||
"theme.docs.paginator.previous": "Anterior",
|
"theme.docs.paginator.previous": "Anterior",
|
||||||
|
@ -58,17 +65,15 @@
|
||||||
"theme.docs.versionBadge.label": "Version: {versionLabel}",
|
"theme.docs.versionBadge.label": "Version: {versionLabel}",
|
||||||
"theme.docs.versions.latestVersionLinkLabel": "última versión",
|
"theme.docs.versions.latestVersionLinkLabel": "última versión",
|
||||||
"theme.docs.versions.latestVersionSuggestionLabel": "Para la documentación actualizada, vea {latestVersionLink} ({versionLabel}).",
|
"theme.docs.versions.latestVersionSuggestionLabel": "Para la documentación actualizada, vea {latestVersionLink} ({versionLabel}).",
|
||||||
"theme.docs.versions.unmaintainedVersionLabel": "Esta es documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.",
|
"theme.docs.versions.unmaintainedVersionLabel": "Esta es la documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.",
|
||||||
"theme.docs.versions.unreleasedVersionLabel": "Esta es documentación sin liberar para {siteTitle} {versionLabel} versión.",
|
"theme.docs.versions.unreleasedVersionLabel": "Esta es la documentación sin publicar para {siteTitle}, versión {versionLabel}.",
|
||||||
"theme.lastUpdated.atDate": " en {date}",
|
"theme.lastUpdated.atDate": " en {date}",
|
||||||
"theme.lastUpdated.byUser": " por {user}",
|
"theme.lastUpdated.byUser": " por {user}",
|
||||||
"theme.lastUpdated.lastUpdatedAtBy": "Última actualización{atDate}{byUser}",
|
"theme.lastUpdated.lastUpdatedAtBy": "Última actualización{atDate}{byUser}",
|
||||||
"theme.navbar.mobileLanguageDropdown.label": "Lenguajes",
|
"theme.navbar.mobileLanguageDropdown.label": "Idiomas",
|
||||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Volver al menú principal",
|
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Volver al menú principal",
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versiones",
|
"theme.navbar.mobileVersionsDropdown.label": "Versiones",
|
||||||
"theme.tags.tagsListLabel": "Etiquetas:",
|
"theme.tags.tagsListLabel": "Etiquetas:",
|
||||||
"theme.tags.tagsPageLink": "Ver Todas las Etiquetas",
|
"theme.tags.tagsPageLink": "Ver Todas las Etiquetas",
|
||||||
"theme.tags.tagsPageTitle": "Etiquetas",
|
"theme.tags.tagsPageTitle": "Etiquetas"
|
||||||
"theme.unlistedContent.message": "Esta página está sin clasificar. Los motores de búsqueda no la indexaran, y solo los usuarios con el enlace directo podrán acceder a esta.",
|
|
||||||
"theme.unlistedContent.title": "Página sin clasificar"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"theme.SearchModal.footer.selectText": "seleccionar",
|
"theme.SearchModal.footer.selectText": "seleccionar",
|
||||||
"theme.SearchModal.noResultsScreen.noResultsText": "Sin resultados para",
|
"theme.SearchModal.noResultsScreen.noResultsText": "Sin resultados para",
|
||||||
"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": "Háganos saber.",
|
"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": "Háganos saber.",
|
||||||
"theme.SearchModal.noResultsScreen.reportMissingResultsText": "Creo que esta consulta debería devolver resultados?",
|
"theme.SearchModal.noResultsScreen.reportMissingResultsText": "Crees que esta consulta debería devolver resultados?",
|
||||||
"theme.SearchModal.noResultsScreen.suggestedQueryText": "Intenta buscando por",
|
"theme.SearchModal.noResultsScreen.suggestedQueryText": "Intenta buscando por",
|
||||||
"theme.SearchModal.placeholder": "Buscar documentos",
|
"theme.SearchModal.placeholder": "Buscar documentos",
|
||||||
"theme.SearchModal.searchBox.cancelButtonText": "Cancelar",
|
"theme.SearchModal.searchBox.cancelButtonText": "Cancelar",
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "hoiatus",
|
"theme.admonition.warning": "hoiatus",
|
||||||
"theme.blog.archive.description": "Arhiiv",
|
"theme.blog.archive.description": "Arhiiv",
|
||||||
"theme.blog.archive.title": "Arhiiv",
|
"theme.blog.archive.title": "Arhiiv",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Blogi lehekülje navigatsioon",
|
"theme.blog.paginator.navAriaLabel": "Blogi lehekülje navigatsioon",
|
||||||
"theme.blog.paginator.newerEntries": "Uuemad sissekanded",
|
"theme.blog.paginator.newerEntries": "Uuemad sissekanded",
|
||||||
"theme.blog.paginator.olderEntries": "Vanemad sissekanded",
|
"theme.blog.paginator.olderEntries": "Vanemad sissekanded",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Redigeeri seda lehte",
|
"theme.common.editThisPage": "Redigeeri seda lehte",
|
||||||
"theme.common.headingLinkTitle": "Link {heading}",
|
"theme.common.headingLinkTitle": "Link {heading}",
|
||||||
"theme.common.skipToMainContent": "Liigu peamise sisu juurde",
|
"theme.common.skipToMainContent": "Liigu peamise sisu juurde",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "See leht ei ole avalik. Otsingumootorid ei indekseeri seda. Sellele lehele pääseb ainult lingiga ligi.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "avalikustamata leht",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 ese|{count} eset",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 ese|{count} eset",
|
||||||
"theme.docs.breadcrumbs.home": "Koduleht",
|
"theme.docs.breadcrumbs.home": "Koduleht",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versioonid",
|
"theme.navbar.mobileVersionsDropdown.label": "Versioonid",
|
||||||
"theme.tags.tagsListLabel": "Märked:",
|
"theme.tags.tagsListLabel": "Märked:",
|
||||||
"theme.tags.tagsPageLink": "Näaita Kõiki Märkeid",
|
"theme.tags.tagsPageLink": "Näaita Kõiki Märkeid",
|
||||||
"theme.tags.tagsPageTitle": "Märked",
|
"theme.tags.tagsPageTitle": "Märked"
|
||||||
"theme.unlistedContent.message": "See leht ei ole avalik. Otsingumootorid ei indekseeri seda. Sellele lehele pääseb ainult lingiga ligi.",
|
|
||||||
"theme.unlistedContent.title": "avalikustamata leht"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "هشدار",
|
"theme.admonition.warning": "هشدار",
|
||||||
"theme.blog.archive.description": "آرشیو",
|
"theme.blog.archive.description": "آرشیو",
|
||||||
"theme.blog.archive.title": "آرشیو",
|
"theme.blog.archive.title": "آرشیو",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "کنترل لیست مطالب وبلاگ",
|
"theme.blog.paginator.navAriaLabel": "کنترل لیست مطالب وبلاگ",
|
||||||
"theme.blog.paginator.newerEntries": "مطالب جدیدتر",
|
"theme.blog.paginator.newerEntries": "مطالب جدیدتر",
|
||||||
"theme.blog.paginator.olderEntries": "مطالب قدیمیتر",
|
"theme.blog.paginator.olderEntries": "مطالب قدیمیتر",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "ویرایش مطالب این صفحه",
|
"theme.common.editThisPage": "ویرایش مطالب این صفحه",
|
||||||
"theme.common.headingLinkTitle": "لینک مستقیم به {heading}",
|
"theme.common.headingLinkTitle": "لینک مستقیم به {heading}",
|
||||||
"theme.common.skipToMainContent": "پرش به مطلب اصلی",
|
"theme.common.skipToMainContent": "پرش به مطلب اصلی",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "این صفحه فهرست نشده است. موتورهای جستجو آن را ایندکس نمی کنند و فقط کاربرانی که لینک مستقیم دارند می توانند به آن دسترسی داشته باشند.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "صفحه فهرست نشده",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "{count} مورد",
|
"theme.docs.DocCard.categoryDescription.plurals": "{count} مورد",
|
||||||
"theme.docs.breadcrumbs.home": "صفحه اصلی",
|
"theme.docs.breadcrumbs.home": "صفحه اصلی",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "نشانگر صفحات",
|
"theme.docs.breadcrumbs.navAriaLabel": "نشانگر صفحات",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "نسخهها",
|
"theme.navbar.mobileVersionsDropdown.label": "نسخهها",
|
||||||
"theme.tags.tagsListLabel": "برچسبها:",
|
"theme.tags.tagsListLabel": "برچسبها:",
|
||||||
"theme.tags.tagsPageLink": "مشاهده تمام برچسبها",
|
"theme.tags.tagsPageLink": "مشاهده تمام برچسبها",
|
||||||
"theme.tags.tagsPageTitle": "برچسبها",
|
"theme.tags.tagsPageTitle": "برچسبها"
|
||||||
"theme.unlistedContent.message": "این صفحه فهرست نشده است. موتورهای جستجو آن را ایندکس نمی کنند و فقط کاربرانی که لینک مستقیم دارند می توانند به آن دسترسی داشته باشند.",
|
|
||||||
"theme.unlistedContent.title": "صفحه فهرست نشده"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Nabegasyón para sa pahina na listahan ng blog",
|
"theme.blog.paginator.navAriaLabel": "Nabegasyón para sa pahina na listahan ng blog",
|
||||||
"theme.blog.paginator.newerEntries": "Mas bagong mga éntri",
|
"theme.blog.paginator.newerEntries": "Mas bagong mga éntri",
|
||||||
"theme.blog.paginator.olderEntries": "Mas lumang mga éntri",
|
"theme.blog.paginator.olderEntries": "Mas lumang mga éntri",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "I-edit ang page",
|
"theme.common.editThisPage": "I-edit ang page",
|
||||||
"theme.common.headingLinkTitle": "Direktang link patungo sa {heading}",
|
"theme.common.headingLinkTitle": "Direktang link patungo sa {heading}",
|
||||||
"theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content",
|
"theme.common.skipToMainContent": "Lumaktaw patungo sa pangunahing content",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "Mga Tag:",
|
"theme.tags.tagsListLabel": "Mga Tag:",
|
||||||
"theme.tags.tagsPageLink": "Tingnan Lahat ng mga Tag",
|
"theme.tags.tagsPageLink": "Tingnan Lahat ng mga Tag",
|
||||||
"theme.tags.tagsPageTitle": "Mga Tag",
|
"theme.tags.tagsPageTitle": "Mga Tag"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "attention",
|
"theme.admonition.warning": "attention",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Pagination de la liste des articles du blog",
|
"theme.blog.paginator.navAriaLabel": "Pagination de la liste des articles du blog",
|
||||||
"theme.blog.paginator.newerEntries": "Nouvelles entrées",
|
"theme.blog.paginator.newerEntries": "Nouvelles entrées",
|
||||||
"theme.blog.paginator.olderEntries": "Anciennes entrées",
|
"theme.blog.paginator.olderEntries": "Anciennes entrées",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Éditer cette page",
|
"theme.common.editThisPage": "Éditer cette page",
|
||||||
"theme.common.headingLinkTitle": "Lien direct vers {heading}",
|
"theme.common.headingLinkTitle": "Lien direct vers {heading}",
|
||||||
"theme.common.skipToMainContent": "Aller au contenu principal",
|
"theme.common.skipToMainContent": "Aller au contenu principal",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Page non répertoriée",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 élément|{count} éléments",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 élément|{count} éléments",
|
||||||
"theme.docs.breadcrumbs.home": "Page d'accueil",
|
"theme.docs.breadcrumbs.home": "Page d'accueil",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Fil d'Ariane",
|
"theme.docs.breadcrumbs.navAriaLabel": "Fil d'Ariane",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "Tags :",
|
"theme.tags.tagsListLabel": "Tags :",
|
||||||
"theme.tags.tagsPageLink": "Voir tous les tags",
|
"theme.tags.tagsPageLink": "Voir tous les tags",
|
||||||
"theme.tags.tagsPageTitle": "Tags",
|
"theme.tags.tagsPageTitle": "Tags"
|
||||||
"theme.unlistedContent.message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
|
|
||||||
"theme.unlistedContent.title": "Page non répertoriée"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "רשימת דפי הבלוג",
|
"theme.blog.paginator.navAriaLabel": "רשימת דפי הבלוג",
|
||||||
"theme.blog.paginator.newerEntries": "הכי חדש",
|
"theme.blog.paginator.newerEntries": "הכי חדש",
|
||||||
"theme.blog.paginator.olderEntries": "ישן יותר",
|
"theme.blog.paginator.olderEntries": "ישן יותר",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "ערוך דף זה",
|
"theme.common.editThisPage": "ערוך דף זה",
|
||||||
"theme.common.headingLinkTitle": "קישור ישיר אל {heading}",
|
"theme.common.headingLinkTitle": "קישור ישיר אל {heading}",
|
||||||
"theme.common.skipToMainContent": "דלג לתוכן הראשי",
|
"theme.common.skipToMainContent": "דלג לתוכן הראשי",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "תגיות:",
|
"theme.tags.tagsListLabel": "תגיות:",
|
||||||
"theme.tags.tagsPageLink": "כל התגיות",
|
"theme.tags.tagsPageLink": "כל התגיות",
|
||||||
"theme.tags.tagsPageTitle": "תגיות",
|
"theme.tags.tagsPageTitle": "תגיות"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archive",
|
"theme.blog.archive.description": "Archive",
|
||||||
"theme.blog.archive.title": "Archive",
|
"theme.blog.archive.title": "Archive",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "ब्लॉग सूची पेज नेविगेशन",
|
"theme.blog.paginator.navAriaLabel": "ब्लॉग सूची पेज नेविगेशन",
|
||||||
"theme.blog.paginator.newerEntries": "नए एंट्रीज़",
|
"theme.blog.paginator.newerEntries": "नए एंट्रीज़",
|
||||||
"theme.blog.paginator.olderEntries": "पुराने एंट्रीज़",
|
"theme.blog.paginator.olderEntries": "पुराने एंट्रीज़",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "इस पेज को बदलें",
|
"theme.common.editThisPage": "इस पेज को बदलें",
|
||||||
"theme.common.headingLinkTitle": "{heading} का सीधा लिंक",
|
"theme.common.headingLinkTitle": "{heading} का सीधा लिंक",
|
||||||
"theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें",
|
"theme.common.skipToMainContent": "मुख्य कंटेंट तक स्किप करें",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Unlisted page",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Home page",
|
"theme.docs.breadcrumbs.home": "Home page",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
"theme.docs.breadcrumbs.navAriaLabel": "Breadcrumbs",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||||
"theme.tags.tagsListLabel": "टैग:",
|
"theme.tags.tagsListLabel": "टैग:",
|
||||||
"theme.tags.tagsPageLink": "सारे टैग देखें",
|
"theme.tags.tagsPageLink": "सारे टैग देखें",
|
||||||
"theme.tags.tagsPageTitle": "टैग",
|
"theme.tags.tagsPageTitle": "टैग"
|
||||||
"theme.unlistedContent.message": "This page is unlisted. Search engines will not index it, and only users having a direct link can access it.",
|
|
||||||
"theme.unlistedContent.title": "Unlisted page"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "vigyázat",
|
"theme.admonition.warning": "vigyázat",
|
||||||
"theme.blog.archive.description": "Archívum",
|
"theme.blog.archive.description": "Archívum",
|
||||||
"theme.blog.archive.title": "Archívum",
|
"theme.blog.archive.title": "Archívum",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Bloglista oldalának navigációja",
|
"theme.blog.paginator.navAriaLabel": "Bloglista oldalának navigációja",
|
||||||
"theme.blog.paginator.newerEntries": "Újabb bejegyzések",
|
"theme.blog.paginator.newerEntries": "Újabb bejegyzések",
|
||||||
"theme.blog.paginator.olderEntries": "Régebbi bejegyzések",
|
"theme.blog.paginator.olderEntries": "Régebbi bejegyzések",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Szerkesztés GitHub-on",
|
"theme.common.editThisPage": "Szerkesztés GitHub-on",
|
||||||
"theme.common.headingLinkTitle": "Közvetlen hivatkozás erre: {heading}",
|
"theme.common.headingLinkTitle": "Közvetlen hivatkozás erre: {heading}",
|
||||||
"theme.common.skipToMainContent": "Ugrás a fő tartalomhoz",
|
"theme.common.skipToMainContent": "Ugrás a fő tartalomhoz",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Ez az oldal nem nyilvános. A keresőmotorok nem indexelik, és csak a közvetlen hivatkozással rendelkező felhasználók érhetik el.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Nem nyilvános oldal",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 elem|{count} elemek",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 elem|{count} elemek",
|
||||||
"theme.docs.breadcrumbs.home": "Kezdőlap",
|
"theme.docs.breadcrumbs.home": "Kezdőlap",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Navigációs sáv a jelenlegi oldalhoz",
|
"theme.docs.breadcrumbs.navAriaLabel": "Navigációs sáv a jelenlegi oldalhoz",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Verziók",
|
"theme.navbar.mobileVersionsDropdown.label": "Verziók",
|
||||||
"theme.tags.tagsListLabel": "Címkék:",
|
"theme.tags.tagsListLabel": "Címkék:",
|
||||||
"theme.tags.tagsPageLink": "Összes címke megtekintése",
|
"theme.tags.tagsPageLink": "Összes címke megtekintése",
|
||||||
"theme.tags.tagsPageTitle": "Címkék",
|
"theme.tags.tagsPageTitle": "Címkék"
|
||||||
"theme.unlistedContent.message": "Ez az oldal nem nyilvános. A keresőmotorok nem indexelik, és csak a közvetlen hivatkozással rendelkező felhasználók érhetik el.",
|
|
||||||
"theme.unlistedContent.title": "Nem nyilvános oldal"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "peringatan",
|
"theme.admonition.warning": "peringatan",
|
||||||
"theme.blog.archive.description": "Arsip",
|
"theme.blog.archive.description": "Arsip",
|
||||||
"theme.blog.archive.title": "Arsip",
|
"theme.blog.archive.title": "Arsip",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Navigasi entri blog",
|
"theme.blog.paginator.navAriaLabel": "Navigasi entri blog",
|
||||||
"theme.blog.paginator.newerEntries": "Entri lebih baru",
|
"theme.blog.paginator.newerEntries": "Entri lebih baru",
|
||||||
"theme.blog.paginator.olderEntries": "Entri lebih lama",
|
"theme.blog.paginator.olderEntries": "Entri lebih lama",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Sunting halaman ini",
|
"theme.common.editThisPage": "Sunting halaman ini",
|
||||||
"theme.common.headingLinkTitle": "Taut langsung ke {heading}",
|
"theme.common.headingLinkTitle": "Taut langsung ke {heading}",
|
||||||
"theme.common.skipToMainContent": "Lewati ke konten utama",
|
"theme.common.skipToMainContent": "Lewati ke konten utama",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Halaman ini tidak terdaftar. Mesin pencari tidak akan mengindeksnya, dan hanya pengguna yang memiliki tautan langsung yang dapat mengaksesnya.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Halaman tak terdaftar",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 butir|{count} butir",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 butir|{count} butir",
|
||||||
"theme.docs.breadcrumbs.home": "Halaman utama",
|
"theme.docs.breadcrumbs.home": "Halaman utama",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Runut navigasi",
|
"theme.docs.breadcrumbs.navAriaLabel": "Runut navigasi",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versi",
|
"theme.navbar.mobileVersionsDropdown.label": "Versi",
|
||||||
"theme.tags.tagsListLabel": "Tag:",
|
"theme.tags.tagsListLabel": "Tag:",
|
||||||
"theme.tags.tagsPageLink": "Lihat Semua Tag",
|
"theme.tags.tagsPageLink": "Lihat Semua Tag",
|
||||||
"theme.tags.tagsPageTitle": "Tag",
|
"theme.tags.tagsPageTitle": "Tag"
|
||||||
"theme.unlistedContent.message": "Halaman ini tidak terdaftar. Mesin pencari tidak akan mengindeksnya, dan hanya pengguna yang memiliki tautan langsung yang dapat mengaksesnya.",
|
|
||||||
"theme.unlistedContent.title": "Halaman tak terdaftar"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "aðvörun",
|
"theme.admonition.warning": "aðvörun",
|
||||||
"theme.blog.archive.description": "Skjalasafn",
|
"theme.blog.archive.description": "Skjalasafn",
|
||||||
"theme.blog.archive.title": "Skjalasafn",
|
"theme.blog.archive.title": "Skjalasafn",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Blogg listsíðu yfirlit",
|
"theme.blog.paginator.navAriaLabel": "Blogg listsíðu yfirlit",
|
||||||
"theme.blog.paginator.newerEntries": "Nýrri færslur",
|
"theme.blog.paginator.newerEntries": "Nýrri færslur",
|
||||||
"theme.blog.paginator.olderEntries": "Eldri færslur",
|
"theme.blog.paginator.olderEntries": "Eldri færslur",
|
||||||
|
@ -40,7 +43,12 @@
|
||||||
"theme.common.editThisPage": "Breyttu þessari síðu",
|
"theme.common.editThisPage": "Breyttu þessari síðu",
|
||||||
"theme.common.headingLinkTitle": "Beinn hlekkur að {heading}",
|
"theme.common.headingLinkTitle": "Beinn hlekkur að {heading}",
|
||||||
"theme.common.skipToMainContent": "Hoppa yfir á aðal efni",
|
"theme.common.skipToMainContent": "Hoppa yfir á aðal efni",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Þessi síða er ólistuð. Leitarvélar munu ekki skrá hana, eingöngu notendur með beinan hlekk geta opnað hana.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Óskráð síða",
|
||||||
"theme.docs.DocCard.categoryDescription": "{count} atriði",
|
"theme.docs.DocCard.categoryDescription": "{count} atriði",
|
||||||
|
"theme.docs.DocCard.categoryDescription.plurals": "1 item|{count} items",
|
||||||
"theme.docs.breadcrumbs.home": "Heimasíða",
|
"theme.docs.breadcrumbs.home": "Heimasíða",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Brauðteningar",
|
"theme.docs.breadcrumbs.navAriaLabel": "Brauðteningar",
|
||||||
"theme.docs.paginator.navAriaLabel": "Skjala síður",
|
"theme.docs.paginator.navAriaLabel": "Skjala síður",
|
||||||
|
@ -68,7 +76,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Útgáfur",
|
"theme.navbar.mobileVersionsDropdown.label": "Útgáfur",
|
||||||
"theme.tags.tagsListLabel": "Merki:",
|
"theme.tags.tagsListLabel": "Merki:",
|
||||||
"theme.tags.tagsPageLink": "Skoða Öll Merki",
|
"theme.tags.tagsPageLink": "Skoða Öll Merki",
|
||||||
"theme.tags.tagsPageTitle": "Merki",
|
"theme.tags.tagsPageTitle": "Merki"
|
||||||
"theme.unlistedContent.message": "Þessi síða er ólistuð. Leitarvélar munu ekki skrá hana, eingöngu notendur með beinan hlekk geta opnað hana.",
|
|
||||||
"theme.unlistedContent.title": "Óskráð síða"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "warning",
|
"theme.admonition.warning": "warning",
|
||||||
"theme.blog.archive.description": "Archivio",
|
"theme.blog.archive.description": "Archivio",
|
||||||
"theme.blog.archive.title": "Archivio",
|
"theme.blog.archive.title": "Archivio",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "Navigazione nella pagina dei post del blog ",
|
"theme.blog.paginator.navAriaLabel": "Navigazione nella pagina dei post del blog ",
|
||||||
"theme.blog.paginator.newerEntries": "Post più recenti",
|
"theme.blog.paginator.newerEntries": "Post più recenti",
|
||||||
"theme.blog.paginator.olderEntries": "Post più vecchi",
|
"theme.blog.paginator.olderEntries": "Post più vecchi",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "Modifica questa pagina",
|
"theme.common.editThisPage": "Modifica questa pagina",
|
||||||
"theme.common.headingLinkTitle": "Link diretto a {heading}",
|
"theme.common.headingLinkTitle": "Link diretto a {heading}",
|
||||||
"theme.common.skipToMainContent": "Passa al contenuto principale",
|
"theme.common.skipToMainContent": "Passa al contenuto principale",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "Questa pagina non è in elenco. I motori di ricerca non lo indicheranno e solo gli utenti con collegamento diretto possono accedervi.",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "Pagina non in elenco",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "1 elemento|{count} elementi",
|
"theme.docs.DocCard.categoryDescription.plurals": "1 elemento|{count} elementi",
|
||||||
"theme.docs.breadcrumbs.home": "Pagina principale",
|
"theme.docs.breadcrumbs.home": "Pagina principale",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "Briciole di pane",
|
"theme.docs.breadcrumbs.navAriaLabel": "Briciole di pane",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "Versioni",
|
"theme.navbar.mobileVersionsDropdown.label": "Versioni",
|
||||||
"theme.tags.tagsListLabel": "Etichette:",
|
"theme.tags.tagsListLabel": "Etichette:",
|
||||||
"theme.tags.tagsPageLink": "Guarda tutte le etichette",
|
"theme.tags.tagsPageLink": "Guarda tutte le etichette",
|
||||||
"theme.tags.tagsPageTitle": "Etichette",
|
"theme.tags.tagsPageTitle": "Etichette"
|
||||||
"theme.unlistedContent.message": "Questa pagina non è in elenco. I motori di ricerca non lo indicheranno e solo gli utenti con collegamento diretto possono accedervi.",
|
|
||||||
"theme.unlistedContent.title": "Pagina non in elenco"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
"theme.admonition.warning": "警告",
|
"theme.admonition.warning": "警告",
|
||||||
"theme.blog.archive.description": "アーカイブ",
|
"theme.blog.archive.description": "アーカイブ",
|
||||||
"theme.blog.archive.title": "アーカイブ",
|
"theme.blog.archive.title": "アーカイブ",
|
||||||
|
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
|
||||||
|
"theme.blog.authorsList.pageTitle": "Authors",
|
||||||
|
"theme.blog.authorsList.viewAll": "View All Authors",
|
||||||
"theme.blog.paginator.navAriaLabel": "ブログ記事一覧のナビゲーション",
|
"theme.blog.paginator.navAriaLabel": "ブログ記事一覧のナビゲーション",
|
||||||
"theme.blog.paginator.newerEntries": "新しい記事",
|
"theme.blog.paginator.newerEntries": "新しい記事",
|
||||||
"theme.blog.paginator.olderEntries": "過去の記事",
|
"theme.blog.paginator.olderEntries": "過去の記事",
|
||||||
|
@ -40,6 +43,10 @@
|
||||||
"theme.common.editThisPage": "このページを編集",
|
"theme.common.editThisPage": "このページを編集",
|
||||||
"theme.common.headingLinkTitle": "{heading} への直接リンク",
|
"theme.common.headingLinkTitle": "{heading} への直接リンク",
|
||||||
"theme.common.skipToMainContent": "メインコンテンツまでスキップ",
|
"theme.common.skipToMainContent": "メインコンテンツまでスキップ",
|
||||||
|
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
|
||||||
|
"theme.contentVisibility.draftBanner.title": "Draft page",
|
||||||
|
"theme.contentVisibility.unlistedBanner.message": "このページは非公開です。 検索対象外となり、このページのリンクに直接アクセスできるユーザーのみに公開されます。",
|
||||||
|
"theme.contentVisibility.unlistedBanner.title": "非公開のページ",
|
||||||
"theme.docs.DocCard.categoryDescription.plurals": "{count}項目",
|
"theme.docs.DocCard.categoryDescription.plurals": "{count}項目",
|
||||||
"theme.docs.breadcrumbs.home": "ホームページ",
|
"theme.docs.breadcrumbs.home": "ホームページ",
|
||||||
"theme.docs.breadcrumbs.navAriaLabel": "パンくずリストのナビゲーション",
|
"theme.docs.breadcrumbs.navAriaLabel": "パンくずリストのナビゲーション",
|
||||||
|
@ -68,7 +75,5 @@
|
||||||
"theme.navbar.mobileVersionsDropdown.label": "他のバージョン",
|
"theme.navbar.mobileVersionsDropdown.label": "他のバージョン",
|
||||||
"theme.tags.tagsListLabel": "タグ:",
|
"theme.tags.tagsListLabel": "タグ:",
|
||||||
"theme.tags.tagsPageLink": "全てのタグを見る",
|
"theme.tags.tagsPageLink": "全てのタグを見る",
|
||||||
"theme.tags.tagsPageTitle": "タグ",
|
"theme.tags.tagsPageTitle": "タグ"
|
||||||
"theme.unlistedContent.message": "このページは非公開です。 検索対象外となり、このページのリンクに直接アクセスできるユーザーのみに公開されます。",
|
|
||||||
"theme.unlistedContent.title": "非公開のページ"
|
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue