adding
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Article
|
||||
|
||||
|
||||
@admin.register(Article)
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "slug", "is_published", "published_at")
|
||||
list_filter = ("is_published",)
|
||||
search_fields = ("title", "slug", "excerpt")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
fields = (
|
||||
"title",
|
||||
"slug",
|
||||
"excerpt",
|
||||
"meta_description",
|
||||
"content_html",
|
||||
"read_minutes",
|
||||
"is_published",
|
||||
"published_at",
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LandingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.landing"
|
||||
verbose_name = "مقالات"
|
||||
@@ -0,0 +1,49 @@
|
||||
"""تبدیل تاریخ میلادی به شمسی برای نمایش تاریخ انتشار مقاله — بدون وابستگی
|
||||
به پکیج خارجی (همان الگوریتم استاندارد تبدیل جلالی)."""
|
||||
|
||||
_PERSIAN_MONTHS = [
|
||||
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
|
||||
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند",
|
||||
]
|
||||
_PERSIAN_DIGITS = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
|
||||
|
||||
|
||||
def gregorian_to_jalali(date) -> tuple[int, int, int]:
|
||||
gy, gm, gd = date.year, date.month, date.day
|
||||
g_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
gy2 = gy - 1600
|
||||
gm2 = gm - 1
|
||||
gd2 = gd - 1
|
||||
g_day_no = 365 * gy2 + (gy2 + 3) // 4 - (gy2 + 99) // 100 + (gy2 + 399) // 400
|
||||
for i in range(gm2):
|
||||
g_day_no += g_days_in_month[i]
|
||||
if gm2 > 1 and ((gy % 4 == 0 and gy % 100 != 0) or gy % 400 == 0):
|
||||
g_day_no += 1
|
||||
g_day_no += gd2
|
||||
|
||||
j_day_no = g_day_no - 79
|
||||
j_np = j_day_no // 12053
|
||||
j_day_no %= 12053
|
||||
jy = 979 + 33 * j_np + 4 * (j_day_no // 1461)
|
||||
j_day_no %= 1461
|
||||
if j_day_no >= 366:
|
||||
jy += (j_day_no - 1) // 365
|
||||
j_day_no = (j_day_no - 1) % 365
|
||||
|
||||
j_days_in_month = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
|
||||
jm = 0
|
||||
for i, days in enumerate(j_days_in_month):
|
||||
if j_day_no < days:
|
||||
jm = i
|
||||
break
|
||||
j_day_no -= days
|
||||
else:
|
||||
jm = 11
|
||||
jd = j_day_no + 1
|
||||
return jy, jm + 1, jd
|
||||
|
||||
|
||||
def format_jalali(date) -> str:
|
||||
jy, jm, jd = gregorian_to_jalali(date)
|
||||
text = f"{jd} {_PERSIAN_MONTHS[jm - 1]} {jy}"
|
||||
return text.translate(_PERSIAN_DIGITS)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 21:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Article',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(max_length=120, unique=True, verbose_name='نامک (slug)')),
|
||||
('title', models.CharField(max_length=200, verbose_name='عنوان')),
|
||||
('meta_description', models.CharField(blank=True, help_text='در تگ\u200cهای meta description و og:description استفاده می\u200cشود.', max_length=300, verbose_name='توضیح متا (SEO)')),
|
||||
('excerpt', models.CharField(help_text='همان متن کوتاهی که زیر عنوان در لیست مقالات نشان داده می\u200cشود.', max_length=300, verbose_name='خلاصه')),
|
||||
('content_html', models.TextField(help_text='محتوای مقاله با تگ\u200cهای HTML ساده مثل <p>، <h2>، <strong>.', verbose_name='محتوا (HTML)')),
|
||||
('read_minutes', models.PositiveSmallIntegerField(default=3, verbose_name='زمان مطالعه (دقیقه)')),
|
||||
('is_published', models.BooleanField(default=True, verbose_name='منتشرشده')),
|
||||
('published_at', models.DateField(verbose_name='تاریخ انتشار')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'مقاله',
|
||||
'verbose_name_plural': 'مقالات',
|
||||
'ordering': ('-published_at', '-created_at'),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,210 @@
|
||||
import datetime
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
ARTICLES = [
|
||||
{
|
||||
"slug": "peyam-goruhi-ba-shomare",
|
||||
"title": "پیام گروهی با شماره چیست و چطور بدون مسدود شدن ارسال کنیم؟",
|
||||
"meta_description": "راهنمای ارسال پیام گروهی با شماره: فرق روش رسمی (Business/Bot API) با روشهای غیررسمی، و چرا روش دوم دیر یا زود منجر به مسدود شدن میشود.",
|
||||
"excerpt": "فرق ارسال گروهی رسمی با روشهای غیررسمی، و چرا مسیر Business/Bot API تنها راه پایدار است.",
|
||||
"read_minutes": 4,
|
||||
"content_html": """
|
||||
<p>
|
||||
«پیام گروهی با شماره» یعنی ارسال یک پیام (پیامک، واتساپ، تلگرام و…) به
|
||||
لیستی از شمارهها، معمولاً با متنی که برای هر گیرنده کمی شخصیسازی
|
||||
شده — مثلاً با نام یا یک اطلاعات اختصاصی دیگر. سوالی که همیشه پشت این
|
||||
نیاز میآید این است: چطور این کار را انجام بدهیم بدون اینکه شماره یا
|
||||
اکانت فرستنده مسدود شود؟
|
||||
</p>
|
||||
|
||||
<h2>چرا اکانتها مسدود میشوند</h2>
|
||||
<p>
|
||||
واتساپ، تلگرام، بله و ایتا همه یک سیستم ضدهرزنامه دارند که رفتار
|
||||
غیرطبیعی (ارسال حجم بالا از یک شمارهی معمولی، چرخاندن چند دستگاه/سشن،
|
||||
شبیهسازی رفتار انسانی برای فرار از تشخیص) را شناسایی و مسدود میکند.
|
||||
این محدودیت تصادفی نیست — دقیقاً همان مکانیزمی است که جلوی اسپم واقعی
|
||||
را میگیرد.
|
||||
</p>
|
||||
|
||||
<h2>راه پایدار: مسیر رسمی هر پلتفرم</h2>
|
||||
<p>
|
||||
هر پلتفرم یک راه رسمی برای ارسال حجمی به کسبوکارها دارد:
|
||||
</p>
|
||||
<p>
|
||||
<strong>پیامک</strong> از طریق درگاههای بالک اپراتور، <strong>واتساپ</strong>
|
||||
از طریق WhatsApp Business Platform (نیاز به تایید Template پیام دارد)،
|
||||
و <strong>تلگرام/بله/ایتا</strong> از طریق Bot API — با یک محدودیت مهم:
|
||||
بات فقط میتواند به کسانی پیام بدهد که خودشان قبلاً چت را با بات شروع
|
||||
کردهاند (opt-in).
|
||||
</p>
|
||||
|
||||
<h2>یعنی چه چیزی امکانپذیر نیست؟</h2>
|
||||
<p>
|
||||
ارسال خودکار به هر شمارهی دلخواه در تلگرام/بله/ایتا بدون اینکه آن
|
||||
شخص قبلاً با بات تعامل داشته باشد، از مسیر رسمی امکانپذیر نیست — و
|
||||
هیچ روش «دور زدن» پایداری هم برایش وجود ندارد. کاسبیار برای این
|
||||
موارد بهجای اتوماسیون مخفی، یک صف ارسال دستیِ یکلمسی نشان میدهد:
|
||||
اپلیکیشن مقصد را با متن آماده باز میکند، خودت دستی میزنی ارسال.
|
||||
</p>
|
||||
|
||||
<h2>جمعبندی</h2>
|
||||
<p>
|
||||
پیام گروهی پایدار یعنی حجم را از مسیر رسمی هر پلتفرم رد کنی، نه اینکه
|
||||
رفتار انسانی را شبیهسازی کنی. کندتر شروع میشود (چون باید Template یا
|
||||
Bot تاییدشده داشته باشی) ولی هرگز اکانتت را به خطر نمیاندازد.
|
||||
</p>
|
||||
""",
|
||||
},
|
||||
{
|
||||
"slug": "field-sefareshi-dar-template",
|
||||
"title": "فیلد سفارشی در قالب پیام یعنی چه؟",
|
||||
"meta_description": "چطور برای هر گروه مخاطب فیلدهای سفارشی مثل شماره گذرنامه یا کد ملی تعریف کنی و در قالب پیام همان گروه استفاده کنی.",
|
||||
"excerpt": "چطور برای هر گروه مخاطب، فیلدهای مخصوص خودش را تعریف کنی و در متن پیام استفاده کنی.",
|
||||
"read_minutes": 3,
|
||||
"content_html": """
|
||||
<p>
|
||||
قالب پیام معمولی یک متن ثابت با چند جایخالی است، مثلاً «سلام {نام}
|
||||
عزیز». اما خیلی از کسبوکارها به بیشتر از نام و شماره نیاز دارند — یک
|
||||
آژانس مسافرتی به «شماره گذرنامه» نیاز دارد، یک کلینیک به «تاریخ نوبت»،
|
||||
یک باشگاه به «کد عضویت». اینجاست که فیلد سفارشی وارد میشود.
|
||||
</p>
|
||||
|
||||
<h2>فیلد سفارشی مال یک گروه است، نه کل مخاطبین</h2>
|
||||
<p>
|
||||
در کاسبیار، مخاطبین را در گروههایی مثل «تور»، «مشتریان VIP» یا
|
||||
«دانشجویان» دستهبندی میکنی. هر گروه فیلدهای سفارشی مخصوص خودش را
|
||||
دارد — یعنی گروه «تور» میتواند «شماره گذرنامه» داشته باشد بدون اینکه
|
||||
این فیلد بیربط در گروه «دانشجویان» هم ظاهر شود.
|
||||
</p>
|
||||
|
||||
<h2>گروه عمومی برای وقتی که پیچیدگی لازم نیست</h2>
|
||||
<p>
|
||||
اگر نیازی به فیلد سفارشی نداری، همان گروه «عمومی» پیشفرض کافیست —
|
||||
فقط نام و شماره. هیچ فیلدی مجبور نیستی اضافه کنی.
|
||||
</p>
|
||||
|
||||
<h2>استفاده در قالب پیام</h2>
|
||||
<p>
|
||||
بعد از تعریف فیلد در یک گروه، هنگام نوشتن قالب پیام برای همان گروه،
|
||||
اسم فیلد بهعنوان یک تگ قابلدرج (مثل <code>{شماره گذرنامه}</code>)
|
||||
کنار فیلدهای پایه (نام، شماره) نمایش داده میشود. برای هر گیرنده، اپ
|
||||
خودکار مقدار واقعی همان مخاطب را جایگزین میکند.
|
||||
</p>
|
||||
""",
|
||||
},
|
||||
{
|
||||
"slug": "merge-mokhatebin-ganoshi",
|
||||
"title": "مرج مخاطبین گوشی با اپ",
|
||||
"meta_description": "سریعترین راه برای شروع ارسال گروهی: مخاطبین گوشیات را بدون تایپ دستی وارد کاسبیار کن و به گروه دلخواه اضافه کن.",
|
||||
"excerpt": "سریعترین راه برای شروع: مخاطبین گوشیات را بدون تایپ دستی وارد کاسبیار کن.",
|
||||
"read_minutes": 2,
|
||||
"content_html": """
|
||||
<p>
|
||||
اگر لیست مخاطبینت همین حالا در دفترچهتلفن گوشیات است، نیازی به
|
||||
تایپ دوبارهی تکتک شمارهها نیست. کاسبیار میتواند مستقیم به
|
||||
مخاطبین گوشیات دسترسی بگیرد (با اجازهی خودت) و هرکدام را که خواستی
|
||||
یکجا وارد اپ کند.
|
||||
</p>
|
||||
|
||||
<h2>چطور کار میکند</h2>
|
||||
<p>
|
||||
از صفحهی «مخاطبین» گزینهی «مرج از مخاطبین گوشی» را بزن. اپ لیست
|
||||
مخاطبین گوشیات را نشان میدهد (فقط آنهایی که هنوز در کاسبیار
|
||||
نیستند)، هرکدام را انتخاب میکنی، یک گروه مقصد مشخص میکنی، و همه
|
||||
یکجا اضافه میشوند.
|
||||
</p>
|
||||
|
||||
<h2>دوبارهکاری نمیشود</h2>
|
||||
<p>
|
||||
مخاطبینی که شمارهشان از قبل در اپ ثبت شده، دوباره در لیست مرج نشان
|
||||
داده نمیشوند — تطبیق بر اساس شمارهی نرمالشده انجام میشود، نه اسم.
|
||||
</p>
|
||||
|
||||
<h2>حریم خصوصی</h2>
|
||||
<p>
|
||||
دسترسی به مخاطبین فقط برای همین عملیات استفاده میشود؛ چیزی خودکار یا
|
||||
در پسزمینه آپلود نمیشود — فقط همان مخاطبینی که خودت تیک میزنی.
|
||||
</p>
|
||||
""",
|
||||
},
|
||||
{
|
||||
"slug": "sms-ya-whatsapp-business",
|
||||
"title": "پیامک یا واتساپبیزینس؟ کدام برای ارسال گروهی بهتر است",
|
||||
"meta_description": "مقایسهی پیامک و واتساپبیزینس برای ارسال پیام گروهی: هزینه، نرخ باز شدن، محدودیت Template، و کدام برای چه کسبوکاری مناسبتر است.",
|
||||
"excerpt": "مقایسهی هزینه، نرخ باز شدن، و محدودیتهای هرکدام برای پیام گروهی کسبوکارها.",
|
||||
"read_minutes": 5,
|
||||
"content_html": """
|
||||
<p>
|
||||
هر دو کانال جای خودشان را دارند و در کاسبیار میتوانی همزمان از
|
||||
هردو (بهعلاوهی تلگرام، بله و ایتا) استفاده کنی. اما اگر مجبوری یکی
|
||||
را اول انتخاب کنی، این تفاوتها کمک میکند:
|
||||
</p>
|
||||
|
||||
<h2>پیامک</h2>
|
||||
<p>
|
||||
به هر شمارهای میرسد، حتی بدون اینترنت یا اپ نصبشده — قابلاطمینانترین
|
||||
کانال از نظر تحویل. هزینهی هر پیام مشخص و قابلپیشبینی است. محدودیت:
|
||||
طول پیام (پیامکهای بلند چند-قسمتی حساب میشوند) و نداشتن تصویر/دکمه.
|
||||
</p>
|
||||
|
||||
<h2>واتساپبیزینس</h2>
|
||||
<p>
|
||||
نرخ باز شدن پیام معمولاً بالاتر از پیامک است و امکاناتی مثل دکمه و
|
||||
رسانه دارد. اما برای ارسال گروهی (business-initiated) باید از یک
|
||||
Template از پیش تاییدشده در Meta Business Manager استفاده کنی —
|
||||
یعنی نمیتوانی هر متن دلخواهی را بیمحدودیت به هرکسی بفرستی؛ باید
|
||||
ساختار پیام از قبل تایید شده باشد.
|
||||
</p>
|
||||
|
||||
<h2>تلگرام، بله، ایتا</h2>
|
||||
<p>
|
||||
اینها فقط به کسانی میرسند که قبلاً با بات کسبوکارت چت را شروع
|
||||
کردهاند. برای مخاطبینی که این کار را نکردهاند، کاسبیار یک صف
|
||||
ارسال دستیِ یکلمسی نشان میدهد.
|
||||
</p>
|
||||
|
||||
<h2>جمعبندی</h2>
|
||||
<p>
|
||||
اگر تحویل تضمینی برایت مهمتر است، پیامک را انتخاب کن. اگر مخاطبینت
|
||||
بیشتر واتساپی هستند و Template تاییدشده داری، واتساپبیزینس گزینهی
|
||||
بهتری است. بهترین نتیجه معمولاً ترکیب چند کانال است — دقیقاً همان
|
||||
چیزی که کاسبیار از یک صفحه انجام میدهد.
|
||||
</p>
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_articles(apps, schema_editor):
|
||||
Article = apps.get_model("landing", "Article")
|
||||
published_at = datetime.date(2025, 8, 23)
|
||||
for data in ARTICLES:
|
||||
Article.objects.update_or_create(
|
||||
slug=data["slug"],
|
||||
defaults={
|
||||
"title": data["title"],
|
||||
"meta_description": data["meta_description"],
|
||||
"excerpt": data["excerpt"],
|
||||
"content_html": data["content_html"],
|
||||
"read_minutes": data["read_minutes"],
|
||||
"is_published": True,
|
||||
"published_at": published_at,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def remove_articles(apps, schema_editor):
|
||||
Article = apps.get_model("landing", "Article")
|
||||
Article.objects.filter(slug__in=[a["slug"] for a in ARTICLES]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("landing", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_articles, remove_articles),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
"""مقالهی لندینگپیج — از پنل ادمین مدیریت میشود و در «/articles/» سرو
|
||||
میشود (بهجای فایلهای HTML استاتیک قبلی)."""
|
||||
|
||||
slug = models.SlugField(unique=True, max_length=120, verbose_name=_("نامک (slug)"))
|
||||
title = models.CharField(max_length=200, verbose_name=_("عنوان"))
|
||||
meta_description = models.CharField(
|
||||
max_length=300, blank=True, verbose_name=_("توضیح متا (SEO)"),
|
||||
help_text=_("در تگهای meta description و og:description استفاده میشود."),
|
||||
)
|
||||
excerpt = models.CharField(
|
||||
max_length=300, verbose_name=_("خلاصه"),
|
||||
help_text=_("همان متن کوتاهی که زیر عنوان در لیست مقالات نشان داده میشود."),
|
||||
)
|
||||
content_html = models.TextField(
|
||||
verbose_name=_("محتوا (HTML)"),
|
||||
help_text=_("محتوای مقاله با تگهای HTML ساده مثل <p>، <h2>، <strong>."),
|
||||
)
|
||||
read_minutes = models.PositiveSmallIntegerField(default=3, verbose_name=_("زمان مطالعه (دقیقه)"))
|
||||
is_published = models.BooleanField(default=True, verbose_name=_("منتشرشده"))
|
||||
published_at = models.DateField(verbose_name=_("تاریخ انتشار"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("مقاله")
|
||||
verbose_name_plural = _("مقالات")
|
||||
ordering = ("-published_at", "-created_at")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -0,0 +1,59 @@
|
||||
{% load landing_extras %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ article.title }} | کاسبیار</title>
|
||||
<meta name="description" content="{{ article.meta_description }}">
|
||||
<link rel="canonical" href="https://mobiniqow.ir/articles/{{ article.slug }}/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="{{ article.title }}">
|
||||
<meta property="og:description" content="{{ article.meta_description }}">
|
||||
<meta name="theme-color" content="#107c41">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/atlas.css">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "{{ article.title }}",
|
||||
"datePublished": "{{ article.published_at|date:'Y-m-d' }}",
|
||||
"author": { "@type": "Organization", "name": "کاسبیار" }
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="nav container">
|
||||
<a class="nav-brand" href="/">کاسبیار</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/#features">امکانات</a>
|
||||
<a href="/articles/">مقالات</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container" style="padding: var(--space-12) 0;">
|
||||
<a class="back-link" href="/articles/">← بازگشت به مقالات</a>
|
||||
|
||||
<div class="article-header">
|
||||
<div class="meta">{{ article.published_at|jalali }} · {{ article.read_minutes|fa_digits }} دقیقه مطالعه</div>
|
||||
<h1>{{ article.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="article-body">
|
||||
{{ article.content_html|safe }}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<span>© کاسبیار — ابزار ارسال پیام گروهی</span>
|
||||
<span><a href="/">صفحهی اصلی</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
{% load landing_extras %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>مقالات — کاسبیار | راهنمای ارسال پیام گروهی</title>
|
||||
<meta name="description" content="راهنما و مقالات کاسبیار دربارهی ارسال پیام گروهی با شماره: پیامک، واتساپبیزینس، فیلد سفارشی در قالب پیام، و مرج مخاطبین گوشی.">
|
||||
<link rel="canonical" href="https://mobiniqow.ir/articles/">
|
||||
<meta name="theme-color" content="#107c41">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/atlas.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="nav container">
|
||||
<a class="nav-brand" href="/">کاسبیار</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/#features">امکانات</a>
|
||||
<a href="/#channels">کانالها</a>
|
||||
<a href="/articles/">مقالات</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container" style="padding: var(--space-12) 0;">
|
||||
<div class="section-head" style="text-align:right; margin-inline:0;">
|
||||
<h1>مقالات</h1>
|
||||
<p>راهنمای ارسال پیام گروهی با شماره — پیامک، واتساپ، فیلد سفارشی و مرج مخاطبین.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-list">
|
||||
{% for article in articles %}
|
||||
<article class="article-row">
|
||||
<span class="meta">{{ article.published_at|jalali }} · {{ article.read_minutes|fa_digits }} دقیقه مطالعه</span>
|
||||
<h2><a href="/articles/{{ article.slug }}/">{{ article.title }}</a></h2>
|
||||
<p>{{ article.excerpt }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<span>© کاسبیار — ابزار ارسال پیام گروهی</span>
|
||||
<span><a href="/">صفحهی اصلی</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
from django import template
|
||||
|
||||
from ..jalali import _PERSIAN_DIGITS, format_jalali
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def jalali(date):
|
||||
if date is None:
|
||||
return ""
|
||||
return format_jalali(date)
|
||||
|
||||
|
||||
@register.filter
|
||||
def fa_digits(value):
|
||||
return str(value).translate(_PERSIAN_DIGITS)
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "landing"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.article_list, name="article_list"),
|
||||
path("<slug:slug>/", views.article_detail, name="article_detail"),
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from .models import Article
|
||||
|
||||
|
||||
def article_list(request):
|
||||
articles = Article.objects.filter(is_published=True)
|
||||
return render(request, "landing/article_list.html", {"articles": articles})
|
||||
|
||||
|
||||
def article_detail(request, slug):
|
||||
article = get_object_or_404(Article, slug=slug, is_published=True)
|
||||
return render(request, "landing/article_detail.html", {"article": article})
|
||||
Reference in New Issue
Block a user