feat: implement ui i18n support
This commit is contained in:
parent
457fb93718
commit
698c411c71
13 changed files with 162 additions and 54 deletions
|
@ -1,10 +1,10 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import {defineConfig} from 'astro/config';
|
||||||
|
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
import mdx from '@astrojs/mdx';
|
import mdx from '@astrojs/mdx';
|
||||||
|
|
||||||
import { remarkWordCount } from './src/plugins/remark/wordcount.js';
|
import {remarkWordCount} from './src/plugins/remark/wordcount.js';
|
||||||
|
|
||||||
import cloudflare from '@astrojs/cloudflare';
|
import cloudflare from '@astrojs/cloudflare';
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
|
@ -14,28 +14,33 @@ import partytown from '@astrojs/partytown';
|
||||||
import {remarkModifiedTime} from "./src/plugins/remark/modified-time.mjs";
|
import {remarkModifiedTime} from "./src/plugins/remark/modified-time.mjs";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://terminal-blog.example.com',
|
site: 'https://terminal-blog.example.com',
|
||||||
base: '/',
|
base: '/',
|
||||||
trailingSlash: 'ignore',
|
trailingSlash: 'ignore',
|
||||||
redirects: {
|
redirects: {
|
||||||
// for the old routes still can be accessed
|
// for the old routes still can be accessed
|
||||||
"/post/[...slug]": "/blog/[...slug]"
|
"/post/[...slug]": "/blog/[...slug]"
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
|
||||||
format: 'directory'
|
|
||||||
},
|
|
||||||
|
|
||||||
markdown: {
|
|
||||||
shikiConfig: {
|
|
||||||
theme: 'nord',
|
|
||||||
wrap: true
|
|
||||||
},
|
},
|
||||||
remarkPlugins: [ remarkMath, remarkWordCount, remarkModifiedTime ],
|
|
||||||
rehypePlugins: [ rehypeKatex ]
|
|
||||||
},
|
|
||||||
|
|
||||||
integrations: [sitemap(), mdx(), partytown()],
|
build: {
|
||||||
|
format: 'directory'
|
||||||
|
},
|
||||||
|
|
||||||
adapter: cloudflare()
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: 'nord',
|
||||||
|
wrap: true
|
||||||
|
},
|
||||||
|
remarkPlugins: [remarkMath, remarkWordCount, remarkModifiedTime],
|
||||||
|
rehypePlugins: [rehypeKatex]
|
||||||
|
},
|
||||||
|
|
||||||
|
i18n: {
|
||||||
|
locales: ["en", "zh_hans"],
|
||||||
|
defaultLocale: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
integrations: [sitemap(), mdx(), partytown()],
|
||||||
|
|
||||||
|
adapter: cloudflare()
|
||||||
});
|
});
|
|
@ -1,8 +1,13 @@
|
||||||
---
|
---
|
||||||
import {siteConfig} from "../config";
|
import {siteConfig} from "../config";
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
const noscript = siteConfig.noClientJavaScript
|
const noscript = siteConfig.noClientJavaScript
|
||||||
---
|
---
|
||||||
{noscript ? <a href="#top"><button id="toTopBtn" style="display: block" title="Go to top">Top</button></a> : <button id="toTopBtn" title="Go to top">Top</button>
|
{noscript ? <a href="#top"><button id="toTopBtn" style="display: block" title={t('widget.back_to_top.title')}>{t('widget.back_to_top')}</button></a> : <button id="toTopBtn" title={t('widget.back_to_top.title')}>{t('widget.back_to_top')}</button>
|
||||||
<script>
|
<script>
|
||||||
// Get the button
|
// Get the button
|
||||||
let toTopButton = document.getElementById("toTopBtn");
|
let toTopButton = document.getElementById("toTopBtn");
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
---
|
---
|
||||||
const { listmonkInstance, listuuid } = Astro.props;
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
|
const { listmonkInstance, listuuid } = Astro.props;
|
||||||
const inputId = Astro.props.listuuid.substring(0,5)
|
const inputId = Astro.props.listuuid.substring(0,5)
|
||||||
---
|
---
|
||||||
<details>
|
<details>
|
||||||
<summary>Subscribe to newsletter</summary>
|
<summary>{t('subscribe.newsletter')}}</summary>
|
||||||
|
<small>{t('subscribe.newsletter.description')}</small>
|
||||||
<form method="post" action={`https://${listmonkInstance}/subscription/form`} class="listmonk-form">
|
<form method="post" action={`https://${listmonkInstance}/subscription/form`} class="listmonk-form">
|
||||||
<div>
|
<div>
|
||||||
<input type="hidden" name="nonce" />
|
<input type="hidden" name="nonce" />
|
||||||
|
|
||||||
<input type="email" name="email" required placeholder="E-mail" />
|
<input type="email" name="email" required placeholder={t('subscribe.newsletter.email_label')} />
|
||||||
<input type="text" name="name" placeholder="Name (optional)" />
|
<input type="text" name="name" placeholder={t('subscribe.newsletter.name_label')} />
|
||||||
<input id={inputId} type="checkbox" name="l" checked="checked" value={listuuid} hidden />
|
<input id={inputId} type="checkbox" name="l" checked="checked" value={listuuid} hidden />
|
||||||
|
|
||||||
<input type="submit" value="Subscribe" />
|
<input type="submit" value="Subscribe" />
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
---
|
---
|
||||||
import {siteConfig} from "../config";
|
import {siteConfig} from "../config";
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
const { title, email = siteConfig.defaultAuthor.email } = Astro.props;
|
const { title, email = siteConfig.defaultAuthor.email } = Astro.props;
|
||||||
---
|
---
|
||||||
<a href={`mailto:${email}?subject=RE:${title}&body=Hi,\n\nI would like to reply to your post "${title}".`}>↩ Reply via Email</a>
|
<a href={`mailto:${email}?subject=RE:${title}&body=${t('article.reply_via_email.body')} "${title}".`}>↩ {t('article.reply_via_email')}</a>
|
|
@ -1,5 +1,10 @@
|
||||||
---
|
---
|
||||||
import {siteConfig} from "../config";
|
import {siteConfig} from "../config";
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
const noscript = siteConfig.noClientJavaScript
|
const noscript = siteConfig.noClientJavaScript
|
||||||
const searchEngine = siteConfig.searchEngine || 'google'
|
const searchEngine = siteConfig.searchEngine || 'google'
|
||||||
const domain = Astro.url.host
|
const domain = Astro.url.host
|
||||||
|
@ -10,7 +15,7 @@ const domain = Astro.url.host
|
||||||
"https://www.google.com/search"} method="GET" target="_blank">
|
"https://www.google.com/search"} method="GET" target="_blank">
|
||||||
<div>
|
<div>
|
||||||
<label for="search-input"><span class="command">search</span></label>
|
<label for="search-input"><span class="command">search</span></label>
|
||||||
<input name="q" type="text" id="search-input" class="search-input" autocomplete="off" placeholder="Type to search..." />
|
<input name="q" type="text" id="search-input" class="search-input" autocomplete="off" placeholder={t('search.placeholder')} />
|
||||||
{searchEngine === "duckduckgo" &&
|
{searchEngine === "duckduckgo" &&
|
||||||
<input type="hidden" name="sites" value={domain} />
|
<input type="hidden" name="sites" value={domain} />
|
||||||
}
|
}
|
||||||
|
@ -32,7 +37,7 @@ const domain = Astro.url.host
|
||||||
type="text"
|
type="text"
|
||||||
id="search-input"
|
id="search-input"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
placeholder="Type to search..."
|
placeholder={t('search.placeholder')}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
---
|
---
|
||||||
import type { MarkdownHeading } from 'astro';
|
import type { MarkdownHeading } from 'astro';
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
headings: MarkdownHeading[];
|
headings: MarkdownHeading[];
|
||||||
|
@ -34,7 +40,7 @@ function buildHierarchy(headings: MarkdownHeading[]) {
|
||||||
const toc = buildHierarchy(filteredHeadings);
|
const toc = buildHierarchy(filteredHeadings);
|
||||||
---
|
---
|
||||||
<details>
|
<details>
|
||||||
<summary>Table of Contents</summary>
|
<summary>{t('article.toc')}</summary>
|
||||||
<ul class="toc-list">
|
<ul class="toc-list">
|
||||||
{
|
{
|
||||||
toc.map((heading) => (
|
toc.map((heading) => (
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
---
|
---
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
---
|
---
|
||||||
<div class="theme-dropdown" id="theme-dropdown">
|
<div class="theme-dropdown" id="theme-dropdown">
|
||||||
<button class="theme-switcher" id="theme-switcher">Theme</button>
|
<button class="theme-switcher" id="theme-switcher">{t('widget.theme_mode')}</button>
|
||||||
<div class="menu-body" id="menu-body">
|
<div class="menu-body" id="menu-body">
|
||||||
<div class="dropdown-item" data-theme="auto">System</div>
|
<div class="dropdown-item" data-theme="auto">{t('widget.theme_mode.auto')}</div>
|
||||||
<div class="dropdown-item" data-theme="dark">Dark</div>
|
<div class="dropdown-item" data-theme="dark">{t('widget.theme_mode.dark')}</div>
|
||||||
<div class="dropdown-item" data-theme="light">Light</div>
|
<div class="dropdown-item" data-theme="light">{t('widget.theme_mode.light')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
---
|
---
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
---
|
---
|
||||||
<div class="theme-switcher">
|
<div class="theme-switcher">
|
||||||
<input type="checkbox" id="theme-toggle" class="theme-toggle" />
|
<input type="checkbox" id="theme-toggle" class="theme-toggle" />
|
||||||
<label for="theme-toggle" class="theme-label">
|
<label for="theme-toggle" class="theme-label">
|
||||||
<span>Theme</span>
|
<span>{t('widget.theme_mode')}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
---
|
---
|
||||||
import { Image } from 'astro:assets';
|
import { Image } from 'astro:assets';
|
||||||
import { getMetadata, getWaybackMetadata } from '../../plugins/get-metadata';
|
import { getMetadata, getWaybackMetadata } from '../../plugins/get-metadata';
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -67,8 +72,8 @@ const archiveUrl = showArchive ? await getWaybackMetadata(url, timestamp) : null
|
||||||
|
|
||||||
{showArchive && archiveUrl && (
|
{showArchive && archiveUrl && (
|
||||||
<div class="link-card__archive">
|
<div class="link-card__archive">
|
||||||
<a href={archiveUrl} target="_blank" rel="noopener noreferrer" title="View archived version">
|
<a href={archiveUrl} target="_blank" rel="noopener noreferrer" title={t('component.link_card.view_archived_version')}>
|
||||||
View archived version
|
{t('component.link_card.view_archived_version')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
---
|
---
|
||||||
import { encrypt } from '../../plugins/encrypt';
|
import { encrypt } from '../../plugins/encrypt';
|
||||||
import {siteConfig} from "../../config";
|
import {siteConfig} from "../../config";
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
password?: string;
|
password?: string;
|
||||||
|
@ -20,10 +25,10 @@ const { encryptedData, iv } = encrypt(content, password);
|
||||||
|
|
||||||
<div class="encrypted-content" data-encrypted={encryptedData} data-iv={iv}>
|
<div class="encrypted-content" data-encrypted={encryptedData} data-iv={iv}>
|
||||||
<div class="password-form">
|
<div class="password-form">
|
||||||
<p>This content is protected. Enter the password to view it:</p>
|
<p>{t('component.protected_content.tip')}</p>
|
||||||
<div>
|
<div>
|
||||||
<input type="password" class="decrypt-password" title="password" />
|
<input type="password" class="decrypt-password" title="password" />
|
||||||
<button class="decrypt-button">Decrypt</button>
|
<button class="decrypt-button">{t('component.protected_content.button_label')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-container hidden"></div>
|
<div class="content-container hidden"></div>
|
||||||
|
|
|
@ -1,19 +1,65 @@
|
||||||
|
import { i18n } from "astro:config/client"
|
||||||
|
|
||||||
export const languages = {
|
export const languages = {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
zh_hans: '中文(简体)',
|
zh_hans: '中文(简体)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultLang = 'en';
|
export const defaultLang: string = i18n?.defaultLocale || 'en';
|
||||||
export const showDefaultLang = false;
|
export const showDefaultLang = false;
|
||||||
|
|
||||||
export const ui = {
|
export const ui = {
|
||||||
en: {
|
en: {
|
||||||
'nav.home': 'Home',
|
'nav.home': 'Home',
|
||||||
'nav.about': 'About',
|
'nav.blog': 'Blog',
|
||||||
'nav.twitter': 'Twitter',
|
'search.placeholder': 'Search posts...',
|
||||||
|
'posts.description': 'Posts from the terminal.',
|
||||||
|
'subscribe.rss': 'Subscribe to RSS feed',
|
||||||
|
'subscribe.newsletter': 'Subscribe to Newsletter',
|
||||||
|
'subscribe.newsletter.description': 'Get the latest posts delivered right to your inbox.',
|
||||||
|
'subscribe.newsletter.email_label': 'E-mail',
|
||||||
|
'subscribe.newsletter.name_label': 'Name (optional)',
|
||||||
|
'widget.back_to_top': 'Top',
|
||||||
|
'widget.back_to_top.title': 'Back to top',
|
||||||
|
'widget.theme_mode': 'Theme',
|
||||||
|
'widget.theme_mode.light': 'Light',
|
||||||
|
'widget.theme_mode.dark': 'Dark',
|
||||||
|
'widget.theme_mode.auto': 'System',
|
||||||
|
'article.last_update': 'Updated on',
|
||||||
|
'article.word_count': 'words',
|
||||||
|
'article.toc': 'Table of Contents',
|
||||||
|
'article.reply_via_email': 'Reply via Email',
|
||||||
|
'article.reply_via_email.body': 'Hi,I would like to reply to your post',
|
||||||
|
'article.back_to_posts': 'Back to posts',
|
||||||
|
'article.comments': 'Comments',
|
||||||
|
'component.protected_content.tip': 'This content is protected. Enter the password to view it:',
|
||||||
|
'component.protected_content.button_label': 'Decrypt',
|
||||||
|
'component.link_card.view_archived_version': 'View archived version',
|
||||||
},
|
},
|
||||||
zh_hans: {
|
zh_hans: {
|
||||||
'nav.home': '首页',
|
'nav.home': '首页',
|
||||||
'nav.about': '关于',
|
'nav.blog': '博客',
|
||||||
|
'search.placeholder': '搜索文章',
|
||||||
|
'subscribe.rss': '订阅 RSS',
|
||||||
|
'subscribe.newsletter': '订阅 Newsletter',
|
||||||
|
'subscribe.newsletter.description': '新文章第一时间送达您的邮箱。',
|
||||||
|
'subscribe.newsletter.email_label': '邮箱',
|
||||||
|
'subscribe.newsletter.name_label': '名称(可选)',
|
||||||
|
'widget.back_to_top': '回顶',
|
||||||
|
'widget.back_to_top.title': '回到顶部',
|
||||||
|
'widget.theme_mode': '主题',
|
||||||
|
'widget.theme_mode.light': '浅色主题',
|
||||||
|
'widget.theme_mode.dark': '深色主题',
|
||||||
|
'widget.theme_mode.auto': '跟随系统',
|
||||||
|
'article.last_update': '编辑于',
|
||||||
|
'article.word_count': '字',
|
||||||
|
'article.toc': '文章目录',
|
||||||
|
'article.reply_via_email': '邮件回复',
|
||||||
|
'article.reply_via_email.body': '您好,我想回复您的文章:',
|
||||||
|
'article.back_to_posts': '返回文章列表',
|
||||||
|
'article.comments': '评论区',
|
||||||
|
'component.protected_content.tip': '此内容已被加密,请输入密码以查看:',
|
||||||
|
'component.protected_content.button_label': '确定',
|
||||||
|
'component.link_card.view_archived_version': '查看互联网档案馆的存档版本',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
|
@ -4,16 +4,20 @@ import { getCollection } from 'astro:content';
|
||||||
import NewsLetter from "../components/NewsLetter.astro";
|
import NewsLetter from "../components/NewsLetter.astro";
|
||||||
import {siteConfig} from "../config";
|
import {siteConfig} from "../config";
|
||||||
import ArticleList from "../components/ArticleList.astro";
|
import ArticleList from "../components/ArticleList.astro";
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
const posts = await getCollection('posts');
|
const posts = await getCollection('posts');
|
||||||
posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime());
|
posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime());
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Blog Posts" description="List all files and folders in the directory.">
|
<Layout title="Blog Posts" description="List all posts on the website.">
|
||||||
<h1 class="title">~/blog</h1>
|
<h1 class="title">~/blog</h1>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="typewriter">Posts from the terminal.</p>
|
<p class="typewriter">{t('posts.description')}</p>
|
||||||
|
|
||||||
<div style="margin-top: 2rem;">
|
<div style="margin-top: 2rem;">
|
||||||
<span class="command">ls -la posts/</span>
|
<span class="command">ls -la posts/</span>
|
||||||
|
@ -22,9 +26,9 @@ posts.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDat
|
||||||
|
|
||||||
<div style="margin-top: 2rem;">
|
<div style="margin-top: 2rem;">
|
||||||
<p>
|
<p>
|
||||||
<span class="command">cat rss.txt</span>
|
<span class="command">cat subscribe.txt</span>
|
||||||
<br />
|
<br />
|
||||||
<a href="/rss.xml" style="margin-left: 1rem;">Subscribe to RSS feed</a>
|
<a href="/rss.xml" style="margin-left: 1rem;">{t('subscribe.rss')}</a>
|
||||||
</p>
|
</p>
|
||||||
{siteConfig.newsletter.enabled && <NewsLetter listmonkInstance={siteConfig.newsletter.listmonk.instanceDomain} listuuid={siteConfig.newsletter.listmonk.listuuid} />}
|
{siteConfig.newsletter.enabled && <NewsLetter listmonkInstance={siteConfig.newsletter.listmonk.instanceDomain} listuuid={siteConfig.newsletter.listmonk.listuuid} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,11 @@ import { ExtractFirstImage } from '../../plugins/extract-images';
|
||||||
import AuthorInfo from "../../components/helper/authors/Info.astro";
|
import AuthorInfo from "../../components/helper/authors/Info.astro";
|
||||||
import TableOfContents from "../../components/TableOfContents.astro";
|
import TableOfContents from "../../components/TableOfContents.astro";
|
||||||
import "katex/dist/katex.css"
|
import "katex/dist/katex.css"
|
||||||
|
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../../i18n/utils';
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const translatePath = useTranslatedPath(lang);
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const blogEntries = await getCollection('posts');
|
const blogEntries = await getCollection('posts');
|
||||||
|
@ -29,6 +34,9 @@ const author = Array.isArray(entry.data.author) ? entry.data.author : (entry.dat
|
||||||
const wordcount = remarkPluginFrontmatter.wordcount;
|
const wordcount = remarkPluginFrontmatter.wordcount;
|
||||||
const lastUpdated = remarkPluginFrontmatter.lastModified;
|
const lastUpdated = remarkPluginFrontmatter.lastModified;
|
||||||
|
|
||||||
|
const pubDate = new Date(entry.data.pubDate).toISOString().split('T')[0]
|
||||||
|
const lastUpdatedDate = new Date(lastUpdated).toISOString().split('T')[0]
|
||||||
|
|
||||||
// Get author data
|
// Get author data
|
||||||
const authorData = await Promise.all((author).map((singleAuthor) => getEntry(singleAuthor).then(authorEntry => authorEntry?.data)))
|
const authorData = await Promise.all((author).map((singleAuthor) => getEntry(singleAuthor).then(authorEntry => authorEntry?.data)))
|
||||||
const authorInfo = authorData.includes(undefined) ? [{data: siteConfig.defaultAuthor}] : authorData;
|
const authorInfo = authorData.includes(undefined) ? [{data: siteConfig.defaultAuthor}] : authorData;
|
||||||
|
@ -56,10 +64,10 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
|
||||||
<article>
|
<article>
|
||||||
<h1 class="title">{entry.data.title}</h1>
|
<h1 class="title">{entry.data.title}</h1>
|
||||||
{authorInfo.map((a: any) => <AuthorInfo data={a} />)}
|
{authorInfo.map((a: any) => <AuthorInfo data={a} />)}
|
||||||
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
<span class="date">{pubDate}</span>
|
||||||
<span class="date">(Updated on {new Date(lastUpdated).toISOString().split('T')[0]})</span>
|
{pubDate === lastUpdatedDate && <span class="date">({t('article.last_update')} {lastUpdatedDate})</span>}
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<span class="wordcount">{wordcount.words} words</span>
|
<span class="wordcount">{wordcount.words} {t('article.word_count')}</span>
|
||||||
{ (cover && cover !== firstImageURL && cover !== `/blog/${slug}/featured.png`) && <Image class="cover" width=720 height=480 src={cover} alt={`cover of ${entry.data.title}`} /> }
|
{ (cover && cover !== firstImageURL && cover !== `/blog/${slug}/featured.png`) && <Image class="cover" width=720 height=480 src={cover} alt={`cover of ${entry.data.title}`} /> }
|
||||||
{headings.length !== 0 && <TableOfContents headings={headings} />}
|
{headings.length !== 0 && <TableOfContents headings={headings} />}
|
||||||
{entry.data.summary && <p class="summary">{entry.data.summary}</p> }
|
{entry.data.summary && <p class="summary">{entry.data.summary}</p> }
|
||||||
|
@ -70,8 +78,8 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
|
||||||
<div class="extra-post" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
<div class="extra-post" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||||
<ReplyViaEmail title={entry.data.title} email={authorInfo[0].email} />
|
<ReplyViaEmail title={entry.data.title} email={authorInfo[0].email} />
|
||||||
<br>
|
<br>
|
||||||
<a href="/blog">← Back to posts</a>
|
<a href="/blog">← {t('article.back_to_posts')}</a>
|
||||||
{!noscript && <h2>Comments</h2> <Comments />}
|
{!noscript && <h2>{t('article.comments')}</h2> <Comments />}
|
||||||
{!noscript &&
|
{!noscript &&
|
||||||
<script>
|
<script>
|
||||||
import "katex/dist/contrib/copy-tex.js"
|
import "katex/dist/contrib/copy-tex.js"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue