Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: i18n #343

Merged
merged 14 commits into from
Feb 23, 2024
2 changes: 2 additions & 0 deletions components/a.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Link from "next/link";
import config from "../lib/config.mjs";

const A = props => {
return (
<Link
{...props}
className="no-underline hover:underline text-black dark:text-white font-normal cursor-pointer"
target={props.self ? "_self" : "_blank"}
locale={config.defaultLocale}
/>
);
};
Expand Down
35 changes: 32 additions & 3 deletions components/blog.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,19 @@ import Disqus from "./disqus";
import Comments from "./comments";

export default withView(props => {
const { children, title, date, author, view, id, tags, pageURL, image } =
props;
const {
children,
title,
date,
author,
view,
id,
tags,
pageURL,
image,
i18n,
locale,
} = props;
const [replies, setReplies] = useState([]);
const [likes, setLikes] = useState([]);

Expand Down Expand Up @@ -75,8 +86,26 @@ export default withView(props => {
</Link>
</div>
<div className="flex-1" />
{i18n?.length > 1 && (
<>
{i18n
.filter(language => language !== locale)
.map(language => {
return (
<Link
className="mr-2 no-underline"
key={language}
href={id}
locale={language}
>
<small>🌐 {language.toUpperCase()}</small>
</Link>
);
})}
</>
)}
<div className={`justify-end ${withImageColor}`} id="views">
{view > 0 && <small>{view} views</small>}
{view > 10 && <small>{view} views</small>}
</div>
</div>
)}
Expand Down
15 changes: 9 additions & 6 deletions components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,15 @@ export default function Header({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: structuredData }}
/>
<link
rel="alternate"
type="application/atom+xml"
title={siteTitle}
href={config.feedPath}
/>
{config.locales.map(locale => (
<link
key={locale}
rel="alternate"
type="application/atom+xml"
title={`${siteTitle} - ${siteDescription} (${locale})`}
href={`/${locale}/${config.feedFile}`}
/>
))}
{enableAdsense && <Adsense />}
</Head>
{image && (
Expand Down
2 changes: 2 additions & 0 deletions components/tag.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link";
import config from "../lib/config.mjs";

export default function Tag(props) {
const highlight = props.highlight
Expand All @@ -8,6 +9,7 @@ export default function Tag(props) {
<Link
href={`/tag/${props.tag.toLowerCase().trim()}`}
className="no-underline"
locale={config.defaultLocale}
>
<span
className={`before:content-['#'] duration-100 transition rounded inline-block p-1 mx-1 text-sm font-mono ${highlight}`}
Expand Down
7 changes: 5 additions & 2 deletions lib/config.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// eslint-disable-next-line import/no-anonymous-default-export
import nextConfig from "../next.config.js";

const config = {
siteTitle: "Lawrence Li",
authorName: "Lawrence",
domain: "lawrenceli.me",
authorEmail: "[email protected]",
baseURL: "https://lawrenceli.me",
feedPath: "/atom.xml",
defaultLocale: nextConfig.i18n.defaultLocale,
locales: nextConfig.i18n.locales,
feedFile: "atom.xml",
feedItemsCount: 10,
siteDescription: "Blog",
activityPubUser: "lawrence",
Expand Down
44 changes: 32 additions & 12 deletions lib/feed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,41 @@ import {
defaultMarkdownDirectory,
} from "./ssg.mjs";
import config from "./config.mjs";
import nextConfig from "../next.config.js";
import fs from "node:fs";

const {
siteTitle,
authorName,
authorEmail,
baseURL,
websubHub,
defaultLocale,
feedFile,
feedItemsCount,
siteTitle,
siteDescription,
websubHub,
} = config;

// https://datatracker.ietf.org/doc/html/rfc4287
const buildFeed = async () => {
const markdownData = getMdPostsData();
const postContents = await Promise.all(
markdownData.map(async item => {
return await getMdContentById(item.id, defaultMarkdownDirectory);
return await getMdContentById(item.fileName, defaultMarkdownDirectory);
}),
);
// only output the recent blog posts
const feed = createRSS(
postContents.slice(0, Math.min(feedItemsCount, postContents.length)),
);
console.log(feed);
const locales = nextConfig.i18n.locales;
locales.forEach(locale => {
console.log("Building feed for", locale);
const feed = createRSS(postContents, locale);
const fileName = `./public/atom.${locale}.xml`;
fs.openSync(fileName, "w");
fs.writeFileSync(fileName, feed);
if (locale === defaultLocale) {
// just an alternate for default locale feed (to keep old link working)
fs.writeFileSync(`./public/${feedFile}`, feed);
}
});
};

function mapToAtomEntry(post) {
Expand Down Expand Up @@ -63,13 +75,21 @@ function decode(string) {
.replace(/'/g, "&apos;");
}

function createRSS(blogPosts = []) {
const postsString = blogPosts.map(mapToAtomEntry).reduce((a, b) => a + b, "");
function createRSS(blogPosts = [], locale = defaultLocale) {
const postsString = blogPosts
.filter(post => post.locale === locale)
.map(mapToAtomEntry)
.slice(0, Math.min(feedItemsCount, blogPosts.length))
.reduce((a, b) => a + b, "");
let feedURL = `${baseURL}/${locale}/${feedFile}`;
if (locale === defaultLocale) {
feedURL = `${baseURL}/${feedFile}`;
}
return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>${siteTitle}</title>
<subtitle>Blog</subtitle>
<link href="${baseURL}/atom.xml" rel="self" type="application/atom+xml" />
<subtitle>${siteDescription}</subtitle>
<link href="${feedURL}" rel="self" type="application/atom+xml" />
<link href="${websubHub}" rel="hub" />
<link href="${baseURL}/"/>
<id>${baseURL}/</id>
Expand Down
37 changes: 34 additions & 3 deletions lib/ssg.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,36 @@
.filter(fileName => fileName.includes(".md"))
.map(mdPostName => {
const id = mdPostName.replace(/\.md$/, "");
const locale = id.includes(".") ? id.split(".")[1] : config.defaultLocale;
const idWithoutLocale = id.replace(`.${locale}`, "");
const fullPath = path.join(mdDirectory, mdPostName);
const mdContent = fs.readFileSync(fullPath, "utf8");
const matterResult = matter(mdContent);
return {
id,
id: idWithoutLocale,
fileName: id,
locale,
...matterResult.data,
content: matterResult.content,
};
});
return mdPostsData.sort(sortByDate).filter(filterVisibility);
};

const getI18nLanguagesByTitle = (i18nTitle, mdPostNames) => {
const languages = new Set();
mdPostNames.forEach(fileName => {
if (fileName.startsWith(i18nTitle)) {
if (fileName.split(".")[1] === "md") {
languages.add(config.defaultLocale);
} else {
languages.add(fileName.split(".")[1]);
}
}
});
return [...languages];
};

export const getMdContentById = (id, mdDirectory, withHTMLString = true) => {
// default using the `./posts` for markdown directory
if (!mdDirectory) {
Expand All @@ -59,22 +77,35 @@
const isBlog = mdDirectory === defaultMarkdownDirectory; // if the markdown is blog article
const mdPostNames = fs.readdirSync(mdDirectory);
const mdPostsData = mdPostNames
.filter(name => name === id + ".md")
.filter(fileName => {
if (fileName.includes(`.${config.defaultLocale}`)) {
return fileName === id + ".md";
}
return fileName === id.replace(`.${config.defaultLocale}`, "") + ".md";
})
.map(async mdPostName => {
const fullPath = path.join(mdDirectory, mdPostName);
const mdContent = fs.readFileSync(fullPath, "utf8");
const fileStat = fs.statSync(fullPath);
const matterResult = matter(mdContent);
const locale = id.includes(".") ? id.split(".")[1] : config.defaultLocale;
Dismissed Show dismissed Hide dismissed
const idWithoutLocale = id.replace(`.${locale}`, "");
const languages = getI18nLanguagesByTitle(
mdPostName.split(".")[0],
mdPostNames,
);
const htmlResult = await renderHTMLfromMarkdownString(
matterResult.content,
isBlog,
);
return {
id,
id: idWithoutLocale,
birthTime: fileStat.birthtime.toISOString(),
// modifiedTime: fileStat.mtime.toISOString(),
author: config.authorName, // dafult authorName
// content: matterResult.content, // original markdown string
i18n: languages,
locale,
htmlStringContent: withHTMLString ? htmlResult.value : "", // rendered html string
htmlAst: isBlog
? fromHtml(htmlResult.value, {
Expand Down
4 changes: 2 additions & 2 deletions lib/websub.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import config from "./config.mjs";
import { getMdPostsData } from "./ssg.mjs";

const { websubHub, baseURL, feedPath } = config;
const URL = baseURL + feedPath;
const { websubHub, baseURL, feedFile } = config;
const URL = baseURL + "/" + feedFile;

/**
* Determine if we need to publish the websub.
Expand Down
6 changes: 6 additions & 0 deletions middleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import cfg from "./lib/config.mjs";

// This function can be marked `async` if using `await` inside
export function middleware(req) {
Expand All @@ -11,6 +12,11 @@ export function middleware(req) {
new URL(`/api/activitypub/blog/${blogId}`, req.url),
);
}
// RSS Feed i18n
if (path.endsWith(cfg.feedFile)) {
const locale = req.nextUrl.locale;
return NextResponse.rewrite(new URL(`/atom.${locale}.xml`, req.url));
}
}

// export const config = {
Expand Down
20 changes: 11 additions & 9 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// next.config.js

module.exports = {
// experimental: {
// appDir: true,
// },
// i18n: {
// locales: ["zh"],
// defaultLocale: "zh",
// },
i18n: {
locales: ["zh", "en", "ja"], // post.en.md, post.ja.md
defaultLocale: "zh", // post.zh.md or post.md
localeDetection: false,
},
transpilePackages: ["react-tweet"],
async headers() {
return [
Expand All @@ -27,11 +29,11 @@ module.exports = {
source: "/.well-known/:param",
destination: "/api/.well-known/:param",
},
{
source: "/feed",
destination: "/atom.xml",
},

// {
// source: "/:locale/atom.xml",
// destination: "/atom.:locale.xml",
// locale: false,
// },
// {
// source: "/blog/:path",
// has: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"markdown": "github",
"scripts": {
"dev": "pnpm feed && next dev -H 0.0.0.0",
"feed": "node ./lib/feed.mjs > ./public/atom.xml",
"feed": "node ./lib/feed.mjs",
"websub": "node ./lib/websub.mjs",
"start": "next start",
"build": "NEXT_PUBLIC_BUILDTIME=$(date '+%s') next build && pnpm feed && next-sitemap",
Expand Down
11 changes: 8 additions & 3 deletions pages/blog/[id].js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export default PathId;

export const getStaticProps = async context => {
const { id } = context.params;
const mdData = await getMdContentById(id, defaultMarkdownDirectory, false);
const mdData = await getMdContentById(
`${id}.${context.locale}`,
defaultMarkdownDirectory,
false,
);
return {
props: mdData,
};
Expand All @@ -50,11 +54,12 @@ export const getStaticPaths = async () => {
const mdPostsData = getMdPostsData(path.join(process.cwd(), "posts"));
const paths = mdPostsData.map(data => {
return {
params: data,
params: { id: data.id },
locale: data.locale,
};
});
// const paths = [
// { params: { id: "hi" } },
// { params: { id: "hi" }, locale: "zh" },
// ];
return {
paths,
Expand Down
1 change: 1 addition & 0 deletions pages/blog/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const blogProps = {
date: "2022-08-26",
someKey: "someValeInJSXProps",
tags: "example, test, jsx",
locale: "en",
visible: true,
};

Expand Down
Loading
Loading