مدل محتوای اجتماعی
یک مدل برای هر موجودیت (پست، نظر، حساب) و مبدلهایی که دادههای API را یک بار به آن تبدیل میکنند
معرفی
همهٔ محصولات پرتو با سه موجودیت کار میکنند: پست، نظر و حساب. تا امروز هر محصول شکل دادهٔ API خودش را مستقیم به کامپوننتها میداد؛ نتیجه این بود که مقدار نامعلوم با صفر پر میشد و «0 بازدید» روی پست عکس نمایش داده میشد، و نرخ تعامل در یک جا کسر بود و در جای دیگر درصد.
مدل محتوای اجتماعی داده را در یک نقطه یکسان میکند و برای هر موجودیت یک کامپوننت دارد که همین مدل را میگیرد:
Post، Comment و Account، و برای
فهرست همهٔ آنها EntityCollection. جدول «از چه چیزی استفاده کنید» در پایین همین بخش
راه درست را نشان میدهد.
SocialPost،SocialCommentوSocialAccountانواع نمایشی سیستم طراحیاند، نه شکل API. اجزای@partodata/ui/socialهیچ فیلد API را نمیخوانند و فقط همین مدل را میگیرند.- هر محصول دادهٔ خودش را یک بار در یک مبدل به این مدل تبدیل میکند. مبدلهای آماده در
@partodata/ui/social/adaptersهستند: برایPostData، برای ردیف خام API جستوجوی محتوا و برای نظرهای کشوی جزئیات. - همین مدل خبر و مقالهٔ وب، بخش رونوشت تلویزیون و رادیو و فایل بارگذاریشده را هم نمایش میدهد؛ اینها پست اجتماعی نیستند ولی در همان فهرستها کنار پستها میآیند.
از چه چیزی استفاده کنید
| کار | کامپوننت (@partodata/ui/social) | داده |
|---|---|---|
| فهرست پستها (نتایج جستوجو، خبرخوان) | EntityCollection entity="post" با Post (پیشفرض card؛ row و tile انتخابی) در ستون فید | SocialPost[] (با fromPostData یا مبدل محصول) |
| جزئیات پست همراه با نظرها (صفحه یا پنل) | Post layout="details" و زیر آن EntityCollection entity="comment" از Comment (thread) | SocialPost و SocialComment[] |
| پستی که یک صفحهٔ تحلیل دربارهٔ آن است | Post layout="summary" | SocialPost |
| نظرها (بحث، شبکهٔ تحلیل نظرها) | Comment (thread، card، row)، فهرستشان با EntityCollection entity="comment" | SocialComment (با fromPostComment) |
| حساب (ردیف منشن، کارت، سربرگ صفحهٔ حساب) | Account (row، card، header) | SocialAccount |
| یک جزء تنها: منبع، شناسه، شاخصها، رسانه، زمان، متن | اجزای پایه (PlatformMark، Handle، MetricGroup، …) | بخشی از SocialPost |
کارت یا ردیف پست را خودتان از اجزای پایه نسازید: Post همان اجزا را با قاعدههای چیدمان (رسانهٔ بدون برش، طول خط خوانا،
ارتفاع ثابت ردیف، اقدامات قابلدسترس با لمس و کیبورد) کنار هم میگذارد. داده را یک بار تبدیل کنید، در لایهٔ دادهٔ محصول:
'use client'
import type { PostData } from '@partodata/ui'
import { EntityCollection, Post, type SocialPost } from '@partodata/ui/social'
import { fromPostData } from '@partodata/ui/social/adapters'
import { useMemo } from 'react'
interface SearchResultsProps {
rows: PostData[]
loading: boolean
onOpen: (post: SocialPost) => void
}
export function SearchResults({ rows, loading, onOpen }: SearchResultsProps) {
const posts = useMemo(() => rows.map((row) => fromPostData(row)), [rows])
return (
<EntityCollection
entity="post"
items={posts}
getId={(post) => post.id}
loading={loading}
label="نتایج جستوجو"
renderItem={(post, item) => <Post post={post} {...item} onOpen={onOpen} />}
/>
)
}کامپوننتهای اجتماعی نسخهٔ 3 (PostList، PostCard، CommentCard، ProfileCard، PageCard و PostDetailsDrawer) و
پیشتنظیمهایشان (@partodata/ui/social/presets) در 5.0 حذف شدند؛ کد آنها به همین کامپوننتها میرود.
سه قاعده
مقدار نامعلوم صفر نیست
هر شمارنده از نوع Count است، یعنی number | null. سه حالت از هم جدا هستند:
| مقدار در مدل | معنا | نمایش |
|---|---|---|
عدد (از جمله 0) | مقدار جمعآوریشده؛ صفر هم مقدار واقعی است | عدد |
null | این شاخص برای این مورد وجود دارد ولی مقدارش را نمیدانیم | «—» |
کلید حذفشده (undefined) | این شاخص برای این مورد معنا ندارد (بازدید رادیو، پسندِ کانال تلگرام) | هیچچیز |
هرگز مقدار نامعلوم را با 0 پر نکنید. اگر API شما برای مقدارِ جمعآورینشده صفر میفرستد، مبدل باید آن را به null
تبدیل کند (toCount(value, { zeroMeansUnknown: true })). آمار حساب هم همینطور است: وبسایت خبری دنبالکننده ندارد، پس
کلید followers آن حذف میشود، نه null.
نسبتها کسرند
Ratio همیشه کسر است، هرگز درصد: 0.034 یعنی 3.4 درصد و 1 یعنی 100 درصد. واحد ورودی در مبدل اعلام میشود، نه حدس
زده: toRatio(6.8, { unit: 'percent' }). امتیازها (اطمینان، سمی بودن) بین 0 و 1 میمانند و سقفشان را اعلام میکنند
(toRatio(value, { max: 1 }))؛ مقدار بالاتر نشانهٔ واحد اشتباه است، نامعلوم (null) میشود و در محیط توسعه هشدار میدهد.
نرخها سقف ندارند: نرخ تعامل پست پربازدیدِ یک حساب کوچک میتواند 1.8 (180 درصد) باشد.
وضعیتها دادهاند
پست حذفشده در منبع، حساب خصوصی، موردی که در حال پردازش است یا در حال بارگذاری، همان موجودیت با یک state است:
'ready' | 'loading' | 'processing' | 'failed' | 'unavailable' | 'private' | 'deleted'. برای هیچکدام کامپوننت جدا
نسازید و پرچم جدا هم نگذارید: حساب خصوصی state: 'private' است.
نمونه بصری
یک پست اینستاگرام، یک خبر وب و یک بخش رونوشت تلویزیون، هر سه از همان نوع SocialPost:

رونمایی محصول جدید در نمایشگاه با استقبال مشتریها همراه شد
هر سه همان Post در چیدمان پیشفرض کارت است؛ تفاوتها (تیتر و نویسندهٔ خبر، برنامه و زمان پخش، شاخصهای اجتماعی) فقط از
رکورد میآید.
پیادهسازی
مبدلها
مبدلها در مسیر جداگانهٔ @partodata/ui/social/adapters هستند. این ماژول نه React دارد و نه 'use client'، پس در
کامپوننت سرور، route handler و worker هم قابل استفاده است.
کدام مبدل
| دادهای که دارید | مبدل |
|---|---|
هر چیزی از نوع PostData | fromPostData |
| ردیف خام API جستوجوی محتوا (همان 20 فیلد، بدون افزونه) | fromSearchContent |
نظرهای کشوی جزئیات (PostComment) | fromPostComment |
| برچسبهای نظر (احتمال 0 تا 100) | fromCommentTag |
حساب پشت یک پست (ردیف خام یا PostData؛ یک مبدل برای هر دو) | accountFromPost |
| API خود محصول | مبدل خودتان، روی toCount، toRatio، toTimestamp |
fromSearchContent نوع PostData را نمیپذیرد (خطای نوع میدهد) و اگر PostData بدون نوع به آن برسد، در محیط توسعه
هشدار میدهد؛ چون افزونههای پرتو (محتوا، پرچم حذف، شاخصهای افزونه، خروجی تحلیل) را دور میریزد. برای ردیف ساده هر دو
مبدل یک نتیجه میدهند.
مسیر import
| چه چیزی | از کجا |
|---|---|
کامپوننتها (SocialText، MetricGroup، MediaFrame، PlatformMark، …) | @partodata/ui/social |
همهٔ تابعها: مبدلها، نرمالکنندهها، توابع مدل (metricPresence، toSocialSource، SOURCE_KEYS) و نامها (metricLabel، sourceLabel) | @partodata/ui/social/adapters |
انواع مدل (SocialPost، SocialMedia، …) | هر دو مسیر (همان انواعاند)؛ از همان مسیری بگیرید که کامپوننت یا تابع را میگیرید |
@partodata/ui/social نشانهٔ 'use client' دارد و برای همین فقط کامپوننت و نوع صادر میکند و هیچ تابعی ندارد؛ کامپوننت
سرور نمیتواند تابعی را از چنین مسیری فراخوانی کند. همهٔ تابعها، از جمله نام شاخص (metricLabel) و نام منبع
(sourceLabel) برای سرستون جدول یا فیلتر، در @partodata/ui/social/adapters هستند که در کامپوننت سرور و کلاینت هر دو کار
میکند. قالببندی عدد و زمان کار خود اجزاست: عدد را با MetricGroup و زمان را با RelativeTime نمایش دهید.
fromSearchContent
یک ردیف خام API جستوجوی محتوا را به SocialPost تبدیل میکند:
| فیلد API | در مدل | قاعده |
|---|---|---|
platform | source | 1 اینستاگرام، 2 فیسبوک، 3 وبسایت، 4 تلگرام؛ کد دیگر { key: 'unknown', code } |
id | id | رشته میشود؛ اگر نباشد یا null یا خالی باشد، شناسهٔ پایدار از منبع، دامنه، نشانی و زمان |
domain | actor.handle | @ (و @ تمامعرض) و نشانههای جهت حذف میشوند؛ از آن نامی ساخته نمیشود |
dateCreated | publishedAt | ثانیه به میلیثانیه تبدیل میشود؛ 0 یعنی زمانی وجود ندارد |
hashtags | hashtags | # و نشانههای جهت حذف میشوند و هر هشتگ یک بار میآید |
thumbnailUrl | media | src تصویر یا poster ویدئو؛ API ابعاد نمیفرستد، پس width/height فقط از گزینهٔ mediaSize |
likeCount، commentCount | likes، comments | صفر، صفر واقعی است؛ در منبعی که این شمارنده را ندارد (تلگرام) حذف میشود |
viewCount، videoPlayCount | views، plays | صفر یعنی نامعلوم؛ پخش فقط برای ویدئو؛ بازدید اینستاگرام فقط برای ویدئو |
engagementRate | engagement | کسر (واحد درصد را در گزینهها اعلام کنید)؛ صفر یعنی نامعلوم؛ ممکن است از 1 بیشتر شود |
followerCount، followingCount، domainRank | actor.metrics | صفر یعنی نامعلوم؛ فقط شمارندههایی که منبع دارد (وبسایت فقط رتبه، کانال تلگرام بدون دنبالشده) |
| ردیف وبسایت | kind: 'article' | title، summary ← excerpt، body ← text؛ بدون شمارندهٔ اجتماعی |
گزینهها: engagementRateUnit ('fraction' یا 'percent')، zeroMeansUnknown (پیشفرض true؛ فقط برای تولیدکنندهای
خاموشش کنید که همهٔ شمارندهها را تضمین میکند)، missingMedia ('unavailable' یا 'pending'، برای پست اینستاگرامی که
تصویرش هنوز نرسیده است) و mediaSize (ابعاد رسانهٔ ردیف؛ پایینتر).
ابعاد رسانه در ردیف API
ردیف API جستوجوی محتوا فقط thumbnailUrl دارد و عرض و ارتفاع ندارد. بدون ابعاد، MediaFrame کادر جایگزین مربع میگیرد:
در کارتِ ستون خبرخوان قابی به عرض ستون و ارتفاع 500 پیکسل که رسانه کامل در آن است (تصویر افقی بالا و پایین نوار محو
میگیرد) و در جزئیات یک مربع. چیزی بریده یا جابهجا نمیشود، ولی تصویر عرض کارت را پر نمیکند. برای کادر درست، ابعاد را با
mediaSize بدهید؛ این تابع برای هر ردیفی که thumbnailUrl دارد یک بار فراخوانی میشود و فقط نسبت عرض به ارتفاع مهم است:
import { fromSearchContent, type MediaSize, type SearchContentPost } from '@partodata/ui/social/adapters'
// Sizes your media proxy reports for each thumbnail URL (or the API's fields, once it sends them).
declare const thumbnailSizes: Map<string, MediaSize>
export function toSocialPost(row: SearchContentPost) {
return fromSearchContent(row, { mediaSize: (r) => thumbnailSizes.get(r.thumbnailUrl) })
}اگر ابعادی ندارید چیزی برنگردانید (undefined)؛ عدد صفر یا منفی هم نادیده گرفته میشود و در محیط توسعه یک هشدار میدهد.
همین گزینه در fromPostData برای ردیفی که content ندارد کار میکند؛ رسانههای content ابعادشان را از aspectRatio هر
مورد میگیرند. درخواست به تیم API: ابعاد تصویر بندانگشتی (thumbnailWidth و thumbnailHeight) در ردیف فرستاده شود تا این
گام برای همهٔ محصولات یکی شود.
fromPostData
PostData فعلی سیستم طراحی (همان ردیف API بهعلاوهٔ افزونههای پرتو) را با همان قاعدههای fromSearchContent به مدل
تبدیل میکند و افزونهها را هم نگه میدارد: content (متن، رسانه با نسبت و وضعیت، مقاله، کلیپ پخش با رونوشت کامل،
استوری)، platformMeta (نمایش تلویزیون، رادیو، مطبوعات، وب و استوری)، channel/program/reporterName،
verified/authorKind/postCount، شاخصهای افزونه (صفرِ صریح حفظ میشود)، خروجی تحلیل، برچسبها، پرچمها (محتوای حساس؛
حذف در منبع ← state: 'deleted')، پست نقلشده، پست والد و بازنشر.
نام رسانه از channel میآید. وبسایتی که channel ندارد نامی نمیگیرد و دامنهاش شناسهٔ آن است، تا دامنه دو بار نمایش
داده نشود. ایستگاه تلویزیون و رادیو شناسه ندارد؛ اگر channel نباشد، domain نام آن میشود.
fromPostComment
نظری را که کشوی جزئیات میگیرد (PostComment در details.comments) به SocialComment تبدیل میکند:
فیلد PostComment | در مدل | قاعده |
|---|---|---|
author | author | @ حذف میشود؛ نامی که با شناسه یکی است تکرار نمیشود؛ بدون آن ناشناس |
timestamp | publishedAt | ثانیه به میلیثانیه؛ هرگز 1970 |
likes، replyCount | metrics.likes، metrics.replies | نبودن یعنی معنا ندارد؛ عدد (از جمله صفر) خودش |
sentiment، toxicityScore (0 تا 1) | signals.sentiment، signals.toxicity | امتیاز بالاتر از 1 نامعلوم میشود |
pinned، edited، authorReply، hiddenByModerator | flags (hidden برای پنهانشده) | |
language، translatedText، replies | lang، translation، replies | پاسخها به همین شکل تبدیل میشوند |
fromCommentTag احتمال برچسب نظر (0 تا 100) را به confidence مدل (0 تا 1) تبدیل میکند.
نرمالکنندهها برای مبدل محصول
مبدلِ دادهٔ خود محصول را روی همین توابع بنویسید تا تصمیم واحد و مقدار نامعلوم در همهجا یکسان باشد:
| تابع | کار |
|---|---|
toCount(value, { zeroMeansUnknown }) | عدد نامنفی یا رشتهٔ فقطرقمی (لاتین، فارسی یا عربی) را به عدد صحیح گرد میکند و هر چیز دیگر را null میکند |
toRatio(value, { unit, max, field }) | کسر یا null؛ واحد اعلام میشود، حدس زده نمیشود؛ max برای امتیازهای سقفدار؛ field نام فیلد API در هشدار محیط توسعه (وقتی واحد را خود مبدل ثابت کرده است) |
toTimestamp(value) | ثانیه را به میلیثانیه تبدیل میکند؛ 0، عدد کوچک (مثل سال 2026) و مقدار نامعتبر یعنی بدون زمان |
searchContentSource(code) | کد پلتفرم API به SocialSource |
شکل قدیمی پست
شکل تودرتوی { source, author, timestamp, body, metrics } در نسخهٔ 3.0.2 حذف شد. کدی که هنوز آن را میسازد قبلاً با پیام
نامفهوم post.hashtags is not iterable از کار میافتاد. حالا مبدلها خطای LegacyPostShapeError میدهند که کلیدهای قدیمی
را در legacyKeys نام میبرد و راه اصلاح را میگوید.
انواع
SocialPost
| فیلد | توضیح |
|---|---|
id | شناسهٔ پایدار (کلید React، انتخاب، وضعیت خواندهشده) |
source | منبع: { key, label?, code? }؛ کد ناشناختهٔ API داده است و با key: 'unknown' میماند |
kind | نوع مورد: text، photo، video، carousel، reel، story، article، transcript، file |
actor | حساب، سایت، رسانه، شبکه یا ایستگاهی که منتشر کرده (name، handle بدون @، avatarUrl، …) |
publishedAt | زمان انتشار؛ برای بخش رونوشت، زمان پخش |
url | نشانی مورد در منبع؛ همیشه یک اقدام جدا («مشاهده در منبع»)، نه کلیک اصلی |
title، text، excerpt | عنوان، متن کامل، و لید خبر یا پنجرهٔ رونوشت |
highlights | عبارتهایی که در متن علامت میخورند (نتیجهٔ جستوجو، کلیدواژهٔ رونوشت) |
media | فهرست رسانهها با ابعاد و وضعیت (پایینتر) |
metrics | شاخصهای پست؛ هر کلید Count است و engagement یک Ratio |
broadcast | ویژگیهای پخش: program، segment، textSource (گفتار یا متن روی تصویر)، speaker، confidence |
byline، category، hashtags، labels، signals | نویسنده، بخش، هشتگها، برچسبها با اطمینان، خروجی تحلیل |
flags | sensitive (true یا دلیل: nsfw، graphic، spoiler)، ad، partnership، pinned، edited، archiveUrl |
quoted، replyTo، repost | پست نقلشده، پستی که به آن پاسخ داده شده، و بازنشر |
state | وضعیت مورد |
آمار حساب (دنبالکننده، دنبالشده، تعداد پست، رتبه) مال حساب است، نه مال هر پست؛ اگر همراه داده بیاید در actor.metrics
قرار میگیرد و جای نمایشش کارت یا ردیف حساب است.
signals خروجی تحلیل است و فقط برای موردی پر میشود که واقعاً تحلیل شده است (برای پست: پستی که در یک خوشه قرار گرفته).
برای پست خامِ جمعآوریشده آن را خالی بگذارید؛ پر کردنش یعنی ساختن تحلیلی که انجام نشده است.
SocialMedia
| فیلد | توضیح |
|---|---|
kind | image، video یا audio |
src | فایل رسانه |
poster | پیشنمایش: پوستر ویدئو، جلد صوت یا نسخهٔ کوچک تصویر (ردیف و کاشی همین را بارگذاری میکنند) |
width، height | ابعاد واقعی به پیکسل؛ قاب رسانه از روی همینها ساخته میشود و صفحه بعد از بارگذاری جابهجا نمیشود؛ بدون آنها قاب مربع است (ردیف API ابعاد ندارد: mediaSize) |
durationSec | طول ویدئو یا صوت |
status | ready، pending (هنوز نرسیده)، failed، expired (منقضی شده)، unavailable (ذخیره نشده) |
sensitive | رسانهٔ حساس تا وقتی کاربر نخواهد محو میماند؛ پستی که flags.sensitive دارد همهٔ رسانههایش را همینطور نمایش میدهد |
SocialComment و SocialAccount
نظر: author (بدون آن ناشناس است)، text، lang، translation، publishedAt، metrics (likes، replies)،
labels با اطمینان 0 تا 1، signals (sentiment، toxicity، authorGender)، flags (pinned، edited،
authorReply، hidden) و replies.
حساب: همهٔ فیلدهای actor بهعلاوهٔ id، source، bio، category، metrics (followers، following، posts،
engagement، rank)، flags (business) و state. حساب خصوصی state: 'private' است. نشان تأییدشده فقط وقتی
verified: true باشد نمایش داده میشود؛ هرگز آن را پیشفرض true نگذارید.
الگوهای رایج
سه مورد غیراجتماعی که محصولات پایش کنار پستها نمایش میدهند، با همین مدل ساخته میشوند.
خبر و مقالهٔ وب
import type { SocialPost } from '@partodata/ui/social/adapters'
export const article: SocialPost = {
id: 'news-42',
source: { key: 'web' },
kind: 'article',
actor: { handle: 'news.example.com', kind: 'organization' },
publishedAt: '2026-09-20T08:30:00Z',
title: 'رونمایی محصول جدید در نمایشگاه',
excerpt: 'محصول جدید امروز در نمایشگاه معرفی شد.',
byline: 'سارا محمدی',
category: 'اقتصادی',
url: 'https://news.example.com/a/42',
}مقاله شاخص تعامل اجتماعی ندارد، پس metrics را نمینویسید (نه صفر، نه null). برای نشریهٔ چاپی از source.key: 'press'
استفاده کنید.
بخش رونوشت تلویزیون و رادیو
import type { SocialPost } from '@partodata/ui/social/adapters'
export const segment: SocialPost = {
id: 'passage:7f3a',
source: { key: 'tv' },
kind: 'transcript',
actor: { name: 'شبکهٔ خبر نمونه', kind: 'organization' },
publishedAt: '2026-09-20T18:05:00Z',
broadcast: { program: 'گفتوگوی اقتصادی', segment: { startSec: 318, endSec: 342 }, textSource: 'speech' },
excerpt: 'در ادامهٔ برنامه دربارهٔ رونمایی محصول جدید صحبت شد.',
highlights: ['رونمایی محصول'],
lang: 'fa',
media: [{ kind: 'video', status: 'expired', durationSec: 24 }],
}شبکه یا ایستگاه actor است و زمان پخش publishedAt. کلیپی که از بازهٔ نگهداری خارج شده status: 'expired' دارد و
فقط رونوشت باقی میماند.
فایل بارگذاریشده
import type { SocialPost } from '@partodata/ui/social/adapters'
export const uploaded: SocialPost = {
id: 'upload-3',
source: { key: 'file', label: 'فایل اکسل' },
kind: 'file',
title: 'نظرات-کمپین-تخفیف.xlsx',
}بهترین روشها و دامهای رایج
بکنید
- هر چیزی از نوع
PostDataرا باfromPostDataتبدیل کنید وfromSearchContentرا فقط برای ردیف خام API نگه دارید. - برای API خودتان یک مبدل بنویسید و آن را روی
toCount،toRatioوtoTimestampبسازید. widthوheightرسانه را هر جا معلوم است در مبدل پر کنید تا قاب رسانه از ابتدا درست باشد؛ برای ردیف API جستوجوی محتوا، که ابعاد ندارد، با گزینهٔmediaSize.- خبر، رونوشت و فایل را با همان
SocialPostوkindدرست بسازید، نه با نوع جدا.
نکنید
- مقدار نامعلوم را با
0پر نکنید؛nullبگذارید، یا اگر شاخص برای این مورد معنا ندارد کلیدش را ننویسید. - نسبت را درصد ذخیره نکنید و در کامپوننت در 100 ضرب یا بر 100 تقسیم نکنید؛ تبدیل واحد فقط در مبدل است.
- شکل API را مستقیم به اجزای
@partodata/ui/socialندهید و نام نمایشی را از روی شناسه نسازید. - کارت یا ردیف پست را خودتان از اجزای پایه نسازید؛
Post،CommentوAccountرا به کار ببرید. - آمار حساب را جزو شاخصهای هر پست نیاورید.
- برای پست حذفشده، خصوصی یا در حال پردازش کامپوننت یا پرچم جدا نسازید؛
stateرا تنظیم کنید.
صفحات مرتبط
- Post، Comment و Account — یک کامپوننت برای هر موجودیت.
- EntityCollection — فهرست و شبکهٔ آنها، با اسکلت، حالتها، انتخاب و کیبورد.
- PlatformMark — نشان منبع برای هر
SocialSource، از جمله وب، تلویزیون، رادیو و منبع ناشناخته. - چیدمان صفحه — فهرست پستها در ستون خبرخوان صفحه قرار میگیرد، نه در تمام عرض آن.
جای فیلترها — سایدبار فیلتر یا نوار ابزار
فیلترهای هر صفحه فقط در یکی از دو جا هستند — سایدبار فیلتر در ستون دوم قاب، با همهچیز داخلش، یا چند فیلتر کشویی در نوار ابزار بالای فهرست؛ یک پاسخ برای هر نوع صفحه
محتوا و لحن
نوشتن متن رابط محصول پرتو به فارسی رسمی — دکمه، پیام، برچسب، عدد، تاریخ، املا، واژگان و یادداشت انتشار.