Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions .eleventy.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,56 @@
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const {
createI18nInstance,
localizeUrl,
validateResources
} = require("./src/i18n");

module.exports = function (eleventyConfig) {
validateResources();
const i18n = createI18nInstance();

// Plugins
eleventyConfig.addPlugin(syntaxHighlight);

// Copy static assets to public paths used by templates
eleventyConfig.addPassthroughCopy({ "src/assets/images": "assets/images" });
eleventyConfig.addPassthroughCopy({ "src/assets/js": "assets/js" });

// Blog collection sorted by date (newest first)
eleventyConfig.addCollection("blogs", function (collectionApi) {
function getBlogPosts(collectionApi) {
return collectionApi.getFilteredByGlob("content/blogs/*.md")
.filter(post => post.fileSlug !== "0template")
.sort((a, b) => b.date - a.date);
}

// Blog collection sorted by date (newest first)
eleventyConfig.addCollection("blogs", function (collectionApi) {
return getBlogPosts(collectionApi);
});

// Get unique tags from all blogs
eleventyConfig.addCollection("blogTags", function (collectionApi) {
const tags = new Set();
collectionApi.getFilteredByGlob("content/blogs/*.md")
.filter(post => post.fileSlug !== "0template")
.forEach(post => {
getBlogPosts(collectionApi).forEach(post => {
if (post.data.tags) {
post.data.tags.forEach(tag => tags.add(tag));
}
});
});
return [...tags].sort();
});

// Date formatting filter
eleventyConfig.addFilter("dateFormat", function (date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
eleventyConfig.addFilter("dateFormat", function (date, locale = "ar") {
return new Date(date).toLocaleDateString(locale === "en" ? "en-US" : "ar-EG", {
year: "numeric",
month: "long",
day: "numeric"
});
});

eleventyConfig.addFilter("dateIso", function (date) {
return new Date(date).toISOString().slice(0, 10);
});

// Reading time filter
eleventyConfig.addFilter("readingTime", function (content) {
if (!content) return 1;
Expand All @@ -56,6 +70,22 @@ module.exports = function (eleventyConfig) {
return arr.slice(0, limit);
});

eleventyConfig.addNunjucksGlobal("t", function (key, locale = "ar", options = {}) {
if (!i18n.exists(key, { lng: locale })) {
throw new Error(`Missing translation key "${key}" for locale "${locale}".`);
}

return i18n.t(key, { lng: locale, ...options });
});

eleventyConfig.addNunjucksGlobal("localeUrl", function (url, locale = "ar") {
return localizeUrl(url, locale);
});

eleventyConfig.addNunjucksGlobal("alternateLocaleUrl", function (url, locale = "ar") {
return localizeUrl(url, locale === "en" ? "ar" : "en");
});

// Ignore node_modules and other non-content directories
eleventyConfig.ignores.add("node_modules/**");
eleventyConfig.ignores.add(".git/**");
Expand All @@ -65,6 +95,8 @@ module.exports = function (eleventyConfig) {
eleventyConfig.ignores.add("src/_data/**");
eleventyConfig.ignores.add("src/_includes/**");
eleventyConfig.ignores.add("src/assets/**");
eleventyConfig.ignores.add("src/i18n/**");
eleventyConfig.ignores.add("scripts/**");

return {
dir: {
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ npm-debug.log*
# Cache
.cache/
.eleventy-cache/
pnpm-lock.yaml
17 changes: 17 additions & 0 deletions content/blogs/blogs.11tydata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
layout: "blog.njk",
pagination: {
data: "locales",
size: 1,
alias: "localeData"
},
eleventyComputed: {
locale: (data) => data.localeData.code,
lang: (data) => data.localeData.lang,
dir: (data) => data.localeData.dir,
permalink: (data) => {
const slug = data.page.fileSlug;
return data.localeData.code === "en" ? `/en/blogs/${slug}/` : `/blogs/${slug}/`;
}
}
};
4 changes: 0 additions & 4 deletions content/blogs/blogs.json

This file was deleted.

49 changes: 48 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"description": "BLEU Community Blog - Open-source, community-driven Programming, Software Enginnering, and Computer Science",
"type": "commonjs",
"scripts": {
"check:i18n": "node scripts/check-i18n.js",
"build:css": "tailwindcss -i ./src/assets/css/input.css -o ./_site/assets/css/style.css --minify",
"watch:css": "tailwindcss -i ./src/assets/css/input.css -o ./_site/assets/css/style.css --watch",
"build:11ty": "eleventy",
"watch:11ty": "eleventy --serve",
"build": "npm run build:css && npm run build:11ty",
"verify:i18n-build": "node scripts/verify-localized-build.js",
"build": "npm run check:i18n && npm run build:css && npm run build:11ty && npm run verify:i18n-build",
"dev": "npm-run-all --parallel watch:*",
"clean": "rm -rf _site"
},
Expand All @@ -29,5 +31,8 @@
"postcss": "^8.4.35",
"postcss-cli": "^11.0.0",
"tailwindcss": "^3.4.1"
},
"dependencies": {
"i18next": "^25.6.0"
}
}
}
4 changes: 4 additions & 0 deletions scripts/check-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { validateResources } = require("../src/i18n");

validateResources();
console.log("i18n dictionaries are in sync.");
36 changes: 36 additions & 0 deletions scripts/verify-localized-build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require("fs");
const path = require("path");

const root = path.join(__dirname, "..", "_site");
const blogDir = path.join(__dirname, "..", "content", "blogs");

function ensureExists(relativePath) {
const absolutePath = path.join(root, relativePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Missing built file: ${relativePath}`);
}
}

const pages = [
"index.html",
path.join("en", "index.html"),
path.join("blogs", "index.html"),
path.join("en", "blogs", "index.html"),
path.join("contributing", "index.html"),
path.join("en", "contributing", "index.html")
];

for (const page of pages) {
ensureExists(page);
}

const postFiles = fs.readdirSync(blogDir)
.filter((file) => file.endsWith(".md") && file !== "0template.md")
.map((file) => path.basename(file, ".md"));

for (const slug of postFiles) {
ensureExists(path.join("blogs", slug, "index.html"));
ensureExists(path.join("en", "blogs", slug, "index.html"));
}

console.log("Localized build output verified.");
16 changes: 16 additions & 0 deletions src/_data/locales.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = [
{
code: "ar",
lang: "ar",
dir: "rtl",
label: "العربية",
prefix: ""
},
{
code: "en",
lang: "en",
dir: "ltr",
label: "English",
prefix: "/en"
}
];
4 changes: 1 addition & 3 deletions src/_data/site.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"name": "BLEU Community",
"tagline": "Building • Learning • Exploring • Uniting",
"description": "A community-driven blog for developers. Share knowledge, learn together, and build the future of technology.",
"url": "https://bleu-io.github.io/BLEU-Website",
"repo": "BLEU-IO/BLEU-Website",
"discord": {
Expand All @@ -13,4 +11,4 @@
"linkedin": "https://www.linkedin.com/company/bleu-io",
"github": "https://github.com/BLEU-IO"
}
}
}
42 changes: 21 additions & 21 deletions src/_includes/components/blog-card.njk
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
<a href="/blogs/{{ post.fileSlug }}/" class="card group hover:shadow-md hover:border-bleu-200 transition-all block">
<a href="{{ localeUrl(post.url, locale) }}" class="card group hover:shadow-md hover:border-bleu-200 transition-all block" data-tags="{{ post.data.tags | join(',') }}">
{% if post.data.thumbnail %}
<div class="mb-4 -mx-6 -mt-6 overflow-hidden rounded-t-2xl">
<img
src="{{ post.data.thumbnail }}"
alt="{{ post.data.title }}"
<img
src="{{ post.data.thumbnail }}"
alt="{{ post.data.title }}"
class="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
>
</div>
{% endif %}

{% if post.data.tags %}
<div class="flex gap-2 flex-wrap mb-3">
<div class="post-tags flex gap-2 flex-wrap mb-3" dir="ltr">
{% for tag in post.data.tags | limit(3) %}
<span class="badge text-xs">{{ tag }}</span>
<span class="badge text-xs" dir="ltr">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
<h3 class="text-xl font-bold text-bleu-950 group-hover:text-bleu-700 transition-colors mb-2">

<h3 class="text-xl font-bold text-bleu-950 group-hover:text-bleu-700 transition-colors mb-2 text-start" dir="auto">
{{ post.data.title }}
</h3>
<p class="text-bleu-700 text-sm mb-4 line-clamp-2">

<p class="text-bleu-700 text-sm mb-4 line-clamp-2 text-start" dir="auto">
{{ post.data.excerpt or post.templateContent | excerpt }}
</p>
<div class="flex items-center justify-between text-sm text-bleu-600">
<div class="flex items-center gap-2">
<img
src="https://github.com/{{ post.data.authorGithub }}.png?size=40"
alt="{{ post.data.author }}"

<div class="flex items-center justify-between text-sm text-bleu-600 gap-4">
<div class="flex items-center gap-2 min-w-0">
<img
src="https://github.com/{{ post.data.authorGithub }}.png?size=40"
alt="{{ post.data.author }}"
class="w-6 h-6 rounded-full"
loading="lazy"
>
<span>{{ post.data.author }}</span>
<span class="truncate">{{ post.data.author }}</span>
</div>
<div class="flex items-center gap-3">
<time>{{ post.data.date | dateFormat }}</time>
<span>{{ post.templateContent | readingTime }} min</span>
<div class="flex items-center gap-3 shrink-0">
<time>{{ post.data.date | dateFormat(locale) }}</time>
<span>{{ t("blog:post.minutesRead", locale, { count: post.templateContent | readingTime }) }}</span>
</div>
</div>
</a>
Loading