diff --git a/src/css/custom.scss b/src/css/custom.scss
index 96e6c90af..cc11e98ca 100644
--- a/src/css/custom.scss
+++ b/src/css/custom.scss
@@ -149,6 +149,29 @@ html {
}
}
+//warning msg for older blogs
+.warning {
+ background-color: var(--ifm-color-primary-lightest);
+ border: 1px solid var(--ifm-color-primary-light);
+ color: var(--ifm-color-primary-darkest);
+ padding: 1rem;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ box-shadow: var(--ifm-global-shadow-md);
+ font-size: 0.95rem;
+ line-height: 1.6;
+
+ strong {
+ color: var(--electron-color-secondary-light);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ background-color: var(--ifm-color-primary-darker);
+ border-color: var(--ifm-color-primary-light);
+ color: var(--ifm-color-primary-lightest);
+ }
+}
+
.footer {
&--dark {
--ifm-footer-background-color: --var(--electron-color-dark);
diff --git a/src/theme/BlogPostItem/Container/index.tsx b/src/theme/BlogPostItem/Container/index.tsx
new file mode 100644
index 000000000..5b09a03db
--- /dev/null
+++ b/src/theme/BlogPostItem/Container/index.tsx
@@ -0,0 +1,9 @@
+import React, { type ReactNode } from 'react';
+import type { Props } from '@theme/BlogPostItem/Container';
+
+export default function BlogPostItemContainer({
+ children,
+ className,
+}: Props): ReactNode {
+ return {children};
+}
diff --git a/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx
new file mode 100644
index 000000000..86a962e9e
--- /dev/null
+++ b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx
@@ -0,0 +1,39 @@
+import React, { type ReactNode } from 'react';
+import Translate, { translate } from '@docusaurus/Translate';
+import Link from '@docusaurus/Link';
+import type { Props } from '@theme/BlogPostItem/Footer/ReadMoreLink';
+
+function ReadMoreLabel() {
+ return (
+
+
+ Read more
+
+
+ );
+}
+
+export default function BlogPostItemFooterReadMoreLink(
+ props: Props,
+): ReactNode {
+ const { blogPostTitle, ...linkProps } = props;
+ return (
+
+
+
+ );
+}
diff --git a/src/theme/BlogPostItem/Footer/index.tsx b/src/theme/BlogPostItem/Footer/index.tsx
new file mode 100644
index 000000000..efdf623d1
--- /dev/null
+++ b/src/theme/BlogPostItem/Footer/index.tsx
@@ -0,0 +1,85 @@
+import React, { type ReactNode } from 'react';
+import clsx from 'clsx';
+import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
+import { ThemeClassNames } from '@docusaurus/theme-common';
+import EditMetaRow from '@theme/EditMetaRow';
+import TagsListInline from '@theme/TagsListInline';
+import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink';
+
+export default function BlogPostItemFooter(): ReactNode {
+ const { metadata, isBlogPostPage } = useBlogPost();
+ const {
+ tags,
+ title,
+ editUrl,
+ hasTruncateMarker,
+ lastUpdatedBy,
+ lastUpdatedAt,
+ } = metadata;
+
+ // A post is truncated if it's in the "list view" and it has a truncate marker
+ const truncatedPost = !isBlogPostPage && hasTruncateMarker;
+
+ const tagsExists = tags.length > 0;
+
+ const renderFooter = tagsExists || truncatedPost || editUrl;
+
+ if (!renderFooter) {
+ return null;
+ }
+
+ // BlogPost footer - details view
+ if (isBlogPostPage) {
+ const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
+
+ return (
+
+ );
+ }
+ // BlogPost footer - list view
+ else {
+ return (
+
+ );
+ }
+}
diff --git a/src/theme/BlogPostItem/Header/Authors/index.tsx b/src/theme/BlogPostItem/Header/Authors/index.tsx
new file mode 100644
index 000000000..13ebccd8d
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Authors/index.tsx
@@ -0,0 +1,49 @@
+import React, { type ReactNode } from 'react';
+import clsx from 'clsx';
+import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
+import BlogAuthor from '@theme/Blog/Components/Author';
+import type { Props } from '@theme/BlogPostItem/Header/Authors';
+import styles from './styles.module.css';
+
+// Component responsible for the authors layout
+export default function BlogPostItemHeaderAuthors({
+ className,
+}: Props): ReactNode {
+ const {
+ metadata: { authors },
+ assets,
+ } = useBlogPost();
+ const authorsCount = authors.length;
+ if (authorsCount === 0) {
+ return null;
+ }
+ const imageOnly = authors.every(({ name }) => !name);
+ const singleAuthor = authors.length === 1;
+ return (
+
+ {authors.map((author, idx) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/theme/BlogPostItem/Header/Authors/styles.module.css b/src/theme/BlogPostItem/Header/Authors/styles.module.css
new file mode 100644
index 000000000..d80c57914
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Authors/styles.module.css
@@ -0,0 +1,13 @@
+.authorCol {
+ max-width: inherit !important;
+}
+
+.imageOnlyAuthorRow {
+ display: flex;
+ flex-flow: row wrap;
+}
+
+.imageOnlyAuthorCol {
+ margin-left: 0.3rem;
+ margin-right: 0.3rem;
+}
diff --git a/src/theme/BlogPostItem/Header/Info/index.tsx b/src/theme/BlogPostItem/Header/Info/index.tsx
new file mode 100644
index 000000000..ca2512409
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Info/index.tsx
@@ -0,0 +1,77 @@
+import React, { type ReactNode } from 'react';
+import clsx from 'clsx';
+import { translate } from '@docusaurus/Translate';
+import { usePluralForm } from '@docusaurus/theme-common';
+import { useDateTimeFormat } from '@docusaurus/theme-common/internal';
+import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
+import type { Props } from '@theme/BlogPostItem/Header/Info';
+
+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 ReadingTime({ readingTime }: { readingTime: number }) {
+ const readingTimePlural = useReadingTimePlural();
+ return <>{readingTimePlural(readingTime)}>;
+}
+
+function DateTime({
+ date,
+ formattedDate,
+}: {
+ date: string;
+ formattedDate: string;
+}) {
+ return ;
+}
+
+function Spacer() {
+ return <>{' · '}>;
+}
+
+export default function BlogPostItemHeaderInfo({
+ className,
+}: Props): ReactNode {
+ const { metadata } = useBlogPost();
+ const { date, readingTime } = metadata;
+
+ const dateTimeFormat = useDateTimeFormat({
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ timeZone: 'UTC',
+ });
+
+ const formatDate = (blogDate: string) =>
+ dateTimeFormat.format(new Date(blogDate));
+
+ return (
+
+
+ {typeof readingTime !== 'undefined' && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/theme/BlogPostItem/Header/Info/styles.module.css b/src/theme/BlogPostItem/Header/Info/styles.module.css
new file mode 100644
index 000000000..27d569e08
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Info/styles.module.css
@@ -0,0 +1,3 @@
+.container {
+ font-size: 0.9rem;
+}
diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx
new file mode 100644
index 000000000..72b25bcc2
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Title/index.tsx
@@ -0,0 +1,20 @@
+import React, { type ReactNode } from 'react';
+import clsx from 'clsx';
+import Link from '@docusaurus/Link';
+import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
+import type { Props } from '@theme/BlogPostItem/Header/Title';
+
+import styles from './styles.module.css';
+
+export default function BlogPostItemHeaderTitle({
+ className,
+}: Props): ReactNode {
+ const { metadata, isBlogPostPage } = useBlogPost();
+ const { permalink, title } = metadata;
+ const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
+ return (
+
+ {isBlogPostPage ? title : {title}}
+
+ );
+}
diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css
new file mode 100644
index 000000000..04a8d6adc
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/Title/styles.module.css
@@ -0,0 +1,12 @@
+.title {
+ font-size: 3rem;
+}
+
+/**
+ Blog post title should be smaller on smaller devices
+**/
+@media (max-width: 576px) {
+ .title {
+ font-size: 2rem;
+ }
+}
diff --git a/src/theme/BlogPostItem/Header/index.tsx b/src/theme/BlogPostItem/Header/index.tsx
new file mode 100644
index 000000000..53cc6830c
--- /dev/null
+++ b/src/theme/BlogPostItem/Header/index.tsx
@@ -0,0 +1,14 @@
+import React, { type ReactNode } from 'react';
+import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title';
+import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info';
+import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors';
+
+export default function BlogPostItemHeader(): ReactNode {
+ return (
+
+ );
+}
diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx
new file mode 100644
index 000000000..d8ae73712
--- /dev/null
+++ b/src/theme/BlogPostItem/index.tsx
@@ -0,0 +1,45 @@
+import React, { type ReactNode } from 'react';
+import clsx from 'clsx';
+import { useBlogPost } from '@docusaurus/plugin-content-blog/client';
+import BlogPostItemContainer from '@theme/BlogPostItem/Container';
+import BlogPostItemHeader from '@theme/BlogPostItem/Header';
+import BlogPostItemContent from '@theme/BlogPostItem/Content';
+import BlogPostItemFooter from '@theme/BlogPostItem/Footer';
+import type { Props } from '@theme/BlogPostItem';
+
+// apply a bottom margin in list view
+function useContainerClassName() {
+ const { isBlogPostPage } = useBlogPost();
+ return !isBlogPostPage ? 'margin-bottom--xl' : undefined;
+}
+
+export default function BlogPostItem({
+ children,
+ className,
+}: Props): ReactNode {
+ const containerClassName = useContainerClassName();
+ const { metadata } = useBlogPost();
+ const blogDate = new Date(metadata.date); // Date on which blog was posted
+ const currentDate = new Date();
+ const blogAge =
+ (currentDate.getTime() - blogDate.getTime()) / (1000 * 60 * 60 * 24 * 365); // converting time in miliseconds to year
+ const isOld = blogAge >= 1;
+ const yearLabel = Math.floor(blogAge) === 1 ? 'year' : 'years'; // grammer check
+ return (
+
+ {isOld && (
+
+ ⚠️ Note: This blog post is over{' '}
+
+ {' '}
+ {Math.floor(blogAge)} {yearLabel} old{' '}
+
+ . Some information may be outdated.
+
+ )}
+
+ {children}
+
+
+ );
+}