First commit
17
.github/workflows/cd.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Build and deploy zola
|
||||||
|
uses: shalzz/zola-deploy-action@master
|
||||||
|
env:
|
||||||
|
PAGES_BRANCH: gh-pages
|
||||||
|
TOKEN: ${{ secrets.PAT }}
|
||||||
|
BUILD_THEMES: true
|
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
public
|
4
.gitmodules
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[submodule "themes/tabi"]
|
||||||
|
path = themes/tabi
|
||||||
|
url = https://github.com/TheAwiteb/tabi.git
|
||||||
|
branch = dev
|
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## My personal blog (Arabic)
|
||||||
|
|
||||||
|
This is my personal blog, where I write about programming, open source, and other things that I find interesting. I write in Arabic
|
198
config.toml
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
base_url = "https://awiteb.is-a.dev"
|
||||||
|
default_language = "ar"
|
||||||
|
theme = "tabi"
|
||||||
|
|
||||||
|
author = "عويتب"
|
||||||
|
description = "المكان الذي اخرج فيه ما يجول في رأسي، وأحاول أن اقدم شيء مفيدة"
|
||||||
|
title = "~عويتب"
|
||||||
|
|
||||||
|
build_search_index = false
|
||||||
|
compile_sass = true
|
||||||
|
generate_feed = true
|
||||||
|
minify_html = false
|
||||||
|
|
||||||
|
taxonomies = [{name = "tags", feed = true}]
|
||||||
|
|
||||||
|
[markdown]
|
||||||
|
highlight_code = true
|
||||||
|
highlight_theme = "css"
|
||||||
|
render_emoji = true
|
||||||
|
smart_punctuation = true
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
# Enable JavaScript theme toggler to allow users to switch between dark/light mode.
|
||||||
|
# If disabled, your site will only use the theme specified in the `default_theme` variable.
|
||||||
|
theme_switcher = false
|
||||||
|
|
||||||
|
# This setting determines the default theme on load ("light" or "dark").
|
||||||
|
# To default to the user's OS-level theme, leave it empty or unset.
|
||||||
|
default_theme = "dark"
|
||||||
|
|
||||||
|
# Choose the colourscheme (skin) for the theme. Default is "teal".
|
||||||
|
# Skin available: blue, lavender, mint, red, sakura, teal, monochrome, lowcontrast_orange, lowcontrast_peach, lowcontrast_pink, indigo_ingot, evangelion
|
||||||
|
# See them live and learn how to create your own: https://welpo.github.io/tabi/blog/customise-tabi/#skins
|
||||||
|
# WARNING! "lowcontrast" skins, while aesthetically pleasing, may not provide optimal
|
||||||
|
# contrast (in light theme) for readability and might not be suitable for all users.
|
||||||
|
# Furthermore, low contrasting elements will affect your Google Lighthouse rating.
|
||||||
|
# All other skins have optimal contrast.
|
||||||
|
skin = "monochrome"
|
||||||
|
|
||||||
|
# Set browser theme colour. Can be a single colour or [light, dark].
|
||||||
|
# Note: Bright colors may be ignored in dark mode.
|
||||||
|
# More details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
|
||||||
|
browser_theme_color = "#032c35"
|
||||||
|
|
||||||
|
# List additional stylesheets to load site-wide.
|
||||||
|
# These stylesheets should be located in your site's `static` directory.
|
||||||
|
# Example: stylesheets = ["extra1.css", "path/extra2.css"]
|
||||||
|
# You can load a stylesheet for a single post by adding it to the [extra] section of the post's front matter, following this same format.
|
||||||
|
stylesheets = [
|
||||||
|
"blog.css",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Show links to previous and next articles at the bottom of posts.
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
|
||||||
|
show_previous_next_article_links = true
|
||||||
|
|
||||||
|
# Invert order of the links to previous and next articles at the bottom of posts.
|
||||||
|
# By default, next articles are on the left side of the page and previous articles are on the right side.
|
||||||
|
# To reverse the order (next articles on the right and previous articles on the left), set it to true.
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
|
||||||
|
invert_previous_next_article_links = true
|
||||||
|
|
||||||
|
# Whether the navigation for previous/next article should match the full width of the site (same as the navigation bar at the top) or the article width.
|
||||||
|
# To match the navigation bar at the top, set it to true.
|
||||||
|
previous_next_article_links_full_width = false
|
||||||
|
|
||||||
|
# Enable a copyright notice for the footer, shown between socials and the "Powered by" text.
|
||||||
|
# $TITLE will be replaced by the website's title.
|
||||||
|
# $CURRENT_YEAR will be replaced by the current year.
|
||||||
|
# $AUTHOR will be replaced by the `author` variable.
|
||||||
|
# $SEPARATOR will be replaced by the `separator` variable.
|
||||||
|
# Markdown is supported (links, emphasis, etc).
|
||||||
|
copyright = "© $CURRENT_YEAR $AUTHOR $SEPARATOR المحتوى الموجود في هذا الموقع متاح بموجب ترخيص [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.ar)."
|
||||||
|
|
||||||
|
# Remote repository for your Zola site.
|
||||||
|
# Used for `show_remote_changes` and `show_remote_source` (see below).
|
||||||
|
# Supports GitHub, GitLab, Gitea, and Codeberg.
|
||||||
|
remote_repository_url = "https://github.com/TheAwiteb/TheAwiteb.github.io"
|
||||||
|
# Set this to "auto" to try and auto-detect the platform based on the repository URL.
|
||||||
|
# Accepted values are "github", "gitlab", "gitea", and "codeberg".
|
||||||
|
remote_repository_git_platform = "github"
|
||||||
|
# Branch in the repo hosting the Zola site.
|
||||||
|
remote_repository_branch = "master"
|
||||||
|
# Show a link to the commit history of updated posts, right next to the last updated date.
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
show_remote_changes = true
|
||||||
|
# Show a link to the repository of the site, right next to the "Powered by Zola & tabi" text.
|
||||||
|
show_remote_source = true
|
||||||
|
|
||||||
|
# Add a "copy" button to codeblocks (loads ~700 bytes of JavaScript).
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
copy_button = true
|
||||||
|
|
||||||
|
# Show the reading time of a page.
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
show_reading_time = true
|
||||||
|
|
||||||
|
# Adds backlinks to footnotes (loads ~500 bytes of JavaScripts).
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
footnote_backlinks = true
|
||||||
|
|
||||||
|
# Enable KaTeX for all posts.
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
katex = false
|
||||||
|
|
||||||
|
# Quick navigation buttons.
|
||||||
|
# Adds "go up" and "go to comments" buttons on the bottom right (hidden for mobile).
|
||||||
|
# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://github.com/welpo/tabi/pull/128
|
||||||
|
quick_navigation_buttons = false
|
||||||
|
|
||||||
|
# Date format used when listing posts (main page, /blog section, tag posts list…)
|
||||||
|
# Default is "6th July 2049" in English and "%d %B %Y" in other languages.
|
||||||
|
long_date_format = "%d %B %Y"
|
||||||
|
|
||||||
|
# Date format used for blog posts.
|
||||||
|
# Default is "6th July 2049" in English and "%-d %B %Y" in other languages.
|
||||||
|
short_date_format = "%d %B %Y"
|
||||||
|
|
||||||
|
# Custom separator used in title tag and posts metadata (between date, time to read, and tags).
|
||||||
|
separator = "•"
|
||||||
|
|
||||||
|
# Use a shorter layout for All tags listing.
|
||||||
|
# Default: tag_name – n post[s]
|
||||||
|
# Compact: tag_name^n (superscript number)
|
||||||
|
compact_tags = false
|
||||||
|
|
||||||
|
# Invert the order of the site title and page title in the browser tab.
|
||||||
|
# Example: true => "Blog • ~/tabi", false => "~/tabi • Blog"
|
||||||
|
invert_title_order = true
|
||||||
|
|
||||||
|
# Full path after the base URL required. So if you were to place it in "static" it would be "/favicon.png"
|
||||||
|
favicon = "/favicon.png"
|
||||||
|
|
||||||
|
# Path to the fallback image for social media cards (the image shown when sharing a link on WhatsApp, LinkedIn…).
|
||||||
|
# Read more: https://nikitahl.com/html-meta-tags-for-social-media
|
||||||
|
# Using an image representative of each post is recommended. You can do so by setting this variable in the [extra] section of a post's front matter.
|
||||||
|
# If a post doesn't have a social media card, the site's default, below, will be used.
|
||||||
|
social_media_card = "img/social_cards/index.jpg"
|
||||||
|
|
||||||
|
menu = [
|
||||||
|
{name = "blog", url = "blog/"},
|
||||||
|
{name = "archive", url = "archive/"},
|
||||||
|
{name = "projects", url = "projects/"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extra menu to show on the footer, below socials section.
|
||||||
|
footer_menu = [
|
||||||
|
{url = "about", name = "about", trailing_slash = true},
|
||||||
|
{url = "pgp", name = "pgp", trailing_slash = true},
|
||||||
|
]
|
||||||
|
|
||||||
|
# The RSS icon will be shown if (1) it's enabled and (2) the following variable is set to true.
|
||||||
|
feed_icon = true
|
||||||
|
|
||||||
|
# Email address for footer's social section.
|
||||||
|
# Protect against spambots:
|
||||||
|
# 1. Use base64 for email (convert at https://www.base64encode.org/ or `printf 'your@email.com' | base64`).
|
||||||
|
# 2. Or, set 'encode_plaintext_email' to true for auto-encoding (only protects on site, not in public repos).
|
||||||
|
email = "QXdpdGViQHBtLm1l"
|
||||||
|
# Decoding requires ~400 bytes of JavaScript. If JS is disabled, the email won't be displayed.
|
||||||
|
encode_plaintext_email = false # Setting is ignored if email is already encoded.
|
||||||
|
|
||||||
|
# The icons available can be found in "social_icons" in the "static" folder
|
||||||
|
socials = [
|
||||||
|
{name = "github", url = "https://github.com/TheAwiteb/", icon = "github"},
|
||||||
|
{name = "telegram", url = "https://t.me/TheAwiteb", icon = "telegram"},
|
||||||
|
{name = "Bassam", url = "https://bassam.social/@me", icon = "mastodon"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Custom security headers. What urls should your website be able to connect to?
|
||||||
|
# You need to specify the CSP and the URLs associated with the directive.
|
||||||
|
# Useful if you want to load remote content safely (embed YouTube videos, which needs frame-src, for example).
|
||||||
|
# Default directive is self.
|
||||||
|
# Default config, allows for https remote images and embedding YouTube and Vimeo content.
|
||||||
|
# This configuration (along with the right webserver settings) gets an A+ in Mozilla's Observatory: https://observatory.mozilla.org
|
||||||
|
allowed_domains = [
|
||||||
|
{directive = "font-src", domains = ["'self'", "data:"]},
|
||||||
|
{directive = "img-src", domains = ["'self'", "https://*", "data:"]},
|
||||||
|
{directive = "script-src", domains = ["'self'"]},
|
||||||
|
{directive = "style-src", domains = ["'self'"]},
|
||||||
|
{directive = "frame-src", domains = ["player.vimeo.com", "https://www.youtube-nocookie.com"]},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Custom subset of characters for the header.
|
||||||
|
# If set to true, the `static/custom_subset.css` file will be loaded first.
|
||||||
|
# This avoids a flashing text issue in Firefox.
|
||||||
|
# Please see https://welpo.github.io/tabi/blog/custom-font-subset/ to learn how to create this file.
|
||||||
|
custom_subset = false
|
||||||
|
|
||||||
|
[extra.utterances]
|
||||||
|
automatic_loading = true
|
||||||
|
dark_theme = "photon-dark"
|
||||||
|
enabled_for_all_posts = true
|
||||||
|
issue_term = "slug"
|
||||||
|
label = "💬"
|
||||||
|
lazy_loading = true
|
||||||
|
light_theme = "github-light"
|
||||||
|
repo = "TheAwiteb/TheAwiteb.github.io"
|
16
content/_index.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
+++
|
||||||
|
path = "/"
|
||||||
|
title = "اخر التدوينات"
|
||||||
|
sort_by = "date"
|
||||||
|
template = "section.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
header = {title = "اهلآ، انا عويتب", img = "/img/awiteb-profile.png"}
|
||||||
|
section_path = "blog/_index.md"
|
||||||
|
social_media_card = "img/social_cards/index.jpg"
|
||||||
|
max_posts = 4
|
||||||
|
+++
|
||||||
|
|
||||||
|
طالب جامعي وهاوي في عالم البرمجة محب للغة Rust وشغوف بالبرمجيات الحرة. هنا اخرج ما يجول في رأسي، وأحاول أن اقدم شيء مفيدة
|
7
content/archive/_index.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
title = "الأرشيف"
|
||||||
|
path = "archive"
|
||||||
|
template = "archive.html"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
+++
|
11
content/blog/_index.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
+++
|
||||||
|
paginate_by = 5
|
||||||
|
path = "/blog"
|
||||||
|
title = "تدويناتي"
|
||||||
|
sort_by = "date"
|
||||||
|
template = "section.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
+++
|
||||||
|
<!-- social_media_card = "img/social_cards/blog.jpg" -->
|
7
content/pages/_index.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
render = false
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
hide_from_feed = true
|
||||||
|
+++
|
46
content/pages/about/index.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
+++
|
||||||
|
title = "عني"
|
||||||
|
template = "info-page.html"
|
||||||
|
path = "about"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
social_media_card = "/img/social_cards/about.jpg"
|
||||||
|
+++
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>المواضيع</summary>
|
||||||
|
<!-- toc -->
|
||||||
|
</details>
|
||||||
|
|
||||||
|
بسم الله الرحمن الرحيم، السلام عليكم ورحمة الله وبركاته. اهلآ بكم في مدونتي، هنا ستتعرف عن المدونة وعن الذي يُقدم بها.
|
||||||
|
|
||||||
|
## من انا؟
|
||||||
|
أنا عويتب طالب جامعي في جامعة الملك عبد العزيز أدرس الإعلام ولم أتخصص به بعد، مساهم في البرمجيات الحرة ومفتوحة المصدر ومحب للغة البرمجة Rust.
|
||||||
|
|
||||||
|
## المحتوى المُقدم
|
||||||
|
هنا سوف اكتب مايدور في رأسي وأحاول ان يكون ذو قيمة، في الغالب مايدور في رأسي يتعلق بالبرمجيات و لغة Rust.
|
||||||
|
|
||||||
|
## كيف تم بناء المدونة
|
||||||
|
هذه المدونة مفتوحة المصدر، يمكنك الوصول إلى شِفرتها البرمجية من هنا [TheAwiteb/TheAwiteb.github.io](https://github.com/TheAwiteb/TheAwiteb.github.io) وهذا يعني انه يمكنك بناء مدونتك اليوم بعد اشتقاقها. تستخدم المدونة مُنشى المواقع الثابتة Zola وتستخدم السمة tabi.
|
||||||
|
|
||||||
|
### مكان إستضافتها
|
||||||
|
يتم استضافتها على [GitHub Pages](https://pages.github.com/) وهي خدمة مقدمة من GitHub لإستضافة المواقع التي لاتتطلب خادم لتعمل.
|
||||||
|
|
||||||
|
### النطاق المُستخدم
|
||||||
|
تستخدم المدونة نطاق [is-a.dev](https://is-a.dev) وهو نطاق مقدم من [Akshay Nair](https://github.com/phenax) إلى العامة لأخذ نطاق فرعي منه بشكل مجاني، لمعرفة المزيد يمكنك إلقاء نظرة على صفحة النطاق.
|
||||||
|
|
||||||
|
## طرق التواصل معي
|
||||||
|
طريقة التواصل التي ستستخدمها للتواصل معي تعتمد على سبب التواصل، وهذه هي الطرق ويجب عليك اختيار طريقة التواصل الصحيحة بناءً على سبب تواصل.
|
||||||
|
|
||||||
|
### تعليقات المدونة
|
||||||
|
لكل تدوينة يوجد خانة تعليقات في أسفلها، ستستخدمها إذا كان سبب التواصل هو الإستفسار عن شيء يخص التدوينة نفسها ولا يخرج عن نطاقها.
|
||||||
|
|
||||||
|
### البريد الإلكتروني
|
||||||
|
تواصل معي عبر البريد الالكتروني إذا كان موضوعك لايتطلب الدردشة ويمكن الرد عليه برسالة واحدة. تجد البريد الخاص بي أسفل جميع صفحات المدونة، وستجد ايضآ مفتاح الـ PGP الخاص بي، ومن المهم أن تستخدمه عند إرسالك رسالة لي، [للمزيد](https://awiteb.is-a.dev/pgp/).
|
||||||
|
|
||||||
|
### التيليجرام
|
||||||
|
تواصل معي عبر تطبيق التيليجرام إذا كان موضوعك يتطلب الدردشة ولايمكن الرد عليه برسالة واحدة. ستجد التيليجرام الخاص بي أسفل جميع صفحات المدونة
|
||||||
|
|
||||||
|
|
||||||
|
## الختام
|
||||||
|
اتمنى ان تنال المدونة على اعجابكم والسلام عليكم ورحمة الله وبركاتة.
|
80
content/pages/pgp/index.md
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
+++
|
||||||
|
title = "مفتاح الـPGP"
|
||||||
|
template = "info-page.html"
|
||||||
|
path = "pgp"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
social_media_card = "/img/social_cards/pgp.jpg"
|
||||||
|
+++
|
||||||
|
|
||||||
|
للتواصل معي بشكل آمن هذا هو مفتاح الـPGP العام الخاص بي:
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>مفتاح الـPGP</summary>
|
||||||
|
<div dir=ltr>
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGW4vgsBEADhexvUlbMz7X0QecUZgwvpzc1KHRSBTFS5Wgdz1xhOPMnYT11z
|
||||||
|
TLlxRpBdAaGXxJyaxavQqYyi9eqGqGvIhTVUM3zpixAydhPM1Z0gR6Svve9AqGjJ
|
||||||
|
QDrLwRBujHd0YF8ke4xVHkkaYJGIIWbMp/yTVj4InhppBMi6rWRMZyKyOIIuFvPh
|
||||||
|
1IsMxlinEOec8G7/CQy8y1v1Js9PakQrSBJMaxRIOuZSacirZU5qUFbthikUynyJ
|
||||||
|
+jrEJMwKIg2jeWuDdO2b0928iKj5hOSEu96KOQYuLGbo3VQGMi3PwFZAhrjdeF8M
|
||||||
|
b5a2BDbPGbtG3u9bhpLTYvrbdD0urStsMXUxVDfOAnt1UQNkrfjhYcNlPyvcMZRb
|
||||||
|
QFgy2nwGminot/i1nEsFHEcYMow2d8sGJYC9i7A6oC2V4LwAuFzWZ68qgYebBllp
|
||||||
|
7bkAwK7JMgYKRBmxBJD+5CP4ihVjcuM9yZr10kFBBqGQUVOLGtEnshXUmJ1iB+zY
|
||||||
|
wXBIEVn0cSe1MGUvLIgKb/0APItGJJD+1hJDnr9RQPDtSGTH67rT3/MGQtjR6YiT
|
||||||
|
m7AVCvkbrTx6nBT7jtCs27myL2FT5TrlOTZhBcNrzcczempvJ/C+QJWIlbMxG1OC
|
||||||
|
77kbi4kZI0ru0cHT3Lhi7PzPIk/uZ+a1oX4EAIgIYKVRZlQhD98gDLYMjwARAQAB
|
||||||
|
tBVBd2l0ZWIgPEF3aXRlYkBwbS5tZT6JAk4EEwEKADgWIQQEp7LcqFGCpldziCQW
|
||||||
|
yK0LScOciAUCZbi+CwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAWyK0L
|
||||||
|
ScOciATuEACAJdT/8w5MOi8lda/S2QVCURKhj0HPcHl0Nb7+ZuhB/ZvYCM55wFl8
|
||||||
|
6eq2vUTdO11FC567TXMbMjxeM/4UvPPUdeR6q+vwi4Xvepq0JW0QN8qa9DX3FRfK
|
||||||
|
NonoZKN/KwpgdRwuecw1OeaSfZvjkWZjpRIZJG/0d5HlLZIPRHLgRCmd2tPqZNV7
|
||||||
|
9evijqQnhHaw8AKB9cNGaCUpVp+RzfEo7DYd4U2/pP+I5f7qYq2gjEK3h1a34Qz0
|
||||||
|
g0nH8q77KKZYyCyXMqa+mYWC3/0Y6MfRRCoG6NxP6A9SdQAw/Gu7sfxsprUqT/Xk
|
||||||
|
GeohcpLcB1AcFOR5fD6FAmEgmmz/tNDBKffAl07NnrytibUcESbVO0qSB+/ibO14
|
||||||
|
/i1BlU/4JGKLfOrnx9+V76gI8shnyAOFDwlj6wPQDrK/BUKGjY48T/yEkWGiiiV1
|
||||||
|
6fUBNp+p2RDIdRX4wEX6ThBzjQXwf43isWTm9HN/arl/heD9VFHkbiUQrTXq8qLU
|
||||||
|
l2hqNqmPodLO8/yIoJuQIVZBppOAkLHpM+6tsLw/cou31msXrwp/V76vJzP1bVWs
|
||||||
|
a8bEW+jRBiR59fF4gePmULBo/n+hoMxPfsQq4/G8++rvhinBWqjgubmEJDrG0qrE
|
||||||
|
LbCj1/1C/1Uw5labutKFDlHrfw2cLHVHPz0lV6tXatXb0iRDcLcLNLkCDQRluL4L
|
||||||
|
ARAArgd/9Zp0aBpjfiOHVG47DNnINDk5fkGkhleVBb1HPnCObiQFQte/cwJJzkZ4
|
||||||
|
sPu+D0Xv88usYCcBwav54QcBTiQHj6ixjzQz7P+D7LHEBLkoj9XJHB9fFXKdt23t
|
||||||
|
2YQ4L2xvh62tlds2yZ4bIy3YehOc++c0USmaRLIxFfCDuPe7nzSKPWtro5T7V1nd
|
||||||
|
RJVxtw8igqzLongALvoNY8i/+tghuVqcvSXDYnmVJx4ciuI+wP0/Uf0yERJfEXIb
|
||||||
|
SEQaypRY+mTZ66zLQG2PU0upywWFmdgRt+ZumLmb5UTTqYNwBdaRQWg3E3OlFeFG
|
||||||
|
UFKdkeB0NX74Ig1HA/+FlT9w6Omf9U9OeQdrH4nrNYME0GYb5uskxFbe5omrHrA/
|
||||||
|
xCfDVsp65kJSztxtD6VKUwijydmRtfWMtudIkihFhXMd1v0E+L+IBPkC4neNJJzE
|
||||||
|
qya8xOUocR2NNbQbd7WrzuHnKoXMnJKsVFyaJs3mevZNaCLx1M3rKf6OLKT2y/94
|
||||||
|
nx+reU+aTJ67GPHvoWJiXyaa2eRE1HJ78t1YiZpJh1x0YvXIwofPt6XCpAgD75Qr
|
||||||
|
vCglVqClKcNAXv/wZeC0hWUCqLNuwZ8IX93pOZAP23vNmtjii/gH5QNXRaaJtc+a
|
||||||
|
8ivDr8iPqcUJeph46dM9Mkd25iZ+Fhw5t5rx621dTFaaLDcAEQEAAYkCNgQYAQoA
|
||||||
|
IBYhBASnstyoUYKmV3OIJBbIrQtJw5yIBQJluL4LAhsMAAoJEBbIrQtJw5yIM5EP
|
||||||
|
/2pwGwgKpxPcTOFpAnIqqeR87i/8yDe/RY+lXgFEmwQhRxWIBPJihISQNCF32Jw4
|
||||||
|
7uYxzZG/arjim/YwVmqHFFqoV02sYOxxYzDUkwGQfoIumO4awb5MbrImr08TPyTi
|
||||||
|
65mUXvJTVWJAQus19QZEF0162c6Ncxo7Cuiiujt3h96RdtiH8dY+CMYHERfwYi3N
|
||||||
|
P1Yy8LMHeyRjXLOn+5vsbnajg1aZybFU57psCzfh0nLbdXnlJU08euORPs3Gq+sV
|
||||||
|
W60JF9jfKdsMQX0TMVNuCfS3ZtFDPXtrGJcZd+sTqYO+O2rwtj0vNmaVe892RMGF
|
||||||
|
mHeMhNEa3KZEFngJykjTSFR/zOw+RtCwTNdOkH3ZlyC7tC0oFi86mGbiS8zY1TyR
|
||||||
|
vfnbLVcy8D0ymjQ9J35ZTK8HmdQhU1cD04ES1avfO5xksgZITY3Nwahld9PudXPR
|
||||||
|
JcppQUURoZjK7E28+UOBemW/sm+O7+WjIHAWi0FVfUA6Y/cLY87tUDAh4hKjyE0F
|
||||||
|
g+IwsICIu+tsCf965AldFEKVsuTwUhLuJInEHEQcWlGoZBfkkxbJl+NWWtuEKF08
|
||||||
|
8Z/Nq3GTmbF2KTKBG1jMWOVRUBtl1rNdh8+GhfpVMPvdYTBg0L5Sk/l8Qki7aO8q
|
||||||
|
IJtqbB8SmKxXEU1kiIALlOGmPkHMASb30SrB1zftwZLw
|
||||||
|
=oSci
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## إذا كان بريدك لايدعم الـPGP
|
||||||
|
إذا كان بريدك الإلكتروني لايدعم ال PGP، يمكنك كتابة رسالتك هنا في [pgp.help] وإرسالها مشفرة وسأقوم بالرد عليك
|
||||||
|
|
||||||
|
## تنويه قبل الإرسال
|
||||||
|
يُفضل إرفاق مفتاحك العام مع الرسالة، ليمكنني الرد عليك بشكل آمن
|
||||||
|
|
||||||
|
[pgp.help]: https://pgp.help/#/permalink?pgp=-----BEGIN%20PGP%20PUBLIC%20KEY%20BLOCK-----%0D%0AVersion:%20OpenPGP.js%20v1.5.7%0D%0AComment:%20https:~2F~2Fpgp.help%0D%0A%0D%0AxsFNBGW4vgsBEADhexvUlbMz7X0QecUZgwvpzc1KHRSBTFS5Wgdz1xhOPMnY%0AT11zTLlxRpBdAaGXxJyaxavQqYyi9eqGqGvIhTVUM3zpixAydhPM1Z0gR6Sv%0Ave9AqGjJQDrLwRBujHd0YF8ke4xVHkkaYJGIIWbMp~2FyTVj4InhppBMi6rWRM%0AZyKyOIIuFvPh1IsMxlinEOec8G7~2FCQy8y1v1Js9PakQrSBJMaxRIOuZSacir%0AZU5qUFbthikUynyJ%2BjrEJMwKIg2jeWuDdO2b0928iKj5hOSEu96KOQYuLGbo%0A3VQGMi3PwFZAhrjdeF8Mb5a2BDbPGbtG3u9bhpLTYvrbdD0urStsMXUxVDfO%0AAnt1UQNkrfjhYcNlPyvcMZRbQFgy2nwGminot~2Fi1nEsFHEcYMow2d8sGJYC9%0Ai7A6oC2V4LwAuFzWZ68qgYebBllp7bkAwK7JMgYKRBmxBJD%2B5CP4ihVjcuM9%0AyZr10kFBBqGQUVOLGtEnshXUmJ1iB%2BzYwXBIEVn0cSe1MGUvLIgKb~2F0APItG%0AJJD%2B1hJDnr9RQPDtSGTH67rT3~2FMGQtjR6YiTm7AVCvkbrTx6nBT7jtCs27my%0AL2FT5TrlOTZhBcNrzcczempvJ~2FC%2BQJWIlbMxG1OC77kbi4kZI0ru0cHT3Lhi%0A7PzPIk~2FuZ%2Ba1oX4EAIgIYKVRZlQhD98gDLYMjwARAQABzRVBd2l0ZWIgPEF3%0AaXRlYkBwbS5tZT7CwY4EEwEKADgWIQQEp7LcqFGCpldziCQWyK0LScOciAUC%0AZbi%2BCwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAWyK0LScOciATu%0AEACAJdT~2F8w5MOi8lda~2FS2QVCURKhj0HPcHl0Nb7%2BZuhB~2FZvYCM55wFl86eq2%0AvUTdO11FC567TXMbMjxeM~2F4UvPPUdeR6q%2Bvwi4Xvepq0JW0QN8qa9DX3FRfK%0ANonoZKN~2FKwpgdRwuecw1OeaSfZvjkWZjpRIZJG~2F0d5HlLZIPRHLgRCmd2tPq%0AZNV79evijqQnhHaw8AKB9cNGaCUpVp%2BRzfEo7DYd4U2~2FpP%2BI5f7qYq2gjEK3%0Ah1a34Qz0g0nH8q77KKZYyCyXMqa%2BmYWC3~2F0Y6MfRRCoG6NxP6A9SdQAw~2FGu7%0AsfxsprUqT~2FXkGeohcpLcB1AcFOR5fD6FAmEgmmz~2FtNDBKffAl07NnrytibUc%0AESbVO0qSB%2B~2FibO14~2Fi1BlU~2F4JGKLfOrnx9%2BV76gI8shnyAOFDwlj6wPQDrK~2F%0ABUKGjY48T~2FyEkWGiiiV16fUBNp%2Bp2RDIdRX4wEX6ThBzjQXwf43isWTm9HN~2F%0Aarl~2FheD9VFHkbiUQrTXq8qLUl2hqNqmPodLO8~2FyIoJuQIVZBppOAkLHpM%2B6t%0AsLw~2Fcou31msXrwp~2FV76vJzP1bVWsa8bEW%2BjRBiR59fF4gePmULBo~2Fn%2BhoMxP%0AfsQq4~2FG8%2B%2BrvhinBWqjgubmEJDrG0qrELbCj1~2F1C~2F1Uw5labutKFDlHrfw2c%0ALHVHPz0lV6tXatXb0iRDcLcLNM7BTQRluL4LARAArgd~2F9Zp0aBpjfiOHVG47%0ADNnINDk5fkGkhleVBb1HPnCObiQFQte~2FcwJJzkZ4sPu%2BD0Xv88usYCcBwav5%0A4QcBTiQHj6ixjzQz7P%2BD7LHEBLkoj9XJHB9fFXKdt23t2YQ4L2xvh62tlds2%0AyZ4bIy3YehOc%2B%2Bc0USmaRLIxFfCDuPe7nzSKPWtro5T7V1ndRJVxtw8igqzL%0AongALvoNY8i~2F%2BtghuVqcvSXDYnmVJx4ciuI%2BwP0~2FUf0yERJfEXIbSEQaypRY%0A%2BmTZ66zLQG2PU0upywWFmdgRt%2BZumLmb5UTTqYNwBdaRQWg3E3OlFeFGUFKd%0AkeB0NX74Ig1HA~2F%2BFlT9w6Omf9U9OeQdrH4nrNYME0GYb5uskxFbe5omrHrA~2F%0AxCfDVsp65kJSztxtD6VKUwijydmRtfWMtudIkihFhXMd1v0E%2BL%2BIBPkC4neN%0AJJzEqya8xOUocR2NNbQbd7WrzuHnKoXMnJKsVFyaJs3mevZNaCLx1M3rKf6O%0ALKT2y~2F94nx%2BreU%2BaTJ67GPHvoWJiXyaa2eRE1HJ78t1YiZpJh1x0YvXIwofP%0At6XCpAgD75QrvCglVqClKcNAXv~2FwZeC0hWUCqLNuwZ8IX93pOZAP23vNmtji%0Ai~2FgH5QNXRaaJtc%2Ba8ivDr8iPqcUJeph46dM9Mkd25iZ%2BFhw5t5rx621dTFaa%0ALDcAEQEAAcLBdgQYAQoAIBYhBASnstyoUYKmV3OIJBbIrQtJw5yIBQJluL4L%0AAhsMAAoJEBbIrQtJw5yIM5EP~2F2pwGwgKpxPcTOFpAnIqqeR87i~2F8yDe~2FRY%2Bl%0AXgFEmwQhRxWIBPJihISQNCF32Jw47uYxzZG~2Farjim~2FYwVmqHFFqoV02sYOxx%0AYzDUkwGQfoIumO4awb5MbrImr08TPyTi65mUXvJTVWJAQus19QZEF0162c6N%0Acxo7Cuiiujt3h96RdtiH8dY%2BCMYHERfwYi3NP1Yy8LMHeyRjXLOn%2B5vsbnaj%0Ag1aZybFU57psCzfh0nLbdXnlJU08euORPs3Gq%2BsVW60JF9jfKdsMQX0TMVNu%0ACfS3ZtFDPXtrGJcZd%2BsTqYO%2BO2rwtj0vNmaVe892RMGFmHeMhNEa3KZEFngJ%0AykjTSFR~2FzOw%2BRtCwTNdOkH3ZlyC7tC0oFi86mGbiS8zY1TyRvfnbLVcy8D0y%0AmjQ9J35ZTK8HmdQhU1cD04ES1avfO5xksgZITY3Nwahld9PudXPRJcppQUUR%0AoZjK7E28%2BUOBemW~2Fsm%2BO7%2BWjIHAWi0FVfUA6Y~2FcLY87tUDAh4hKjyE0Fg%2BIw%0AsICIu%2BtsCf965AldFEKVsuTwUhLuJInEHEQcWlGoZBfkkxbJl%2BNWWtuEKF08%0A8Z~2FNq3GTmbF2KTKBG1jMWOVRUBtl1rNdh8%2BGhfpVMPvdYTBg0L5Sk~2Fl8Qki7%0AaO8qIJtqbB8SmKxXEU1kiIALlOGmPkHMASb30SrB1zftwZLw%0D%0A%3DJTtY%0D%0A-----END%20PGP%20PUBLIC%20KEY%20BLOCK-----%0D%0A%0D%0A
|
9
content/projects/_index.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
+++
|
||||||
|
title = "مشاريعي"
|
||||||
|
sort_by = "weight"
|
||||||
|
template = "cards.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
social_media_card = "/img/social_cards/projects.jpg"
|
||||||
|
+++
|
32
content/projects/salvo-captcha/index.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
+++
|
||||||
|
title = "الوسيطة salvo-captcha"
|
||||||
|
description = "وسيطة (middleware) في إطار العمل salvo تمكنك من إضافة CAPTCHA إلى تطبيقك بسهولة"
|
||||||
|
weight = 30
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
local_image = "/img/projects/salvo-captcha/banner.png"
|
||||||
|
canonical_url = "https://awiteb.is-a.dev/projects/doteki/"
|
||||||
|
+++
|
||||||
|
<!-- social_media_card = "social_cards/projects_doteki.jpg" -->
|
||||||
|
|
||||||
|
هي وسيطة تساعدك على اضافة CAPTCHA إلى تطبيقك بسهولة.
|
||||||
|
|
||||||
|
## مثال
|
||||||
|
![المثال](/img/projects/salvo-captcha/example.gif)
|
||||||
|
|
||||||
|
## طريقة إستخدامها
|
||||||
|
|
||||||
|
اولآ عليك ان تضيفها في `Cargo.toml` كالتالي:
|
||||||
|
|
||||||
|
<div dir=ltr>
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
# ...
|
||||||
|
salvo-captcha = "0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
.... لم يتم الانتهاء من الكتابة
|
||||||
|
|
197
scripts/article.py
Executable file
|
@ -0,0 +1,197 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Script to manage articles.
|
||||||
|
#
|
||||||
|
# Current functionalities:
|
||||||
|
# - Update the header of an article or add a new header (command: update)
|
||||||
|
#
|
||||||
|
# License: MIT License
|
||||||
|
# Copyright: (c) 2024 Awiteb <Awiteb@pm.me>
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def help(command: str | None) -> str:
|
||||||
|
if command is None:
|
||||||
|
help_message = """Script to manage articles.
|
||||||
|
Usage: ./scripts/article.py <command>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
- update: Update the header of an article or add a new header
|
||||||
|
- new: Create a new article
|
||||||
|
- card: Create a new social card for an article
|
||||||
|
- help: Folloing by a command, it will display the help of the command
|
||||||
|
"""
|
||||||
|
elif command == "update":
|
||||||
|
help_message = """Update the header of an article or add a new header
|
||||||
|
Usage: ./scripts/article.py update <article_name> <header_scope> <header_name> <header_value>
|
||||||
|
Example: ./scripts/article.py update "hello-world" global "title" "Hello World"
|
||||||
|
Header scopes: [global, taxonomies, extra]
|
||||||
|
"""
|
||||||
|
elif command == "new":
|
||||||
|
help_message = """Create a new article
|
||||||
|
Usage: ./scripts/article.py new <article_name>
|
||||||
|
Example: ./scripts/article.py new "hello-world"
|
||||||
|
"""
|
||||||
|
elif command == "card":
|
||||||
|
help_message = """Create a new social card for an article
|
||||||
|
Usage: ./scripts/article.py card <article_name>
|
||||||
|
Example: ./scripts/article.py card "hello-world"
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
help_message = f"The command `{command}` is not recognized. Available commands: [update, help]"
|
||||||
|
return help_message
|
||||||
|
|
||||||
|
def now() -> str:
|
||||||
|
"""Return the current date in the format "YYYY-MM-DD"""
|
||||||
|
return datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
class Article:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.path = f"content/blog/{name}/index.md"
|
||||||
|
self.headers = {
|
||||||
|
"global": {},
|
||||||
|
"taxonomies": {},
|
||||||
|
"extra": {},
|
||||||
|
}
|
||||||
|
self.content = ""
|
||||||
|
|
||||||
|
self.__reload_header()
|
||||||
|
self.__reload_content()
|
||||||
|
|
||||||
|
def __reload_header(self) -> None:
|
||||||
|
"""Reloads the content of the article
|
||||||
|
|
||||||
|
The header of the article is between the first and second "+++" in the file.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(self.path, "r") as file:
|
||||||
|
current_header = "global"
|
||||||
|
|
||||||
|
for idx, line in enumerate(file):
|
||||||
|
if line == "+++\n" and idx > 0:
|
||||||
|
break
|
||||||
|
if (line == "+++\n" and idx == 0) or line.isspace():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update the current header if the line is a header
|
||||||
|
if line.startswith("[") and line.endswith("]\n"):
|
||||||
|
current_header = line[1:-2]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len((splited_line := line.split("="))) == 2:
|
||||||
|
key, value = (splited_line[0], splited_line[1])
|
||||||
|
self.headers[current_header][key.strip()] = value.strip()
|
||||||
|
|
||||||
|
def __reload_content(self) -> None:
|
||||||
|
"""Reloads the content of the article"""
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(self.path, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
# Remove the header from the file, and keep the rest as the content of the article
|
||||||
|
self.content = content[content.index("+++", 3) + 3 :].strip() + "\n"
|
||||||
|
|
||||||
|
def export(self) -> None:
|
||||||
|
"""Rewrite the file with the updated headers and content"""
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
||||||
|
with open(self.path, "w") as file:
|
||||||
|
file.write("+++\n")
|
||||||
|
for scope, headers in self.headers.items():
|
||||||
|
if scope != "global":
|
||||||
|
file.write(f"[{scope}]\n")
|
||||||
|
for key, value in headers.items():
|
||||||
|
file.write(f"{key} = {value}\n")
|
||||||
|
file.write("\n")
|
||||||
|
file.write("+++\n")
|
||||||
|
file.write(self.content)
|
||||||
|
|
||||||
|
|
||||||
|
def update_header(
|
||||||
|
article_name: str, header_scope: str, header_name: str, header_value: str
|
||||||
|
) -> None:
|
||||||
|
if header_scope not in ["global", "taxonomies", "extra"]:
|
||||||
|
print(
|
||||||
|
f"The header scope {header_scope} is not recognized. Available scopes: [global, taxonomies, extra]"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
article = Article(article_name)
|
||||||
|
if (
|
||||||
|
article.headers[header_scope].get(header_name) is not None
|
||||||
|
and article.headers[header_scope][header_name].startswith('"')
|
||||||
|
) or all([c.isalpha() or c.isspace() for c in header_value]):
|
||||||
|
header_value = f'"{header_value}"'
|
||||||
|
article.headers[header_scope][header_name] = header_value
|
||||||
|
article.export()
|
||||||
|
print("The header has been updated successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
def create_new_article(article_name: str) -> None:
|
||||||
|
article = Article(article_name)
|
||||||
|
if os.path.exists(article.path):
|
||||||
|
print(f"The article {article_name} already exists.")
|
||||||
|
sys.exit(1)
|
||||||
|
article.headers["global"]["title"] = '""'
|
||||||
|
article.headers["global"]["description"] = '""'
|
||||||
|
article.headers["global"]["date"] = now()
|
||||||
|
article.headers["global"]["updated"] = now()
|
||||||
|
article.headers["global"]["draft"] = "true"
|
||||||
|
|
||||||
|
article.headers["taxonomies"]["tags"] = '["tag1", "tag2"]'
|
||||||
|
|
||||||
|
article.content = "Write your content here."
|
||||||
|
article.export()
|
||||||
|
print(f"The article {article_name} has been created successfully.")
|
||||||
|
print(f"Path: {article.path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_social_card(article_name: str) -> None:
|
||||||
|
article = Article(article_name)
|
||||||
|
if not os.path.exists(article.path):
|
||||||
|
print(f"The article {article_name} does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
os.system(
|
||||||
|
f"./scripts/social-cards-zola -i blog/{article_name}/index.md -b http://127.0.0.1:1111 -o static/img/social_cards/"
|
||||||
|
)
|
||||||
|
print(f"The social card for the article {article_name} has been created successfully.")
|
||||||
|
print(f"The url path is: /img/social_cards/blog_{article_name.replace('-', '_')}.jpg")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Check the current working directory, it should be the root of the project
|
||||||
|
if not (
|
||||||
|
"content" in os.listdir()
|
||||||
|
and "scripts" in os.listdir()
|
||||||
|
and "static" in os.listdir()
|
||||||
|
):
|
||||||
|
print("The script should be executed from the root of the project.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(help(None))
|
||||||
|
sys.exit(1)
|
||||||
|
elif sys.argv[1] == "help" and len(sys.argv) == 3:
|
||||||
|
print(help(sys.argv[2]))
|
||||||
|
elif sys.argv[1] == "update" and len(sys.argv) == 6:
|
||||||
|
article_name, header_scope, header_name, header_value = sys.argv[2:]
|
||||||
|
update_header(article_name, header_scope, header_name, header_value)
|
||||||
|
elif sys.argv[1] == "new" and len(sys.argv) == 3:
|
||||||
|
create_new_article(sys.argv[2])
|
||||||
|
elif sys.argv[1] == "card" and len(sys.argv) == 3:
|
||||||
|
create_social_card(sys.argv[2])
|
||||||
|
else:
|
||||||
|
print(help(None))
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
228
scripts/social-cards-zola
Executable file
|
@ -0,0 +1,228 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# This script takes a markdown post, crafts the corresponding URL, checks if it's accessible,
|
||||||
|
# takes a screenshot, and saves it to a specified location.
|
||||||
|
# It can update the front matter of the post with the path to the generated image (-u | --update-front-matter option).
|
||||||
|
# It's meant to be used as a pre-commit hook to generate social media cards for Zola sites using the tabi theme.
|
||||||
|
# More details: https://osc.garden/blog/automating-social-media-cards-zola/
|
||||||
|
|
||||||
|
function help_function(){
|
||||||
|
echo "This script automates the creation of social media cards for Zola websites."
|
||||||
|
echo "It takes a Markdown post and saves its live screenshot to a specified location."
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT! It needs to be run from the root of the Zola site."
|
||||||
|
echo ""
|
||||||
|
echo "Usage: social-cards-zola [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -h, --help Show this help message and exit."
|
||||||
|
echo " -b, --base_url URL The base URL where the Zola site is hosted. Default is http://127.0.0.1:1111."
|
||||||
|
echo " -i, --input INPUT_PATH The relative path to the markdown file of the post/section you want to capture. Should be in the format 'content/blog/post_name.language.md'."
|
||||||
|
echo " -k, --key KEY The front matter key to update. Default is 'social_media_card'."
|
||||||
|
echo " -o, --output_path PATH The directory where the generated image will be saved."
|
||||||
|
echo " -p, --print_output Print the path to the resulting screenshot at the end."
|
||||||
|
echo " -u, --update-front-matter Update or add the 'social_media_card' key in the front matter of the Markdown file."
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo " social-cards-zola --base_url https://example.com --input content/blog/my_post.md --output_path static/img/social_cards"
|
||||||
|
echo " social-cards-zola -u -b http://127.0.0.1:1025 -i content/archive/_index.es.md -o static/img"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function convert_filename_to_url() {
|
||||||
|
# Remove .md extension.
|
||||||
|
local post_name="${1%.md}"
|
||||||
|
|
||||||
|
# Remove "content/" prefix.
|
||||||
|
local url="${post_name#content/}"
|
||||||
|
|
||||||
|
# Extract language code.
|
||||||
|
local lang_code="${url##*.}"
|
||||||
|
if [[ "$lang_code" == "$url" ]]; then
|
||||||
|
lang_code="" # No language code.
|
||||||
|
else
|
||||||
|
lang_code="${lang_code}/" # Add trailing slash.
|
||||||
|
url="${url%.*}" # Remove the language code from the URL.
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle co-located index.md by stripping it and using the directory as the URL.
|
||||||
|
if [[ "$url" == */index ]]; then
|
||||||
|
url="${url%/*}" # Remove the /index suffix.
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove "_index" suffix.
|
||||||
|
if [[ "$url" == *"_index"* ]]; then
|
||||||
|
url="${url%%_index*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return the final URL with a single trailing slash.
|
||||||
|
full_url="${lang_code}${url}"
|
||||||
|
echo "${full_url%/}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
function error_exit() {
|
||||||
|
echo "ERROR: $1" >&2
|
||||||
|
exit "${2:-1}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate_input_params() {
|
||||||
|
missing_params=()
|
||||||
|
if [[ -z "$base_url" ]]; then
|
||||||
|
missing_params+=("base_url")
|
||||||
|
fi
|
||||||
|
if [[ -z "$input" ]]; then
|
||||||
|
missing_params+=("input")
|
||||||
|
fi
|
||||||
|
if [[ -z "$output_path" ]]; then
|
||||||
|
missing_params+=("output_path")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#missing_params[@]} -ne 0 ]; then
|
||||||
|
error_exit "The following required settings are missing: ${missing_params[*]}. Use -h or --help for usage."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_dependencies() {
|
||||||
|
for cmd in "curl" "shot-scraper"; do
|
||||||
|
if ! command -v $cmd &> /dev/null; then
|
||||||
|
error_exit "$cmd could not be found. Please install it."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_status() {
|
||||||
|
local retry_count=0
|
||||||
|
local max_retries=5
|
||||||
|
local status
|
||||||
|
while [[ $retry_count -lt $max_retries ]]; do
|
||||||
|
status=$(curl -s -o /dev/null -I -w "%{http_code}" "${base_url}${post_url}")
|
||||||
|
if [[ "$status" -eq "200" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
retry_count=$((retry_count + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
error_exit "Post $input is not accessible. Max retries ($max_retries) reached."
|
||||||
|
}
|
||||||
|
|
||||||
|
function capture_screenshot() {
|
||||||
|
temp_file=$(mktemp /tmp/social-zola.XXXXXX)
|
||||||
|
trap 'rm -f "$temp_file"' EXIT
|
||||||
|
shot-scraper --silent "${base_url}/${post_url}" -w 700 -h 400 --retina --quality 60 -o "$temp_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
function move_file() {
|
||||||
|
local safe_filename=$(echo "${post_url%/}" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
|
|
||||||
|
# Create the output directory if it doesn't exist.
|
||||||
|
mkdir -p "$output_path"
|
||||||
|
|
||||||
|
image_filename="${output_path}/${safe_filename:-index}.jpg" # If the filename is empty, use "index".
|
||||||
|
mv "$temp_file" "$image_filename" || error_exit "Failed to move the file to $image_filename"
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_front_matter {
|
||||||
|
local md_file_path="$1"
|
||||||
|
local image_output="${2#static/}"
|
||||||
|
# Temporary file for awk processing
|
||||||
|
temp_awk=$(mktemp /tmp/frontmatter.XXXXXX)
|
||||||
|
|
||||||
|
awk -v card_path="$image_output" '
|
||||||
|
# Initialize flags for tracking state.
|
||||||
|
BEGIN { in_extra=done=front_matter=extra_exists=0; }
|
||||||
|
|
||||||
|
# Function to insert the social_media_card path.
|
||||||
|
function insert_card() { print "social_media_card = \"" card_path "\""; done=1; }
|
||||||
|
|
||||||
|
{
|
||||||
|
# If card has been inserted, simply output remaining lines.
|
||||||
|
if (done) { print; next; }
|
||||||
|
|
||||||
|
# Toggle front_matter flag at its start, denoted by +++
|
||||||
|
if (/^\+\+\+/ && front_matter == 0) {
|
||||||
|
front_matter = 1;
|
||||||
|
print "+++";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect [extra] section and set extra_exists flag.
|
||||||
|
if (/^\[extra\]/) { in_extra=1; extra_exists=1; print; next; }
|
||||||
|
|
||||||
|
# Update existing social_media_card.
|
||||||
|
if (in_extra && /^social_media_card =/) { insert_card(); in_extra=0; next; }
|
||||||
|
|
||||||
|
# End of front matter or start of new section.
|
||||||
|
if (in_extra && (/^\[[a-zA-Z_-]+\]/ || (/^\+\+\+/ && front_matter == 1))) {
|
||||||
|
insert_card(); # Add the missing social_media_card.
|
||||||
|
in_extra=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Insert missing [extra] section.
|
||||||
|
if (/^\+\+\+/ && front_matter == 1 && in_extra == 0 && extra_exists == 0) {
|
||||||
|
print "\n[extra]";
|
||||||
|
insert_card();
|
||||||
|
in_extra=0;
|
||||||
|
front_matter = 0;
|
||||||
|
print "+++";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print all other lines as-is.
|
||||||
|
print;
|
||||||
|
}' "$md_file_path" > "$temp_awk"
|
||||||
|
|
||||||
|
# Move the temporary file back to the original markdown file.
|
||||||
|
mv "$temp_awk" "$md_file_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help)
|
||||||
|
help_function;;
|
||||||
|
-b|--base_url)
|
||||||
|
base_url="$2"
|
||||||
|
shift 2;;
|
||||||
|
-i|--input)
|
||||||
|
input="$2"
|
||||||
|
shift 2;;
|
||||||
|
-o|--output_path)
|
||||||
|
output_path="$2"
|
||||||
|
shift 2;;
|
||||||
|
-k|--key)
|
||||||
|
front_matter_key="$2"
|
||||||
|
shift 2;;
|
||||||
|
-u|--update-front-matter)
|
||||||
|
update="true"
|
||||||
|
shift 1;;
|
||||||
|
-p|--print_output)
|
||||||
|
print_output="true"
|
||||||
|
shift 1;;
|
||||||
|
*)
|
||||||
|
error_exit "Unknown option: $1";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
validate_input_params
|
||||||
|
check_dependencies
|
||||||
|
|
||||||
|
: "${base_url:="http://127.0.0.1:1111"}"
|
||||||
|
: "${front_matter_key:="social_media_card"}"
|
||||||
|
base_url="${base_url%/}/" # Ensure one trailing slash.
|
||||||
|
post_url="$(convert_filename_to_url "$input")"
|
||||||
|
|
||||||
|
fetch_status
|
||||||
|
capture_screenshot
|
||||||
|
move_file
|
||||||
|
|
||||||
|
if [[ "$update" == "true" ]]; then
|
||||||
|
update_front_matter "$input" "$image_filename"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$print_output" == "true" ]]; then
|
||||||
|
echo "$image_filename"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
14
scripts/update-posts.nu
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#! /usr/bin/env nu
|
||||||
|
|
||||||
|
def main () {
|
||||||
|
let paths: list<string> = (git diff --name-only | split row "\n" | filter {|p| $p | str contains "content/blog"});
|
||||||
|
let posts: string = (ls content/blog | filter {|p| $p.type == "dir"} | each {|p| $p.name | sed 's/content\/blog\///g'});
|
||||||
|
|
||||||
|
for post in $posts {
|
||||||
|
if $"content/blog/($post)/index.md" in $paths {
|
||||||
|
echo $"`($post)` post has been modified";
|
||||||
|
python3 ./scripts/article.py card $post
|
||||||
|
python3 ./scripts/article.py update $post "global" "updated" $"(date now | format date '%Y-%m-%d')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
static/blog.css
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "pfdintextuniversal-regular-1";
|
||||||
|
src: url(fonts/pfdintextuniversal-regular-1.otf) format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "pfdintextuniversal-regular-1" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background: #0f1315 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #16222d !important;
|
||||||
|
color: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
code {
|
||||||
|
background: #1a2127 !important;
|
||||||
|
color: #488bc9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #121920 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.card {
|
||||||
|
background: #151a1d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.card:hover {
|
||||||
|
background: #1a2127 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards .card:hover .card-description {
|
||||||
|
color: #D4D4D4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: #bbb7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #d3d3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #436b8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #325069;
|
||||||
|
background-color: #0f1315;
|
||||||
|
}
|
||||||
|
|
||||||
|
img#banner-home-img {
|
||||||
|
padding: 0.3rem;
|
||||||
|
}
|
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
static/fonts/pfdintextuniversal-regular-1.otf
Normal file
BIN
static/img/awiteb-profile.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
static/img/projects/salvo-captcha/banner.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
static/img/projects/salvo-captcha/example.gif
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
static/img/social_cards/about.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
static/img/social_cards/index.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
static/img/social_cards/pgp.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
static/img/social_cards/projects.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
1
themes/tabi
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 8537e301f7656870255f9066aed9aa874f7c168d
|