Merge pull request #39 from tfmurad/v2

Multilingual Feature Added
This commit is contained in:
Al Murad Uzzaman
2024-07-31 13:04:09 +06:00
committed by GitHub
82 changed files with 1986 additions and 464 deletions
+15 -1
View File
@@ -7,12 +7,26 @@ import { defineConfig, squooshImageService } from "astro/config";
import remarkCollapse from "remark-collapse";
import remarkToc from "remark-toc";
import config from "./src/config/config.json";
import languagesJSON from "./src/config/language.json";
const { default_language } = config.settings;
const supportedLang = [...languagesJSON.map((lang) => lang.languageCode)];
const disabledLanguages = config.settings.disable_languages;
// Filter out disabled languages from supportedLang
const filteredSupportedLang = supportedLang.filter(
(lang) => !disabledLanguages.includes(lang),
);
// https://astro.build/config
export default defineConfig({
site: config.site.base_url ? config.site.base_url : "http://examplesite.com",
base: config.site.base_path ? config.site.base_path : "/",
trailingSlash: config.site.trailing_slash ? "always" : "never",
trailingSlash: config.site.trailing_slash ? "always" : "ignore",
i18n: {
locales: filteredSupportedLang,
defaultLocale: default_language,
},
image: {
service: squooshImageService(),
},
+29 -28
View File
@@ -1,6 +1,6 @@
{
"name": "astroplate",
"version": "3.4.5",
"version": "4.0.0",
"description": "Astro and Tailwindcss boilerplate",
"author": "zeon.studio",
"license": "MIT",
@@ -11,50 +11,51 @@
"preview": "astro preview",
"format": "prettier -w ./src",
"generate-json": "node scripts/jsonGenerator.js",
"remove-darkmode": "node scripts/removeDarkmode.js && yarn format"
"remove-darkmode": "node scripts/removeDarkmode.js && yarn format",
"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"
}
}
+6 -3
View File
@@ -6,7 +6,7 @@
<p align=center> If you find this project useful, please give it a ⭐ to show your support. </p>
<h2 align="center"> <a target="_blank" href="https://zeon.studio/preview?project=astroplate" rel="nofollow">👀 Demo</a> | <a target="_blank" href="https://pagespeed.web.dev/analysis/https-astroplate-netlify-app/yzx3foum3w?form_factor=desktop">Page Speed (100%)🚀</a>
<h2 align="center"> <a target="_blank" href="https://astroplate.netlify.app/" rel="nofollow">👀 Demo</a> | <a target="_blank" href="https://pagespeed.web.dev/analysis/https-astroplate-netlify-app/yzx3foum3w?form_factor=desktop">Page Speed (100%)🚀</a>
</h2>
<p align=center>
@@ -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). Its 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/)
+54 -37
View File
@@ -1,49 +1,66 @@
const fs = require("fs");
const path = require("path");
const matter = require("gray-matter");
const languages = require("../src/config/language.json");
const CONTENT_DEPTH = 2;
const JSON_FOLDER = "./.json";
const BLOG_FOLDER = "src/content/blog";
const CONTENT_ROOT = "src/content";
const CONTENT_DEPTH = 3;
const BLOG_FOLDER = "blog";
// get data from markdown
const getData = (folder, groupDepth) => {
const getPath = fs.readdirSync(folder);
const removeIndex = getPath.filter((item) => !item.startsWith("-"));
const getData = (folder, groupDepth, langIndex = 0) => {
const getPaths = languages
.map((lang, index) => {
const langFolder = lang.contentDir ? lang.contentDir : lang.languageCode;
const dir = path.join(CONTENT_ROOT, folder, langFolder);
return fs
.readdirSync(dir)
.filter(
(filename) =>
!filename.startsWith("-") &&
(filename.endsWith(".md") || filename.endsWith(".mdx")),
)
.flatMap((filename) => {
const filepath = path.join(dir, filename);
const stats = fs.statSync(filepath);
const isFolder = stats.isDirectory();
const getPaths = removeIndex.flatMap((filename) => {
const filepath = path.join(folder, filename);
const stats = fs.statSync(filepath);
const isFolder = stats.isDirectory();
if (isFolder) {
return getData(filepath, groupDepth, index);
} else {
const file = fs.readFileSync(filepath, "utf-8");
const { data, content } = matter(file);
const pathParts = filepath.split(path.sep);
if (isFolder) {
return getData(filepath, groupDepth);
} else if (filename.endsWith(".md") || filename.endsWith(".mdx")) {
const file = fs.readFileSync(filepath, "utf-8");
const { data, content } = matter(file);
const pathParts = filepath.split(path.sep);
const slug =
data.slug ||
pathParts
.slice(CONTENT_DEPTH)
.join("/")
.replace(/\.[^/.]+$/, "");
const group = pathParts[groupDepth];
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(/\.[^/.]+$/, "");
slug = `${BLOG_FOLDER}/${slug.split("/").slice(1).join("/")}`;
}
data.slug = slug;
const group = "blog";
return {
group: group,
slug: slug,
frontmatter: data,
content: content,
};
} else {
return [];
}
});
return {
lang: languages[index].languageCode, // Set the correct language code dynamically
group: group,
slug: data.slug,
frontmatter: data,
content: content,
};
}
});
})
.flat();
const publishedPages = getPaths.filter(
(page) => !page.frontmatter?.draft && page,
);
const publishedPages = getPaths.filter((page) => !page.frontmatter?.draft);
return publishedPages;
};
@@ -56,10 +73,10 @@ try {
// create json files
fs.writeFileSync(
`${JSON_FOLDER}/posts.json`,
JSON.stringify(getData(BLOG_FOLDER, 2)),
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));
+72
View File
@@ -0,0 +1,72 @@
const fs = require("fs");
const path = require("path");
const languages = require("../src/config/language.json");
// Filter out the English language
const englishLang = languages.filter((item) => item.languageCode === "en");
const filterLangs = languages.filter((item) => item.languageCode !== "en");
const contentDir = "src/content";
const configDir = "src/config";
const i18nDir = "src/i18n";
// Update language.json to only include the English language
fs.writeFileSync(
path.join(configDir, "language.json"),
JSON.stringify(englishLang, null, 2),
);
// Remove content directories for languages other than English
filterLangs.forEach((lang) => {
const langContentDir = path.join(contentDir, lang.contentDir);
fs.rm(langContentDir, { recursive: true, force: true }, (err) => {
if (err) {
console.error(`Error deleting folder ${langContentDir}:`, err);
return;
}
console.log(`Folder ${langContentDir} deleted successfully`);
});
});
// Remove other menu.{lang}.json files except menu.en.json
fs.readdir(configDir, (err, files) => {
if (err) {
console.error("Error reading config directory:", err);
return;
}
files.forEach((file) => {
if (file.startsWith("menu.") && file !== "menu.en.json") {
const filePath = path.join(configDir, file);
fs.unlink(filePath, (err) => {
if (err) {
console.error(`Error deleting file ${filePath}:`, err);
return;
}
console.log(`File ${filePath} deleted successfully`);
});
}
});
});
// Remove other language files from i18n folder except en.json
fs.readdir(i18nDir, (err, files) => {
if (err) {
console.error("Error reading i18n directory:", err);
return;
}
files.forEach((file) => {
if (file !== "en.json") {
const filePath = path.join(i18nDir, file);
fs.unlink(filePath, (err) => {
if (err) {
console.error(`Error deleting file ${filePath}:`, err);
return;
}
console.log(`File ${filePath} deleted successfully`);
});
}
});
});
console.log("Cleanup completed.");
+4 -1
View File
@@ -19,7 +19,10 @@
"default_theme": "system",
"pagination": 2,
"summary_length": 200,
"blog_folder": "blog"
"blog_folder": "blog",
"default_language": "en",
"disable_languages": [],
"default_language_in_subdir": false
},
"params": {
+14
View File
@@ -0,0 +1,14 @@
[
{
"languageName": "En",
"languageCode": "en",
"contentDir": "english",
"weight": 1
},
{
"languageName": "Fr",
"languageCode": "fr",
"contentDir": "french",
"weight": 2
}
]
+24
View File
@@ -0,0 +1,24 @@
{
"main": [
{ "name": "Home", "url": "/" },
{ "name": "About", "url": "/about" },
{ "name": "Elements", "url": "/elements" },
{
"name": "Pages",
"url": "",
"hasChildren": true,
"children": [
{ "name": "Contact", "url": "/contact" },
{ "name": "Blog", "url": "/blog" },
{ "name": "Authors", "url": "/authors" },
{ "name": "Categories", "url": "/categories" },
{ "name": "Tags", "url": "/tags" },
{ "name": "404 Page", "url": "/404" }
]
}
],
"footer": [
{ "name": "Elements", "url": "/elements" },
{ "name": "Privacy Policy", "url": "/privacy-policy" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"main": [
{ "name": "Accueil", "url": "/" },
{ "name": "À propos", "url": "/about" },
{ "name": "Éléments", "url": "/elements" },
{
"name": "Pages",
"url": "",
"hasChildren": true,
"children": [
{ "name": "Contact", "url": "/contact" },
{ "name": "Blog", "url": "/blog" },
{ "name": "Auteurs", "url": "/authors" },
{ "name": "Catégories", "url": "/categories" },
{ "name": "Étiquettes", "url": "/tags" },
{ "name": "Page 404", "url": "/404" }
]
}
],
"footer": [
{ "name": "Éléments", "url": "/elements" },
{ "name": "Politique de confidentialité", "url": "/privacy-policy" }
]
}
-57
View File
@@ -1,57 +0,0 @@
{
"main": [
{
"name": "Home",
"url": "/"
},
{
"name": "About",
"url": "/about"
},
{
"name": "Elements",
"url": "/elements"
},
{
"name": "Pages",
"url": "",
"hasChildren": true,
"children": [
{
"name": "Contact",
"url": "/contact"
},
{
"name": "Blog",
"url": "/blog"
},
{
"name": "Authors",
"url": "/authors"
},
{
"name": "Categories",
"url": "/categories"
},
{
"name": "Tags",
"url": "/tags"
},
{
"name": "404 Page",
"url": "/404"
}
]
}
],
"footer": [
{
"name": "Elements",
"url": "/elements"
},
{
"name": "Privacy Policy",
"url": "/privacy-policy"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
---
title: "Hé, je suis John Doe !"
meta_title: "About"
description: "this is meta description"
image: "/images/avatar.png"
draft: false
---
L'entreprise elle-même est une entreprise très prospère. Ils ne connaissent pas les bienfaits du corps, ou sauf qu'ils le recevront à d'autres moments, le tout, les temps de travail, qu'il déteste à partir de ce moment mais. Il fuit les plaisirs perçus pour être supposés n'être rien, tout ou, et la douleur est ce qui est la plus grande option, car cela lui est facile, et avec ce que cela s'ensuit, ils lui procurent de la douleur et ainsi de suite ! Car ledit expédient de la vérité repousse le plaisir et empêche la douleur, ils fournissent comme si
+5
View File
@@ -0,0 +1,5 @@
---
title: "Auteurs"
meta_title: ""
description: "this is meta description"
---
+20
View File
@@ -0,0 +1,20 @@
---
title: John Doe
email: johndoe@email.com
image: "/images/avatar.png"
description: this is meta description
social:
- name: github
icon: FaGithub
link: https://github.com
- name: twitter
icon: FaTwitter
link: https://twitter.com
- name: linkedin
icon: FaLinkedin
link: https://linkedin.com
---
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua.
+20
View File
@@ -0,0 +1,20 @@
---
title: Sam Wilson
email: samwilson@email.com
image: "/images/avatar.png"
description: this is meta description
social:
- name: github
icon: FaGithub
link: https://github.com
- name: twitter
icon: FaTwitter
link: https://twitter.com
- name: linkedin
icon: FaLinkedin
link: https://linkedin.com
---
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua.
@@ -0,0 +1,20 @@
---
title: Guillaume Jacob
email: williamjacob@email.com
image: "/images/avatar.png"
description: this is meta description
social:
- name: github
icon: FaGithub
link: https://github.com
- name: twitter
icon: FaTwitter
link: https://twitter.com
- name: linkedin
icon: FaLinkedin
link: https://linkedin.com
---
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua.
+5
View File
@@ -0,0 +1,5 @@
---
title: "Articles de blog"
meta_title: ""
description: "Ceci est une méta-description"
---
+23
View File
@@ -0,0 +1,23 @@
---
title: "Comment créer une application avec des technologies modernes"
meta_title: ""
description: "Ceci est une méta-description"
date: 2022-04-04T05:00:00Z
image: "/images/image-placeholder.png"
categories: ["french","Application", "Data"]
author: "John Doe"
tags: ["nextjs", "tailwind", "react"]
draft: false
---
Personne ne veut même sortir un maquillage de l'urne des soins empoisonnés. C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
## Design Créatif
Car en guise de maquillage, l'urne du poison C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
> Le client lui-même doit pouvoir poursuivre l'adipisicing. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
+23
View File
@@ -0,0 +1,23 @@
---
title: "Comment créer une application avec des technologies modernes"
meta_title: ""
description: "Ceci est une méta-description"
date: 2022-04-04T05:00:00Z
image: "/images/image-placeholder.png"
categories: ["Technology", "Data"]
author: "Sam Wilson"
tags: ["technology", "tailwind"]
draft: false
---
Personne ne veut même sortir un maquillage de l'urne des soins empoisonnés. C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
## Design Créatif
Car en guise de maquillage, l'urne du poison C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
> Le client lui-même doit pouvoir poursuivre l'adipisicing. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
+23
View File
@@ -0,0 +1,23 @@
---
title: "Comment créer une application avec des technologies modernes"
meta_title: ""
description: "Ceci est une méta-description"
date: 2022-04-04T05:00:00Z
image: "/images/image-placeholder.png"
categories: ["Software"]
author: "John Doe"
tags: ["software", "tailwind"]
draft: false
---
Personne ne veut même sortir un maquillage de l'urne des soins empoisonnés. C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
## Design Créatif
Car en guise de maquillage, l'urne du poison C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
> Le client lui-même doit pouvoir poursuivre l'adipisicing. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
+23
View File
@@ -0,0 +1,23 @@
---
title: "Comment créer une application avec des technologies modernes"
meta_title: ""
description: "Ceci est une méta-description"
date: 2022-04-04T05:00:00Z
image: "/images/image-placeholder.png"
categories: ["Architecture"]
author: "John Doe"
tags: ["silicon", "technology"]
draft: false
---
Personne ne veut même sortir un maquillage de l'urne des soins empoisonnés. C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
## Design Créatif
Car en guise de maquillage, l'urne du poison C'était un week-end. Je suis un footballeur complet. Pour boire, le lac occupe le plus grand porche. Chacune des cibles de la vie ne flatte pas Euismod.
> Le client lui-même doit pouvoir poursuivre l'adipisicing. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
L'entreprise elle-même est une entreprise très prospère. Personne ne prend même la peine de l'ouvrir. Alors je vais ouvrir la naissance pour choisir ? Être rejeté par certaines personnes est un choix commode du présent pour ressentir une douleur comme la sienne !
+88 -1
View File
@@ -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,
};
+6
View File
@@ -0,0 +1,6 @@
---
title: "Contact"
meta_title: ""
description: "this is meta description"
draft: false
---
+53
View File
@@ -0,0 +1,53 @@
---
# Banner
banner:
title: "Le modèle de démarrage ultime dont vous avez besoin pour démarrer votre projet Astro"
content: "Astroplate est un modèle de démarrage gratuit construit avec Astro et TailwindCSS, fournissant tout ce dont vous avez besoin pour démarrer votre projet Astro et gagner un temps précieux."
image: "/images/banner.png"
button:
enable: true
label: "Commencer gratuitement"
link: "https://github.com/zeon-studio/astroplate"
# Features
features:
- title: "Ce qui est inclus dans Astroplate"
image: "/images/service-1.png"
content: "Astroplate est un modèle de démarrage complet qui inclut tout ce dont vous avez besoin pour démarrer votre projet Astro. Ce qui est inclus dans Astroplate"
bulletpoints:
- "10+ pages pré-construites"
- "Score Google Pagespeed de 95+"
- "Construit avec Astro et TailwindCSS pour un style facile et personnalisable"
- "Entièrement responsive sur tous les appareils"
- "Optimisé pour le référencement pour de meilleurs classements dans les moteurs de recherche"
- "**Open-source et gratuit** pour une utilisation personnelle et commerciale"
button:
enable: false
label: "Commencer maintenant"
link: "#"
- title: "Découvrez les fonctionnalités clés d'Astro"
image: "/images/service-2.png"
content: "Astro est un framework web tout-en-un pour construire des sites web rapides et centrés sur le contenu. Il offre une gamme de fonctionnalités excitantes pour les développeurs et les créateurs de sites web. Certaines des fonctionnalités clés sont :"
bulletpoints:
- "Zéro JS, par défaut : Aucun surcoût d'exécution JavaScript pour vous ralentir."
- "Personnalisable : Tailwind, MDX et plus de 100 autres intégrations au choix."
- "Agnostique à l'UI : Supporte React, Preact, Svelte, Vue, Solid, Lit et plus encore."
button:
enable: true
label: "Commencer maintenant"
link: "https://github.com/zeon-studio/astroplate"
- title: "Les principales raisons de choisir Astro pour votre prochain projet"
image: "/images/service-3.png"
content: "Avec Astro, vous pouvez construire des sites web modernes et centrés sur le contenu sans sacrifier la performance ou la facilité d'utilisation."
bulletpoints:
- "Charge instantanée des sites statiques pour une meilleure expérience utilisateur et SEO."
- "Syntaxe intuitive et support pour les frameworks populaires rendent l'apprentissage et l'utilisation d'Astro un jeu d'enfant."
- "Utilisez n'importe quelle bibliothèque ou framework front-end, ou construisez des composants personnalisés, pour tout type de projet."
- "Construit sur une technologie de pointe pour garder vos projets à jour avec les dernières normes web."
button:
enable: false
label: ""
link: ""
---
+255
View File
@@ -0,0 +1,255 @@
---
title: "Éléments"
meta_title: ""
description: "Ceci est une méta-description"
draft: false
---
# Titre 1
## Titre 2
### Titre 3
#### Titre 4
##### Titre 5
###### Titre 6
---
### Paragraphe
Êtes-vous venu ici pour quelque chose en particulier ou juste pour critiquer Riker? Et en entrant à vitesse maximale, vous êtes apparu pour un instant être à deux endroits à la fois. Nous avons un saboteur à bord. Nous savons que vous vous occupez de minerai volé. Mais je veux parler de la tentative d'assassinat sur le Lieutenant Worf. Quelqu'un pourrait-il survivre dans un tampon de téléporteur pendant 75 ans? Le destin. Il protège les fous, les petits enfants et les navires.
Êtes-vous venu ici pour quelque chose en particulier ou juste pour critiquer Riker? Et en entrant à vitesse maximale, vous êtes apparu pour un instant être à deux endroits à la fois. Nous avons un saboteur à bord. Nous savons que vous vous occupez de minerai volé. Mais je veux parler de la tentative d'assassinat sur le Lieutenant Worf. Quelqu'un pourrait-il survivre dans un tampon de téléporteur pendant 75 ans? Le destin. Il protège les fous, les petits enfants et les navires.
---
### Emphase
1. Êtes-vous venu ici pour quelque chose en **particulier** ou juste en général
2. Êtes-vous venu ici pour quelque chose en <ins>particulier</ins>
3. _Êtes-vous venu ici_
4. Êtes-vous venu ici pour **quelque chose** en particulier
5. Êtes-vous venu ici pour quelque chose en particulier
6. Êtes-vous venu ici pour quelque chose en particulier
7. Les URLs et les URLs entre crochets seront automatiquement transformées en liens. [http://www.example.com](http://www.example.com) ou
8. [http://www.example.com](http://www.example.com) et parfois example.com (mais pas sur Github, par exemple).
---
### Lien
[Je suis un lien en ligne](https://www.google.com)
[Je suis un lien en ligne avec titre](https://www.google.com "Page d'accueil de Google")
[Je suis un lien de référence][texte de référence insensible à la casse]
[Je suis une référence relative à un fichier de dépôt](../blob/master/LICENSE)
[Vous pouvez utiliser des chiffres pour les définitions de liens de référence][1]
Ou laissez-le vide et utilisez [le texte du lien lui-même].
example.com (mais pas sur Github, par exemple).
Du texte pour montrer que les liens de référence peuvent suivre plus tard.
[texte de référence insensible à la casse]: https://www.themefisher.com
[1]: https://gethugothemes.com
[le texte du lien lui-même]: https://www.getjekyllthemes.com
---
### Liste Ordonnée
1. Élément de liste
2. Élément de liste
3. Élément de liste
4. Élément de liste
5. Élément de liste
---
### Liste Non Ordonnée
- Élément de liste
- Élément de liste
- Élément de liste
- Élément de liste
- Élément de liste
---
### Code and Syntax Highlighting
#### HTML
```html
<ul>
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="about/">About</a>
</li>
</ul>
```
---
#### CSS
```css
img {
vertical-align: middle;
border: 0;
max-width: 100%;
height: auto;
}
```
---
#### JavaScript
```javascript
window.addEventListener("load", (e) => {
document.querySelector(".preloader").style.display = "none";
});
```
---
### Button
<Button label="Button" link="#" style="solid" />
---
### Quote
> Êtes-vous venu ici pour quelque chose en particulier ou simplement pour dénigrer Riker en général ? Et en soufflant à la vitesse de distorsion maximale, vous avez semblé pendant un instant être à deux endroits à la fois.
---
### Notice
<Notice type="note">Ceci est une simple remarque.</Notice>
<Notice type="tip">Ceci est une simple remarque.</Notice>
<Notice type="info">Ceci est une simple remarque.</Notice>
<Notice type="warning">Ceci est une simple remarque.</Notice>
---
### Tab
<Tabs client:load>
<Tab name="Tab 1">
#### Did you come here for something in particular?
Êtes-vous venu ici pour quelque chose en particulier ou simplement pour dénigrer Riker en général ? Et en soufflant à la vitesse de distorsion maximale, vous avez semblé pendant un instant être à deux endroits à la fois. Nous avons un saboteur à bord. Nous savons que vous faites du trafic de minerai volé. Mais je veux parler de la tentative d'assassinat du lieutenant Worf.
</Tab>
<Tab name="Tab 2">
#### I wanna talk about the assassination attempt
La douleur elle-même est grande, l'élitr sadipscing est réglé, mais les diam nonumy eirmo pendant un temps ils envient qu'avec le travail et la douleur c'était quelque chose de grand, mais le diam voluptiva. Mais en effet, je les accuserai et seulement deux douleurs et je les reprendrai. Stet clita kasd gubergren, aucun takimata de mer n'est sacré.
La douleur elle-même est grande, l'élitr sadipscing est réglé, mais les diam nonumy eirmo pendant un temps ils envient qu'avec le travail et la douleur c'était quelque chose de grand, mais le diam voluptiva. Mais en effet, je les accuserai et seulement deux douleurs et je les reprendrai. Stet clita kasd gubergren, aucun takimata de mer n'est sacré.
</Tab>
<Tab name="Tab 3">
#### We know youre dealing in stolen ore
La douleur elle-même est grande, l'élitr sadipscing est réglé, mais les diam nonumy eirmo pendant un temps ils envient qu'avec le travail et la douleur c'était quelque chose de grand, mais le diam voluptiva. Mais en effet, je les accuserai et seulement deux douleurs et je les reprendrai. Stet clita kasd gubergren, aucun takimata de mer n'est sacré.
La douleur elle-même est grande, l'élitr sadipscing est réglé, mais les diam nonumy eirmo pendant un temps ils envient qu'avec le travail et la douleur c'était quelque chose de grand, mais le diam voluptiva. Mais en effet, je les accuserai et seulement deux douleurs et je les reprendrai. Stet clita kasd gubergren, aucun takimata de mer n'est sacré.
</Tab>
</Tabs>
---
### Table
| # | First | Last | Handle |
| :-- | :----------: | :----------: | -----------: |
| 1 | Row:1 Cell:1 | Row:1 Cell:2 | Row:1 Cell:3 |
| 2 | Row:2 Cell:1 | Row:2 Cell:2 | Row:2 Cell:3 |
| 3 | Row:3 Cell:1 | Row:3 Cell:2 | Row:3 Cell:3 |
---
### Accordion
<Accordion client:load title="Why should you need to do this?">
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
</Accordion>
<Accordion client:load title="How can I adjust Horizontal centering">
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
</Accordion>
<Accordion client:load title="Should you use Negative margin?">
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
- C'est une chose.
</Accordion>
---
### Image
![image](/images/image-placeholder.png)
---
### Youtube video
<Youtube client:load id="ZEe-IFezQok" title="Youtube Video Test Title" />
---
### Custom video
<Video
width="100%"
src="https://joy1.videvo.net/videvo_files/video/free/video0467/large_watermarked/_import_61516692993d77.04238324_preview.mp4"
/>
@@ -0,0 +1,30 @@
---
title: "Confidentialité"
meta_title: ""
description: "this is meta description"
draft: false
---
#### Responsabilité des contributeurs
Le client est très important, le client sera suivi par le client. Purus, jusqu'à présent tu es eros, ullamcorper id feugiat chaque personne avec quelques flèches. J'étais laide, mais la masse des dessins animés était grosse d'un frémissement. Je ne veux pas de la douleur dun footballeur puissant. Ultrices amoat, dans sera traité de l'arc du lit de la maladie. Il a été touché par des flèches lors d'un cours de torture. Le problème avec la fondue, c'est qu'il avait besoin de chocolat. Maintenant le bureau mais parfois le lac. Mais qui se soucie des dessins animés
le prix peut être certain. C'est un élément très important, tincidunt eros, nibh en lion. Malesuada est un lac pur, mais suspendu pendant un certain temps. Qui se soucie du temps, veut naître. Mais il faut en prendre soin. Eu veut naître du souci erhdfvssfvrgss a besoin de dessins animés et d'éléments. Un lac dans lequel il est facile de se baigner.
#### Collecte d'informations personnelles
Le client est très important, le client sera suivi par le client. Purus, jusqu'à présent tu es eros, ullamcorper id feugiat chaque personne avec quelques flèches. J'étais laide, mais la masse des dessins animés était grosse d'un frémissement. Ne me donne pas la douleur d'un joueur de football puissant. Les ultrices vont être guéries par l'arc du lit de la maladie. Il a été touché par des flèches lors d'un cours de torture. Le problème avec la fondue, c'est qu'il avait besoin de chocolat. Maintenant le bureau mais parfois le lac. Mais qui se soucie des dessins animés ?
#### Protection des informations personnelles
Le client est très important, le client sera suivi par le client. Purus, jusqu'à présent tu es eros, ullamcorper id feugiat chaque personne avec quelques flèches. J'étais laide, mais la masse des dessins animés était grosse d'un frémissement. Je ne veux pas de la douleur dun footballeur puissant. Les ultrices vont être guéries par l'arc du lit de la maladie. Il a été touché par des flèches lors d'un cours de macro.
Le problème avec la marmite, c'était qu'il avait besoin de chocolat. Maintenant le bureau mais parfois le lac. Mais qui se soucie des dessins animés ?
Le client est très important, le client sera suivi par le client. Purus, jusqu'à maintenant tu es eros, ullamcorper id feugiat
#### Modifications de la politique de confidentialité
1. Tous les articles Themefisher sont conçus pour être les plus récents, nous vérifions tous
2. commentaires qui menacent ou nuisent à la réputation de toute personne ou organisation
3. informations personnelles, y compris, sans toutefois s'y limiter, les adresses e-mail et les numéros de téléphone
4. Toute mise à jour arrive. Le client technologique recevra une notification automatique.
@@ -0,0 +1,10 @@
---
enable: true
title: "Prêt à construire votre prochain projet avec Astro ?"
image: "/images/call-to-action.png"
description: "Expérimente le futur du développement web avec Astroplate et Astro. Créez des sites statiques à charge rapide et personnalisables en toute facilité."
button:
enable: true
label: "Commencez maintenant"
link: "https://github.com/zeon-studio/astroplate"
---
@@ -0,0 +1,27 @@
---
enable: true
title: "Ce que disent les utilisateurs d'Astroplate"
description: "Ne vous contentez pas de nous croire sur parole - écoutez certains de nos utilisateurs satisfaits ! Consultez quelques-uns de nos témoignages ci-dessous pour voir ce que les autres disent à propos d'Astroplate."
# Testimonials
testimonials:
- name: "Marvin McKinney"
designation: "Concepteur Web"
avatar: "/images/avatar-sm.png"
content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam."
- name: "Marvin McKinney"
designation: "Concepteur Web"
avatar: "/images/avatar-sm.png"
content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam."
- name: "Marvin McKinney"
designation: "Concepteur Web"
avatar: "/images/avatar-sm.png"
content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam."
- name: "Marvin McKinney"
designation: "Concepteur Web"
avatar: "/images/avatar-sm.png"
content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam."
---
+16
View File
@@ -0,0 +1,16 @@
{
"full_name": "Full Name",
"full_name_placeholder": "Enter your full name",
"mail_placeholder": "john.doe@email.com",
"message": "Anything else?",
"message_placeholder": "Your message here...",
"submit": "Submit",
"read_more": "Read More",
"page_not_found": "Page Not Found",
"working_mail": "Work Email",
"anything_else": "Anything else?",
"contact_message_placeholder": "Your message here...",
"page_not_found_content": "The page you are looking for may have been removed, renamed, or is temporarily unavailable.",
"back_to_home": "Back to Home",
"get_started": "Get Started"
}
+16
View File
@@ -0,0 +1,16 @@
{
"full_name": "Nom complet",
"full_name_placeholder": "Entrez votre nom complet",
"mail_placeholder": "john.doe@email.com",
"message": "Autre chose?",
"message_placeholder": "Votre message ici...",
"submit": "Soumettre",
"read_more": "Lire la suite",
"page_not_found": "Page non trouvée",
"working_mail": "Courrier de travail",
"anything_else": "Autre chose ?",
"contact_message_placeholder": "Votre message ici...",
"page_not_found_content": "La page que vous recherchez a peut-être été supprimée, changée de nom ou temporairement inaccessible.",
"back_to_home": "Retour à l'accueil",
"get_started": "Commencez à utiliser"
}
+10 -6
View File
@@ -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;
@@ -18,12 +19,12 @@ let fontPrimary, fontSecondary;
if (theme.fonts.font_family.primary) {
fontPrimary = theme.fonts.font_family.primary
.replace(/\+/g, " ")
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;.]+/gi, "");
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, "");
}
if (theme.fonts.font_family.secondary) {
fontSecondary = theme.fonts.font_family.secondary
.replace(/\+/g, " ")
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;.]+/gi, "");
.replace(/:[ital,]*[ital@]*[wght@]*[0-9,;]+/gi, "");
}
// types for frontmatters
@@ -34,15 +35,18 @@ export interface Props {
image?: string;
noindex?: boolean;
canonical?: string;
lang?: string;
}
// destructure frontmatter
const { title, meta_title, description, image, noindex, canonical } =
// distructure frontmatters
const { title, meta_title, description, image, noindex, canonical, lang } =
Astro.props;
const language = lang || getLangFromUrl(Astro.url);
---
<!doctype html>
<html lang="en">
<html lang={lang}>
<head>
<!-- favicon -->
<link rel="shortcut icon" href={config.site.favicon} />
@@ -173,7 +177,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<body>
<TwSizeIndicator />
<Header />
<SearchModal client:load />
<SearchModal lang={language} client:load />
<main id="main-content">
<slot />
</main>
+18 -4
View File
@@ -1,18 +1,30 @@
---
import BlogCard from "@/components/BlogCard.astro";
import Share from "@/components/Share.astro";
import config from "@/config/config.json";
import Disqus from "@/helpers/Disqus";
import { getSinglePage } from "@/lib/contentParser.astro";
import dateFormat from "@/lib/utils/dateFormat";
import { slugSelector } from "@/lib/utils/languageParser";
import similarItems from "@/lib/utils/similarItems";
import { humanize, markdownify, slugify } from "@/lib/utils/textConverter";
import type { ContentEntryMap } from "astro:content";
import { FaRegClock, FaRegFolder, FaRegUserCircle } from "react-icons/fa";
import ImageMod from "./components/ImageMod.astro";
const { default_language } = config.settings;
const COLLECTION_FOLDER = "blog";
const { post } = Astro.props;
let { lang } = Astro.params;
const posts = await getSinglePage(COLLECTION_FOLDER);
if (!lang) {
lang = default_language;
}
const posts = await getSinglePage(
COLLECTION_FOLDER,
lang as keyof ContentEntryMap
);
const similarPosts = similarItems(post, posts);
const { Content } = await post.render();
const { title, description, author, categories, image, date, tags } = post.data;
@@ -39,7 +51,7 @@ const { title, description, author, categories, image, date, tags } = post.data;
<h1 set:html={markdownify(title)} class="h2 mb-4" />
<ul class="mb-4">
<li class="mr-4 inline-block">
<a href={`/authors/${slugify(author)}`}>
<a href={slugSelector(`/authors/${slugify(author)}`, lang)}>
<FaRegUserCircle className={"mr-2 -mt-1 inline-block"} />
{humanize(author)}
</a>
@@ -48,7 +60,9 @@ const { title, description, author, categories, image, date, tags } = post.data;
<FaRegFolder className={"mr-2 -mt-1 inline-block"} />
{
categories.map((category: string, index: number) => (
<a href={`/categories/${slugify(category)}`}>
<a
href={slugSelector(`/categories/${slugify(category)}`, lang)}
>
{humanize(category)}
{index !== categories.length - 1 && ","}
</a>
@@ -72,7 +86,7 @@ const { title, description, author, categories, image, date, tags } = post.data;
<li class="inline-block">
<a
class="m-1 block rounded bg-theme-light px-3 py-1 hover:bg-primary hover:text-white dark:bg-darkmode-theme-light dark:hover:bg-darkmode-primary dark:hover:text-dark"
href={`/tags/${slugify(tag)}`}
href={slugSelector(`/tags/${slugify(tag)}`, lang)}
>
{humanize(tag)}
</a>
+3 -1
View File
@@ -1,10 +1,12 @@
---
import { getLangFromUrl, slugSelector } from "@/lib/utils/languageParser";
import { plainify } from "@/lib/utils/textConverter";
import ImageMod from "./ImageMod.astro";
import Social from "./Social.astro";
const { data } = Astro.props;
const { title, image, social } = data.data;
const lang = getLangFromUrl(Astro.url);
---
<div
@@ -23,7 +25,7 @@ const { title, image, social } = data.data;
)
}
<h4 class="mb-3">
<a href={`/authors/${data.slug}`}>{title}</a>
<a href={slugSelector(`/${data.slug}`, lang)}>{title}</a>
</h4>
<p class="mb-4">
{plainify(data.body?.slice(0, 100))}
+22 -9
View File
@@ -1,16 +1,28 @@
---
import config from "@/config/config.json";
import dateFormat from "@/lib/utils/dateFormat";
import {
getLangFromUrl,
getTranslations,
slugSelector,
} from "@/lib/utils/languageParser";
import { humanize, plainify, slugify } from "@/lib/utils/textConverter";
import type { ContentEntryMap } from "astro:content";
import { FaRegFolder, FaRegUserCircle } from "react-icons/fa";
import ImageMod from "./ImageMod.astro";
const {
summary_length,
blog_folder,
}: { summary_length: number; blog_folder: string } = config.settings;
const { summary_length }: { summary_length: number; blog_folder: string } =
config.settings;
const { data } = Astro.props;
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">
@@ -27,22 +39,23 @@ const { title, image, date, author, categories } = data.data;
)
}
<h4 class="mb-3">
<a href={`/${blog_folder}/${data.slug}`}>
<a href={slugSelector(`/${data.slug}`, lang)}>
{title}
</a>
</h4>
<ul class="mb-4">
<li class="mr-4 inline-block">
<a href={`/authors/${slugify(author)}`}>
<a href={slugSelector(`/authors/${slugify(author)}`, lang)}>
<FaRegUserCircle className={"mr-2 -mt-1 inline-block"} />
{humanize(author)}
</a>
</li>
<li class="mr-4 inline-block">
<FaRegFolder className={"mr-2 -mt-1 inline-block"} />
{
categories.map((category: string, index: number) => (
<a href={`/categories/${slugify(category)}`}>
<a href={slugSelector(`/categories/${slugify(category)}`, lang)}>
{humanize(category)}
{index !== categories.length - 1 && ","}
</a>
@@ -54,8 +67,8 @@ const { title, image, date, author, categories } = data.data;
<p class="mb-6">{plainify(data.body?.slice(0, Number(summary_length)))}</p>
<a
class="btn btn-outline-primary btn-sm"
href={`/${blog_folder}/${data.slug}`}
href={slugSelector(`/${data.slug}`, lang)}
>
read more
{read_more}
</a>
</div>
+15 -4
View File
@@ -1,19 +1,30 @@
---
import { supportedLang } from "@/lib/utils/languageParser";
import { humanize } from "@/lib/utils/textConverter";
const { className }: { className?: string } = Astro.props;
const paths = Astro.url.pathname.split("/").filter((x) => x);
let lang = "";
if (supportedLang.includes(paths[0])) {
lang = paths.shift()!;
}
let baseHref = lang ? `/${lang}` : "/";
let parts = [
{
label: "Home",
href: "/",
"aria-label": Astro.url.pathname === "/" ? "page" : undefined,
href: baseHref,
"aria-label":
Astro.url.pathname === baseHref || Astro.url.pathname === `${baseHref}/`
? "page"
: undefined,
},
];
paths.forEach((label: string, i: number) => {
const href = `/${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, " ")) || "",
@@ -28,7 +39,7 @@ paths.forEach((label: string, i: number) => {
{
parts.map(({ label, ...attrs }, index) => (
<li class="mx-1 capitalize" role="listitem">
{index > 0 && <span class="inlin-block mr-1">/</span>}
{index > 0 && <span class="inline-block mr-1">/</span>}
{index !== parts.length - 1 ? (
<a class="text-primary dark:text-darkmode-primary" {...attrs}>
{label}
+10 -1
View File
@@ -1,6 +1,7 @@
---
import config from "@/config/config.json";
import ImageMod from "./ImageMod.astro";
import { getLangFromUrl, slugSelector } from "@/lib/utils/languageParser";
const { src, srcDarkmode }: { src?: string; srcDarkmode?: string } =
Astro.props;
@@ -21,9 +22,17 @@ const {
} = config.site;
const { theme_switcher }: { theme_switcher: boolean } = config.settings;
const { default_language } = config.settings;
let lang = getLangFromUrl(Astro.url);
const disabledLanguages = config.settings.disable_languages as string[];
if (disabledLanguages.includes(lang)) {
lang = default_language;
}
---
<a href="/" class="navbar-brand inline-block">
<a href={slugSelector("/", lang)} class="navbar-brand inline-block">
{
src || srcDarkmode || logo || logo_darkmode ? (
<>
+18 -7
View File
@@ -1,4 +1,8 @@
---
import { getLangFromUrl, slugSelector } from "@/lib/utils/languageParser";
const lang = getLangFromUrl(Astro.url);
type Pagination = {
section?: string;
currentPage?: number;
@@ -27,8 +31,11 @@ for (let i = 1; i <= totalPages; i++) {
<a
href={
indexPageLink
? `${section ? "/" + section : "/"}`
: `${section ? "/" + section : ""}/page/${currentPage - 1}`
? slugSelector(`/${section ? section : ""}`, lang)
: slugSelector(
`/${section ? section : ""}/page/${currentPage - 1}`,
lang
)
}
class="rounded px-2 py-1.5 text-dark hover:bg-theme-light dark:text-darkmode-dark dark:hover:bg-darkmode-theme-light"
>
@@ -77,11 +84,12 @@ for (let i = 1; i <= totalPages; i++) {
</span>
) : (
<a
href={
href={slugSelector(
i === 0
? `${section ? "/" + section : "/"}`
: `${section ? "/" + section : ""}/page/${pagination}`
}
? `/${section ? section : ""}`
: `/${section ? section : ""}/page/${pagination}`,
lang
)}
aria-current="page"
class="rounded px-4 py-2 text-dark hover:bg-theme-light dark:text-darkmode-dark dark:hover:bg-darkmode-theme-light"
>
@@ -93,7 +101,10 @@ for (let i = 1; i <= totalPages; i++) {
{/* next page */}
{hasNextPage ? (
<a
href={`${section ? "/" + section : ""}/page/${currentPage + 1}`}
href={slugSelector(
`/${section ? section : ""}/page/${currentPage + 1}`,
lang
)}
class="rounded px-2 py-1.5 text-dark hover:bg-theme-light dark:text-darkmode-dark dark:hover:bg-darkmode-theme-light"
>
<span class="sr-only">Next</span>
+65
View File
@@ -0,0 +1,65 @@
import config from "@/config/config.json";
import languages from "@/config/language.json";
import React from "react";
const LanguageSwitcher = ({
lang,
pathname,
}: {
lang: string;
pathname: string;
}) => {
const { default_language, default_language_in_subdir } = config.settings;
// Function to remove trailing slash if necessary
const removeTrailingSlash = (path: string) => {
if (!config.site.trailing_slash) {
return path.replace(/\/$/, "");
}
return path;
};
// 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 ${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) {
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}
>
{sortedLanguages.map((language) => (
<option
className="dark:text-dark"
key={language.languageCode}
value={language.languageCode}
>
{language.languageName}
</option>
))}
</select>
</div>
);
};
export default LanguageSwitcher;
+15 -3
View File
@@ -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>
+12 -4
View File
@@ -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>
+14 -4
View File
@@ -2,11 +2,21 @@
import Logo from "@/components/Logo.astro";
import Social from "@/components/Social.astro";
import config from "@/config/config.json";
import menu from "@/config/menu.json";
import social from "@/config/social.json";
import {
getLangFromUrl,
getTranslations,
slugSelector,
} from "@/lib/utils/languageParser";
import { markdownify } from "@/lib/utils/textConverter";
import type { ContentEntryMap } from "astro:content";
const { footer }: { footer: { name: string; url: string }[] } = menu;
const lang = getLangFromUrl(Astro.url);
const menu = await getTranslations(lang as keyof ContentEntryMap);
let footer: any = [];
if (menu) {
footer = menu.footer;
}
---
<footer class="bg-theme-light dark:bg-darkmode-theme-light">
@@ -18,9 +28,9 @@ const { footer }: { footer: { name: string; url: string }[] } = menu;
<div class="mb-8 text-center lg:col-6 lg:mb-0">
<ul>
{
footer.map((menu) => (
footer.map((menu: any) => (
<li class="m-3 inline-block">
<a href={menu.url}>{menu.name}</a>
<a href={slugSelector(menu.url, lang)}>{menu.name}</a>
</li>
))
}
+41 -29
View File
@@ -2,33 +2,37 @@
import Logo from "@/components/Logo.astro";
import ThemeSwitcher from "@/components/ThemeSwitcher.astro";
import config from "@/config/config.json";
import menu from "@/config/menu.json";
import languages from "@/config/language.json";
import LanguageSwitcher from "@/helpers/LanguageSwitcher";
import {
getLangFromUrl,
getTranslations,
slugSelector,
} from "@/lib/utils/languageParser";
import type { ContentEntryMap } from "astro:content";
import { IoSearch } from "react-icons/io5";
export interface ChildNavigationLink {
name: string;
url: string;
}
let lang = getLangFromUrl(Astro.url);
const menu = await getTranslations(lang as keyof ContentEntryMap);
export interface NavigationLink {
name: string;
url: string;
hasChildren?: boolean;
children?: ChildNavigationLink[];
}
const { main }: { main: NavigationLink[] } = menu;
const { navigation_button, settings } = config;
const { default_language } = config.settings;
const { pathname } = Astro.url;
const { get_started } = await getTranslations(lang as keyof ContentEntryMap);
const disabledLanguages = config.settings.disable_languages as string[];
if (disabledLanguages.includes(lang)) {
lang = default_language;
}
---
<header class={`header z-30 ${settings.sticky_header && "sticky top-0"}`}>
<nav class="navbar container">
<!-- logo -->
{/* logo */}
<div class="order-0">
<Logo />
</div>
<!-- navbar toggler -->
{/* navbar toggler */}
<input id="nav-toggle" type="checkbox" class="hidden" />
<label
for="nav-toggle"
@@ -45,21 +49,23 @@ const { pathname } = Astro.url;
transform="rotate(45 10 10)"></polygon>
</svg>
</label>
<!-- /navbar toggler -->
{/* /navbar toggler */}
<ul
id="nav-menu"
class="navbar-nav order-3 hidden w-full pb-6 lg:order-1 lg:flex lg:w-auto lg:space-x-2 lg:pb-0 xl:space-x-8"
>
{
main.map((menu) => (
menu?.main.map((menu: any) => (
<>
{menu.hasChildren ? (
<li class="nav-item nav-dropdown group relative">
<span
class={`nav-link inline-flex items-center ${
menu.children?.map(({ url }) => url).includes(pathname) ||
menu.children
?.map(({ url }) => `${url}/`)
?.map(({ url }: { url: string }) =>
slugSelector(url, lang)
)
.includes(pathname)
? "active"
: ""
@@ -71,15 +77,13 @@ const { pathname } = Astro.url;
</svg>
</span>
<ul class="nav-dropdown-list hidden group-hover:block lg:invisible lg:absolute lg:block lg:opacity-0 lg:group-hover:visible lg:group-hover:opacity-100">
{menu.children?.map((child) => (
{menu.children?.map((child: any) => (
<li class="nav-dropdown-item">
<a
href={child.url}
href={slugSelector(child.url, lang)}
aria-label={child.name}
class={`nav-dropdown-link block ${
(pathname === `${child.url}/` ||
pathname === child.url) &&
"active"
pathname === slugSelector(child.url, lang) && "active"
}`}
>
{child.name}
@@ -91,10 +95,9 @@ const { pathname } = Astro.url;
) : (
<li class="nav-item">
<a
href={menu.url}
href={slugSelector(menu.url, lang)}
class={`nav-link block ${
(pathname === `${menu.url}/` || pathname === menu.url) &&
"active"
pathname === slugSelector(menu.url, lang) && "active"
}`}
>
{menu.name}
@@ -104,6 +107,7 @@ const { pathname } = Astro.url;
</>
))
}
{/* Navigation button */}
{
navigation_button.enable && (
<li class="mt-4 inline-block lg:hidden">
@@ -111,13 +115,15 @@ const { pathname } = Astro.url;
class="btn btn-outline-primary btn-sm"
href={navigation_button.link}
>
{navigation_button.label}
{get_started}
</a>
</li>
)
}
</ul>
<div class="order-1 ml-auto flex items-center md:order-2 lg:ml-0">
{/* Search button */}
{
settings.search && (
<button
@@ -129,14 +135,20 @@ const { pathname } = Astro.url;
</button>
)
}
{/* Theme switcher */}
<ThemeSwitcher className="mr-5" />
{/* Language switcher */}
<LanguageSwitcher client:load lang={lang} pathname={pathname} />
{/* Navigation button */}
{
navigation_button.enable && (
<a
class="btn btn-outline-primary btn-sm hidden lg:inline-block"
href={navigation_button.link}
>
{navigation_button.label}
{get_started}
</a>
)
}
+4 -2
View File
@@ -1,7 +1,9 @@
---
import { getLangFromUrl, slugSelector } from "@/lib/utils/languageParser";
import { humanize } from "@/lib/utils/textConverter";
const { tags, categories, allCategories } = Astro.props;
const lang = getLangFromUrl(Astro.url);
---
<div class="lg:col-4">
@@ -19,7 +21,7 @@ const { tags, categories, allCategories } = Astro.props;
<li>
<a
class="flex justify-between hover:text-primary dark:hover:text-darkmode-primary"
href={`/categories/${category}`}
href={slugSelector(`/categories/${category}`, lang)}
>
{humanize(category)} <span>({count})</span>
</a>
@@ -41,7 +43,7 @@ const { tags, categories, allCategories } = Astro.props;
<li class="inline-block">
<a
class="m-1 block rounded bg-white px-3 py-1 hover:bg-primary hover:text-white dark:bg-darkmode-body dark:hover:bg-darkmode-primary dark:hover:text-dark"
href={`/tags/${tag}`}
href={slugSelector(`/tags/${tag}`, lang)}
>
{humanize(tag)}
</a>
+1 -1
View File
@@ -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" />
+121 -3
View File
@@ -3,14 +3,132 @@ import {
getCollection,
type CollectionEntry,
type CollectionKey,
type ContentEntryMap,
} from "astro:content";
import config from "@/config/config.json";
import languages from "@/config/language.json";
export const getSP = 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(
contentDir as any,
({ id }: any) => {
return id.startsWith(collectionName) && !id.endsWith("-index.md");
}
)) as CollectionEntry<C>[];
// @ts-ignore
const removeDrafts = pages.filter((data) => !data.data.draft);
return removeDrafts;
};
export const getLP = 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(
contentDir as any,
({ id }: any) => {
return id.startsWith(collectionName);
}
)) as CollectionEntry<C>[];
return pages;
};
export const getSinglePage = async <C extends CollectionKey>(
collectionName: C,
lang: keyof ContentEntryMap | undefined,
subCollectionName?: string
): Promise<CollectionEntry<C>[]> => {
const allPages = await getCollection(collectionName);
const removeIndex = allPages.filter((data) => data.id.match(/^(?!-)/));
const removeDrafts = removeIndex.filter((data) => !data.data.draft);
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;
};
---
+50 -5
View File
@@ -1,10 +1,34 @@
---
import { getSinglePage } from "@/lib/contentParser.astro";
import config from "@/config/config.json";
import languages from "@/config/language.json";
import { slugify } from "@/lib/utils/textConverter";
import type { ContentEntryMap } from "astro:content";
import { getCollection } from "astro:content";
// get taxonomy from frontmatter
export const getTaxonomy = async (collection: any, name: string) => {
const singlePages = await getSinglePage(collection);
export const getTaxonomy = async (collection: string, name: string) => {
const { default_language } = config.settings;
const language = languages.find((l) => l.languageCode === collection);
let actualCollection = default_language;
if (language) {
actualCollection = language.contentDir;
} else {
const defaultLanguageMatch = languages.find(
(l) => l.languageCode === default_language
);
if (defaultLanguageMatch) {
actualCollection = defaultLanguageMatch.contentDir;
}
}
const singlePages = await getCollection(
"blog" as keyof ContentEntryMap,
({ id }: any) => {
return id.startsWith(actualCollection) && !id.endsWith("-index.md");
}
);
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
let taxonomies: string[] = [];
for (let i = 0; i < taxonomyPages.length; i++) {
@@ -18,8 +42,29 @@ export const getTaxonomy = async (collection: any, name: string) => {
};
// get all taxonomies from frontmatter
export const getAllTaxonomy = async (collection: any, name: string) => {
const singlePages = await getSinglePage(collection);
export const getAllTaxonomy = async (collection: string, name: string) => {
const { default_language } = config.settings;
const language = languages.find((l) => l.languageCode === collection);
let actualCollection = default_language;
if (language) {
actualCollection = language.contentDir;
} else {
const defaultLanguageMatch = languages.find(
(l) => l.languageCode === default_language
);
if (defaultLanguageMatch) {
actualCollection = defaultLanguageMatch.contentDir;
}
}
const singlePages = await getCollection(
"blog" as keyof ContentEntryMap,
({ id }: any) => {
return id.startsWith(actualCollection) && !id.endsWith("-index.md");
}
);
const taxonomyPages = singlePages.map((page: any) => page.data[name]);
let taxonomies: string[] = [];
for (let i = 0; i < taxonomyPages.length; i++) {
+151
View File
@@ -0,0 +1,151 @@
import { getRelativeLocaleUrl } from "astro:i18n";
import config from "../../config/config.json";
import languagesJSON from "../../config/language.json";
const { default_language } = config.settings;
const locales: { [key: string]: any } = {};
// Load menu and dictionary dynamically
languagesJSON.forEach((language) => {
const { languageCode } = language;
import(`../../config/menu.${languageCode}.json`).then((menu) => {
import(`../../i18n/${languageCode}.json`).then((dictionary) => {
locales[languageCode] = { ...menu, ...dictionary };
});
});
});
// Extract all languages from the locales object
const languages = Object.keys(locales);
// Export the locales and languages
export { languages, locales };
export function getLangFromUrl(url: URL): string {
const [, lang] = url.pathname.split("/");
if (locales.hasOwnProperty(lang)) {
return lang;
}
return default_language;
}
export const getTranslations = async (lang: string) => {
const {
default_language,
disable_languages,
}: { default_language: string; disable_languages: string[] } =
config.settings;
if (disable_languages.includes(lang)) {
lang = default_language;
}
let language = languagesJSON.find((l) => l.languageCode === lang);
if (!language) {
lang = default_language;
language = languagesJSON.find((l) => l.languageCode === default_language);
}
if (!language) {
throw new Error("Default language not found");
}
const contentDir = language.contentDir;
let menu, dictionary;
try {
menu = await import(`../../config/menu.${lang}.json`);
dictionary = await import(`../../i18n/${lang}.json`);
} catch (error) {
menu = await import(`../../config/menu.${default_language}.json`);
dictionary = await import(`../../i18n/${default_language}.json`);
}
return { ...menu.default, ...dictionary.default, contentDir };
};
const supportedLang = ["", ...languagesJSON.map((lang) => lang.languageCode)];
const disabledLanguages = config.settings.disable_languages as string[];
// Filter out disabled languages from supportedLang
const filteredSupportedLang = supportedLang.filter(
(lang) => !disabledLanguages.includes(lang),
);
export { filteredSupportedLang as supportedLang };
// export const slugSelector = (url: string, lang: string) => {
// const { default_language, default_language_in_subdir } = config.settings;
// const { trailing_slash } = config.site;
// let constructedUrl;
// // Determine the initial URL structure based on language
// if (url === "/") {
// constructedUrl = lang === default_language ? "/" : `/${lang}`;
// } else {
// constructedUrl = getRelativeLocaleUrl(lang, url, {
// normalizeLocale: false,
// });
// }
// // Adjust for trailing slash
// if (trailing_slash) {
// if (!constructedUrl.endsWith("/")) {
// constructedUrl += "/";
// }
// } else {
// if (constructedUrl.endsWith("/")) {
// 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_subdir
// ? `/${lang}${constructedUrl}`
// : constructedUrl;
// }
// return constructedUrl;
// };
export const slugSelector = (url: string, lang: string) => {
const { default_language, default_language_in_subdir } = config.settings;
const { trailing_slash } = config.site;
let constructedUrl;
// Determine the initial URL structure based on language
if (url === "/") {
constructedUrl = lang === default_language ? "/" : `/${lang}`;
} else {
constructedUrl = getRelativeLocaleUrl(lang, url, {
normalizeLocale: false,
});
}
// 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("/") && constructedUrl !== "/") {
constructedUrl = constructedUrl.slice(0, -1);
}
}
return constructedUrl;
};
+30 -6
View File
@@ -1,5 +1,30 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import {
getLangFromUrl,
getTranslations,
supportedLang,
slugSelector,
} from "@/lib/utils/languageParser";
import type { ContentEntryMap } from "astro:content";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
let lang = getLangFromUrl(Astro.url);
const disabledLanguages = config.settings.disable_languages as string[];
if (disabledLanguages.includes(lang)) {
lang = config.settings.default_language;
}
const { page_not_found_content, page_not_found, back_to_home } =
await getTranslations(lang as keyof ContentEntryMap);
---
<Base title="Page Not Found">
@@ -12,14 +37,13 @@ import Base from "@/layouts/Base.astro";
>
404
</span>
<h1 class="h2 mb-4">Page not found</h1>
<h1 class="h2 mb-4">{page_not_found}</h1>
<div class="content">
<p>
The page you are looking for might have been removed, had its name
changed, or is temporarily unavailable.
</p>
<p>{page_not_found_content}</p>
</div>
<a href="/" class="btn btn-primary mt-8">Back to home</a>
<a href={slugSelector("/", lang)} class="btn btn-primary mt-8"
>{back_to_home}</a
>
</div>
</div>
</div>
@@ -1,21 +1,27 @@
---
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import PageHeader from "@/partials/PageHeader.astro";
import type { ContentEntryMap } from "astro:content";
// get static paths for all pages
export async function getStaticPaths() {
const COLLECTION_FOLDER = "pages";
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const pages = await getSinglePage("pages", lang as keyof ContentEntryMap);
const pages = await getSinglePage(COLLECTION_FOLDER);
const paths = pages.map((page) => ({
params: {
regular: page.slug,
},
props: { page },
}));
return paths;
return pages.map((page) => ({
params: {
lang: lang || undefined,
regular: page.slug.split("/").pop(),
},
props: {
page,
},
}));
})
);
return paths.flat();
}
const { page } = Astro.props;
@@ -1,12 +1,23 @@
---
import ImageMod from "@/components/ImageMod.astro";
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 { getEntry } from "astro:content";
import type { ContentEntryMap } from "astro:content";
const about = await getEntry("about", "-index");
const { Content } = await about.render();
const { title, description, meta_title, image } = about.data;
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const { lang } = Astro.params;
const about = await getListPage("about", lang as keyof ContentEntryMap);
const { Content } = await about[0].render();
const { title, description, meta_title, image } = about[0].data;
---
<Base
@@ -4,31 +4,45 @@ import ImageMod from "@/components/ImageMod.astro";
import Social from "@/components/Social.astro";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import { slugify } from "@/lib/utils/textConverter";
import type { ContentEntryMap } from "astro:content";
// get all static paths for authors
export async function getStaticPaths() {
const COLLECTION_FOLDER = "authors";
const authors = await getSinglePage(COLLECTION_FOLDER);
const paths = authors.map((author) => ({
params: {
single: author.slug,
},
props: { author },
}));
return paths;
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const authors = await getSinglePage(
COLLECTION_FOLDER,
lang as keyof ContentEntryMap
);
return authors.map((author) => ({
params: {
lang: lang || undefined,
single: author.slug.split("/").pop(),
},
props: {
author,
lang,
},
}));
})
);
return paths.flat();
}
const { author } = Astro.props;
const { author, lang } = Astro.props;
const { title, social, meta_title, description, image } = author.data;
const { Content } = await author.render();
// get all posts by author
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER);
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
const postFilterByAuthor = posts.filter(
(post) => slugify(post.data.author) === slugify(title),
(post) => slugify(post.data.author) === slugify(title)
);
---
+44
View File
@@ -0,0 +1,44 @@
---
import AuthorCard from "@/components/AuthorCard.astro";
import Base from "@/layouts/Base.astro";
import { getListPage, getSinglePage } from "@/lib/contentParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import PageHeader from "@/partials/PageHeader.astro";
import { type ContentEntryMap } from "astro:content";
const COLLECTION_FOLDER = "authors";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const { lang } = Astro.params;
const authorIndex = await getListPage(
COLLECTION_FOLDER,
lang as keyof ContentEntryMap
);
const authors = await getSinglePage(
COLLECTION_FOLDER,
lang as keyof ContentEntryMap
);
---
<Base title={authorIndex[0].data.title}>
<PageHeader title={authorIndex[0].data.title} />
<section class="section-sm pb-0">
<div class="container">
<div class="row justify-center">
{
authors.map((author) => (
<div class="mb-14 md:col-6 lg:col-4">
<AuthorCard data={author} />
</div>
))
}
</div>
</div>
</section>
</Base>
+43
View File
@@ -0,0 +1,43 @@
---
import Base from "@/layouts/Base.astro";
import PostSingle from "@/layouts/PostSingle.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import type { ContentEntryMap } from "astro:content";
export async function getStaticPaths() {
const BLOG_FOLDER = "blog";
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const posts = await getSinglePage(
BLOG_FOLDER,
lang as keyof ContentEntryMap
);
return posts.map((post) => ({
params: {
lang: lang || undefined,
single: post.slug.split("/").pop(),
},
props: {
post,
},
}));
})
);
return paths.flat();
}
const { post } = Astro.props;
const { title, meta_title, description, image } = post.data;
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
<PostSingle post={post} />
</Base>
@@ -3,32 +3,44 @@ import BlogCard from "@/components/BlogCard.astro";
import Pagination from "@/components/Pagination.astro";
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getListPage, getSinglePage } from "@/lib/contentParser.astro";
import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import { sortByDate } from "@/lib/utils/sortFunctions";
import PageHeader from "@/partials/PageHeader.astro";
import PostSidebar from "@/partials/PostSidebar.astro";
import { getEntry } from "astro:content";
import type { ContentEntryMap } from "astro:content";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const { lang } = Astro.params;
const BLOG_FOLDER = "blog";
const postIndex = await getListPage(
BLOG_FOLDER,
lang as keyof ContentEntryMap
);
const langCollection: keyof ContentEntryMap = lang as keyof ContentEntryMap;
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
const postIndex = await getEntry(BLOG_FOLDER, "-index");
const posts = await getSinglePage(BLOG_FOLDER);
const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories");
const categories = await getTaxonomy(BLOG_FOLDER, "categories");
const tags = await getTaxonomy(BLOG_FOLDER, "tags");
const allCategories = await getAllTaxonomy(langCollection, "categories");
const categories = await getTaxonomy(langCollection, "categories");
const tags = await getTaxonomy(langCollection, "tags");
const sortedPosts = sortByDate(posts);
const totalPages: number = Math.ceil(posts.length / config.settings.pagination);
const currentPosts = sortedPosts.slice(0, config.settings.pagination);
---
<Base
title={postIndex.data.title}
meta_title={postIndex.data.meta_title}
image={postIndex.data.image}
description={postIndex.data.description}
title={postIndex[0].data.title}
meta_title={postIndex[0].data.meta_title}
image={postIndex[0].data.image}
description={postIndex[0].data.description}
>
<PageHeader title={postIndex.data.title} />
<PageHeader title={postIndex[0].data.title} />
<section class="section">
<div class="container">
<div class="row gx-5">
@@ -3,21 +3,36 @@ import BlogCard from "@/components/BlogCard.astro";
import Pagination from "@/components/Pagination.astro";
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getListPage, getSinglePage } from "@/lib/contentParser.astro";
import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import { sortByDate } from "@/lib/utils/sortFunctions";
import PageHeader from "@/partials/PageHeader.astro";
import PostSidebar from "@/partials/PostSidebar.astro";
import { getEntry } from "astro:content";
import type { ContentEntryMap } from "astro:content";
const BLOG_FOLDER = "blog";
const { default_language } = config.settings;
const { slug } = Astro.params;
const postIndex = await getEntry(BLOG_FOLDER, "-index");
const posts = await getSinglePage(BLOG_FOLDER);
const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories");
const categories = await getTaxonomy(BLOG_FOLDER, "categories");
const tags = await getTaxonomy(BLOG_FOLDER, "tags");
let { slug, lang } = Astro.params;
if (!lang) {
lang = default_language;
}
const postIndex: any = await getListPage(
BLOG_FOLDER,
lang as keyof ContentEntryMap
);
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
const allCategories = await getAllTaxonomy(
lang as keyof ContentEntryMap,
"categories"
);
const categories = await getTaxonomy(
lang as keyof ContentEntryMap,
"categories"
);
const tags = await getTaxonomy(lang as keyof ContentEntryMap, "tags");
const sortedPosts = sortByDate(posts);
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const currentPage = slug && !isNaN(Number(slug)) ? Number(slug) : 1;
@@ -27,28 +42,41 @@ const currentPosts = sortedPosts.slice(indexOfFirstPost, indexOfLastPost);
export async function getStaticPaths() {
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER);
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const paths = [];
for (let i = 1; i < totalPages; i++) {
paths.push({
params: {
slug: (i + 1).toString(),
},
});
}
return paths;
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const posts = await getSinglePage(
BLOG_FOLDER,
lang as keyof ContentEntryMap
);
const totalPages = Math.ceil(posts.length / config.settings.pagination);
const langString = lang.toString();
const langPaths = [];
for (let i = 1; i < totalPages; i++) {
langPaths.push({
params: {
lang: langString || undefined,
slug: (i + 1).toString(),
},
});
}
return langPaths;
})
);
return paths.flat();
}
---
<Base
title={postIndex.data.title}
meta_title={postIndex.data.meta_title}
image={postIndex.data.image}
description={postIndex.data.description}
title={postIndex[0].data.title}
meta_title={postIndex[0].data.meta_title}
image={postIndex[0].data.image}
description={postIndex[0].data.description}
>
<PageHeader title={postIndex.data.title} />
<PageHeader title={postIndex[0].data.title} />
<section class="section">
<div class="container">
<div class="row gx-5">
+78
View File
@@ -0,0 +1,78 @@
---
import BlogCard from "@/components/BlogCard.astro";
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import { sortByDate } from "@/lib/utils/sortFunctions";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import PageHeader from "@/partials/PageHeader.astro";
import type { ContentEntryMap } from "astro:content";
// get all static paths for categories
export async function getStaticPaths() {
const { default_language } = config.settings;
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const categories = await getTaxonomy(
lang as keyof ContentEntryMap,
"categories"
);
return categories.map((category) => ({
params: {
lang: lang || undefined,
category: category,
},
props: {
category,
},
}));
})
);
// Handle default path (no lang)
const defaultCategories = await getTaxonomy(
default_language as keyof ContentEntryMap,
"categories"
);
const defaultPaths = defaultCategories.map((category) => ({
params: {
lang: undefined,
category: category,
},
props: {
category,
},
}));
return [...paths.flat(), ...defaultPaths];
}
const { category, lang } = Astro.params;
// get posts by category
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
const filterByCategories = taxonomyFilter(posts, "categories", category!);
const sortedPosts = sortByDate(filterByCategories);
---
<Base title={category}>
<PageHeader title={category} />
<div class="section-sm pb-0">
<div class="container">
<div class="row">
{
sortedPosts.map((post) => (
<div class="mb-14 md:col-6 lg:col-4">
<BlogCard data={post} />
</div>
))
}
</div>
</div>
</div>
</Base>
@@ -1,13 +1,28 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro";
import { slugSelector, supportedLang } from "@/lib/utils/languageParser";
import { humanize } from "@/lib/utils/textConverter";
import PageHeader from "@/partials/PageHeader.astro";
import type { ContentEntryMap } from "astro:content";
const { default_language } = config.settings;
const BLOG_FOLDER = "blog";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const categories = await getTaxonomy(BLOG_FOLDER, "categories");
const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories");
let { lang } = Astro.params;
if (!lang) {
lang = default_language;
}
const langCollection: keyof ContentEntryMap = lang as keyof ContentEntryMap;
const categories = await getTaxonomy(langCollection, "categories");
const allCategories = await getAllTaxonomy(langCollection, "categories");
---
<Base title={"Categories"}>
@@ -21,7 +36,7 @@ const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories");
return (
<li class="m-3 inline-block">
<a
href={`/categories/${category}`}
href={slugSelector(`/categories/${category}`, lang)}
class="block rounded bg-theme-light px-4 py-2 text-xl text-dark dark:bg-darkmode-theme-light dark:text-darkmode-dark"
>
{humanize(category)}{" "}
@@ -1,12 +1,31 @@
---
import config from "@/config/config.json";
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 { getEntry } from "astro:content";
import { type ContentEntryMap } from "astro:content";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const { lang } = Astro.params;
const contact = await getListPage("contact", lang as keyof ContentEntryMap);
const contact = await getEntry("contact", "-index");
const { contact_form_action }: { contact_form_action: string } = config.params;
const { title, description, meta_title, image } = contact.data;
const { title, description, meta_title, image } = contact[0].data;
const {
working_mail,
full_name,
anything_else,
contact_message_placeholder,
submit,
} = await getTranslations(lang as keyof ContentEntryMap);
---
<Base
@@ -23,7 +42,8 @@ const { title, description, meta_title, image } = contact.data;
<form action={contact_form_action} method="POST">
<div class="mb-6">
<label for="name" class="form-label">
Full Name <span class="text-red-500">*</span>
{full_name}
<span class="text-red-500">*</span>
</label>
<input
id="name"
@@ -35,7 +55,8 @@ const { title, description, meta_title, image } = contact.data;
</div>
<div class="mb-6">
<label for="email" class="form-label">
Working Mail <span class="text-red-500">*</span>
{working_mail}
<span class="text-red-500">*</span>
</label>
<input
id="email"
@@ -47,16 +68,17 @@ const { title, description, meta_title, image } = contact.data;
</div>
<div class="mb-6">
<label for="message" class="form-label">
Anything else? <span class="text-red-500">*</span>
{anything_else}
<span class="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
class="form-input"
placeholder="Message goes here..."
placeholder={contact_message_placeholder}
rows="8"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-primary">{submit}</button>
</form>
</div>
</div>
@@ -1,27 +1,37 @@
---
import ImageMod from "@/components/ImageMod.astro";
import Base from "@/layouts/Base.astro";
import { getListPage, getSinglePage } from "@/lib/contentParser.astro";
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 { getEntry } 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) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const homepage = await getEntry("homepage", "-index");
const testimonial = await getEntry("sections", "testimonial");
const call_to_action = await getEntry("sections", "call-to-action");
const { banner, features }: Homepage = homepage.data;
const { lang } = Astro.params;
const homepage = await getListPage("homepage", lang as keyof ContentEntryMap);
const { banner, features } = homepage[0].data;
const testimonial = await getSinglePage(
"sections",
lang as keyof ContentEntryMap,
"testimonial"
);
const call_to_action = await getSinglePage(
"sections",
lang as keyof ContentEntryMap,
"call-to-action"
);
---
<Base>
@@ -70,7 +80,7 @@ const { banner, features }: Homepage = homepage.data;
<!-- Features -->
{
features.map((feature, index: number) => (
features.map((feature: Feature, index: number) => (
<section class={`section-sm ${index % 2 === 0 && "bg-gradient"}`}>
<div class="container">
<div class="row items-center justify-between">
@@ -109,6 +119,6 @@ const { banner, features }: Homepage = homepage.data;
}
<!-- /Features -->
<Testimonial testimonial={testimonial} />
<CallToAction call_to_action={call_to_action} />
<Testimonial testimonial={testimonial[0]} />
<CallToAction call_to_action={call_to_action[0]} />
</Base>
+75
View File
@@ -0,0 +1,75 @@
---
import BlogCard from "@/components/BlogCard.astro";
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { supportedLang } from "@/lib/utils/languageParser";
import { sortByDate } from "@/lib/utils/sortFunctions";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import PageHeader from "@/partials/PageHeader.astro";
import type { ContentEntryMap } from "astro:content";
// get all static paths for tags
export async function getStaticPaths() {
const { default_language } = config.settings;
const paths = await Promise.all(
supportedLang.map(async (lang) => {
const tags = await getTaxonomy(lang as keyof ContentEntryMap, "tags");
return tags.map((tag) => ({
params: {
lang: lang || undefined,
tag: tag,
},
props: {
tag,
},
}));
})
);
// Handle default path (no lang)
const defaultCategories = await getTaxonomy(
default_language as keyof ContentEntryMap,
"tags"
);
const defaultPaths = defaultCategories.map((tag) => ({
params: {
lang: undefined,
tag: tag,
},
props: {
tag,
},
}));
return [...paths.flat(), ...defaultPaths];
}
const { tag, lang } = Astro.params;
// get posts by tag
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER, lang as keyof ContentEntryMap);
const filterByTags = taxonomyFilter(posts, "tags", tag!);
const sortedPosts = sortByDate(filterByTags);
---
<Base title={tag}>
<PageHeader title={tag} />
<div class="section-sm pb-0">
<div class="container">
<div class="row">
{
sortedPosts.map((post) => (
<div class="mb-14 md:col-6 lg:col-4">
<BlogCard data={post} />
</div>
))
}
</div>
</div>
</div>
</Base>
+19 -4
View File
@@ -1,13 +1,28 @@
---
import config from "@/config/config.json";
import Base from "@/layouts/Base.astro";
import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro";
import { slugSelector, supportedLang } from "@/lib/utils/languageParser";
import { humanize } from "@/lib/utils/textConverter";
import PageHeader from "@/partials/PageHeader.astro";
import type { ContentEntryMap } from "astro:content";
const { default_language } = config.settings;
const BLOG_FOLDER = "blog";
export function getStaticPaths() {
const paths = supportedLang.map((lang) => ({
params: { lang: lang || undefined },
}));
return paths;
}
const tags = await getTaxonomy(BLOG_FOLDER, "tags");
const allTags = await getAllTaxonomy(BLOG_FOLDER, "tags");
let { lang } = Astro.params;
if (!lang) {
lang = default_language;
}
const langCollection: keyof ContentEntryMap = lang as keyof ContentEntryMap;
const tags = await getTaxonomy(langCollection, "tags");
const allTags = await getAllTaxonomy(langCollection, "tags");
---
<Base title={"Tags"}>
@@ -21,7 +36,7 @@ const allTags = await getAllTaxonomy(BLOG_FOLDER, "tags");
return (
<li class="m-3 inline-block">
<a
href={`/tags/${tag}`}
href={slugSelector(`/tags/${tag}`, lang)}
class="block rounded bg-theme-light px-4 py-2 text-xl text-dark dark:bg-darkmode-theme-light dark:text-darkmode-dark"
>
{humanize(tag)}{" "}
-29
View File
@@ -1,29 +0,0 @@
---
import AuthorCard from "@/components/AuthorCard.astro";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import PageHeader from "@/partials/PageHeader.astro";
import { getEntry } from "astro:content";
const COLLECTION_FOLDER = "authors";
const authorIndex = await getEntry(COLLECTION_FOLDER, "-index");
const authors = await getSinglePage(COLLECTION_FOLDER);
---
<Base title={authorIndex.data.title}>
<PageHeader title={authorIndex.data.title} />
<section class="section-sm pb-0">
<div class="container">
<div class="row justify-center">
{
authors.map((author) => (
<div class="mb-14 md:col-6 lg:col-4">
<AuthorCard data={author} />
</div>
))
}
</div>
</div>
</section>
</Base>
-30
View File
@@ -1,30 +0,0 @@
---
import Base from "@/layouts/Base.astro";
import PostSingle from "@/layouts/PostSingle.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
export async function getStaticPaths() {
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER);
const paths = posts.map((post) => ({
params: {
single: post.slug,
},
props: { post },
}));
return paths;
}
const { post } = Astro.props;
const { title, meta_title, description, image } = post.data;
---
<Base
title={title}
meta_title={meta_title}
description={description}
image={image}
>
<PostSingle post={post} />
</Base>
-46
View File
@@ -1,46 +0,0 @@
---
import BlogCard from "@/components/BlogCard.astro";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import PageHeader from "@/partials/PageHeader.astro";
// get static paths for all categories
export async function getStaticPaths() {
const BLOG_FOLDER = "blog";
const categories = await getTaxonomy(BLOG_FOLDER, "categories");
return categories.map((category) => {
return {
params: { category },
};
});
}
const { category } = Astro.params;
// get posts by category
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER);
const filterByCategories = taxonomyFilter(posts, "categories", category!);
const sortedPosts = sortByDate(filterByCategories);
---
<Base title={category}>
<PageHeader title={category} />
<div class="section-sm pb-0">
<div class="container">
<div class="row">
{
sortedPosts.map((post) => (
<div class="mb-14 md:col-6 lg:col-4">
<BlogCard data={post} />
</div>
))
}
</div>
</div>
</div>
</Base>
-45
View File
@@ -1,45 +0,0 @@
---
import BlogCard from "@/components/BlogCard.astro";
import Base from "@/layouts/Base.astro";
import { getSinglePage } from "@/lib/contentParser.astro";
import { getTaxonomy } from "@/lib/taxonomyParser.astro";
import { sortByDate } from "@/lib/utils/sortFunctions";
import taxonomyFilter from "@/lib/utils/taxonomyFilter";
import PageHeader from "@/partials/PageHeader.astro";
export async function getStaticPaths() {
const BLOG_FOLDER = "blog";
const tags = await getTaxonomy(BLOG_FOLDER, "tags");
return tags.map((tag) => {
return {
params: { tag },
};
});
}
const { tag } = Astro.params;
// get posts by tag
const BLOG_FOLDER = "blog";
const posts = await getSinglePage(BLOG_FOLDER);
const filterByTags = taxonomyFilter(posts, "tags", tag!);
const sortedPosts = sortByDate(filterByTags);
---
<Base title={tag}>
<PageHeader title={tag} />
<div class="section-sm pb-0">
<div class="container">
<div class="row">
{
sortedPosts.map((post) => (
<div class="mb-14 md:col-6 lg:col-4">
<BlogCard data={post} />
</div>
))
}
</div>
</div>
</div>
</Base>
+1 -1
View File
@@ -3,7 +3,7 @@ html {
}
body {
@apply bg-body text-base dark:bg-darkmode-body font-primary font-normal leading-relaxed text-text dark:text-darkmode-text;
@apply bg-body dark:bg-darkmode-body font-primary font-normal leading-relaxed text-text dark:text-darkmode-text;
}
h1,
+2
View File
@@ -1,3 +1,5 @@
import type { ContentEntryMap } from "astro:content";
export type Feature = {
button: button;
image: string;