adding
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
# 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'),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,210 +0,0 @@
|
||||
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),
|
||||
]
|
||||
@@ -3,14 +3,32 @@
|
||||
<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 name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>{{ article.title }} | شاپ شیپ</title>
|
||||
<meta name="description" content="{{ article.meta_description|default:article.excerpt }}">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="author" content="niqow.ir">
|
||||
<link rel="canonical" href="https://shopship.ir/articles/{{ article.slug }}/">
|
||||
<link rel="alternate" hreflang="fa-ir" href="https://shopship.ir/articles/{{ article.slug }}/">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon-16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/img/apple-touch-icon.png">
|
||||
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:site_name" content="شاپ شیپ">
|
||||
<meta property="og:title" content="{{ article.title }}">
|
||||
<meta property="og:description" content="{{ article.meta_description }}">
|
||||
<meta name="theme-color" content="#107c41">
|
||||
<meta property="og:description" content="{{ article.meta_description|default:article.excerpt }}">
|
||||
<meta property="og:url" content="https://shopship.ir/articles/{{ article.slug }}/">
|
||||
<meta property="og:locale" content="fa_IR">
|
||||
<meta property="og:image" content="https://shopship.ir/assets/img/og-image.png">
|
||||
<meta property="article:published_time" content="{{ article.published_at|date:'Y-m-d' }}">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ article.title }}">
|
||||
<meta name="twitter:image" content="https://shopship.ir/assets/img/og-image.png">
|
||||
|
||||
<meta name="theme-color" content="#2d2a55">
|
||||
<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">
|
||||
@@ -19,20 +37,50 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "{{ article.title }}",
|
||||
"headline": "{{ article.title|escapejs }}",
|
||||
"description": "{{ article.meta_description|default:article.excerpt|escapejs }}",
|
||||
"url": "https://shopship.ir/articles/{{ article.slug }}/",
|
||||
"datePublished": "{{ article.published_at|date:'Y-m-d' }}",
|
||||
"author": { "@type": "Organization", "name": "کاسبیار" }
|
||||
"dateModified": "{{ article.updated_at|date:'Y-m-d' }}",
|
||||
"inLanguage": "fa-IR",
|
||||
"image": "https://shopship.ir/assets/img/og-image.png",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "niqow.ir",
|
||||
"url": "https://niqow.ir",
|
||||
"logo": { "@type": "ImageObject", "url": "https://shopship.ir/assets/img/icon-512.png" }
|
||||
},
|
||||
"author": { "@type": "Organization", "name": "شاپ شیپ", "url": "https://shopship.ir/" },
|
||||
"mainEntityOfPage": { "@type": "WebPage", "@id": "https://shopship.ir/articles/{{ article.slug }}/" }
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{ "@type": "ListItem", "position": 1, "name": "شاپ شیپ", "item": "https://shopship.ir/" },
|
||||
{ "@type": "ListItem", "position": 2, "name": "مقالات", "item": "https://shopship.ir/articles/" },
|
||||
{ "@type": "ListItem", "position": 3, "name": "{{ article.title|escapejs }}", "item": "https://shopship.ir/articles/{{ article.slug }}/" }
|
||||
]
|
||||
}
|
||||
</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 class="nav-wrap" id="siteHeader">
|
||||
<div class="nav container">
|
||||
<a class="nav-brand" href="/">
|
||||
<img src="/assets/img/icon-192.png" alt="" width="28" height="28" class="nav-brand-icon">
|
||||
شاپ شیپ
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/#tools">بخشهای اپ</a>
|
||||
<a href="/#pricing">قیمتگذاری</a>
|
||||
<a href="/articles/">مقالات</a>
|
||||
</nav>
|
||||
<a class="btn btn-primary btn-sm" href="/#cta">دریافت اپ</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container" style="padding: var(--space-12) 0;">
|
||||
@@ -46,14 +94,39 @@
|
||||
<div class="article-body">
|
||||
{{ article.content_html|safe }}
|
||||
</div>
|
||||
|
||||
{% if related %}
|
||||
<div class="related-articles">
|
||||
<h2>مقالات مرتبط</h2>
|
||||
<ul>
|
||||
{% for item in related %}
|
||||
<li><a href="/articles/{{ item.slug }}/">{{ item.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<span>© کاسبیار — ابزار ارسال پیام گروهی</span>
|
||||
<span><a href="/">صفحهی اصلی</a></span>
|
||||
<div class="footer-brand">
|
||||
<strong>شاپ شیپ</strong>
|
||||
<span>محصولی از <a href="https://niqow.ir" target="_blank" rel="noopener">niqow.ir</a></span>
|
||||
</div>
|
||||
<span><a href="/articles/">همهی مقالات</a></span>
|
||||
<span>© شاپ شیپ</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var header = document.getElementById('siteHeader');
|
||||
if (header) {
|
||||
var onScroll = function () {
|
||||
header.classList.toggle('is-scrolled', window.scrollY > 8);
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,31 +3,69 @@
|
||||
<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">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>مقالات — شاپ شیپ | راهنمای مدیریت کسبوکار کوچک</title>
|
||||
<meta name="description" content="راهنما و مقالات شاپ شیپ دربارهی مدیریت کسبوکار کوچک: فاکتور رسمی، دفتر درآمد و هزینه و فروش قسطی، انبارداری، پیگیری سفارش، و ارسال پیام گروهی با شماره.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="author" content="niqow.ir">
|
||||
<link rel="canonical" href="https://shopship.ir/articles/">
|
||||
<link rel="alternate" hreflang="fa-ir" href="https://shopship.ir/articles/">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon-16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/img/apple-touch-icon.png">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="شاپ شیپ">
|
||||
<meta property="og:title" content="مقالات — شاپ شیپ">
|
||||
<meta property="og:description" content="راهنمای مدیریت کسبوکار کوچک: فاکتور، دفتر درآمد و هزینه، انبارداری، پیگیری سفارش و پیام گروهی.">
|
||||
<meta property="og:url" content="https://shopship.ir/articles/">
|
||||
<meta property="og:locale" content="fa_IR">
|
||||
<meta property="og:image" content="https://shopship.ir/assets/img/og-image.png">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="مقالات — شاپ شیپ">
|
||||
<meta name="twitter:image" content="https://shopship.ir/assets/img/og-image.png">
|
||||
|
||||
<meta name="theme-color" content="#2d2a55">
|
||||
<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": "CollectionPage",
|
||||
"name": "مقالات شاپ شیپ",
|
||||
"url": "https://shopship.ir/articles/",
|
||||
"inLanguage": "fa-IR",
|
||||
"isPartOf": { "@type": "WebSite", "name": "شاپ شیپ", "url": "https://shopship.ir/" },
|
||||
"publisher": { "@type": "Organization", "name": "niqow.ir", "url": "https://niqow.ir" }
|
||||
}
|
||||
</script>
|
||||
</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 class="nav-wrap" id="siteHeader">
|
||||
<div class="nav container">
|
||||
<a class="nav-brand" href="/">
|
||||
<img src="/assets/img/icon-192.png" alt="" width="28" height="28" class="nav-brand-icon">
|
||||
شاپ شیپ
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/#tools">بخشهای اپ</a>
|
||||
<a href="/#who">برای کیه</a>
|
||||
<a href="/#pricing">قیمتگذاری</a>
|
||||
<a href="/articles/">مقالات</a>
|
||||
</nav>
|
||||
<a class="btn btn-primary btn-sm" href="/#cta">دریافت اپ</a>
|
||||
</div>
|
||||
</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>
|
||||
<p>راهنمای مدیریت کسبوکار کوچک — فاکتور، دفتر درآمد و هزینه، انبارداری، پیگیری سفارش و پیام گروهی با شماره.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-list">
|
||||
@@ -37,16 +75,32 @@
|
||||
<h2><a href="/articles/{{ article.slug }}/">{{ article.title }}</a></h2>
|
||||
<p>{{ article.excerpt }}</p>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>هنوز مقالهای منتشر نشده — بهزودی برمیگردیم.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<span>© کاسبیار — ابزار ارسال پیام گروهی</span>
|
||||
<div class="footer-brand">
|
||||
<strong>شاپ شیپ</strong>
|
||||
<span>محصولی از <a href="https://niqow.ir" target="_blank" rel="noopener">niqow.ir</a></span>
|
||||
</div>
|
||||
<span><a href="/">صفحهی اصلی</a></span>
|
||||
<span>© شاپ شیپ</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var header = document.getElementById('siteHeader');
|
||||
if (header) {
|
||||
var onScroll = function () {
|
||||
header.classList.toggle('is-scrolled', window.scrollY > 8);
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.landing.models import Article
|
||||
|
||||
|
||||
def _make_article(**overrides):
|
||||
defaults = {
|
||||
"slug": "test-article",
|
||||
"title": "عنوان تست",
|
||||
"excerpt": "خلاصه تست",
|
||||
"content_html": "<p>متن تست</p>",
|
||||
"published_at": datetime.date(2020, 1, 1),
|
||||
"is_published": True,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return Article.objects.create(**defaults)
|
||||
|
||||
|
||||
class ArticlePublishScheduleTests(TestCase):
|
||||
"""published_at یک gate واقعی است — مقالهی is_published=True با تاریخِ
|
||||
آینده نباید تا رسیدن آن تاریخ در سایت دیده شود (انتشار پلکانی/زمانبندیشده)."""
|
||||
|
||||
def test_past_dated_article_appears_in_list(self):
|
||||
_make_article(slug="past-one", published_at=datetime.date(2020, 1, 1))
|
||||
response = self.client.get(reverse("landing:article_list"))
|
||||
self.assertContains(response, "عنوان تست")
|
||||
|
||||
def test_future_dated_article_hidden_from_list(self):
|
||||
future = datetime.date.today() + datetime.timedelta(days=10)
|
||||
_make_article(slug="future-one", published_at=future)
|
||||
response = self.client.get(reverse("landing:article_list"))
|
||||
self.assertNotContains(response, "عنوان تست")
|
||||
|
||||
def test_future_dated_article_detail_is_404(self):
|
||||
future = datetime.date.today() + datetime.timedelta(days=10)
|
||||
article = _make_article(slug="future-two", published_at=future)
|
||||
response = self.client.get(reverse("landing:article_detail", args=[article.slug]))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_unpublished_article_hidden_regardless_of_date(self):
|
||||
_make_article(slug="draft-one", is_published=False, published_at=datetime.date(2020, 1, 1))
|
||||
response = self.client.get(reverse("landing:article_list"))
|
||||
self.assertNotContains(response, "عنوان تست")
|
||||
|
||||
def test_today_dated_article_is_visible(self):
|
||||
_make_article(slug="today-one", published_at=datetime.date.today())
|
||||
response = self.client.get(reverse("landing:article_list"))
|
||||
self.assertContains(response, "عنوان تست")
|
||||
+13
-3
@@ -1,13 +1,23 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Article
|
||||
|
||||
|
||||
def _visible_articles():
|
||||
# published_at یعنی «از این تاریخ به بعد دیده شود» — نه فقط یک برچسبِ
|
||||
# نمایشی. همین باعث میشود بشود چند مقاله را از قبل با is_published=True
|
||||
# و published_at آینده ثبت کرد و هرکدام سرِ تاریخِ خودش خودکار ظاهر شود
|
||||
# (مثلا برای انتشار پلکانیِ هر چند روز یکبار، بدون نیاز به کرون/تسک جدا).
|
||||
return Article.objects.filter(is_published=True, published_at__lte=timezone.localdate())
|
||||
|
||||
|
||||
def article_list(request):
|
||||
articles = Article.objects.filter(is_published=True)
|
||||
articles = _visible_articles()
|
||||
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})
|
||||
article = get_object_or_404(_visible_articles(), slug=slug)
|
||||
related = _visible_articles().exclude(pk=article.pk)[:3]
|
||||
return render(request, "landing/article_detail.html", {"article": article, "related": related})
|
||||
|
||||
Reference in New Issue
Block a user