mirror of
https://github.com/10h30/astroplate.git
synced 2026-07-11 18:56:06 +09:00
+26
-26
@@ -15,47 +15,47 @@
|
||||
"remove-multilang": "node scripts/removeMultilang.js && yarn format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^2.3.0",
|
||||
"@astrojs/react": "^3.3.0",
|
||||
"@astrojs/rss": "^4.0.5",
|
||||
"@astrojs/sitemap": "^3.1.2",
|
||||
"@astrojs/mdx": "^3.1.3",
|
||||
"@astrojs/react": "^3.6.1",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"astro": "^4.6.1",
|
||||
"astro": "^4.12.3",
|
||||
"astro-auto-import": "^0.4.2",
|
||||
"astro-font": "^0.0.80",
|
||||
"astro-font": "^0.0.81",
|
||||
"date-fns": "^3.6.0",
|
||||
"disqus-react": "^1.1.5",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^12.0.1",
|
||||
"prettier-plugin-astro": "^0.13.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.13",
|
||||
"marked": "^13.0.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-lite-youtube-embed": "^2.4.0",
|
||||
"remark-collapse": "^0.1.2",
|
||||
"remark-toc": "^9.0.0",
|
||||
"swiper": "^11.1.1"
|
||||
"swiper": "^11.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/react": "18.2.78",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"@types/node": "22.0.0",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-astro": "^0.13.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.13",
|
||||
"sass": "^1.75.0",
|
||||
"sharp": "0.33.1",
|
||||
"eslint": "^9.8.0",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"sass": "^1.77.8",
|
||||
"sharp": "0.33.4",
|
||||
"tailwind-bootstrap-grid": "^5.1.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "5.4.5"
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
## 📌 Key Features
|
||||
|
||||
- 👥 Multi-Authors
|
||||
- 🌐 Multilingual
|
||||
- 🎯 Similar Posts Suggestion
|
||||
- 🔍 Search Functionality
|
||||
- 🌑 Dark Mode
|
||||
@@ -65,10 +66,10 @@
|
||||
|
||||
### 📦 Dependencies
|
||||
|
||||
- astro 4.0+
|
||||
- astro v4.12+
|
||||
- node v20.10+
|
||||
- npm v10.2+
|
||||
- tailwind v3.3+
|
||||
- tailwind v3.4+
|
||||
|
||||
### 👉 Install Dependencies
|
||||
|
||||
@@ -109,12 +110,14 @@ docker run -it --rm astroplate ash
|
||||
```
|
||||
|
||||
<!-- reporting issue -->
|
||||
|
||||
## 🐞 Reporting Issues
|
||||
|
||||
We use GitHub Issues as the official bug tracker for this Template. Please Search [existing issues](https://github.com/zeon-studio/astroplate/issues). It’s possible someone has already reported the same problem.
|
||||
If your problem or idea has not been addressed yet, feel free to [open a new issue](https://github.com/zeon-studio/astroplate/issues).
|
||||
|
||||
<!-- licence -->
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright (c) 2023 - Present, Designed & Developed by [Zeon Studio](https://zeon.studio/)
|
||||
|
||||
+22
-16
@@ -9,11 +9,11 @@ const CONTENT_DEPTH = 3;
|
||||
const BLOG_FOLDER = "blog";
|
||||
|
||||
// get data from markdown
|
||||
const getData = (folder, groupDepth) => {
|
||||
// get paths
|
||||
const getData = (folder, groupDepth, langIndex = 0) => {
|
||||
const getPaths = languages
|
||||
.map((lang) => {
|
||||
const dir = path.join(CONTENT_ROOT, lang.contentDir, folder);
|
||||
.map((lang, index) => {
|
||||
const langFolder = lang.contentDir ? lang.contentDir : lang.languageCode;
|
||||
const dir = path.join(CONTENT_ROOT, folder, langFolder);
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter(
|
||||
@@ -21,29 +21,37 @@ const getData = (folder, groupDepth) => {
|
||||
!filename.startsWith("-") &&
|
||||
(filename.endsWith(".md") || filename.endsWith(".mdx")),
|
||||
)
|
||||
.map((filename) => {
|
||||
.flatMap((filename) => {
|
||||
const filepath = path.join(dir, filename);
|
||||
const stats = fs.statSync(filepath);
|
||||
const isFolder = stats.isDirectory();
|
||||
|
||||
if (isFolder) {
|
||||
return getData(filepath, groupDepth);
|
||||
return getData(filepath, groupDepth, index);
|
||||
} else {
|
||||
const file = fs.readFileSync(filepath, "utf-8");
|
||||
const { data, content } = matter(file);
|
||||
const pathParts = filepath.split(path.sep);
|
||||
const slug =
|
||||
data.slug ||
|
||||
pathParts
|
||||
|
||||
let slug;
|
||||
if (data.slug) {
|
||||
const slugParts = data.slug.split("/");
|
||||
slugParts[0] = BLOG_FOLDER;
|
||||
slug = slugParts.join("/");
|
||||
} else {
|
||||
slug = pathParts
|
||||
.slice(CONTENT_DEPTH)
|
||||
.join("/")
|
||||
.replace(/\.[^/.]+$/, "");
|
||||
const group = pathParts[groupDepth];
|
||||
slug = `${BLOG_FOLDER}/${slug.split("/").slice(1).join("/")}`;
|
||||
}
|
||||
data.slug = slug;
|
||||
const group = "blog";
|
||||
|
||||
return {
|
||||
lang: lang.languageCode,
|
||||
lang: languages[index].languageCode, // Set the correct language code dynamically
|
||||
group: group,
|
||||
slug: slug,
|
||||
slug: data.slug,
|
||||
frontmatter: data,
|
||||
content: content,
|
||||
};
|
||||
@@ -52,9 +60,7 @@ const getData = (folder, groupDepth) => {
|
||||
})
|
||||
.flat();
|
||||
|
||||
const publishedPages = getPaths.filter(
|
||||
(page) => !page.frontmatter?.draft && page,
|
||||
);
|
||||
const publishedPages = getPaths.filter((page) => !page.frontmatter?.draft);
|
||||
return publishedPages;
|
||||
};
|
||||
|
||||
@@ -70,7 +76,7 @@ try {
|
||||
JSON.stringify(getData(BLOG_FOLDER, 3)),
|
||||
);
|
||||
|
||||
// merger json files for search
|
||||
// merge json files for search
|
||||
const posts = require(`../${JSON_FOLDER}/posts.json`);
|
||||
const search = [...posts];
|
||||
fs.writeFileSync(`${JSON_FOLDER}/search.json`, JSON.stringify(search));
|
||||
|
||||
+85
-55
@@ -1,66 +1,96 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const rootDirs = ["src/pages", "src/hooks", "src/layouts", "src/styles"];
|
||||
(function () {
|
||||
const rootDirs = ["src/pages", "src/hooks", "src/layouts", "src/styles"];
|
||||
|
||||
const configFiles = [
|
||||
{
|
||||
filePath: "tailwind.config.js",
|
||||
patterns: ["darkmode:\\s*{[^}]*},", 'darkMode:\\s*"class",'],
|
||||
},
|
||||
{ filePath: "src/config/theme.json", patterns: ["colors.darkmode"] },
|
||||
];
|
||||
const deleteAssetList = [
|
||||
"public/images/logo-darkmode.png",
|
||||
"src/layouts/components/ThemeSwitcher.astro",
|
||||
];
|
||||
|
||||
rootDirs.forEach(removeDarkModeFromPages);
|
||||
configFiles.forEach(removeDarkMode);
|
||||
const configFiles = [
|
||||
{
|
||||
filePath: "tailwind.config.js",
|
||||
patterns: ["darkmode:\\s*{[^}]*},", 'darkMode:\\s*"class",'],
|
||||
},
|
||||
{ filePath: "src/config/theme.json", patterns: ["colors.darkmode"] },
|
||||
];
|
||||
|
||||
function removeDarkModeFromFiles(filePath, regexPatterns) {
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
let updatedContent = fileContent;
|
||||
regexPatterns.forEach((pattern) => {
|
||||
const regex = new RegExp(pattern, "g");
|
||||
updatedContent = updatedContent.replace(regex, "");
|
||||
});
|
||||
fs.writeFileSync(filePath, updatedContent, "utf8");
|
||||
}
|
||||
const filePaths = [
|
||||
{
|
||||
filePath: "src/layouts/partials/Header.astro",
|
||||
patterns: [
|
||||
"<ThemeSwitchers*(?:\\s+[^>]+)?\\s*(?:\\/\\>|>([\\s\\S]*?)<\\/ThemeSwitchers*>)",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function removeDarkModeFromPages(directoryPath) {
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
removeDarkModeFromPages(filePath);
|
||||
} else if (stats.isFile()) {
|
||||
removeDarkModeFromFiles(filePath, [
|
||||
'(?:(?!["])\\S)*dark:(?:(?![,;"])\\S)*',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeDarkMode(configFile) {
|
||||
const { filePath, patterns } = configFile;
|
||||
if (filePath === "tailwind.config.js") {
|
||||
filePaths.forEach(({ filePath, patterns }) => {
|
||||
removeDarkModeFromFiles(filePath, patterns);
|
||||
} else {
|
||||
const contentFile = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
patterns.forEach((pattern) => deleteNestedProperty(contentFile, pattern));
|
||||
fs.writeFileSync(filePath, JSON.stringify(contentFile));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function deleteNestedProperty(obj, propertyPath) {
|
||||
const properties = propertyPath.split(".");
|
||||
let currentObj = obj;
|
||||
for (let i = 0; i < properties.length - 1; i++) {
|
||||
const property = properties[i];
|
||||
if (currentObj.hasOwnProperty(property)) {
|
||||
currentObj = currentObj[property];
|
||||
} else {
|
||||
return; // Property not found, no need to continue
|
||||
deleteAssetList.forEach(deleteAsset);
|
||||
function deleteAsset(asset) {
|
||||
try {
|
||||
fs.unlinkSync(asset);
|
||||
console.log(`${path.basename(asset)} deleted successfully!`);
|
||||
} catch (error) {
|
||||
console.error(`${asset} not found`);
|
||||
}
|
||||
}
|
||||
delete currentObj[properties[properties.length - 1]];
|
||||
}
|
||||
|
||||
rootDirs.forEach(removeDarkModeFromPages);
|
||||
configFiles.forEach(removeDarkMode);
|
||||
|
||||
function removeDarkModeFromFiles(filePath, regexPatterns) {
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
let updatedContent = fileContent;
|
||||
regexPatterns.forEach((pattern) => {
|
||||
const regex = new RegExp(pattern, "g");
|
||||
updatedContent = updatedContent.replace(regex, "");
|
||||
});
|
||||
fs.writeFileSync(filePath, updatedContent, "utf8");
|
||||
}
|
||||
|
||||
function removeDarkModeFromPages(directoryPath) {
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
removeDarkModeFromPages(filePath);
|
||||
} else if (stats.isFile()) {
|
||||
removeDarkModeFromFiles(filePath, [
|
||||
'(?:(?!["])\\S)*dark:(?:(?![,;"])\\S)*',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeDarkMode(configFile) {
|
||||
const { filePath, patterns } = configFile;
|
||||
if (filePath === "tailwind.config.js") {
|
||||
removeDarkModeFromFiles(filePath, patterns);
|
||||
} else {
|
||||
const contentFile = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
patterns.forEach((pattern) => deleteNestedProperty(contentFile, pattern));
|
||||
fs.writeFileSync(filePath, JSON.stringify(contentFile));
|
||||
}
|
||||
}
|
||||
|
||||
function deleteNestedProperty(obj, propertyPath) {
|
||||
const properties = propertyPath.split(".");
|
||||
let currentObj = obj;
|
||||
for (let i = 0; i < properties.length - 1; i++) {
|
||||
const property = properties[i];
|
||||
if (currentObj.hasOwnProperty(property)) {
|
||||
currentObj = currentObj[property];
|
||||
} else {
|
||||
return; // Property not found, no need to continue
|
||||
}
|
||||
}
|
||||
delete currentObj[properties[properties.length - 1]];
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"blog_folder": "blog",
|
||||
"default_language": "en",
|
||||
"disable_languages": [],
|
||||
"default_language_in_path": false
|
||||
"default_language_in_subdir": false
|
||||
},
|
||||
|
||||
"params": {
|
||||
|
||||
+88
-1
@@ -1,6 +1,6 @@
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
// Post collection schema
|
||||
// Blog collection schema
|
||||
const blogCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
@@ -49,9 +49,96 @@ const pagesCollection = defineCollection({
|
||||
}),
|
||||
});
|
||||
|
||||
// Contact collection schema
|
||||
const contactCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
meta_title: z.string().optional(),
|
||||
description: z.string(),
|
||||
image: z.string().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// About collection schema
|
||||
const aboutCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
meta_title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
image: z.string(),
|
||||
draft: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Banner schema
|
||||
const bannerSchema = z.object({
|
||||
title: z.string(),
|
||||
content: z.string(),
|
||||
image: z.string(),
|
||||
button: z.object({
|
||||
enable: z.boolean(),
|
||||
label: z.string(),
|
||||
link: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Features schema
|
||||
const featureSchema = z.object({
|
||||
title: z.string(),
|
||||
image: z.string(),
|
||||
content: z.string(),
|
||||
bulletpoints: z.array(z.string()),
|
||||
button: z.object({
|
||||
enable: z.boolean(),
|
||||
label: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Content schema (for the main content structure with banner and features)
|
||||
const contentSchema = z.object({
|
||||
banner: bannerSchema,
|
||||
features: z.array(featureSchema),
|
||||
});
|
||||
|
||||
// Content collection schema
|
||||
const contentCollection = defineCollection({
|
||||
schema: contentSchema,
|
||||
});
|
||||
|
||||
// Testimonial schema
|
||||
const testimonialSchema = z.object({
|
||||
name: z.string(),
|
||||
designation: z.string(),
|
||||
avatar: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
// Testimonials schema
|
||||
const testimonialsSchema = z.array(testimonialSchema);
|
||||
|
||||
// Call to Action schema
|
||||
const callToActionSchema = z.object({
|
||||
enable: z.boolean(),
|
||||
title: z.string(),
|
||||
image: z.string(),
|
||||
description: z.string(),
|
||||
button: z.object({
|
||||
enable: z.boolean(),
|
||||
label: z.string(),
|
||||
link: z.string().url(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Export collections
|
||||
export const collections = {
|
||||
blog: blogCollection,
|
||||
authors: authorsCollection,
|
||||
pages: pagesCollection,
|
||||
contact: contactCollection,
|
||||
about: aboutCollection,
|
||||
content: contentCollection,
|
||||
testimonials: testimonialsSchema,
|
||||
callToAction: callToActionSchema,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import "@/styles/main.scss";
|
||||
import { AstroFont } from "astro-font";
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
import SearchModal from "./helpers/SearchModal";
|
||||
import { getLangFromUrl } from "@/lib/utils/languageParser";
|
||||
|
||||
// font families
|
||||
const pf = theme.fonts.font_family.primary;
|
||||
@@ -40,6 +41,8 @@ export interface Props {
|
||||
// distructure frontmatters
|
||||
const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
Astro.props;
|
||||
|
||||
const language = lang || getLangFromUrl(Astro.url);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -108,7 +111,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<meta
|
||||
name="description"
|
||||
content={plainify(
|
||||
description ? description : config.metadata.meta_description,
|
||||
description ? description : config.metadata.meta_description
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -121,7 +124,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<meta
|
||||
property="og:title"
|
||||
content={plainify(
|
||||
meta_title ? meta_title : title ? title : config.site.title,
|
||||
meta_title ? meta_title : title ? title : config.site.title
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -129,7 +132,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<meta
|
||||
property="og:description"
|
||||
content={plainify(
|
||||
description ? description : config.metadata.meta_description,
|
||||
description ? description : config.metadata.meta_description
|
||||
)}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -142,7 +145,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={plainify(
|
||||
meta_title ? meta_title : title ? title : config.site.title,
|
||||
meta_title ? meta_title : title ? title : config.site.title
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -150,7 +153,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={plainify(
|
||||
description ? description : config.metadata.meta_description,
|
||||
description ? description : config.metadata.meta_description
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -174,7 +177,7 @@ const { title, meta_title, description, image, noindex, canonical, lang } =
|
||||
<body>
|
||||
<TwSizeIndicator />
|
||||
<Header />
|
||||
<SearchModal client:load />
|
||||
<SearchModal lang={language} client:load />
|
||||
<main id="main-content">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@@ -18,6 +18,11 @@ const { title, image, date, author, categories } = data.data;
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const { read_more } = await getTranslations(lang as keyof ContentEntryMap);
|
||||
|
||||
const slugParts = data.slug.split("/");
|
||||
slugParts[0] = "blog";
|
||||
const modifiedSlug = slugParts.join("/");
|
||||
data.slug = modifiedSlug;
|
||||
---
|
||||
|
||||
<div class="bg-body dark:bg-darkmode-body">
|
||||
|
||||
@@ -11,19 +11,20 @@ if (supportedLang.includes(paths[0])) {
|
||||
lang = paths.shift()!;
|
||||
}
|
||||
|
||||
let baseHref = lang ? `/${lang}` : "/";
|
||||
let parts = [
|
||||
{
|
||||
label: "Home",
|
||||
href: `/${lang}`,
|
||||
href: baseHref,
|
||||
"aria-label":
|
||||
Astro.url.pathname === `/${lang}` || Astro.url.pathname === `/${lang}/`
|
||||
Astro.url.pathname === baseHref || Astro.url.pathname === `${baseHref}/`
|
||||
? "page"
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
paths.forEach((label: string, i: number) => {
|
||||
const href = `/${lang}/${paths.slice(0, i + 1).join("/")}`;
|
||||
const href = `${baseHref}${paths.slice(0, i + 1).join("/")}`;
|
||||
label !== "page" &&
|
||||
parts.push({
|
||||
label: humanize(label.replace(".html", "").replace(/[-_]/g, " ")) || "",
|
||||
|
||||
@@ -9,7 +9,7 @@ const LanguageSwitcher = ({
|
||||
lang: string;
|
||||
pathname: string;
|
||||
}) => {
|
||||
const { default_language } = config.settings;
|
||||
const { default_language, default_language_in_subdir } = config.settings;
|
||||
|
||||
// Function to remove trailing slash if necessary
|
||||
const removeTrailingSlash = (path: string) => {
|
||||
@@ -21,22 +21,29 @@ const LanguageSwitcher = ({
|
||||
|
||||
// Sort languages by weight and filter out disabled languages
|
||||
const sortedLanguages = languages
|
||||
// @ts-ignore
|
||||
.filter(language => !config.settings.disable_languages.includes(language.languageCode))
|
||||
.sort((a, b) => a.weight - b.weight);
|
||||
|
||||
return (
|
||||
<div className="mr-5">
|
||||
<div className={`mr-5 ${sortedLanguages.length > 1 ? "block" : "hidden"}`}>
|
||||
<select
|
||||
className="border border-dark text-dark bg-transparent dark:border-darkmode-primary dark:text-white py-1 rounded-sm cursor-pointer focus:ring-0 focus:border-dark dark:focus:border-darkmode-primary"
|
||||
onChange={(e) => {
|
||||
const selectedLang = e.target.value;
|
||||
let newPath;
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
if (selectedLang === default_language) {
|
||||
newPath = `${baseUrl}${removeTrailingSlash(pathname.replace(`/${lang}`, ""))}`;
|
||||
if (default_language_in_subdir) {
|
||||
newPath = `${baseUrl}/${default_language}${removeTrailingSlash(pathname.replace(`/${lang}`, ""))}`;
|
||||
} else {
|
||||
newPath = `${baseUrl}${removeTrailingSlash(pathname.replace(`/${lang}`, ""))}`;
|
||||
}
|
||||
} else {
|
||||
newPath = `/${selectedLang}${removeTrailingSlash(pathname.replace(`/${lang}`, ""))}`;
|
||||
}
|
||||
|
||||
window.location.href = newPath;
|
||||
}}
|
||||
value={lang}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import searchData from ".json/search.json";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import SearchResult, { type ISearchItem } from "./SearchResult";
|
||||
import config from "@/config/config.json";
|
||||
const { default_language } = config.settings;
|
||||
|
||||
const SearchModal = () => {
|
||||
const SearchModal = ({ lang }: { lang: string | undefined }) => {
|
||||
lang = lang || default_language;
|
||||
const [searchString, setSearchString] = useState("");
|
||||
|
||||
// handle input change
|
||||
@@ -39,9 +42,13 @@ const SearchModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// filter language specific search data
|
||||
const filterSearchData = searchData.filter((item) => item.lang === lang);
|
||||
|
||||
|
||||
// get search result
|
||||
const startTime = performance.now();
|
||||
const searchResult = doSearch(searchData);
|
||||
const searchResult = doSearch(filterSearchData);
|
||||
const endTime = performance.now();
|
||||
const totalTime = ((endTime - startTime) / 1000).toFixed(3);
|
||||
|
||||
@@ -169,10 +176,15 @@ const SearchModal = () => {
|
||||
name="search"
|
||||
value={searchString}
|
||||
onChange={handleSearch}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<SearchResult searchResult={searchResult} searchString={searchString} />
|
||||
<SearchResult
|
||||
searchResult={searchResult}
|
||||
searchString={searchString}
|
||||
lang={lang}
|
||||
/>
|
||||
<div className="search-wrapper-footer">
|
||||
<span className="flex items-center">
|
||||
<kbd>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { slugSelector } from "@/lib/utils/languageParser";
|
||||
import { plainify, titleify } from "@/lib/utils/textConverter";
|
||||
import React from "react";
|
||||
|
||||
export interface ISearchItem {
|
||||
lang: string;
|
||||
group: string;
|
||||
slug: string;
|
||||
frontmatter: {
|
||||
@@ -33,10 +35,13 @@ export interface ISearchGroup {
|
||||
const SearchResult = ({
|
||||
searchResult,
|
||||
searchString,
|
||||
lang
|
||||
}: {
|
||||
searchResult: ISearchItem[];
|
||||
searchString: string;
|
||||
lang: string;
|
||||
}) => {
|
||||
|
||||
// generate search result group
|
||||
const generateSearchGroup = (searchResult: ISearchItem[]) => {
|
||||
const joinDataByGroup: ISearchGroup[] = searchResult.reduce(
|
||||
@@ -83,6 +88,7 @@ const SearchResult = ({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// match underline
|
||||
const matchUnderline = (text: string, substring: string) => {
|
||||
const parts = text?.split(new RegExp(`(${substring})`, "gi"));
|
||||
@@ -148,12 +154,14 @@ const SearchResult = ({
|
||||
<img
|
||||
src={item.frontmatter.image}
|
||||
alt={item.frontmatter.title}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="search-result-item-body">
|
||||
<a
|
||||
href={`/${item.slug}`}
|
||||
href={`${slugSelector(item.slug, lang)}`}
|
||||
className="search-result-item-title search-result-item-link"
|
||||
>
|
||||
{matchUnderline(item.frontmatter.title, searchString)}
|
||||
@@ -188,8 +196,8 @@ const SearchResult = ({
|
||||
{matchUnderline(category, searchString)}
|
||||
{item.frontmatter.categories &&
|
||||
index !==
|
||||
item.frontmatter.categories.length -
|
||||
1 && <>, </>}
|
||||
item.frontmatter.categories.length -
|
||||
1 && <>, </>}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
@@ -211,7 +219,7 @@ const SearchResult = ({
|
||||
{matchUnderline(tag, searchString)}
|
||||
{item.frontmatter.tags &&
|
||||
index !==
|
||||
item.frontmatter.tags.length - 1 && <>, </>}
|
||||
item.frontmatter.tags.length - 1 && <>, </>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,11 @@ import ThemeSwitcher from "@/components/ThemeSwitcher.astro";
|
||||
import config from "@/config/config.json";
|
||||
import languages from "@/config/language.json";
|
||||
import LanguageSwitcher from "@/helpers/LanguageSwitcher";
|
||||
import { getLangFromUrl, getTranslations, slugSelector } from "@/lib/utils/languageParser";
|
||||
import {
|
||||
getLangFromUrl,
|
||||
getTranslations,
|
||||
slugSelector,
|
||||
} from "@/lib/utils/languageParser";
|
||||
import type { ContentEntryMap } from "astro:content";
|
||||
import { IoSearch } from "react-icons/io5";
|
||||
|
||||
@@ -133,12 +137,10 @@ if (disabledLanguages.includes(lang)) {
|
||||
}
|
||||
{/* Theme switcher */}
|
||||
<ThemeSwitcher className="mr-5" />
|
||||
|
||||
{/* Language switcher */}
|
||||
{
|
||||
languages.length > 1 && (
|
||||
<LanguageSwitcher client:load lang={lang} pathname={pathname} />
|
||||
)
|
||||
}
|
||||
<LanguageSwitcher client:load lang={lang} pathname={pathname} />
|
||||
|
||||
{/* Navigation button */}
|
||||
{
|
||||
navigation_button.enable && (
|
||||
|
||||
@@ -67,7 +67,7 @@ const { testimonial } = Astro.props;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div class="testimonial-slider-pagination mt-9 flex items-center justify-center text-center" />
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import config from "@/config/config.json";
|
||||
import languages from "@/config/language.json";
|
||||
|
||||
export const getSinglePage = async <C extends CollectionKey>(
|
||||
export const getSP = async <C extends CollectionKey>(
|
||||
collectionName: C,
|
||||
lang: keyof ContentEntryMap | undefined
|
||||
): Promise<CollectionEntry<C>[]> => {
|
||||
@@ -40,7 +40,7 @@ export const getSinglePage = async <C extends CollectionKey>(
|
||||
return removeDrafts;
|
||||
};
|
||||
|
||||
export const getListPage = async <C extends CollectionKey>(
|
||||
export const getLP = async <C extends CollectionKey>(
|
||||
collectionName: C,
|
||||
lang: keyof ContentEntryMap | undefined
|
||||
): Promise<CollectionEntry<C>[]> => {
|
||||
@@ -67,4 +67,68 @@ export const getListPage = async <C extends CollectionKey>(
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
export const getSinglePage = async <C extends CollectionKey>(
|
||||
collectionName: C,
|
||||
lang: keyof ContentEntryMap | undefined,
|
||||
subCollectionName?: string
|
||||
): Promise<CollectionEntry<C>[]> => {
|
||||
const { default_language } = config.settings;
|
||||
|
||||
const selectedLanguageCode = lang || default_language;
|
||||
|
||||
const language = languages.find(
|
||||
(l: any) => l.languageCode === selectedLanguageCode
|
||||
);
|
||||
|
||||
if (!language) {
|
||||
throw new Error("Language not found");
|
||||
}
|
||||
|
||||
const { contentDir } = language;
|
||||
|
||||
const path = subCollectionName
|
||||
? `${contentDir}/${subCollectionName}`
|
||||
: contentDir;
|
||||
|
||||
const pages: CollectionEntry<C>[] = (await getCollection(
|
||||
collectionName as any,
|
||||
({ id }: any) => {
|
||||
return id.startsWith(path) && !id.endsWith("-index.md");
|
||||
}
|
||||
)) as CollectionEntry<C>[];
|
||||
|
||||
// @ts-ignore
|
||||
const removeDrafts = pages.filter((data) => !data.data.draft);
|
||||
|
||||
return removeDrafts;
|
||||
};
|
||||
|
||||
export const getListPage = async <C extends CollectionKey>(
|
||||
collectionName: C,
|
||||
lang: keyof ContentEntryMap | undefined
|
||||
): Promise<CollectionEntry<C>[]> => {
|
||||
const { default_language } = config.settings;
|
||||
|
||||
const selectedLanguageCode = lang || default_language;
|
||||
|
||||
const language = languages.find(
|
||||
(l: any) => l.languageCode == selectedLanguageCode
|
||||
);
|
||||
|
||||
if (!language) {
|
||||
throw new Error("Language not found");
|
||||
}
|
||||
|
||||
const { contentDir } = language;
|
||||
|
||||
const pages: CollectionEntry<C>[] = (await getCollection(
|
||||
collectionName as any,
|
||||
({ id }: any) => {
|
||||
return id.startsWith(contentDir);
|
||||
}
|
||||
)) as CollectionEntry<C>[];
|
||||
|
||||
return pages;
|
||||
};
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ export const getTaxonomy = async (collection: string, name: string) => {
|
||||
actualCollection = language.contentDir;
|
||||
} else {
|
||||
const defaultLanguageMatch = languages.find(
|
||||
(l) => l.languageCode === default_language,
|
||||
(l) => l.languageCode === default_language
|
||||
);
|
||||
if (defaultLanguageMatch) {
|
||||
actualCollection = defaultLanguageMatch.contentDir;
|
||||
@@ -24,10 +24,10 @@ export const getTaxonomy = async (collection: string, name: string) => {
|
||||
}
|
||||
|
||||
const singlePages = await getCollection(
|
||||
actualCollection as keyof ContentEntryMap,
|
||||
"blog" as keyof ContentEntryMap,
|
||||
({ id }: any) => {
|
||||
return id.startsWith("blog") && !id.endsWith("-index.md");
|
||||
},
|
||||
return id.startsWith(actualCollection) && !id.endsWith("-index.md");
|
||||
}
|
||||
);
|
||||
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
|
||||
let taxonomies: string[] = [];
|
||||
@@ -52,7 +52,7 @@ export const getAllTaxonomy = async (collection: string, name: string) => {
|
||||
actualCollection = language.contentDir;
|
||||
} else {
|
||||
const defaultLanguageMatch = languages.find(
|
||||
(l) => l.languageCode === default_language,
|
||||
(l) => l.languageCode === default_language
|
||||
);
|
||||
if (defaultLanguageMatch) {
|
||||
actualCollection = defaultLanguageMatch.contentDir;
|
||||
@@ -60,10 +60,10 @@ export const getAllTaxonomy = async (collection: string, name: string) => {
|
||||
}
|
||||
|
||||
const singlePages = await getCollection(
|
||||
actualCollection as keyof ContentEntryMap,
|
||||
"blog" as keyof ContentEntryMap,
|
||||
({ id }: any) => {
|
||||
return id.startsWith("blog") && !id.endsWith("-index.md");
|
||||
},
|
||||
return id.startsWith(actualCollection) && !id.endsWith("-index.md");
|
||||
}
|
||||
);
|
||||
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
|
||||
let taxonomies: string[] = [];
|
||||
|
||||
@@ -76,7 +76,7 @@ const filteredSupportedLang = supportedLang.filter(
|
||||
export { filteredSupportedLang as supportedLang };
|
||||
|
||||
export const slugSelector = (url: string, lang: string) => {
|
||||
const { default_language, default_language_in_path } = config.settings;
|
||||
const { default_language, default_language_in_subdir } = config.settings;
|
||||
const { trailing_slash } = config.site;
|
||||
|
||||
let constructedUrl;
|
||||
@@ -90,28 +90,21 @@ export const slugSelector = (url: string, lang: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add language path if necessary
|
||||
if (lang === default_language && default_language_in_subdir) {
|
||||
constructedUrl = `/${lang}${constructedUrl}`;
|
||||
}
|
||||
|
||||
// Adjust for trailing slash
|
||||
if (trailing_slash) {
|
||||
if (!constructedUrl.endsWith("/")) {
|
||||
constructedUrl += "/";
|
||||
}
|
||||
} else {
|
||||
if (constructedUrl.endsWith("/")) {
|
||||
if (constructedUrl.endsWith("/") && constructedUrl !== "/") {
|
||||
constructedUrl = constructedUrl.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure home URL is absolute
|
||||
if (constructedUrl === "") {
|
||||
constructedUrl = "/";
|
||||
}
|
||||
|
||||
// Add language path if necessary
|
||||
if (lang === default_language) {
|
||||
constructedUrl = default_language_in_path
|
||||
? `/${lang}${constructedUrl}`
|
||||
: constructedUrl;
|
||||
}
|
||||
|
||||
return constructedUrl;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function getStaticPaths() {
|
||||
supportedLang.map(async (lang) => {
|
||||
const pages = await getSinglePage("pages", lang as keyof ContentEntryMap);
|
||||
|
||||
return pages.map((page: any) => ({
|
||||
return pages.map((page) => ({
|
||||
params: {
|
||||
lang: lang || undefined,
|
||||
regular: page.slug.split("/").pop(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import Base from "@/layouts/Base.astro";
|
||||
import { getListPage } from "@/lib/contentParser.astro";
|
||||
import { supportedLang } from "@/lib/utils/languageParser";
|
||||
import { markdownify } from "@/lib/utils/textConverter";
|
||||
import type { ContentCollectionKey, ContentEntryMap } from "astro:content";
|
||||
import type { ContentEntryMap } from "astro:content";
|
||||
|
||||
export function getStaticPaths() {
|
||||
const paths = supportedLang.map((lang) => ({
|
||||
@@ -14,10 +14,7 @@ export function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const about: any = await getListPage(
|
||||
"about" as ContentCollectionKey,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
const about = await getListPage("about", lang as keyof ContentEntryMap);
|
||||
|
||||
const { Content } = await about[0].render();
|
||||
const { title, description, meta_title, image } = about[0].data;
|
||||
|
||||
@@ -14,12 +14,12 @@ export async function getStaticPaths() {
|
||||
|
||||
const paths = await Promise.all(
|
||||
supportedLang.map(async (lang) => {
|
||||
const authors: any = await getSinglePage(
|
||||
const authors = await getSinglePage(
|
||||
COLLECTION_FOLDER,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
|
||||
return authors.map((author: any) => ({
|
||||
return authors.map((author) => ({
|
||||
params: {
|
||||
lang: lang || undefined,
|
||||
single: author.slug.split("/").pop(),
|
||||
@@ -42,7 +42,7 @@ const { Content } = await author.render();
|
||||
const BLOG_FOLDER = "blog";
|
||||
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
|
||||
const postFilterByAuthor = posts.filter(
|
||||
(post: any) => slugify(post.data.author) === slugify(title)
|
||||
(post) => slugify(post.data.author) === slugify(title)
|
||||
);
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const authorIndex: any = await getListPage(
|
||||
const authorIndex = await getListPage(
|
||||
COLLECTION_FOLDER,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
|
||||
@@ -10,12 +10,12 @@ export async function getStaticPaths() {
|
||||
|
||||
const paths = await Promise.all(
|
||||
supportedLang.map(async (lang) => {
|
||||
const posts: any = await getSinglePage(
|
||||
const posts = await getSinglePage(
|
||||
BLOG_FOLDER,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
|
||||
return posts.map((post: any) => ({
|
||||
return posts.map((post) => ({
|
||||
params: {
|
||||
lang: lang || undefined,
|
||||
single: post.slug.split("/").pop(),
|
||||
|
||||
@@ -19,7 +19,7 @@ export function getStaticPaths() {
|
||||
}
|
||||
const { lang } = Astro.params;
|
||||
const BLOG_FOLDER = "blog";
|
||||
const postIndex: any = await getListPage(
|
||||
const postIndex = await getListPage(
|
||||
BLOG_FOLDER,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import Base from "@/layouts/Base.astro";
|
||||
import { getListPage } from "@/lib/contentParser.astro";
|
||||
import { getTranslations, supportedLang } from "@/lib/utils/languageParser";
|
||||
import PageHeader from "@/partials/PageHeader.astro";
|
||||
import type { CollectionKey } from "astro:content";
|
||||
import { type ContentEntryMap } from "astro:content";
|
||||
|
||||
export function getStaticPaths() {
|
||||
@@ -15,10 +14,7 @@ export function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const contact: any = await getListPage(
|
||||
"contact" as CollectionKey,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
const contact = await getListPage("contact", lang as keyof ContentEntryMap);
|
||||
|
||||
const { contact_form_action }: { contact_form_action: string } = config.params;
|
||||
const { title, description, meta_title, image } = contact[0].data;
|
||||
|
||||
@@ -6,18 +6,9 @@ import { supportedLang } from "@/lib/utils/languageParser";
|
||||
import { markdownify } from "@/lib/utils/textConverter";
|
||||
import CallToAction from "@/partials/CallToAction.astro";
|
||||
import Testimonial from "@/partials/Testimonial.astro";
|
||||
import type { Button, Feature } from "@/types";
|
||||
import type { ContentCollectionKey, ContentEntryMap } from "astro:content";
|
||||
import type { Feature } from "@/types";
|
||||
import type { ContentEntryMap } from "astro:content";
|
||||
import { FaCheck } from "react-icons/fa";
|
||||
interface Homepage {
|
||||
banner: {
|
||||
title: string;
|
||||
content: string;
|
||||
image: string;
|
||||
button: Button;
|
||||
};
|
||||
features: Feature[];
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
const paths = supportedLang.map((lang) => ({
|
||||
@@ -27,20 +18,19 @@ export function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { lang } = Astro.params;
|
||||
const homepage: any = await getListPage(
|
||||
"homepage" as ContentCollectionKey,
|
||||
lang as keyof ContentEntryMap
|
||||
);
|
||||
const { banner, features }: Homepage = homepage[0].data;
|
||||
const homepage = await getListPage("homepage", lang as keyof ContentEntryMap);
|
||||
const { banner, features } = homepage[0].data;
|
||||
|
||||
const testimonial = await getSinglePage(
|
||||
"sections/testimonial" as ContentCollectionKey,
|
||||
lang as keyof ContentEntryMap
|
||||
"sections",
|
||||
lang as keyof ContentEntryMap,
|
||||
"testimonial"
|
||||
);
|
||||
|
||||
const call_to_action = await getSinglePage(
|
||||
"sections/call-to-action" as ContentCollectionKey,
|
||||
lang as keyof ContentEntryMap
|
||||
"sections",
|
||||
lang as keyof ContentEntryMap,
|
||||
"call-to-action"
|
||||
);
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user