diff --git a/.gitignore b/.gitignore index f01f106d..6e71e474 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ # Generated files .docusaurus .cache-loader +/changelog +/downloaded-changelogs +/changelog-processing # Generated Graphql Docs /graphql diff --git a/docusaurus.config.js b/docusaurus.config.js index c5b80ece..6218f09a 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,5 +1,8 @@ /* eslint-disable prettier/prettier */ /* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs'); +const path = require('path'); + const darkCodeTheme = require('prism-react-renderer/themes/dracula'); const lightCodeTheme = require('prism-react-renderer/themes/github'); @@ -7,6 +10,43 @@ const graphqlMarkdownConfig = require('./graphql-markdown.config'); const { specs } = require('./redoc.config'); const DOCS_URL = 'https://docs.epilot.io'; +const changelogProcessingDir = path.join(__dirname, 'changelog-processing'); + +const apiChangelogPlugins = specs + .filter((spec) => { + // Use the last segment after the last slash as the id, e.g. access-token + const id = spec.routePath.replace('/api/', '').replace(/\/$/, ''); + const changelogFile = path.join(changelogProcessingDir, `${id}.md`); + + return spec.specUrl && fs.existsSync(changelogFile); + }) + .map((spec) => { + const id = `${spec.routePath.replace('/api/', '').replace(/\/$/, '')}`; + const changelogFilename = path.join(changelogProcessingDir, `${id}.md`); + + return [ + require.resolve('./src/plugins/changelog/index.js'), + { + id: id, + blogTitle: `${spec.layout.title} Changelog`, + blogDescription: `Changelog for ${spec.layout.title}`, + blogSidebarCount: 'ALL', + blogSidebarTitle: 'Changelog', + routeBasePath: `${spec.routePath}/changelog`, + changelogFilename: changelogFilename, + showReadingTime: false, + postsPerPage: 20, + archiveBasePath: null, + feedOptions: { + type: 'all', + title: `${spec.layout.title} Changelog`, + description: `Changelog for ${spec.layout.title}`, + language: 'en', + }, + }, + ]; + }); + // With JSDoc @type annotations, IDEs can provide config autocompletion /** @type {import('@docusaurus/types').DocusaurusConfig} */ (module.exports = { @@ -19,7 +59,7 @@ const DOCS_URL = 'https://docs.epilot.io'; favicon: 'img/favicon.svg', organizationName: 'epilot-dev', - projectName: 'docs', + projectName: 'docs', presets: [ [ '@docusaurus/preset-classic', @@ -73,6 +113,7 @@ const DOCS_URL = 'https://docs.epilot.io'; sidebarPath: require.resolve('./sidebars.js'), }, ], + ...apiChangelogPlugins, // Spread the dynamically generated changelog plugins ], themeConfig: diff --git a/package.json b/package.json index 9c5de007..c0c7e7b5 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "npm run graphql:generate && docusaurus start", - "build": "npm run graphql:generate && docusaurus build", + "start": "npm run graphql:generate && npm run fetch-changelogs && docusaurus start", + "build": "npm run graphql:generate && npm run fetch-changelogs && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "npm run graphql:generate && docusaurus deploy", "clear": "docusaurus clear", @@ -16,7 +16,8 @@ "lint": "eslint src --ext js,jsx,ts,tsx,json", "lint:fix": "eslint src --ext js,jsx,ts,tsx,json --fix", "pdf": "./scripts/build-pdf.sh", - "graphql:generate": "./scripts/generate-graphql.sh" + "graphql:generate": "./scripts/generate-graphql.sh", + "fetch-changelogs": "./scripts/get-changelogs.sh" }, "dependencies": { "@cmfcmf/docusaurus-search-local": "^0.10.0", diff --git a/scripts/get-changelogs.sh b/scripts/get-changelogs.sh new file mode 100755 index 00000000..9bc6371f --- /dev/null +++ b/scripts/get-changelogs.sh @@ -0,0 +1,46 @@ +#!/bin/sh +set -e + +# This needs to be set via variable during build process +# S3_BUCKET="api-docs-changelog-demo" +S3_PREFIX="changelogs/" +LOCAL_DIR="./downloaded-changelogs" +PROCESSING_DIR="./changelog-processing" + +if [ -z "$S3_BUCKET" ]; then + echo 'error: $S3_BUCKET is not set' >&2 + exit 1 +fi + +# Ensure local directory exists +mkdir -p "$LOCAL_DIR" +mkdir -p "$PROCESSING_DIR" + +# Download only .md files from S3 +aws s3 sync "s3://$S3_BUCKET/$S3_PREFIX" "$LOCAL_DIR" --exclude "*" --include "*.md" + +cp "$LOCAL_DIR"/*.md "$PROCESSING_DIR" + +# Process downloaded files: update H1 header to only the last semantic version after "vs. " +for file in "$PROCESSING_DIR"/*.md; do + [ -e "$file" ] || continue + sed -i -E '1s/^# .*vs\. ([0-9]+\.[0-9]+\.[0-9]+).*$/# \1/' "$file" +done + +echo "Changelog files downloaded and processed." + +# Concatenate changelogs by type, highest version at top, lowest at end +for type in $(ls "$PROCESSING_DIR" | grep -oP '^.*(?=-[0-9]+\.[0-9]+\.[0-9]+\.md$)' | sort -u); do + files=$(ls "$PROCESSING_DIR"/${type}-*.md 2>/dev/null | sort -V -r) + [ -z "$files" ] && continue + out_file="$PROCESSING_DIR/${type}.md" + true > "$out_file" + for f in $files; do + cat "$f" >> "$out_file" + printf "\n" >> "$out_file" + done + echo "Concatenated $type changelogs into $out_file" + # Delete the numbered markdown files after processing + rm -f $files + echo "Deleted processed files: $files" +done \ No newline at end of file diff --git a/sidebars.js b/sidebars.js index 00658cbf..42ddbfad 100644 --- a/sidebars.js +++ b/sidebars.js @@ -11,7 +11,6 @@ module.exports = { // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], appsSidebar: [ { type: 'autogenerated', diff --git a/src/components/ApiSidebar.tsx b/src/components/ApiSidebar.tsx new file mode 100644 index 00000000..eaac8fbf --- /dev/null +++ b/src/components/ApiSidebar.tsx @@ -0,0 +1,39 @@ +// src/components/ApiSidebar.tsx +import { useLocation } from '@docusaurus/router'; +import DocPageStyles from '@docusaurus/theme-classic/lib-next/theme/DocPage/styles.module.css'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import DocSidebar from '@theme/DocSidebar'; +import React from 'react'; + +interface Spec { + layout: { + title: string; + }; + routePath: string; + specUrl: string; +} + +export default function ApiSidebar() { + const location = useLocation(); + const { siteConfig } = useDocusaurusContext(); + + const redocusaurusOpts = siteConfig.presets.find((preset) => preset[0] === 'redocusaurus'); + const specs: Spec[] = redocusaurusOpts?.[1]?.['specs'] ?? []; + + const sidebar = specs.map((spec) => ({ + type: 'link', + href: spec.routePath, + label: spec.layout.title, + })); + + const pathname = location.pathname; + // Match /api/{api-name}/changelog or /api/{api-name}/changelog/... and extract /api/{api-name} + const changelogMatch = pathname.match(/^\/api\/([^/]+)\/changelog(\/|$)/); + const highlightPath = changelogMatch ? `/api/${changelogMatch[1]}` : pathname; + + return ( + + ); +} diff --git a/src/components/RedocPage.tsx b/src/components/RedocPage.tsx index 5a97be5a..708ede59 100644 --- a/src/components/RedocPage.tsx +++ b/src/components/RedocPage.tsx @@ -1,7 +1,5 @@ -import { useLocation } from '@docusaurus/router'; import DocPageStyles from '@docusaurus/theme-classic/lib-next/theme/DocPage/styles.module.css'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import DocSidebar from '@theme/DocSidebar'; +import ApiSidebar from '@site/src/components/ApiSidebar'; import Layout from '@theme/Layout'; import Redoc from '@theme/Redoc'; import { ApiDocProps as Props } from 'docusaurus-theme-redoc/src/types/common'; @@ -9,40 +7,14 @@ import React from 'react'; import styles from './RedocPage.module.css'; -interface Spec { - layout: { - title: string; - }; - routePath: string; - specUrl: string; -} - function RedocPage({ layoutProps, spec: propSpec }: Props): JSX.Element { const { title = 'API Docs', description = 'Open API Reference Docs for the API' } = layoutProps || {}; - const location = useLocation(); - const { siteConfig } = useDocusaurusContext(); - - const redocusaurusOpts = siteConfig.presets.find((preset) => preset[0] === 'redocusaurus'); - const specs: Spec[] = redocusaurusOpts?.[1]?.['specs'] ?? []; - const specUrl: string | undefined = propSpec.type === 'url' ? propSpec.content : undefined; return ( - - +
diff --git a/src/plugins/changelog/index.js b/src/plugins/changelog/index.js new file mode 100644 index 00000000..f0789bb0 --- /dev/null +++ b/src/plugins/changelog/index.js @@ -0,0 +1,218 @@ +/** + * 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. + * + * Orignally copied as per https://github.com/facebook/docusaurus/discussions/10882 + */ + +const path = require('path'); + +const pluginContentBlog = require('@docusaurus/plugin-content-blog'); +const { aliasedSitePath, docuHash, normalizeUrl } = require('@docusaurus/utils'); +const fs = require('fs-extra'); +const Joi = require('joi'); + +// If you ever consider adding the date to the post (which we might), then also consider +// Looking back at the details of the link above to the GitHub Discussion and linked +// source code from which this plugin was derrived. +// It included an approach to allow for multiple postings per day by incrementing the hour +// to maintain correct and intended order of items. + +/** + * @param {string} section + * @param {string} routeBasePath + */ +function processSection(section, routeBasePath) { + // Match '# ...' at the start of the section or after a newline + const titleMatch = section.match(/^(# .*)|\n# .*/); + const title = titleMatch ? titleMatch[0].replace('\n', '').replace('# ', '').trim() : null; + if (!title) { + return null; + } + const content = section.replace(/^(# .*)|\n# .*/, '').trim(); + + const apiRoutesAndMethods = (content.match(/^## (.*)/gm) || []) + .map((h) => `\`${h.replace(/^## /, '')}\``) + .join(' \n'); + + // Clean version for slug (remove date, spaces, etc) + const versionSlug = title.replace(/\s.*$/, ''); + const slug = `${routeBasePath.replace(/\/$/, '')}/${versionSlug}`; + + return { + title: title.replace(/ \(.*\)/, ''), + content: `--- +title: ${title.replace(/ \(.*\)/, '')} +slug: ${slug} +--- + +# ${title.replace(/ \(.*\)/, '')} + +Changes to... + +${apiRoutesAndMethods} + + + +${content}`, + }; +} + +/** + * Docusaurus Changelog Plugin + * + * This plugin processes a single changelog markdown file (specified by options.changelogFilename) for a specific API. + * It splits the changelog into versioned sections, generates individual markdown files with YAML frontmatter for each version, + * and outputs them to a dedicated directory for that API. The plugin then delegates to the Docusaurus blog plugin to handle + * rendering and routing, ensuring each API's changelog is isolated and only APIs with changelogs are included. + * + * Key features: + * - Only processes the changelog file specified in options.changelogFilename (one per plugin instance) + * - Splits changelog into versioned sections and generates output files with frontmatter + * - Ensures correct output structure and avoids duplicate React import/build errors + * - Integrates with Docusaurus blog plugin for rendering and navigation + * + * @param {import('@docusaurus/types').LoadContext} context - Docusaurus site context + * @param {object} options - Plugin options (must include changelogFilename, id, and routeBasePath) + * @returns {import('@docusaurus/types').Plugin} + */ +async function ChangelogPlugin(context, options) { + const changelogsDir = path.join(__dirname, '../../../changelog-processing'); + const changelogOutputRoot = path.join(context.siteDir, 'changelog'); + const blogPlugin = await pluginContentBlog.default(context, { + ...options, + path: path.join(changelogOutputRoot, `changelog-${options.id}`), // Not used for writing, but required by plugin + blogListComponent: '@theme/ChangelogList', + blogPostComponent: '@theme/ChangelogPage', + }); + + return { + ...blogPlugin, + name: 'changelog-plugin', + /** + * Loads and processes the changelog for a single API instance. + * + * - Reads the changelog markdown file specified by options.changelogFilename. + * - Splits the file into versioned sections and generates individual markdown files with YAML frontmatter for each version. + * - Writes the generated files to a dedicated output directory for this API. + * - Delegates to the Docusaurus blog plugin for further processing and rendering. + * - Ensures only APIs with changelogs are included and avoids duplicate processing/build errors. + * + * @returns {Promise} The content object as expected by the Docusaurus blog plugin. + */ + async loadContent() { + // === Changelog Processing Start === + // Only process the changelog file specified in options.changelogFilename + // This ensures each plugin instance is isolated to its own changelog file and output directory. + if (!options.changelogFilename) { + throw new Error('Changelog plugin requires options.changelogFilename to be set'); + } + + const filePath = options.changelogFilename; + const baseName = path.basename(filePath, '.md'); + // Output directory for this changelog's generated markdown files (no 'source' subdir) + const outputDir = path.join(changelogOutputRoot, `changelog-${baseName}`); + await fs.ensureDir(outputDir); + + const fileContent = await fs.readFile(filePath, 'utf-8'); + + // Split the file into sections, each starting with a version heading (# ) - This could later include the date. + // Note: The regex below expects headings to start with a single # followed by a space and a digit (e.g., # 1.0.0) + let versionSections = fileContent.split(/(?=^# [0-9])/m).filter((s) => s.match(/^# [0-9]/m)); + + // This didn't effect the order. Think it must only be date based. + // Found a newer version of docusarus (above 3) has custom sorting, while this version (fixed at beta of 2) does not. + // // Sort sections by version (descending) + // versionSections.sort((a, b) => { + // const getVersion = (section) => { + // const m = section.match(/^# ([0-9]+\.[0-9]+\.[0-9]+)/m); + // return m ? m[1] : '0.0.0'; + // }; + // return semver.rcompare(getVersion(a), getVersion(b)); + // }); + + let allSections = []; + // Process each version section in parallel and write to output + await Promise.all( + versionSections.map(async (section) => { + // Parse the section for title and content, including YAML frontmatter + const processed = processSection(section, options.routeBasePath); + if (!processed) return; + + const { title, content } = processed; + // Write the processed markdown to the output directory + const outPath = path.join(outputDir, `${title}.md`); + await fs.outputFile(outPath, content); + allSections.push({ + title, + content, + outputPath: outPath, + }); + }), + ); + + // === Docusaurus Blog Plugin Integration === + // Let the wrapped blog plugin handle the rest of the content loading + const content = await blogPlugin.loadContent(); + // Patch blog post metadata for correct list page links + content.blogPosts.forEach((post, index) => { + const pageIndex = Math.floor(index / options.postsPerPage); + post.metadata.listPageLink = normalizeUrl([ + context.baseUrl, + options.routeBasePath, + pageIndex === 0 ? '/' : `/page/${pageIndex + 1}`, + ]); + }); + + return content; + }, + configureWebpack(...args) { + const config = blogPlugin.configureWebpack(...args); + const pluginDataDirRoot = path.join( + context.generatedFilesDir, + 'changelog-plugin', + options.id, // Use the dynamically provided id + ); + // Redirect the metadata path to the correct folder + config.module.rules[0].use[1].options.metadataPath = (mdxPath) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedPath = aliasedSitePath(mdxPath, context.siteDir); + + return path.join(pluginDataDirRoot, `${docuHash(aliasedPath)}.json`); + }; + + return config; + }, + getThemePath() { + return path.resolve(__dirname, './theme'); // Ensure this points to the plugin's theme folder + }, + getPathsToWatch() { + // Don't watch the generated dir + return [changelogsDir]; + }, + }; +} + +// Call the original blog plugin's validateOptions, then validate our custom option +ChangelogPlugin.validateOptions = (params) => { + // Remove our custom option before passing to the blog plugin's validation + const { options, validate } = params; + const { changelogFilename, ...blogOptions } = options; + + // Validate blog options and get defaults + const validatedBlogOptions = pluginContentBlog.validateOptions({ ...params, options: blogOptions }); + + // Validate our custom changelogFilename option + const schema = Joi.object({ + changelogFilename: Joi.string().required(), + }); + validate(schema, { changelogFilename }); + + // Merge validated blog options (with defaults) and our custom option + return { ...validatedBlogOptions, changelogFilename }; +}; + +module.exports = ChangelogPlugin; diff --git a/src/plugins/changelog/theme/ChangelogItem/index.tsx b/src/plugins/changelog/theme/ChangelogItem/index.tsx new file mode 100644 index 00000000..1a93a7c6 --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogItem/index.tsx @@ -0,0 +1,140 @@ +/** + * 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 Link from '@docusaurus/Link'; +import { usePluralForm } from '@docusaurus/theme-common'; +import Translate from '@docusaurus/Translate'; +import { useBaseUrlUtils } from '@docusaurus/useBaseUrl'; +import { blogPostContainerID } from '@docusaurus/utils-common'; +import { MDXProvider } from '@mdx-js/react'; +import type { Props } from '@theme/BlogPostItem'; +import EditThisPage from '@theme/EditThisPage'; +import MDXComponents from '@theme/MDXComponents'; +import TagsListInline from '@theme/TagsListInline'; +import clsx from 'clsx'; +import React from 'react'; + +import styles from './styles.module.css'; + +// Very simple pluralization: probably good enough for now +function useReadingTimePlural() { + const { selectMessage } = usePluralForm(); + + return (readingTimeFloat: number) => { + const readingTime = Math.ceil(readingTimeFloat); + + return selectMessage( + readingTime, + Translate( + { + id: 'theme.blog.post.readingTime.plurals', + description: + 'Pluralized label for "{readingTime} min read". 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 min read|{readingTime} min read', + }, + { readingTime }, + ), + ); + }; +} + +function ChangelogItem(props: Props): JSX.Element { + const readingTimePlural = useReadingTimePlural(); + const { withBaseUrl } = useBaseUrlUtils(); + const { children, frontMatter, assets, metadata, truncated, isBlogPostPage = false } = props; + const { date, formattedDate, permalink, tags, readingTime, title, editUrl } = metadata; + + const image = assets.image ?? frontMatter.image; + const truncatedPost = !isBlogPostPage && truncated; + const tagsExists = tags.length > 0; + const TitleHeading = isBlogPostPage ? 'h1' : 'h2'; + + return ( +
+
+ + {isBlogPostPage ? ( + title + ) : ( + + {title} + + )} + +
+ + + {typeof readingTime !== 'undefined' && ( + <> + {' · '} + {readingTimePlural(readingTime)} + + )} +
+
+ + {image && } + +
+ {children} +
+ + {(tagsExists || truncated) && ( +
+ {tagsExists && ( +
+ +
+ )} + + {isBlogPostPage && editUrl && ( +
+ +
+ )} + + {truncatedPost && ( +
+ + + + Read More + + + +
+ )} +
+ )} +
+ ); +} + +export default ChangelogItem; diff --git a/src/plugins/changelog/theme/ChangelogItem/styles.module.css b/src/plugins/changelog/theme/ChangelogItem/styles.module.css new file mode 100644 index 00000000..33fa6c8f --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogItem/styles.module.css @@ -0,0 +1,10 @@ +/** + * 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. + */ + +.changelogItemContainer { + margin-bottom: 1rem; +} diff --git a/src/plugins/changelog/theme/ChangelogList/index.tsx b/src/plugins/changelog/theme/ChangelogList/index.tsx new file mode 100644 index 00000000..2e5a7b24 --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogList/index.tsx @@ -0,0 +1,85 @@ +/** + * 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 { ThemeClassNames } from '@docusaurus/theme-common'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import ApiSidebar from '@site/src/components/ApiSidebar'; +import BlogLayout from '@theme/BlogLayout'; +import type { Props } from '@theme/BlogListPage'; +import BlogListPaginator from '@theme/BlogListPaginator'; +import BlogPostItem from '@theme/BlogPostItem'; +import Layout from '@theme/Layout'; +import React from 'react'; + +import styles from './styles.module.css'; + +function ChangelogList(props: Props): JSX.Element { + const { metadata, items, sidebar } = props; + const { + siteConfig: { title: siteTitle }, + } = useDocusaurusContext(); + const { blogDescription, blogTitle, permalink } = metadata; + const isBlogOnlyMode = permalink === '/'; + const title = isBlogOnlyMode ? siteTitle : blogTitle; + + return ( + + +
+ + + ← Back to API definition + +

{title}

+ + Get changelog updates via RSS Feed + + {items.map(({ content: BlogPostContent }) => { + const meta = BlogPostContent.metadata; + if (!meta) return null; + + return ( + +
+

{meta.title}

+
+ +
+ ); + })} + +
+
+
+ ); +} + +export default ChangelogList; diff --git a/src/plugins/changelog/theme/ChangelogList/styles.module.css b/src/plugins/changelog/theme/ChangelogList/styles.module.css new file mode 100644 index 00000000..7854ddb3 --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogList/styles.module.css @@ -0,0 +1,18 @@ + +.changelogBlogContainer { + width: 100%; + min-height: 100vh; +} + +.changelogBlogContainer > div { + max-width: 100%; +} +@media screen and (min-width: 997px) { + .changelogBlogContainer > div { + max-width: calc(100vw - var(--doc-sidebar-width) - 15px); + } +} + +:global(.blog-list-page) { + display:flex +} \ No newline at end of file diff --git a/src/plugins/changelog/theme/ChangelogPage/index.tsx b/src/plugins/changelog/theme/ChangelogPage/index.tsx new file mode 100644 index 00000000..4d0fb1cf --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogPage/index.tsx @@ -0,0 +1,92 @@ +/** + * 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 Link from '@docusaurus/Link'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import ApiSidebar from '@site/src/components/ApiSidebar'; +import BlogLayout from '@theme/BlogLayout'; +import type { Props } from '@theme/BlogPostPage'; +import ChangelogItem from '@theme/ChangelogItem'; +import ChangelogPaginator from '@theme/ChangelogPaginator'; +import Layout from '@theme/Layout'; +import Seo from '@theme/Seo'; +import TOC from '@theme/TOC'; +import React from 'react'; + +import styles from './styles.module.css'; + +function ChangelogPage(props: Props): JSX.Element { + const { content: ChangelogContent, sidebar } = props; + const { assets, metadata } = ChangelogContent; + const { title, description, nextItem, prevItem, date, tags, authors, frontMatter } = metadata; + const { + hide_table_of_contents: hideTableOfContents, + keywords, + toc_min_heading_level: tocMinHeadingLevel, + toc_max_heading_level: tocMaxHeadingLevel, + } = frontMatter; + + const image = assets.image ?? frontMatter.image; + + return ( + + +
+ 0 ? ( + + ) : undefined + } + > + + + + {authors.some((author) => author.url) && ( + author.url) + .filter(Boolean) + .join(',')} + /> + )} + {tags.length > 0 && tag.label).join(',')} />} + + +
+ + ← Back to {metadata.blogTitle || 'Changelog Index'} + +
+ + + + + + {(nextItem || prevItem) && } +
+
+
+ ); +} + +export default ChangelogPage; diff --git a/src/plugins/changelog/theme/ChangelogPage/styles.module.css b/src/plugins/changelog/theme/ChangelogPage/styles.module.css new file mode 100644 index 00000000..5ca35a28 --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogPage/styles.module.css @@ -0,0 +1,18 @@ + +.changelogBlogContainer { + width: 100%; + min-height: 100vh; +} + +.changelogBlogContainer > div { + max-width: 100%; +} +@media screen and (min-width: 997px) { + .changelogBlogContainer > div { + max-width: calc(100vw - var(--doc-sidebar-width) - 15px); + } +} + +:global(.blog-post-page) { + display:flex +} \ No newline at end of file diff --git a/src/plugins/changelog/theme/ChangelogPaginator/index.tsx b/src/plugins/changelog/theme/ChangelogPaginator/index.tsx new file mode 100644 index 00000000..4709c6bd --- /dev/null +++ b/src/plugins/changelog/theme/ChangelogPaginator/index.tsx @@ -0,0 +1,56 @@ +/** + * 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. + */ + +// Changed the text labels. + +import Translate, { translate } from '@docusaurus/Translate'; +import type { Props } from '@theme/BlogPostPaginator'; +import PaginatorNavLink from '@theme/PaginatorNavLink'; +import React from 'react'; + +export default function ChangelogPaginator(props: Props): JSX.Element { + const { nextItem, prevItem } = props; + + return ( + + ); +} diff --git a/src/plugins/changelog/theme/types.d.ts b/src/plugins/changelog/theme/types.d.ts new file mode 100644 index 00000000..4063c35d --- /dev/null +++ b/src/plugins/changelog/theme/types.d.ts @@ -0,0 +1,16 @@ +/** + * 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. + */ + +declare module '@theme/ChangelogPaginator'; + +declare module '@theme/ChangelogItem'; +declare module '@theme/ChangelogItem/Header'; +declare module '@theme/ChangelogItem/Header/Author'; +declare module '@theme/ChangelogItem/Header/Authors'; + +declare module '@theme/ChangelogList'; +declare module '@theme/ChangelogList/Header'; diff --git a/src/theme/Layout/index.js b/src/theme/Layout/index.js new file mode 100644 index 00000000..74409f1d --- /dev/null +++ b/src/theme/Layout/index.js @@ -0,0 +1,48 @@ +/** + * 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. + */ + +// The Layout was swizzled (modified) to allwo for hiding the navbar using hideNavBar. +// The ability to do such was added in later versions of Docusaurus, so it's likely you can +// just delete this file without any concerns if you're upgrading Docusaurus. + +import ErrorBoundary from '@docusaurus/ErrorBoundary'; +import { ThemeClassNames, useKeyboardNavigation } from '@docusaurus/theme-common'; +import AnnouncementBar from '@theme/AnnouncementBar'; +import ErrorPageContent from '@theme/ErrorPageContent'; +import Footer from '@theme/Footer'; +import LayoutHead from '@theme/LayoutHead'; +import LayoutProviders from '@theme/LayoutProviders'; +import Navbar from '@theme/Navbar'; +import SkipToContent from '@theme/SkipToContent'; +import clsx from 'clsx'; +import React from 'react'; +import './styles.css'; + +function Layout(props) { + const { children, noFooter, wrapperClassName, pageClassName, hideNavBar } = props; + useKeyboardNavigation(); + + return ( + + + + + + + + {!hideNavBar && } + +
+ {children} +
+ + {!noFooter &&