fix
5
.astro/settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1783587315248
|
||||
}
|
||||
}
|
||||
1
.astro/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
9
astro.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
})
|
||||
});
|
||||
BIN
data/cms.db
Normal file
BIN
data/cms.db-shm
Normal file
BIN
data/cms.db-wal
Normal file
71
data/cms.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"settings": {
|
||||
"siteInfo": {
|
||||
"name": "روس امروز",
|
||||
"tagline": "خبرگزاری",
|
||||
"description": "خبرگزاری تخصصی پوشش اخبار روسیه، اوراسیا و روابط منطقهای",
|
||||
"copyright": "۱۴۰۵ روس امروز — تمامی حقوق محفوظ است"
|
||||
},
|
||||
"breakingNews": {
|
||||
"label": "فوری",
|
||||
"href": "/posts/آستانا-میزبان-جام-قهرمانان-چهار-قاره-در-اسکیت-نمایشی-در-سال-۲۰۲۷-1782120219014/",
|
||||
"items": [
|
||||
"آستانا میزبان جام قهرمانان چهار قاره در اسکیت نمایشی در سال ۲۰۲۷"
|
||||
],
|
||||
"postSlugs": [
|
||||
"آستانا-میزبان-جام-قهرمانان-چهار-قاره-در-اسکیت-نمایشی-در-سال-۲۰۲۷-1782120219014"
|
||||
]
|
||||
},
|
||||
"hotTopics": [
|
||||
"انرژی",
|
||||
"تحریم",
|
||||
"نفت",
|
||||
"گاز",
|
||||
"اوراسیا",
|
||||
"SCO",
|
||||
"روبل",
|
||||
"تجارت",
|
||||
"قفقاز",
|
||||
"ناتو",
|
||||
"بریکس"
|
||||
],
|
||||
"homeLayout": {
|
||||
"featuredSlug": "در-کوبان-به-فرد-حمله-کننده-در-مرکز-خرید-اتهام-قتل-و-تلاش-برای-قتل-وارد-شد-1782119386187",
|
||||
"sideFeaturedSlugs": [
|
||||
"south-caucasus-geopolitics",
|
||||
"oil-price-new-record"
|
||||
],
|
||||
"latestCount": 4,
|
||||
"showAd": true
|
||||
},
|
||||
"ad": {
|
||||
"enabled": true,
|
||||
"label": "آگهی",
|
||||
"href": "/advertise/",
|
||||
"text": "جایگاه تبلیغات"
|
||||
},
|
||||
"agents": {
|
||||
"tass": {
|
||||
"processedLinks": [
|
||||
"https://tass.ru/proisshestviya/27834845",
|
||||
"https://tass.ru/sport/27834851",
|
||||
"https://tass.ru/obschestvo/27834629",
|
||||
"https://tass.ru/mezhdunarodnaya-panorama/27834975",
|
||||
"https://tass.ru/obschestvo/27834957",
|
||||
"https://tass.ru/politika/27834811",
|
||||
"https://tass.ru/obschestvo/27818171",
|
||||
"https://tass.ru/obschestvo/27835073",
|
||||
"https://tass.ru/sport/27835167"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories": [
|
||||
"اقتصاد",
|
||||
"سیاست",
|
||||
"انرژی",
|
||||
"فناوری",
|
||||
"بینالملل"
|
||||
],
|
||||
"posts": []
|
||||
}
|
||||
1
dist/client/_astro/_page_.RC7LoXVl.css
vendored
Normal file
7
dist/client/_astro/hoisted.ClNlXW51.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
const d={file:"#new-image-file",image:"#new-image-field",thumb:"#new-thumb-field",preview:"#new-image-preview",status:"#upload-status",button:"#new-upload-btn"},u=(e,t)=>{const n=document.querySelector(d.status);n&&(n.textContent=t,n.className="text-xs rounded-lg px-2.5 py-1 "+(e==="busy"?"bg-gray-100 text-gray-600":e==="ok"?"bg-green-50 text-green-700":e==="err"?"bg-red-50 text-brand":"text-gray-400"))},m=e=>{const t=document.querySelector(d.preview);t&&(t.innerHTML=e?`<img src="${e}" alt="" class="w-full h-full object-cover" />`:'<span class="text-gray-400 text-sm">تصویری انتخاب نشده</span>')},l={async uploadImage(e){const t=new FormData;t.append("image",e);const n=await fetch("/api/admin/upload",{method:"POST",body:t}),r=await n.json().catch(()=>({error:"پاسخ نامعتبر سرور"}));if(!n.ok||r.error)throw new Error(r.error??"آپلود ناموفق بود");return r},async runTassAgent(e=3){const t=await fetch("/api/agents/tass",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({limit:e})});if(!t.ok)throw new Error("خطا در اجرای agent");return t.json()},initUpload(){const e=document.querySelector(d.file);if(!e)return;const t=document.querySelector(d.image),n=document.querySelector(d.thumb),r=document.querySelector(d.button),c=()=>{const a=t?.value??"";document.querySelectorAll("[data-insert-image]").forEach(o=>{o.dataset.insertImage=a,o.disabled=!a})},i=(a,o)=>{t&&(t.value=a),n&&(n.value=o),m(o||a),c()},s=async a=>{u("busy","در حال آپلود و پردازش..."),r&&(r.disabled=!0);try{const o=await l.uploadImage(a);i(o.image,o.thumbnail),u("ok","تصویر آپلود شد و آماده استفاده است.")}catch(o){u("err",o instanceof Error?o.message:"آپلود ناموفق بود.")}finally{r&&(r.disabled=!1)}};e.addEventListener("change",()=>{const a=e.files?.[0];a&&s(a)}),r?.addEventListener("click",()=>{const a=e.files?.[0];a?s(a):u("err","ابتدا یک فایل انتخاب کنید.")}),c()},initEditor(){const e=document.querySelector("#post-body-editor");if(!e)return;const t=n=>{const r=e.selectionStart,c=e.selectionEnd,i=e.value.slice(0,r),s=e.value.slice(c),a=i.endsWith(`
|
||||
`)||i.length===0?"":`
|
||||
|
||||
`,o=s.startsWith(`
|
||||
`)||s.length===0?"":`
|
||||
|
||||
`;e.value=`${i}${a}${n}${o}${s}`,e.focus();const g=i.length+a.length+n.length;e.setSelectionRange(g,g)};document.querySelectorAll("[data-insert-image]").forEach(n=>{n.addEventListener("click",()=>{const r=n.dataset.insertImage;r&&t(`[image:${r}|توضیح تصویر]`)})}),document.querySelectorAll("[data-editor-command]").forEach(n=>{n.addEventListener("click",()=>{const r=n.dataset.editorCommand;r==="heading"&&t("## تیتر میانی"),r==="quote"&&t("> متن نقلقول"),r==="separator"&&t("---")})})},initConfirm(){document.querySelectorAll("[data-confirm-form]").forEach(e=>{e.addEventListener("submit",t=>{const n=e.dataset.confirmMessage||"برای تایید حذف عبارت confirm را تایپ کنید.",r=window.prompt(n);if(r!=="confirm"){t.preventDefault(),window.alert("حذف لغو شد. عبارت واردشده باید دقیقاً confirm باشد.");return}const c=e.querySelector("[data-confirm-input]");c&&(c.value=r)})})},initTass(){const e=document.querySelector("#run-tass-agent"),t=document.querySelector("#tass-agent-result");!e||!t||e.addEventListener("click",async()=>{e.disabled=!0,e.textContent="در حال اجرا...",t.classList.remove("hidden"),t.textContent="در حال دریافت و ترجمه خبرها...";try{const n=await l.runTassAgent(3);t.textContent=JSON.stringify(n,null,2)}catch(n){t.textContent=n instanceof Error?n.message:"خطا در اجرای agent"}finally{e.disabled=!1,e.textContent="اجرای agent و ساخت پست"}})},initTabs(){const t=document.querySelector("[data-active-tab]")?.dataset.activeTab??"pending",n=Array.from(document.querySelectorAll("[data-tab]")),r=Array.from(document.querySelectorAll("[data-panel]")),c=i=>{n.forEach(s=>{const a=s.dataset.tab===i;s.classList.toggle("bg-brand",a),s.classList.toggle("text-white",a),s.classList.toggle("border-brand",a),s.classList.toggle("bg-white",!a),s.classList.toggle("text-gray-700",!a),s.classList.toggle("border-gray-200",!a),a?s.setAttribute("aria-current","page"):s.removeAttribute("aria-current")}),r.forEach(s=>{s.classList.toggle("hidden",s.dataset.panel!==i)})};c(t),n.forEach(i=>{i.addEventListener("click",s=>{const a=i.dataset.tab;if(!a)return;s.preventDefault(),c(a);const o=new URL(window.location.href);o.searchParams.set("tab",a),o.searchParams.delete("pendingPage"),o.searchParams.delete("approvedPage"),o.searchParams.delete("edit"),window.history.pushState({},"",o)})}),window.addEventListener("popstate",()=>{const i=new URLSearchParams(window.location.search);c(i.get("tab")||t)})}};l.initTabs();l.initUpload();l.initEditor();l.initConfirm();l.initTass();
|
||||
BIN
dist/client/assets/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 934 KiB |
BIN
dist/client/uploads/0dfaf688-a3f5-4914-b0a5-14a720c212bf.webp
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
dist/client/uploads/10d5c9bf-849e-4ddd-a7d8-dd5dc6a2e666.webp
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
dist/client/uploads/15a3a254-12db-4851-a434-cae6e631838d.webp
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dist/client/uploads/23ed9cea-411b-4169-a905-a40bd503d8c6.webp
vendored
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
dist/client/uploads/2d77aee7-e390-4ca2-8100-d0e6528c145a.webp
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
dist/client/uploads/3b043c9d-57bf-46f2-9fb2-9c8be53bbcf5.webp
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
dist/client/uploads/553d2a2a-b924-401e-988d-1db34a907355.webp
vendored
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
dist/client/uploads/70cc796b-ee3c-49c8-8939-d0fe0dde1554.webp
vendored
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
dist/client/uploads/7c59fcfa-93cc-4d1d-b2be-d491eb482302.webp
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
dist/client/uploads/9de7af87-a5f2-4299-bc4e-6c3a17ee9f4d.webp
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
dist/client/uploads/ad228b50-fe89-40a6-b17d-87440c24d6a5.webp
vendored
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
dist/client/uploads/af246b9a-cf64-43c2-934d-edb5d8f0b8ee.webp
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
dist/client/uploads/d0858db4-fcc3-43b4-b120-53cc41a4473c.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/d3a18a75-4a6f-48c6-b3a5-c10f6fe798d7.webp
vendored
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
dist/client/uploads/eaebf85a-c702-4eb6-ae0d-02a752761ea5.webp
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
dist/client/uploads/eb8a9dae-70da-4887-8a36-75f660ccd42a.webp
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
dist/client/uploads/f1b83771-f192-46f1-8629-6982d42c3b48.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/fcdcd26a-1029-46ec-adae-8013cbe48022.webp
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
dist/client/uploads/thumbs/0dfaf688-a3f5-4914-b0a5-14a720c212bf-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
dist/client/uploads/thumbs/10d5c9bf-849e-4ddd-a7d8-dd5dc6a2e666-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/thumbs/15a3a254-12db-4851-a434-cae6e631838d-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/thumbs/23ed9cea-411b-4169-a905-a40bd503d8c6-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
dist/client/uploads/thumbs/2d77aee7-e390-4ca2-8100-d0e6528c145a-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/thumbs/3b043c9d-57bf-46f2-9fb2-9c8be53bbcf5-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dist/client/uploads/thumbs/553d2a2a-b924-401e-988d-1db34a907355-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
dist/client/uploads/thumbs/70cc796b-ee3c-49c8-8939-d0fe0dde1554-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
dist/client/uploads/thumbs/7c59fcfa-93cc-4d1d-b2be-d491eb482302-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
dist/client/uploads/thumbs/9de7af87-a5f2-4299-bc4e-6c3a17ee9f4d-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
dist/client/uploads/thumbs/ad228b50-fe89-40a6-b17d-87440c24d6a5-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
dist/client/uploads/thumbs/af246b9a-cf64-43c2-934d-edb5d8f0b8ee-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
dist/client/uploads/thumbs/d0858db4-fcc3-43b4-b120-53cc41a4473c-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
dist/client/uploads/thumbs/d3a18a75-4a6f-48c6-b3a5-c10f6fe798d7-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
dist/client/uploads/thumbs/eaebf85a-c702-4eb6-ae0d-02a752761ea5-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
dist/client/uploads/thumbs/eb8a9dae-70da-4887-8a36-75f660ccd42a-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
dist/client/uploads/thumbs/f1b83771-f192-46f1-8629-6982d42c3b48-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
dist/client/uploads/thumbs/fcdcd26a-1029-46ec-adae-8013cbe48022-thumb.webp
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
dist/server/_@astrojs-ssr-adapter.mjs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { c as createExports, a as start } from './chunks/_@astrojs-ssr-adapter_BSED7qqF.mjs';
|
||||
3
dist/server/_noop-middleware.mjs
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const onRequest = (_, next) => next();
|
||||
|
||||
export { onRequest };
|
||||
88
dist/server/chunks/BaseLayout_Bc2tHXHa.mjs
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import { c as createComponent, m as maybeRenderHead, a as addAttribute, r as renderTemplate, b as createAstro, e as renderHead, d as renderComponent, f as renderSlot } from './astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import 'clsx';
|
||||
import { r as readCms, b as getApprovedPosts, c as getPostBySlug, j as getPrimaryNav, e as getCategoryPath, k as getFooterPageLinks } from './cms_CRTVymfB.mjs';
|
||||
/* empty css */
|
||||
|
||||
const $$Astro$2 = createAstro();
|
||||
const $$Header = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro$2, $$props, $$slots);
|
||||
Astro2.self = $$Header;
|
||||
const cms = await readCms();
|
||||
const { breakingNews } = cms.settings;
|
||||
const approvedPosts = getApprovedPosts(cms);
|
||||
const breakingPosts = [
|
||||
...(breakingNews.postSlugs ?? []).map((slug) => getPostBySlug(approvedPosts, slug)).filter(Boolean),
|
||||
...approvedPosts.filter((post) => post.urgent)
|
||||
].filter((post, index, list) => list.findIndex((item) => item.slug === post.slug) === index);
|
||||
const primaryNav = getPrimaryNav();
|
||||
const pathname = Astro2.url.pathname;
|
||||
const normalizePath = (value) => decodeURIComponent(value.replace(/\/+$/, "") || "/");
|
||||
const normalizedPathname = normalizePath(pathname);
|
||||
const isActive = (href) => {
|
||||
const target = normalizePath(href);
|
||||
if (target === "/") {
|
||||
return normalizedPathname === "/";
|
||||
}
|
||||
return normalizedPathname === target || normalizedPathname.startsWith(`${target}/`);
|
||||
};
|
||||
const categoryTabs = [
|
||||
{ label: "\u0647\u0645\u0647", href: "/posts/" },
|
||||
...cms.categories.map((category) => ({
|
||||
label: category,
|
||||
href: getCategoryPath(category)
|
||||
}))
|
||||
];
|
||||
const breakingText = breakingPosts.length > 0 ? breakingPosts.map((post) => post.title).join(" \u2014 ") : breakingNews.items.join(" \u2014 ");
|
||||
const breakingHref = breakingPosts[0] ? `/posts/${breakingPosts[0].slug}/` : breakingNews.href;
|
||||
return renderTemplate`${maybeRenderHead()}<header class="bg-white shadow-sm sticky top-0 z-50"> <div class="max-w-7xl mx-auto px-4 lg:px-8"> <div class="flex items-center justify-between h-14 lg:h-16"> <a href="/" class="flex items-center gap-2.5 flex-shrink-0"> <img src="/assets/logo.png"${addAttribute(cms.settings.siteInfo.name, "alt")} class="h-24 w-auto object-contain block" loading="eager"> </a> <nav class="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-600"> ${primaryNav.map((item) => renderTemplate`<a${addAttribute(item.href, "href")}${addAttribute([
|
||||
"transition-colors",
|
||||
isActive(item.href) ? "text-brand font-bold" : "text-gray-600 hover:text-brand"
|
||||
], "class:list")}> ${item.label} </a>`)} </nav> <div class="flex items-center gap-2"> <a${addAttribute(breakingHref, "href")} class="hidden sm:flex items-center gap-1 bg-red-50 text-brand text-xs px-3 py-1.5 rounded-full font-medium"> <span class="material-icons text-sm leading-none">bolt</span> ${breakingNews.label} </a> <form action="/search/" method="get" class="relative"> <input type="search" name="q" placeholder="جستجو..." class="w-28 sm:w-40 lg:w-52 bg-gray-100 text-gray-700 text-sm rounded-full py-2 pr-9 pl-3 outline-none focus:ring-1 focus:ring-brand focus:bg-white transition"> <button type="submit" class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-brand transition-colors" aria-label="جستجو"> <span class="material-icons text-xl">search</span> </button> </form> <button class="p-2 text-gray-500 hover:text-brand transition-colors hidden sm:block" aria-label="اعلانها"> <span class="material-icons">notifications_none</span> </button> </div> </div> <div class="flex overflow-x-auto gap-1.5 pb-3 scrollbar-hide -mx-1 px-1"> ${categoryTabs.map((category, index) => renderTemplate`<a${addAttribute(category.href, "href")}${addAttribute([
|
||||
"whitespace-nowrap text-xs px-3.5 py-1.5 rounded-full font-medium transition-colors",
|
||||
isActive(category.href) ? "bg-brand text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
], "class:list")}> ${category.label} </a>`)} </div> </div> </header> <div class="bg-brand text-white"> <div class="max-w-7xl mx-auto px-4 lg:px-8 py-2.5 flex items-center gap-3"> <a${addAttribute(breakingHref, "href")} class="bg-white text-brand text-xs font-bold px-2 py-0.5 rounded flex-shrink-0">${breakingNews.label}</a> <div class="overflow-hidden relative flex-1"> <p class="text-sm truncate"> ${breakingText} </p> </div> <span class="material-icons text-yellow-300 text-base flex-shrink-0">bolt</span> </div> </div>`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/components/Header.astro", void 0);
|
||||
|
||||
const $$Astro$1 = createAstro();
|
||||
const $$Footer = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro$1, $$props, $$slots);
|
||||
Astro2.self = $$Footer;
|
||||
const cms = await readCms();
|
||||
const footerPageLinks = getFooterPageLinks();
|
||||
const pathname = Astro2.url.pathname;
|
||||
const normalizePath = (value) => decodeURIComponent(value.replace(/\/+$/, "") || "/");
|
||||
const normalizedPathname = normalizePath(pathname);
|
||||
const isNavActive = (href) => {
|
||||
const target = normalizePath(href);
|
||||
if (target === "/") {
|
||||
return normalizedPathname === "/";
|
||||
}
|
||||
return normalizedPathname === target || normalizedPathname.startsWith(`${target}/`);
|
||||
};
|
||||
const mobileNav = [
|
||||
{ icon: "home", label: "\u062E\u0627\u0646\u0647", href: "/" },
|
||||
{ icon: "article", label: "\u0627\u062E\u0628\u0627\u0631", href: "/posts/" },
|
||||
{ icon: "insights", label: "\u0627\u0642\u062A\u0635\u0627\u062F", href: getCategoryPath("\u0627\u0642\u062A\u0635\u0627\u062F") },
|
||||
{ icon: "explore", label: "\u062C\u0647\u0627\u0646", href: getCategoryPath("\u0628\u06CC\u0646\u200C\u0627\u0644\u0645\u0644\u0644") },
|
||||
{ icon: "bolt", label: "\u0627\u0646\u0631\u0698\u06CC", href: getCategoryPath("\u0627\u0646\u0631\u0698\u06CC") }
|
||||
].map((item) => ({ ...item, active: isNavActive(item.href) }));
|
||||
return renderTemplate`${maybeRenderHead()}<footer class="bg-gray-900 text-white mt-12 pt-10 pb-28 lg:pb-10"> <div class="max-w-7xl mx-auto px-4 lg:px-8"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-8"> <div class="col-span-2 lg:col-span-1"> <div class="flex items-center gap-2 mb-3"> <img src="/assets/logo.png"${addAttribute(cms.settings.siteInfo.name, "alt")} class="h-12 w-auto object-contain block brightness-0 invert" loading="lazy"> </div> <p class="text-gray-400 text-sm leading-relaxed">${cms.settings.siteInfo.description}</p> </div> <div> <h4 class="font-bold mb-3 text-sm">بخشها</h4> <ul class="space-y-2 text-gray-400 text-sm"> ${cms.categories.map((item) => renderTemplate`<li><a${addAttribute(getCategoryPath(item), "href")} class="hover:text-white transition-colors">${item}</a></li>`)} </ul> </div> <div> <h4 class="font-bold mb-3 text-sm">درباره ما</h4> <ul class="space-y-2 text-gray-400 text-sm"> ${footerPageLinks.map((item) => renderTemplate`<li><a${addAttribute(item.href, "href")} class="hover:text-white transition-colors">${item.label}</a></li>`)} </ul> </div> <div> <h4 class="font-bold mb-3 text-sm">خبرنامه</h4> <p class="text-gray-400 text-sm mb-3">آخرین اخبار را در ایمیل دریافت کنید</p> <div class="flex gap-2"> <input type="email" placeholder="ایمیل شما" class="flex-1 bg-gray-800 text-white text-sm px-3 py-2 rounded-lg outline-none focus:ring-1 focus:ring-brand min-w-0"> <button class="bg-brand text-white text-sm px-3 py-2 rounded-lg hover:bg-red-700 transition-colors flex-shrink-0">ثبت</button> </div> </div> </div> <div class="border-t border-gray-800 pt-6 text-center text-gray-500 text-xs">
|
||||
© ${cms.settings.siteInfo.copyright} </div> </div> </footer> <nav class="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 shadow-2xl z-50"> <div class="flex justify-around items-center py-2 px-2"> ${mobileNav.map((item) => renderTemplate`<a${addAttribute(item.href, "href")}${addAttribute([
|
||||
"flex flex-col items-center gap-0.5 px-3 py-1 transition-colors relative",
|
||||
item.active ? "text-brand" : "text-gray-400"
|
||||
], "class:list")}> ${item.active && renderTemplate`<span class="absolute top-0 left-1/2 -translate-x-1/2 h-1 w-6 rounded-full bg-brand"></span>`} <span class="material-icons text-2xl">${item.icon}</span> <span${addAttribute(["text-xs", item.active && "font-bold"], "class:list")}>${item.label}</span> </a>`)} </div> </nav>`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/components/Footer.astro", void 0);
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$BaseLayout = createComponent(($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$BaseLayout;
|
||||
const {
|
||||
title = "\u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632",
|
||||
description = "\u062E\u0628\u0631\u06AF\u0632\u0627\u0631\u06CC \u062A\u062E\u0635\u0635\u06CC \u067E\u0648\u0634\u0634 \u0627\u062E\u0628\u0627\u0631 \u0631\u0648\u0633\u06CC\u0647\u060C \u0627\u0648\u0631\u0627\u0633\u06CC\u0627 \u0648 \u0631\u0648\u0627\u0628\u0637 \u0645\u0646\u0637\u0642\u0647\u200C\u0627\u06CC"
|
||||
} = Astro2.props;
|
||||
return renderTemplate`<html lang="fa" dir="rtl"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description"${addAttribute(description, "content")}><title>${title}</title><link rel="stylesheet" href="https://rc0.ir/material-icons/iconfont/material-icons.css"><link rel="stylesheet" href="https://rc0.ir/vazirmatn/Vazirmatn-font-face.css">${renderHead()}</head> <body class="bg-gray-100 font-vazir text-gray-900"> ${renderComponent($$result, "Header", $$Header, {})} ${renderSlot($$result, $$slots["default"])} ${renderComponent($$result, "Footer", $$Footer, {})} </body></html>`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/layouts/BaseLayout.astro", void 0);
|
||||
|
||||
export { $$BaseLayout as $ };
|
||||
19
dist/server/chunks/Pagination_BeoGb-H9.mjs
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import { c as createComponent, d as renderComponent, F as Fragment, r as renderTemplate, b as createAstro, m as maybeRenderHead, a as addAttribute } from './astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Pagination = createComponent(($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Pagination;
|
||||
const { currentPage, totalPages, basePath = "/posts/" } = Astro2.props;
|
||||
const pages = Array.from({ length: totalPages }, (_, index) => index + 1);
|
||||
const getPageHref = (page) => page === 1 ? basePath : `${basePath}?page=${page}`;
|
||||
const previousPage = Math.max(1, currentPage - 1);
|
||||
const nextPage = Math.min(totalPages, currentPage + 1);
|
||||
return renderTemplate`${totalPages > 1 && renderTemplate`${renderComponent($$result, "Fragment", Fragment, {}, { "default": ($$result2) => renderTemplate`${maybeRenderHead()}<nav class="flex items-center justify-center gap-1.5 mt-8" aria-label="صفحهبندی">${currentPage === 1 ? renderTemplate`<span class="w-9 h-9 rounded-full border border-gray-200 bg-white flex items-center justify-center opacity-40" aria-disabled="true"><span class="material-icons text-base">chevron_right</span></span>` : renderTemplate`<a${addAttribute(getPageHref(previousPage), "href")} class="w-9 h-9 rounded-full border border-gray-200 bg-white flex items-center justify-center transition-colors hover:bg-brand hover:text-white hover:border-brand" aria-label="صفحه قبلی"><span class="material-icons text-base">chevron_right</span></a>`}${pages.map((page) => renderTemplate`<a${addAttribute(getPageHref(page), "href")}${addAttribute(page === currentPage ? "page" : void 0, "aria-current")}${addAttribute([
|
||||
"w-9 h-9 rounded-full border border-gray-200 flex items-center justify-center text-sm font-medium transition-colors",
|
||||
page === currentPage ? "bg-brand text-white border-brand" : "bg-white hover:bg-gray-50"
|
||||
], "class:list")}>${page}</a>`)}${currentPage === totalPages ? renderTemplate`<span class="w-9 h-9 rounded-full border border-gray-200 bg-white flex items-center justify-center opacity-40" aria-disabled="true"><span class="material-icons text-base">chevron_left</span></span>` : renderTemplate`<a${addAttribute(getPageHref(nextPage), "href")} class="w-9 h-9 rounded-full border border-gray-200 bg-white flex items-center justify-center transition-colors hover:bg-brand hover:text-white hover:border-brand" aria-label="صفحه بعدی"><span class="material-icons text-base">chevron_left</span></a>`}</nav><p class="text-center text-xs text-gray-400 mt-2">صفحه ${currentPage} از ${totalPages}</p>` })}`}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/components/Pagination.astro", void 0);
|
||||
|
||||
export { $$Pagination as $ };
|
||||
16
dist/server/chunks/PostCard_D8td2Gog.mjs
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { c as createComponent, m as maybeRenderHead, a as addAttribute, r as renderTemplate, b as createAstro } from './astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import 'clsx';
|
||||
import { f as formatPostRelativeTime } from './cms_CRTVymfB.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$PostCard = createComponent(($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$PostCard;
|
||||
const { post } = Astro2.props;
|
||||
const image = post.thumbnail || post.image;
|
||||
const timeLabel = formatPostRelativeTime(post);
|
||||
return renderTemplate`${maybeRenderHead()}<article class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow group"> <a${addAttribute(`/posts/${post.slug}/`, "href")} class="flex gap-4 p-4"> <div class="img-placeholder w-28 sm:w-36 h-24 sm:h-28 rounded-xl flex-shrink-0 relative overflow-hidden"> ${image ? renderTemplate`<img${addAttribute(image, "src")}${addAttribute(post.title, "alt")} class="w-full h-full object-cover" loading="lazy">` : renderTemplate`<div class="absolute inset-0 flex items-center justify-center text-gray-400"> <span class="material-icons text-2xl opacity-30">image</span> </div>`} </div> <div class="flex-1 min-w-0 flex flex-col justify-between"> <div> <span class="text-brand text-xs font-bold">${post.category}</span> <h3 class="font-bold text-sm sm:text-base mt-1 leading-snug line-clamp-2 group-hover:text-brand transition-colors">${post.title}</h3> <p class="text-gray-500 text-xs sm:text-sm mt-1 leading-relaxed line-clamp-2 hidden sm:block">${post.excerpt}</p> </div> <div class="flex items-center justify-between mt-2"> <div class="flex items-center gap-2 text-xs text-gray-400"> <span>${timeLabel}</span> <span>·</span> <span>${post.readTime}</span> <span>·</span> <span>${post.category}</span> </div> <div class="flex items-center gap-1 text-gray-300"> <span class="material-icons text-lg">bookmark_border</span> <span class="material-icons text-lg">share</span> </div> </div> </div> </a> </article>`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/components/PostCard.astro", void 0);
|
||||
|
||||
export { $$PostCard as $ };
|
||||
3083
dist/server/chunks/_@astrojs-ssr-adapter_BSED7qqF.mjs
vendored
Normal file
27
dist/server/chunks/adminAuth_CY4OwU1a.mjs
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
const sessionCookie = "ruseemrooz_admin";
|
||||
const getAdminUser = () => "admin";
|
||||
const getAdminPassword = () => "admin123";
|
||||
const getSessionSecret = () => "local-session";
|
||||
const encodeToken = () => Buffer.from(`${getAdminUser()}:${getSessionSecret()}`).toString("base64url");
|
||||
const isValidLogin = (username, password) => username === getAdminUser() && password === getAdminPassword();
|
||||
const setAdminSession = (context) => {
|
||||
context.cookies.set(sessionCookie, encodeToken(), {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
maxAge: 60 * 60 * 8
|
||||
});
|
||||
};
|
||||
const clearAdminSession = (context) => {
|
||||
context.cookies.delete(sessionCookie, { path: "/" });
|
||||
};
|
||||
const isAdminAuthenticated = (context) => context.cookies.get(sessionCookie)?.value === encodeToken();
|
||||
const requireAdmin = (context) => {
|
||||
if (!isAdminAuthenticated(context)) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export { isValidLogin as a, clearAdminSession as c, isAdminAuthenticated as i, requireAdmin as r, setAdminSession as s };
|
||||
16
dist/server/chunks/apiAuth_DcNSM7D5.mjs
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
const getToken = () => "admin123";
|
||||
const requireApiToken = (context) => {
|
||||
const authorization = context.request.headers.get("authorization") ?? "";
|
||||
const headerToken = context.request.headers.get("x-api-key") ?? "";
|
||||
const bearerToken = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
|
||||
const token = bearerToken || headerToken;
|
||||
if (token !== getToken()) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json; charset=utf-8" }
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export { requireApiToken as r };
|
||||
281
dist/server/chunks/astro-designed-error-pages_DsfhG0WT.mjs
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
import { N as NOOP_MIDDLEWARE_HEADER, D as DEFAULT_404_COMPONENT } from './astro/server_DrnzQWw3.mjs';
|
||||
import { parse } from 'devalue';
|
||||
import { escape } from 'html-escaper';
|
||||
|
||||
const NOOP_MIDDLEWARE_FN = async (_ctx, next) => {
|
||||
const response = await next();
|
||||
response.headers.set(NOOP_MIDDLEWARE_HEADER, "true");
|
||||
return response;
|
||||
};
|
||||
|
||||
const ACTION_QUERY_PARAMS = {
|
||||
actionName: "_astroAction"};
|
||||
|
||||
const __vite_import_meta_env__ = {"ASSETS_PREFIX": undefined, "BASE_URL": "/", "DEV": false, "MODE": "production", "PROD": true, "SITE": undefined, "SSR": true};
|
||||
const codeToStatusMap = {
|
||||
// Implemented from tRPC error code table
|
||||
// https://trpc.io/docs/server/error-handling#error-codes
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
TIMEOUT: 405,
|
||||
CONFLICT: 409,
|
||||
PRECONDITION_FAILED: 412,
|
||||
PAYLOAD_TOO_LARGE: 413,
|
||||
UNSUPPORTED_MEDIA_TYPE: 415,
|
||||
UNPROCESSABLE_CONTENT: 422,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
CLIENT_CLOSED_REQUEST: 499,
|
||||
INTERNAL_SERVER_ERROR: 500
|
||||
};
|
||||
const statusToCodeMap = Object.entries(codeToStatusMap).reduce(
|
||||
// reverse the key-value pairs
|
||||
(acc, [key, value]) => ({ ...acc, [value]: key }),
|
||||
{}
|
||||
);
|
||||
class ActionError extends Error {
|
||||
type = "AstroActionError";
|
||||
code = "INTERNAL_SERVER_ERROR";
|
||||
status = 500;
|
||||
constructor(params) {
|
||||
super(params.message);
|
||||
this.code = params.code;
|
||||
this.status = ActionError.codeToStatus(params.code);
|
||||
if (params.stack) {
|
||||
this.stack = params.stack;
|
||||
}
|
||||
}
|
||||
static codeToStatus(code) {
|
||||
return codeToStatusMap[code];
|
||||
}
|
||||
static statusToCode(status) {
|
||||
return statusToCodeMap[status] ?? "INTERNAL_SERVER_ERROR";
|
||||
}
|
||||
static fromJson(body) {
|
||||
if (isInputError(body)) {
|
||||
return new ActionInputError(body.issues);
|
||||
}
|
||||
if (isActionError(body)) {
|
||||
return new ActionError(body);
|
||||
}
|
||||
return new ActionError({
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
});
|
||||
}
|
||||
}
|
||||
function isActionError(error) {
|
||||
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionError";
|
||||
}
|
||||
function isInputError(error) {
|
||||
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionInputError" && "issues" in error && Array.isArray(error.issues);
|
||||
}
|
||||
class ActionInputError extends ActionError {
|
||||
type = "AstroActionInputError";
|
||||
// We don't expose all ZodError properties.
|
||||
// Not all properties will serialize from server to client,
|
||||
// and we don't want to import the full ZodError object into the client.
|
||||
issues;
|
||||
fields;
|
||||
constructor(issues) {
|
||||
super({
|
||||
message: `Failed to validate: ${JSON.stringify(issues, null, 2)}`,
|
||||
code: "BAD_REQUEST"
|
||||
});
|
||||
this.issues = issues;
|
||||
this.fields = {};
|
||||
for (const issue of issues) {
|
||||
if (issue.path.length > 0) {
|
||||
const key = issue.path[0].toString();
|
||||
this.fields[key] ??= [];
|
||||
this.fields[key]?.push(issue.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getActionQueryString(name) {
|
||||
const searchParams = new URLSearchParams({ [ACTION_QUERY_PARAMS.actionName]: name });
|
||||
return `?${searchParams.toString()}`;
|
||||
}
|
||||
function deserializeActionResult(res) {
|
||||
if (res.type === "error") {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(res.body);
|
||||
} catch {
|
||||
return {
|
||||
data: void 0,
|
||||
error: new ActionError({
|
||||
message: res.body,
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
})
|
||||
};
|
||||
}
|
||||
if (Object.assign(__vite_import_meta_env__, { _: process.env._ })?.PROD) {
|
||||
return { error: ActionError.fromJson(json), data: void 0 };
|
||||
} else {
|
||||
const error = ActionError.fromJson(json);
|
||||
error.stack = actionResultErrorStack.get();
|
||||
return {
|
||||
error,
|
||||
data: void 0
|
||||
};
|
||||
}
|
||||
}
|
||||
if (res.type === "empty") {
|
||||
return { data: void 0, error: void 0 };
|
||||
}
|
||||
return {
|
||||
data: parse(res.body, {
|
||||
URL: (href) => new URL(href)
|
||||
}),
|
||||
error: void 0
|
||||
};
|
||||
}
|
||||
const actionResultErrorStack = /* @__PURE__ */ function actionResultErrorStackFn() {
|
||||
let errorStack;
|
||||
return {
|
||||
set(stack) {
|
||||
errorStack = stack;
|
||||
},
|
||||
get() {
|
||||
return errorStack;
|
||||
}
|
||||
};
|
||||
}();
|
||||
|
||||
function template({
|
||||
title,
|
||||
pathname,
|
||||
statusCode = 404,
|
||||
tabTitle,
|
||||
body
|
||||
}) {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${tabTitle}</title>
|
||||
<style>
|
||||
:root {
|
||||
--gray-10: hsl(258, 7%, 10%);
|
||||
--gray-20: hsl(258, 7%, 20%);
|
||||
--gray-30: hsl(258, 7%, 30%);
|
||||
--gray-40: hsl(258, 7%, 40%);
|
||||
--gray-50: hsl(258, 7%, 50%);
|
||||
--gray-60: hsl(258, 7%, 60%);
|
||||
--gray-70: hsl(258, 7%, 70%);
|
||||
--gray-80: hsl(258, 7%, 80%);
|
||||
--gray-90: hsl(258, 7%, 90%);
|
||||
--black: #13151A;
|
||||
--accent-light: #E0CCFA;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--black);
|
||||
color-scheme: dark;
|
||||
accent-color: var(--accent-light);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray-10);
|
||||
color: var(--gray-80);
|
||||
font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 8px;
|
||||
color: white;
|
||||
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-weight: 700;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.statusCode {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.astro-icon {
|
||||
height: 124px;
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
padding: 2px 8px;
|
||||
background: rgba(0,0,0, 0.25);
|
||||
border: 1px solid rgba(255,255,255, 0.25);
|
||||
border-radius: 4px;
|
||||
font-size: 1.2em;
|
||||
margin-top: 0;
|
||||
max-width: 60em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="center">
|
||||
<svg class="astro-icon" xmlns="http://www.w3.org/2000/svg" width="64" height="80" viewBox="0 0 64 80" fill="none"> <path d="M20.5253 67.6322C16.9291 64.3531 15.8793 57.4632 17.3776 52.4717C19.9755 55.6188 23.575 56.6157 27.3035 57.1784C33.0594 58.0468 38.7122 57.722 44.0592 55.0977C44.6709 54.7972 45.2362 54.3978 45.9045 53.9931C46.4062 55.4451 46.5368 56.9109 46.3616 58.4028C45.9355 62.0362 44.1228 64.8429 41.2397 66.9705C40.0868 67.8215 38.8669 68.5822 37.6762 69.3846C34.0181 71.8508 33.0285 74.7426 34.403 78.9491C34.4357 79.0516 34.4649 79.1541 34.5388 79.4042C32.6711 78.5705 31.3069 77.3565 30.2674 75.7604C29.1694 74.0757 28.6471 72.2121 28.6196 70.1957C28.6059 69.2144 28.6059 68.2244 28.4736 67.257C28.1506 64.8985 27.0406 63.8425 24.9496 63.7817C22.8036 63.7192 21.106 65.0426 20.6559 67.1268C20.6215 67.2865 20.5717 67.4446 20.5218 67.6304L20.5253 67.6322Z" fill="white"/> <path d="M20.5253 67.6322C16.9291 64.3531 15.8793 57.4632 17.3776 52.4717C19.9755 55.6188 23.575 56.6157 27.3035 57.1784C33.0594 58.0468 38.7122 57.722 44.0592 55.0977C44.6709 54.7972 45.2362 54.3978 45.9045 53.9931C46.4062 55.4451 46.5368 56.9109 46.3616 58.4028C45.9355 62.0362 44.1228 64.8429 41.2397 66.9705C40.0868 67.8215 38.8669 68.5822 37.6762 69.3846C34.0181 71.8508 33.0285 74.7426 34.403 78.9491C34.4357 79.0516 34.4649 79.1541 34.5388 79.4042C32.6711 78.5705 31.3069 77.3565 30.2674 75.7604C29.1694 74.0757 28.6471 72.2121 28.6196 70.1957C28.6059 69.2144 28.6059 68.2244 28.4736 67.257C28.1506 64.8985 27.0406 63.8425 24.9496 63.7817C22.8036 63.7192 21.106 65.0426 20.6559 67.1268C20.6215 67.2865 20.5717 67.4446 20.5218 67.6304L20.5253 67.6322Z" fill="url(#paint0_linear_738_686)"/> <path d="M0 51.6401C0 51.6401 10.6488 46.4654 21.3274 46.4654L29.3786 21.6102C29.6801 20.4082 30.5602 19.5913 31.5538 19.5913C32.5474 19.5913 33.4275 20.4082 33.7289 21.6102L41.7802 46.4654C54.4274 46.4654 63.1076 51.6401 63.1076 51.6401C63.1076 51.6401 45.0197 2.48776 44.9843 2.38914C44.4652 0.935933 43.5888 0 42.4073 0H20.7022C19.5206 0 18.6796 0.935933 18.1251 2.38914C18.086 2.4859 0 51.6401 0 51.6401Z" fill="white"/> <defs> <linearGradient id="paint0_linear_738_686" x1="31.554" y1="75.4423" x2="39.7462" y2="48.376" gradientUnits="userSpaceOnUse"> <stop stop-color="#D83333"/> <stop offset="1" stop-color="#F041FF"/> </linearGradient> </defs> </svg>
|
||||
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ""}<span class="statusMessage">${title}</span></h1>
|
||||
${body || `
|
||||
<pre>Path: ${escape(pathname)}</pre>
|
||||
`}
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const DEFAULT_404_ROUTE = {
|
||||
component: DEFAULT_404_COMPONENT,
|
||||
generate: () => "",
|
||||
params: [],
|
||||
pattern: /\/404/,
|
||||
prerender: false,
|
||||
pathname: "/404",
|
||||
segments: [[{ content: "404", dynamic: false, spread: false }]],
|
||||
type: "page",
|
||||
route: "/404",
|
||||
fallbackRoutes: [],
|
||||
isIndex: false
|
||||
};
|
||||
function ensure404Route(manifest) {
|
||||
if (!manifest.routes.some((route) => route.route === "/404")) {
|
||||
manifest.routes.push(DEFAULT_404_ROUTE);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
async function default404Page({ pathname }) {
|
||||
return new Response(
|
||||
template({
|
||||
statusCode: 404,
|
||||
title: "Not found",
|
||||
tabTitle: "404: Not Found",
|
||||
pathname
|
||||
}),
|
||||
{ status: 404, headers: { "Content-Type": "text/html; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
default404Page.isAstroComponentFactory = true;
|
||||
const default404Instance = {
|
||||
default: default404Page
|
||||
};
|
||||
|
||||
export { DEFAULT_404_ROUTE as D, NOOP_MIDDLEWARE_FN as N, default404Instance as a, deserializeActionResult as d, ensure404Route as e, getActionQueryString as g };
|
||||
666
dist/server/chunks/astro/assets-service_BZIICsh3.mjs
vendored
Normal file
@@ -0,0 +1,666 @@
|
||||
import { isRemotePath, joinPaths } from '@astrojs/internal-helpers/path';
|
||||
|
||||
const ClientAddressNotAvailable = {
|
||||
name: "ClientAddressNotAvailable",
|
||||
title: "`Astro.clientAddress` is not available in current adapter.",
|
||||
message: (adapterName) => `\`Astro.clientAddress\` is not available in the \`${adapterName}\` adapter. File an issue with the adapter to add support.`
|
||||
};
|
||||
const PrerenderClientAddressNotAvailable = {
|
||||
name: "PrerenderClientAddressNotAvailable",
|
||||
title: "`Astro.clientAddress` cannot be used inside prerendered routes.",
|
||||
message: `\`Astro.clientAddress\` cannot be used inside prerendered routes`
|
||||
};
|
||||
const StaticClientAddressNotAvailable = {
|
||||
name: "StaticClientAddressNotAvailable",
|
||||
title: "`Astro.clientAddress` is not available in static mode.",
|
||||
message: "`Astro.clientAddress` is only available when using `output: 'server'` or `output: 'hybrid'`. Update your Astro config if you need SSR features.",
|
||||
hint: "See https://docs.astro.build/en/guides/server-side-rendering/ for more information on how to enable SSR."
|
||||
};
|
||||
const NoMatchingStaticPathFound = {
|
||||
name: "NoMatchingStaticPathFound",
|
||||
title: "No static path found for requested path.",
|
||||
message: (pathName) => `A \`getStaticPaths()\` route pattern was matched, but no matching static path was found for requested path \`${pathName}\`.`,
|
||||
hint: (possibleRoutes) => `Possible dynamic routes being matched: ${possibleRoutes.join(", ")}.`
|
||||
};
|
||||
const OnlyResponseCanBeReturned = {
|
||||
name: "OnlyResponseCanBeReturned",
|
||||
title: "Invalid type returned by Astro page.",
|
||||
message: (route, returnedValue) => `Route \`${route ? route : ""}\` returned a \`${returnedValue}\`. Only a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) can be returned from Astro files.`,
|
||||
hint: "See https://docs.astro.build/en/guides/server-side-rendering/#response for more information."
|
||||
};
|
||||
const MissingMediaQueryDirective = {
|
||||
name: "MissingMediaQueryDirective",
|
||||
title: "Missing value for `client:media` directive.",
|
||||
message: 'Media query not provided for `client:media` directive. A media query similar to `client:media="(max-width: 600px)"` must be provided'
|
||||
};
|
||||
const NoMatchingRenderer = {
|
||||
name: "NoMatchingRenderer",
|
||||
title: "No matching renderer found.",
|
||||
message: (componentName, componentExtension, plural, validRenderersCount) => `Unable to render \`${componentName}\`.
|
||||
|
||||
${validRenderersCount > 0 ? `There ${plural ? "are" : "is"} ${validRenderersCount} renderer${plural ? "s" : ""} configured in your \`astro.config.mjs\` file,
|
||||
but ${plural ? "none were" : "it was not"} able to server-side render \`${componentName}\`.` : `No valid renderer was found ${componentExtension ? `for the \`.${componentExtension}\` file extension.` : `for this file extension.`}`}`,
|
||||
hint: (probableRenderers) => `Did you mean to enable the ${probableRenderers} integration?
|
||||
|
||||
See https://docs.astro.build/en/guides/framework-components/ for more information on how to install and configure integrations.`
|
||||
};
|
||||
const NoClientEntrypoint = {
|
||||
name: "NoClientEntrypoint",
|
||||
title: "No client entrypoint specified in renderer.",
|
||||
message: (componentName, clientDirective, rendererName) => `\`${componentName}\` component has a \`client:${clientDirective}\` directive, but no client entrypoint was provided by \`${rendererName}\`.`,
|
||||
hint: "See https://docs.astro.build/en/reference/integrations-reference/#addrenderer-option for more information on how to configure your renderer."
|
||||
};
|
||||
const NoClientOnlyHint = {
|
||||
name: "NoClientOnlyHint",
|
||||
title: "Missing hint on client:only directive.",
|
||||
message: (componentName) => `Unable to render \`${componentName}\`. When using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.`,
|
||||
hint: (probableRenderers) => `Did you mean to pass \`client:only="${probableRenderers}"\`? See https://docs.astro.build/en/reference/directives-reference/#clientonly for more information on client:only`
|
||||
};
|
||||
const InvalidGetStaticPathsEntry = {
|
||||
name: "InvalidGetStaticPathsEntry",
|
||||
title: "Invalid entry inside getStaticPath's return value",
|
||||
message: (entryType) => `Invalid entry returned by getStaticPaths. Expected an object, got \`${entryType}\``,
|
||||
hint: "If you're using a `.map` call, you might be looking for `.flatMap()` instead. See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths."
|
||||
};
|
||||
const InvalidGetStaticPathsReturn = {
|
||||
name: "InvalidGetStaticPathsReturn",
|
||||
title: "Invalid value returned by getStaticPaths.",
|
||||
message: (returnType) => `Invalid type returned by \`getStaticPaths\`. Expected an \`array\`, got \`${returnType}\``,
|
||||
hint: "See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths."
|
||||
};
|
||||
const GetStaticPathsExpectedParams = {
|
||||
name: "GetStaticPathsExpectedParams",
|
||||
title: "Missing params property on `getStaticPaths` route.",
|
||||
message: "Missing or empty required `params` property on `getStaticPaths` route.",
|
||||
hint: "See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths."
|
||||
};
|
||||
const GetStaticPathsInvalidRouteParam = {
|
||||
name: "GetStaticPathsInvalidRouteParam",
|
||||
title: "Invalid value for `getStaticPaths` route parameter.",
|
||||
message: (key, value, valueType) => `Invalid getStaticPaths route parameter for \`${key}\`. Expected undefined, a string or a number, received \`${valueType}\` (\`${value}\`)`,
|
||||
hint: "See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths."
|
||||
};
|
||||
const GetStaticPathsRequired = {
|
||||
name: "GetStaticPathsRequired",
|
||||
title: "`getStaticPaths()` function required for dynamic routes.",
|
||||
message: "`getStaticPaths()` function is required for dynamic routes. Make sure that you `export` a `getStaticPaths` function from your dynamic route.",
|
||||
hint: `See https://docs.astro.build/en/guides/routing/#dynamic-routes for more information on dynamic routes.
|
||||
|
||||
Alternatively, set \`output: "server"\` or \`output: "hybrid"\` in your Astro config file to switch to a non-static server build. This error can also occur if using \`export const prerender = true;\`.
|
||||
See https://docs.astro.build/en/guides/server-side-rendering/ for more information on non-static rendering.`
|
||||
};
|
||||
const ReservedSlotName = {
|
||||
name: "ReservedSlotName",
|
||||
title: "Invalid slot name.",
|
||||
message: (slotName) => `Unable to create a slot named \`${slotName}\`. \`${slotName}\` is a reserved slot name. Please update the name of this slot.`
|
||||
};
|
||||
const NoMatchingImport = {
|
||||
name: "NoMatchingImport",
|
||||
title: "No import found for component.",
|
||||
message: (componentName) => `Could not render \`${componentName}\`. No matching import has been found for \`${componentName}\`.`,
|
||||
hint: "Please make sure the component is properly imported."
|
||||
};
|
||||
const InvalidComponentArgs = {
|
||||
name: "InvalidComponentArgs",
|
||||
title: "Invalid component arguments.",
|
||||
message: (name) => `Invalid arguments passed to${name ? ` <${name}>` : ""} component.`,
|
||||
hint: "Astro components cannot be rendered directly via function call, such as `Component()` or `{items.map(Component)}`."
|
||||
};
|
||||
const PageNumberParamNotFound = {
|
||||
name: "PageNumberParamNotFound",
|
||||
title: "Page number param not found.",
|
||||
message: (paramName) => `[paginate()] page number param \`${paramName}\` not found in your filepath.`,
|
||||
hint: "Rename your file to `[page].astro` or `[...page].astro`."
|
||||
};
|
||||
const ImageMissingAlt = {
|
||||
name: "ImageMissingAlt",
|
||||
title: 'Image missing required "alt" property.',
|
||||
message: 'Image missing "alt" property. "alt" text is required to describe important images on the page.',
|
||||
hint: 'Use an empty string ("") for decorative images.'
|
||||
};
|
||||
const InvalidImageService = {
|
||||
name: "InvalidImageService",
|
||||
title: "Error while loading image service.",
|
||||
message: "There was an error loading the configured image service. Please see the stack trace for more information."
|
||||
};
|
||||
const MissingImageDimension = {
|
||||
name: "MissingImageDimension",
|
||||
title: "Missing image dimensions",
|
||||
message: (missingDimension, imageURL) => `Missing ${missingDimension === "both" ? "width and height attributes" : `${missingDimension} attribute`} for ${imageURL}. When using remote images, both dimensions are required in order to avoid CLS.`,
|
||||
hint: "If your image is inside your `src` folder, you probably meant to import it instead. See [the Imports guide for more information](https://docs.astro.build/en/guides/imports/#other-assets). You can also use `inferSize={true}` for remote images to get the original dimensions."
|
||||
};
|
||||
const FailedToFetchRemoteImageDimensions = {
|
||||
name: "FailedToFetchRemoteImageDimensions",
|
||||
title: "Failed to retrieve remote image dimensions",
|
||||
message: (imageURL) => `Failed to get the dimensions for ${imageURL}.`,
|
||||
hint: "Verify your remote image URL is accurate, and that you are not using `inferSize` with a file located in your `public/` folder."
|
||||
};
|
||||
const UnsupportedImageFormat = {
|
||||
name: "UnsupportedImageFormat",
|
||||
title: "Unsupported image format",
|
||||
message: (format, imagePath, supportedFormats) => `Received unsupported format \`${format}\` from \`${imagePath}\`. Currently only ${supportedFormats.join(
|
||||
", "
|
||||
)} are supported by our image services.`,
|
||||
hint: "Using an `img` tag directly instead of the `Image` component might be what you're looking for."
|
||||
};
|
||||
const UnsupportedImageConversion = {
|
||||
name: "UnsupportedImageConversion",
|
||||
title: "Unsupported image conversion",
|
||||
message: "Converting between vector (such as SVGs) and raster (such as PNGs and JPEGs) images is not currently supported."
|
||||
};
|
||||
const PrerenderDynamicEndpointPathCollide = {
|
||||
name: "PrerenderDynamicEndpointPathCollide",
|
||||
title: "Prerendered dynamic endpoint has path collision.",
|
||||
message: (pathname) => `Could not render \`${pathname}\` with an \`undefined\` param as the generated path will collide during prerendering. Prevent passing \`undefined\` as \`params\` for the endpoint's \`getStaticPaths()\` function, or add an additional extension to the endpoint's filename.`,
|
||||
hint: (filename) => `Rename \`${filename}\` to \`${filename.replace(/\.(?:js|ts)/, (m) => `.json` + m)}\``
|
||||
};
|
||||
const ExpectedImage = {
|
||||
name: "ExpectedImage",
|
||||
title: "Expected src to be an image.",
|
||||
message: (src, typeofOptions, fullOptions) => `Expected \`src\` property for \`getImage\` or \`<Image />\` to be either an ESM imported image or a string with the path of a remote image. Received \`${src}\` (type: \`${typeofOptions}\`).
|
||||
|
||||
Full serialized options received: \`${fullOptions}\`.`,
|
||||
hint: "This error can often happen because of a wrong path. Make sure the path to your image is correct. If you're passing an async function, make sure to call and await it."
|
||||
};
|
||||
const ExpectedImageOptions = {
|
||||
name: "ExpectedImageOptions",
|
||||
title: "Expected image options.",
|
||||
message: (options) => `Expected getImage() parameter to be an object. Received \`${options}\`.`
|
||||
};
|
||||
const ExpectedNotESMImage = {
|
||||
name: "ExpectedNotESMImage",
|
||||
title: "Expected image options, not an ESM-imported image.",
|
||||
message: "An ESM-imported image cannot be passed directly to `getImage()`. Instead, pass an object with the image in the `src` property.",
|
||||
hint: "Try changing `getImage(myImage)` to `getImage({ src: myImage })`"
|
||||
};
|
||||
const IncompatibleDescriptorOptions = {
|
||||
name: "IncompatibleDescriptorOptions",
|
||||
title: "Cannot set both `densities` and `widths`",
|
||||
message: "Only one of `densities` or `widths` can be specified. In most cases, you'll probably want to use only `widths` if you require specific widths.",
|
||||
hint: "Those attributes are used to construct a `srcset` attribute, which cannot have both `x` and `w` descriptors."
|
||||
};
|
||||
const NoImageMetadata = {
|
||||
name: "NoImageMetadata",
|
||||
title: "Could not process image metadata.",
|
||||
message: (imagePath) => `Could not process image metadata${imagePath ? ` for \`${imagePath}\`` : ""}.`,
|
||||
hint: "This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue."
|
||||
};
|
||||
const ResponseSentError = {
|
||||
name: "ResponseSentError",
|
||||
title: "Unable to set response.",
|
||||
message: "The response has already been sent to the browser and cannot be altered."
|
||||
};
|
||||
const MiddlewareNoDataOrNextCalled = {
|
||||
name: "MiddlewareNoDataOrNextCalled",
|
||||
title: "The middleware didn't return a `Response`.",
|
||||
message: "Make sure your middleware returns a `Response` object, either directly or by returning the `Response` from calling the `next` function."
|
||||
};
|
||||
const MiddlewareNotAResponse = {
|
||||
name: "MiddlewareNotAResponse",
|
||||
title: "The middleware returned something that is not a `Response` object.",
|
||||
message: "Any data returned from middleware must be a valid `Response` object."
|
||||
};
|
||||
const EndpointDidNotReturnAResponse = {
|
||||
name: "EndpointDidNotReturnAResponse",
|
||||
title: "The endpoint did not return a `Response`.",
|
||||
message: "An endpoint must return either a `Response`, or a `Promise` that resolves with a `Response`."
|
||||
};
|
||||
const LocalsNotAnObject = {
|
||||
name: "LocalsNotAnObject",
|
||||
title: "Value assigned to `locals` is not accepted.",
|
||||
message: "`locals` can only be assigned to an object. Other values like numbers, strings, etc. are not accepted.",
|
||||
hint: "If you tried to remove some information from the `locals` object, try to use `delete` or set the property to `undefined`."
|
||||
};
|
||||
const AstroResponseHeadersReassigned = {
|
||||
name: "AstroResponseHeadersReassigned",
|
||||
title: "`Astro.response.headers` must not be reassigned.",
|
||||
message: "Individual headers can be added to and removed from `Astro.response.headers`, but it must not be replaced with another instance of `Headers` altogether.",
|
||||
hint: "Consider using `Astro.response.headers.add()`, and `Astro.response.headers.delete()`."
|
||||
};
|
||||
const LocalImageUsedWrongly = {
|
||||
name: "LocalImageUsedWrongly",
|
||||
title: "Local images must be imported.",
|
||||
message: (imageFilePath) => `\`Image\`'s and \`getImage\`'s \`src\` parameter must be an imported image or an URL, it cannot be a string filepath. Received \`${imageFilePath}\`.`,
|
||||
hint: "If you want to use an image from your `src` folder, you need to either import it or if the image is coming from a content collection, use the [image() schema helper](https://docs.astro.build/en/guides/images/#images-in-content-collections). See https://docs.astro.build/en/guides/images/#src-required for more information on the `src` property."
|
||||
};
|
||||
const AstroGlobUsedOutside = {
|
||||
name: "AstroGlobUsedOutside",
|
||||
title: "Astro.glob() used outside of an Astro file.",
|
||||
message: (globStr) => `\`Astro.glob(${globStr})\` can only be used in \`.astro\` files. \`import.meta.glob(${globStr})\` can be used instead to achieve a similar result.`,
|
||||
hint: "See Vite's documentation on `import.meta.glob` for more information: https://vite.dev/guide/features.html#glob-import"
|
||||
};
|
||||
const AstroGlobNoMatch = {
|
||||
name: "AstroGlobNoMatch",
|
||||
title: "Astro.glob() did not match any files.",
|
||||
message: (globStr) => `\`Astro.glob(${globStr})\` did not return any matching files.`,
|
||||
hint: "Check the pattern for typos."
|
||||
};
|
||||
const MissingSharp = {
|
||||
name: "MissingSharp",
|
||||
title: "Could not find Sharp.",
|
||||
message: "Could not find Sharp. Please install Sharp (`sharp`) manually into your project or migrate to another image service.",
|
||||
hint: "See Sharp's installation instructions for more information: https://sharp.pixelplumbing.com/install. If you are not relying on `astro:assets` to optimize, transform, or process any images, you can configure a passthrough image service instead of installing Sharp. See https://docs.astro.build/en/reference/errors/missing-sharp for more information.\n\nSee https://docs.astro.build/en/guides/images/#default-image-service for more information on how to migrate to another image service."
|
||||
};
|
||||
const i18nNoLocaleFoundInPath = {
|
||||
name: "i18nNoLocaleFoundInPath",
|
||||
title: "The path doesn't contain any locale",
|
||||
message: "You tried to use an i18n utility on a path that doesn't contain any locale. You can use `pathHasLocale` first to determine if the path has a locale."
|
||||
};
|
||||
const RewriteWithBodyUsed = {
|
||||
name: "RewriteWithBodyUsed",
|
||||
title: "Cannot use Astro.rewrite after the request body has been read",
|
||||
message: "Astro.rewrite() cannot be used if the request body has already been read. If you need to read the body, first clone the request."
|
||||
};
|
||||
|
||||
function normalizeLF(code) {
|
||||
return code.replace(/\r\n|\r(?!\n)|\n/g, "\n");
|
||||
}
|
||||
|
||||
function codeFrame(src, loc) {
|
||||
if (!loc || loc.line === void 0 || loc.column === void 0) {
|
||||
return "";
|
||||
}
|
||||
const lines = normalizeLF(src).split("\n").map((ln) => ln.replace(/\t/g, " "));
|
||||
const visibleLines = [];
|
||||
for (let n = -2; n <= 2; n++) {
|
||||
if (lines[loc.line + n]) visibleLines.push(loc.line + n);
|
||||
}
|
||||
let gutterWidth = 0;
|
||||
for (const lineNo of visibleLines) {
|
||||
let w = `> ${lineNo}`;
|
||||
if (w.length > gutterWidth) gutterWidth = w.length;
|
||||
}
|
||||
let output = "";
|
||||
for (const lineNo of visibleLines) {
|
||||
const isFocusedLine = lineNo === loc.line - 1;
|
||||
output += isFocusedLine ? "> " : " ";
|
||||
output += `${lineNo + 1} | ${lines[lineNo]}
|
||||
`;
|
||||
if (isFocusedLine)
|
||||
output += `${Array.from({ length: gutterWidth }).join(" ")} | ${Array.from({
|
||||
length: loc.column
|
||||
}).join(" ")}^
|
||||
`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
class AstroError extends Error {
|
||||
loc;
|
||||
title;
|
||||
hint;
|
||||
frame;
|
||||
type = "AstroError";
|
||||
constructor(props, options) {
|
||||
const { name, title, message, stack, location, hint, frame } = props;
|
||||
super(message, options);
|
||||
this.title = title;
|
||||
this.name = name;
|
||||
if (message) this.message = message;
|
||||
this.stack = stack ? stack : this.stack;
|
||||
this.loc = location;
|
||||
this.hint = hint;
|
||||
this.frame = frame;
|
||||
}
|
||||
setLocation(location) {
|
||||
this.loc = location;
|
||||
}
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
setMessage(message) {
|
||||
this.message = message;
|
||||
}
|
||||
setHint(hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
setFrame(source, location) {
|
||||
this.frame = codeFrame(source, location);
|
||||
}
|
||||
static is(err) {
|
||||
return err.type === "AstroError";
|
||||
}
|
||||
}
|
||||
|
||||
const URL_PROTOCOL_REGEX = /^(?:(?:http|ftp|https|ws):?\/\/|\/\/)/;
|
||||
function isCoreRemotePath(path) {
|
||||
return URL_PROTOCOL_REGEX.test(path) || isRemotePath(path);
|
||||
}
|
||||
|
||||
const VALID_SUPPORTED_FORMATS = [
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"tiff",
|
||||
"webp",
|
||||
"gif",
|
||||
"svg",
|
||||
"avif"
|
||||
];
|
||||
const DEFAULT_OUTPUT_FORMAT = "webp";
|
||||
const DEFAULT_HASH_PROPS = ["src", "width", "height", "format", "quality"];
|
||||
|
||||
function isESMImportedImage(src) {
|
||||
return typeof src === "object";
|
||||
}
|
||||
function isRemoteImage(src) {
|
||||
return typeof src === "string";
|
||||
}
|
||||
async function resolveSrc(src) {
|
||||
return typeof src === "object" && "then" in src ? (await src).default ?? await src : src;
|
||||
}
|
||||
|
||||
function matchPattern(url, remotePattern) {
|
||||
return matchProtocol(url, remotePattern.protocol) && matchHostname(url, remotePattern.hostname, true) && matchPort(url, remotePattern.port) && matchPathname(url, remotePattern.pathname);
|
||||
}
|
||||
function matchPort(url, port) {
|
||||
return !port || port === url.port;
|
||||
}
|
||||
function matchProtocol(url, protocol) {
|
||||
return !protocol || protocol === url.protocol.slice(0, -1);
|
||||
}
|
||||
function matchHostname(url, hostname, allowWildcard) {
|
||||
if (!hostname) {
|
||||
return true;
|
||||
} else if (!allowWildcard || !hostname.startsWith("*")) {
|
||||
return hostname === url.hostname;
|
||||
} else if (hostname.startsWith("**.")) {
|
||||
const slicedHostname = hostname.slice(2);
|
||||
return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
|
||||
} else if (hostname.startsWith("*.")) {
|
||||
const slicedHostname = hostname.slice(1);
|
||||
const additionalSubdomains = url.hostname.replace(slicedHostname, "").split(".").filter(Boolean);
|
||||
return additionalSubdomains.length === 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function matchPathname(url, pathname, allowWildcard) {
|
||||
if (!pathname) {
|
||||
return true;
|
||||
} else if (!pathname.endsWith("*")) {
|
||||
return pathname === url.pathname;
|
||||
} else if (pathname.endsWith("/**")) {
|
||||
const slicedPathname = pathname.slice(0, -2);
|
||||
return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
|
||||
} else if (pathname.endsWith("/*")) {
|
||||
const slicedPathname = pathname.slice(0, -1);
|
||||
const additionalPathChunks = url.pathname.replace(slicedPathname, "").split("/").filter(Boolean);
|
||||
return additionalPathChunks.length === 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isRemoteAllowed(src, {
|
||||
domains = [],
|
||||
remotePatterns = []
|
||||
}) {
|
||||
if (!isCoreRemotePath(src)) return false;
|
||||
const url = new URL(src);
|
||||
return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
|
||||
}
|
||||
|
||||
function isLocalService(service) {
|
||||
if (!service) {
|
||||
return false;
|
||||
}
|
||||
return "transform" in service;
|
||||
}
|
||||
function parseQuality(quality) {
|
||||
let result = parseInt(quality);
|
||||
if (Number.isNaN(result)) {
|
||||
return quality;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const baseService = {
|
||||
validateOptions(options) {
|
||||
if (!options.src || typeof options.src !== "string" && typeof options.src !== "object") {
|
||||
throw new AstroError({
|
||||
...ExpectedImage,
|
||||
message: ExpectedImage.message(
|
||||
JSON.stringify(options.src),
|
||||
typeof options.src,
|
||||
JSON.stringify(options, (_, v) => v === void 0 ? null : v)
|
||||
)
|
||||
});
|
||||
}
|
||||
if (!isESMImportedImage(options.src)) {
|
||||
if (options.src.startsWith("/@fs/") || !isCoreRemotePath(options.src) && !options.src.startsWith("/")) {
|
||||
throw new AstroError({
|
||||
...LocalImageUsedWrongly,
|
||||
message: LocalImageUsedWrongly.message(options.src)
|
||||
});
|
||||
}
|
||||
let missingDimension;
|
||||
if (!options.width && !options.height) {
|
||||
missingDimension = "both";
|
||||
} else if (!options.width && options.height) {
|
||||
missingDimension = "width";
|
||||
} else if (options.width && !options.height) {
|
||||
missingDimension = "height";
|
||||
}
|
||||
if (missingDimension) {
|
||||
throw new AstroError({
|
||||
...MissingImageDimension,
|
||||
message: MissingImageDimension.message(missingDimension, options.src)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!VALID_SUPPORTED_FORMATS.includes(options.src.format)) {
|
||||
throw new AstroError({
|
||||
...UnsupportedImageFormat,
|
||||
message: UnsupportedImageFormat.message(
|
||||
options.src.format,
|
||||
options.src.src,
|
||||
VALID_SUPPORTED_FORMATS
|
||||
)
|
||||
});
|
||||
}
|
||||
if (options.widths && options.densities) {
|
||||
throw new AstroError(IncompatibleDescriptorOptions);
|
||||
}
|
||||
if (options.src.format === "svg") {
|
||||
options.format = "svg";
|
||||
}
|
||||
if (options.src.format === "svg" && options.format !== "svg" || options.src.format !== "svg" && options.format === "svg") {
|
||||
throw new AstroError(UnsupportedImageConversion);
|
||||
}
|
||||
}
|
||||
if (!options.format) {
|
||||
options.format = DEFAULT_OUTPUT_FORMAT;
|
||||
}
|
||||
if (options.width) options.width = Math.round(options.width);
|
||||
if (options.height) options.height = Math.round(options.height);
|
||||
return options;
|
||||
},
|
||||
getHTMLAttributes(options) {
|
||||
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
||||
const { src, width, height, format, quality, densities, widths, formats, ...attributes } = options;
|
||||
return {
|
||||
...attributes,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
loading: attributes.loading ?? "lazy",
|
||||
decoding: attributes.decoding ?? "async"
|
||||
};
|
||||
},
|
||||
getSrcSet(options) {
|
||||
const srcSet = [];
|
||||
const { targetWidth } = getTargetDimensions(options);
|
||||
const { widths, densities } = options;
|
||||
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
||||
let imageWidth = options.width;
|
||||
let maxWidth = Infinity;
|
||||
if (isESMImportedImage(options.src)) {
|
||||
imageWidth = options.src.width;
|
||||
maxWidth = imageWidth;
|
||||
}
|
||||
const {
|
||||
width: transformWidth,
|
||||
height: transformHeight,
|
||||
...transformWithoutDimensions
|
||||
} = options;
|
||||
const allWidths = [];
|
||||
if (densities) {
|
||||
const densityValues = densities.map((density) => {
|
||||
if (typeof density === "number") {
|
||||
return density;
|
||||
} else {
|
||||
return parseFloat(density);
|
||||
}
|
||||
});
|
||||
const densityWidths = densityValues.sort().map((density) => Math.round(targetWidth * density));
|
||||
allWidths.push(
|
||||
...densityWidths.map((width, index) => ({
|
||||
maxTargetWidth: Math.min(width, maxWidth),
|
||||
descriptor: `${densityValues[index]}x`
|
||||
}))
|
||||
);
|
||||
} else if (widths) {
|
||||
allWidths.push(
|
||||
...widths.map((width) => ({
|
||||
maxTargetWidth: Math.min(width, maxWidth),
|
||||
descriptor: `${width}w`
|
||||
}))
|
||||
);
|
||||
}
|
||||
for (const { maxTargetWidth, descriptor } of allWidths) {
|
||||
const srcSetTransform = { ...transformWithoutDimensions };
|
||||
if (maxTargetWidth !== imageWidth) {
|
||||
srcSetTransform.width = maxTargetWidth;
|
||||
} else {
|
||||
if (options.width && options.height) {
|
||||
srcSetTransform.width = options.width;
|
||||
srcSetTransform.height = options.height;
|
||||
}
|
||||
}
|
||||
srcSet.push({
|
||||
transform: srcSetTransform,
|
||||
descriptor,
|
||||
attributes: {
|
||||
type: `image/${targetFormat}`
|
||||
}
|
||||
});
|
||||
}
|
||||
return srcSet;
|
||||
},
|
||||
getURL(options, imageConfig) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (isESMImportedImage(options.src)) {
|
||||
searchParams.append("href", options.src.src);
|
||||
} else if (isRemoteAllowed(options.src, imageConfig)) {
|
||||
searchParams.append("href", options.src);
|
||||
} else {
|
||||
return options.src;
|
||||
}
|
||||
const params = {
|
||||
w: "width",
|
||||
h: "height",
|
||||
q: "quality",
|
||||
f: "format"
|
||||
};
|
||||
Object.entries(params).forEach(([param, key]) => {
|
||||
options[key] && searchParams.append(param, options[key].toString());
|
||||
});
|
||||
const imageEndpoint = joinPaths("/", "/_image");
|
||||
return `${imageEndpoint}?${searchParams}`;
|
||||
},
|
||||
parseURL(url) {
|
||||
const params = url.searchParams;
|
||||
if (!params.has("href")) {
|
||||
return void 0;
|
||||
}
|
||||
const transform = {
|
||||
src: params.get("href"),
|
||||
width: params.has("w") ? parseInt(params.get("w")) : void 0,
|
||||
height: params.has("h") ? parseInt(params.get("h")) : void 0,
|
||||
format: params.get("f"),
|
||||
quality: params.get("q")
|
||||
};
|
||||
return transform;
|
||||
}
|
||||
};
|
||||
function getTargetDimensions(options) {
|
||||
let targetWidth = options.width;
|
||||
let targetHeight = options.height;
|
||||
if (isESMImportedImage(options.src)) {
|
||||
const aspectRatio = options.src.width / options.src.height;
|
||||
if (targetHeight && !targetWidth) {
|
||||
targetWidth = Math.round(targetHeight * aspectRatio);
|
||||
} else if (targetWidth && !targetHeight) {
|
||||
targetHeight = Math.round(targetWidth / aspectRatio);
|
||||
} else if (!targetWidth && !targetHeight) {
|
||||
targetWidth = options.src.width;
|
||||
targetHeight = options.src.height;
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetWidth,
|
||||
targetHeight
|
||||
};
|
||||
}
|
||||
|
||||
let sharp;
|
||||
const qualityTable = {
|
||||
low: 25,
|
||||
mid: 50,
|
||||
high: 80,
|
||||
max: 100
|
||||
};
|
||||
async function loadSharp() {
|
||||
let sharpImport;
|
||||
try {
|
||||
sharpImport = (await import('sharp')).default;
|
||||
} catch {
|
||||
throw new AstroError(MissingSharp);
|
||||
}
|
||||
sharpImport.cache(false);
|
||||
return sharpImport;
|
||||
}
|
||||
const sharpService = {
|
||||
validateOptions: baseService.validateOptions,
|
||||
getURL: baseService.getURL,
|
||||
parseURL: baseService.parseURL,
|
||||
getHTMLAttributes: baseService.getHTMLAttributes,
|
||||
getSrcSet: baseService.getSrcSet,
|
||||
async transform(inputBuffer, transformOptions, config) {
|
||||
if (!sharp) sharp = await loadSharp();
|
||||
const transform = transformOptions;
|
||||
if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
|
||||
const result = sharp(inputBuffer, {
|
||||
failOnError: false,
|
||||
pages: -1,
|
||||
limitInputPixels: config.service.config.limitInputPixels
|
||||
});
|
||||
result.rotate();
|
||||
if (transform.height && !transform.width) {
|
||||
result.resize({ height: Math.round(transform.height) });
|
||||
} else if (transform.width) {
|
||||
result.resize({ width: Math.round(transform.width) });
|
||||
}
|
||||
if (transform.format) {
|
||||
let quality = void 0;
|
||||
if (transform.quality) {
|
||||
const parsedQuality = parseQuality(transform.quality);
|
||||
if (typeof parsedQuality === "number") {
|
||||
quality = parsedQuality;
|
||||
} else {
|
||||
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : void 0;
|
||||
}
|
||||
}
|
||||
result.toFormat(transform.format, { quality });
|
||||
}
|
||||
const { data, info } = await result.toBuffer({ resolveWithObject: true });
|
||||
return {
|
||||
data,
|
||||
format: info.format
|
||||
};
|
||||
}
|
||||
};
|
||||
var sharp_default = sharpService;
|
||||
|
||||
const sharp$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: sharp_default
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
export { AstroError as A, ReservedSlotName as B, RewriteWithBodyUsed as C, DEFAULT_HASH_PROPS as D, EndpointDidNotReturnAResponse as E, FailedToFetchRemoteImageDimensions as F, GetStaticPathsRequired as G, PrerenderClientAddressNotAvailable as H, InvalidComponentArgs as I, ClientAddressNotAvailable as J, AstroResponseHeadersReassigned as K, LocalsNotAnObject as L, MissingMediaQueryDirective as M, NoMatchingImport as N, OnlyResponseCanBeReturned as O, PageNumberParamNotFound as P, sharp$1 as Q, ResponseSentError as R, StaticClientAddressNotAvailable as S, AstroGlobUsedOutside as a, AstroGlobNoMatch as b, NoMatchingRenderer as c, NoClientOnlyHint as d, NoClientEntrypoint as e, NoImageMetadata as f, ExpectedImageOptions as g, ExpectedImage as h, ExpectedNotESMImage as i, isRemoteImage as j, isCoreRemotePath as k, isESMImportedImage as l, isLocalService as m, InvalidImageService as n, ImageMissingAlt as o, isRemoteAllowed as p, i18nNoLocaleFoundInPath as q, resolveSrc as r, MiddlewareNoDataOrNextCalled as s, MiddlewareNotAResponse as t, InvalidGetStaticPathsReturn as u, InvalidGetStaticPathsEntry as v, GetStaticPathsExpectedParams as w, GetStaticPathsInvalidRouteParam as x, NoMatchingStaticPathFound as y, PrerenderDynamicEndpointPathCollide as z };
|
||||
4
dist/server/chunks/astro/env-setup_Cr6XTFvb.mjs
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
function setGetEnv(fn, reset = false) {
|
||||
}
|
||||
|
||||
export { setGetEnv };
|
||||
2055
dist/server/chunks/astro/server_DrnzQWw3.mjs
vendored
Normal file
322
dist/server/chunks/cms_CRTVymfB.mjs
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const settings = {
|
||||
siteInfo: {
|
||||
name: "روس امروز",
|
||||
tagline: "خبرگزاری",
|
||||
description: "خبرگزاری تخصصی پوشش اخبار روسیه، اوراسیا و روابط منطقهای",
|
||||
copyright: "۱۴۰۵ روس امروز — تمامی حقوق محفوظ است"
|
||||
},
|
||||
breakingNews: {
|
||||
label: "فوری",
|
||||
href: "/posts/آستانا-میزبان-جام-قهرمانان-چهار-قاره-در-اسکیت-نمایشی-در-سال-۲۰۲۷-1782120219014/",
|
||||
items: [
|
||||
"آستانا میزبان جام قهرمانان چهار قاره در اسکیت نمایشی در سال ۲۰۲۷"
|
||||
],
|
||||
postSlugs: [
|
||||
"آستانا-میزبان-جام-قهرمانان-چهار-قاره-در-اسکیت-نمایشی-در-سال-۲۰۲۷-1782120219014"
|
||||
]
|
||||
},
|
||||
hotTopics: [
|
||||
"انرژی",
|
||||
"تحریم",
|
||||
"نفت",
|
||||
"گاز",
|
||||
"اوراسیا",
|
||||
"SCO",
|
||||
"روبل",
|
||||
"تجارت",
|
||||
"قفقاز",
|
||||
"ناتو",
|
||||
"بریکس"
|
||||
],
|
||||
homeLayout: {
|
||||
featuredSlug: "در-کوبان-به-فرد-حمله-کننده-در-مرکز-خرید-اتهام-قتل-و-تلاش-برای-قتل-وارد-شد-1782119386187",
|
||||
sideFeaturedSlugs: [
|
||||
"south-caucasus-geopolitics",
|
||||
"oil-price-new-record"
|
||||
],
|
||||
latestCount: 4,
|
||||
showAd: true
|
||||
},
|
||||
ad: {
|
||||
enabled: true,
|
||||
label: "آگهی",
|
||||
href: "/advertise/",
|
||||
text: "جایگاه تبلیغات"
|
||||
},
|
||||
agents: {
|
||||
tass: {
|
||||
processedLinks: [
|
||||
"https://tass.ru/proisshestviya/27834845",
|
||||
"https://tass.ru/sport/27834851",
|
||||
"https://tass.ru/obschestvo/27834629",
|
||||
"https://tass.ru/mezhdunarodnaya-panorama/27834975",
|
||||
"https://tass.ru/obschestvo/27834957",
|
||||
"https://tass.ru/politika/27834811",
|
||||
"https://tass.ru/obschestvo/27818171",
|
||||
"https://tass.ru/obschestvo/27835073",
|
||||
"https://tass.ru/sport/27835167"
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
const categories = [
|
||||
"اقتصاد",
|
||||
"سیاست",
|
||||
"انرژی",
|
||||
"فناوری",
|
||||
"بینالملل"
|
||||
];
|
||||
const posts = [
|
||||
];
|
||||
const fallbackCms = {
|
||||
settings: settings,
|
||||
categories: categories,
|
||||
posts: posts
|
||||
};
|
||||
|
||||
const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
||||
const toPersianDigits = (value) => String(value).replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)] ?? digit);
|
||||
const getPublishedAt = (post) => {
|
||||
if (post.publishedAt) {
|
||||
const date = new Date(post.publishedAt);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
const match = post.slug.match(/(\d{13})$/);
|
||||
if (match) {
|
||||
const date = new Date(Number(match[1]));
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const derivePublishedAt = (post) => {
|
||||
const date = getPublishedAt(post);
|
||||
return date ? date.toISOString() : void 0;
|
||||
};
|
||||
const formatShamsiDate = (date) => {
|
||||
const formatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
return toPersianDigits(formatter.format(date));
|
||||
};
|
||||
const formatShamsiDay = (date) => {
|
||||
const formatter = new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
return toPersianDigits(formatter.format(date));
|
||||
};
|
||||
const formatRelativeTime = (date) => {
|
||||
const now = Date.now();
|
||||
const diffSeconds = Math.max(0, Math.round((now - date.getTime()) / 1e3));
|
||||
if (diffSeconds < 60) {
|
||||
return "هماکنون";
|
||||
}
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
if (diffMinutes < 60) {
|
||||
return `${toPersianDigits(diffMinutes)} دقیقه پیش`;
|
||||
}
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
if (diffHours < 24) {
|
||||
return `${toPersianDigits(diffHours)} ساعت پیش`;
|
||||
}
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffDays < 30) {
|
||||
return `${toPersianDigits(diffDays)} روز پیش`;
|
||||
}
|
||||
return formatShamsiDay(date);
|
||||
};
|
||||
const formatPostRelativeTime = (post) => {
|
||||
const date = getPublishedAt(post);
|
||||
return date ? formatRelativeTime(date) : post.time;
|
||||
};
|
||||
const formatPostShamsiDate = (post) => {
|
||||
const date = getPublishedAt(post);
|
||||
return date ? formatShamsiDate(date) : post.time;
|
||||
};
|
||||
|
||||
const dbPath = path.join(process.cwd(), "data", "cms.db");
|
||||
const CMS_KEY = "cms";
|
||||
let dbInstance = null;
|
||||
const getDb = () => {
|
||||
if (dbInstance) {
|
||||
return dbInstance;
|
||||
}
|
||||
const directory = path.dirname(dbPath);
|
||||
if (!existsSync(directory)) {
|
||||
mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
const db = new Database(dbPath);
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cms (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
dbInstance = db;
|
||||
return db;
|
||||
};
|
||||
const seedIfEmpty = (db) => {
|
||||
const existing = db.prepare("SELECT value FROM cms WHERE key = ?").get(CMS_KEY);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
db.prepare("INSERT INTO cms (key, value, updated_at) VALUES (?, ?, ?)").run(
|
||||
CMS_KEY,
|
||||
JSON.stringify(fallbackCms),
|
||||
Date.now()
|
||||
);
|
||||
};
|
||||
const staticPages = [
|
||||
{
|
||||
slug: "about",
|
||||
title: "درباره ما",
|
||||
description: "آشنایی با مأموریت، رویکرد خبری و تیم تحریریه روس امروز",
|
||||
body: [
|
||||
"روس امروز رسانهای تخصصی برای پوشش خبرها، گزارشها و تحلیلهای مرتبط با روسیه، اوراسیا و روابط منطقهای است.",
|
||||
"هدف ما ارائه روایت دقیق، سریع و قابل اتکا از روندهای سیاسی، اقتصادی، انرژی و فناوری در این حوزه جغرافیایی است.",
|
||||
"تحریریه روس امروز تلاش میکند با زبان روشن و تحلیل دادهمحور، تصویر کاملتری از تحولات منطقه ارائه دهد."
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: "contact",
|
||||
title: "ارتباط با ما",
|
||||
description: "راههای ارتباط با تحریریه و واحد بازرگانی روس امروز",
|
||||
body: [
|
||||
"برای ارسال خبر، پیشنهاد همکاری یا پیگیری موضوعات رسانهای میتوانید از طریق ایمیل info@ruseemrooz.ir با ما در تماس باشید.",
|
||||
"پیامهای مرتبط با آگهی و همکاری تجاری توسط واحد بازرگانی بررسی و در کوتاهترین زمان پاسخ داده میشود."
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: "editorial-team",
|
||||
title: "تیم تحریریه",
|
||||
description: "معرفی ساختار تحریریه و حوزههای تخصصی روس امروز",
|
||||
footerLabel: "تیم تحریریه",
|
||||
body: [
|
||||
"تیم تحریریه روس امروز از نویسندگان و تحلیلگرانی تشکیل شده که حوزههای روسیه، قفقاز، آسیای مرکزی، انرژی و اقتصاد منطقهای را دنبال میکنند.",
|
||||
"مطالب منتشرشده پس از بررسی محتوایی و ویرایشی در سایت قرار میگیرند تا دقت و خوانایی حفظ شود."
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: "advertise",
|
||||
title: "آگهی در سایت",
|
||||
description: "فرصتهای تبلیغاتی و همکاری رسانهای با روس امروز",
|
||||
body: [
|
||||
"روس امروز امکان انتشار آگهی، رپورتاژ و کمپینهای هدفمند برای مخاطبان علاقهمند به روسیه و اوراسیا را فراهم میکند.",
|
||||
"برای دریافت تعرفه و پیشنهاد رسانهای اختصاصی، با واحد بازرگانی سایت تماس بگیرید."
|
||||
]
|
||||
},
|
||||
{
|
||||
slug: "privacy",
|
||||
title: "حریم خصوصی",
|
||||
description: "سیاست حفظ حریم خصوصی کاربران روس امروز",
|
||||
body: [
|
||||
"روس امروز اطلاعات کاربران را تنها برای بهبود تجربه کاربری، ارسال خبرنامه و تحلیل عملکرد سایت استفاده میکند.",
|
||||
"اطلاعات تماس کاربران بدون رضایت آنها در اختیار اشخاص ثالث قرار نمیگیرد."
|
||||
]
|
||||
}
|
||||
];
|
||||
const readCms = async () => {
|
||||
try {
|
||||
const db = getDb();
|
||||
seedIfEmpty(db);
|
||||
const row = db.prepare("SELECT value FROM cms WHERE key = ?").get(CMS_KEY);
|
||||
const data = row ? JSON.parse(row.value) : fallbackCms;
|
||||
return normalizeCms(data);
|
||||
} catch {
|
||||
return normalizeCms(fallbackCms);
|
||||
}
|
||||
};
|
||||
const writeCms = async (data) => {
|
||||
const db = getDb();
|
||||
const json = JSON.stringify(normalizeCms(data));
|
||||
db.prepare(
|
||||
`INSERT INTO cms (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
||||
).run(CMS_KEY, json, Date.now());
|
||||
};
|
||||
const normalizeCms = (data) => ({
|
||||
...data,
|
||||
settings: {
|
||||
...data.settings,
|
||||
breakingNews: {
|
||||
...data.settings.breakingNews,
|
||||
postSlugs: data.settings.breakingNews.postSlugs ?? []
|
||||
},
|
||||
agents: {
|
||||
...data.settings.agents,
|
||||
tass: {
|
||||
processedLinks: data.settings.agents?.tass?.processedLinks ?? []
|
||||
}
|
||||
}
|
||||
},
|
||||
posts: data.posts.map((post) => ({
|
||||
...post,
|
||||
status: post.status ?? "pending",
|
||||
body: Array.isArray(post.body) ? post.body : [],
|
||||
urgent: post.urgent ?? false,
|
||||
sidebar: post.sidebar ?? false,
|
||||
publishedAt: post.publishedAt ?? derivePublishedAt(post)
|
||||
}))
|
||||
});
|
||||
const getApprovedPosts = (data) => data.posts.filter((post) => post.status === "approved");
|
||||
const getCategoryPath = (category) => `/categories/${encodeURIComponent(category)}/`;
|
||||
const getStaticPageBySlug = (slug) => staticPages.find((page) => page.slug === slug);
|
||||
const getPrimaryNav = () => [
|
||||
{ label: "صفحه اصلی", href: "/" },
|
||||
...staticPages.filter((page) => ["about", "contact"].includes(page.slug)).map((page) => ({ label: page.navLabel ?? page.title, href: `/${page.slug}/` }))
|
||||
];
|
||||
const getFooterPageLinks = () => staticPages.map((page) => ({
|
||||
label: page.footerLabel ?? page.navLabel ?? page.title,
|
||||
href: `/${page.slug}/`
|
||||
}));
|
||||
const getPostBySlug = (posts, slug) => posts.find((post) => post.slug === slug);
|
||||
const getPostsByCategory = (posts, category) => posts.filter((post) => post.category === category);
|
||||
const getPaginatedPosts = (page, perPage, source) => {
|
||||
const totalPages = Math.max(1, Math.ceil(source.length / perPage));
|
||||
const currentPage = Math.min(Math.max(Math.floor(page), 1), totalPages);
|
||||
const start = (currentPage - 1) * perPage;
|
||||
return {
|
||||
currentPage,
|
||||
totalPages,
|
||||
posts: source.slice(start, start + perPage)
|
||||
};
|
||||
};
|
||||
const searchPosts = (posts, query) => {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase("fa-IR");
|
||||
if (!normalizedQuery) {
|
||||
return [];
|
||||
}
|
||||
return posts.filter((post) => {
|
||||
const searchableText = [
|
||||
post.title,
|
||||
post.category,
|
||||
post.excerpt,
|
||||
...post.body
|
||||
].join(" ").toLocaleLowerCase("fa-IR");
|
||||
return searchableText.includes(normalizedQuery);
|
||||
});
|
||||
};
|
||||
const slugify = (value) => value.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
||||
const parseImageBlock = (value) => {
|
||||
const match = value.match(/^\[image:(.+?)(?:\|(.*))?\]$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
src: match[1].trim(),
|
||||
caption: match[2]?.trim() ?? ""
|
||||
};
|
||||
};
|
||||
|
||||
export { getPostsByCategory as a, getApprovedPosts as b, getPostBySlug as c, getPaginatedPosts as d, getCategoryPath as e, formatPostRelativeTime as f, getStaticPageBySlug as g, formatPostShamsiDate as h, searchPosts as i, getPrimaryNav as j, getFooterPageLinks as k, parseImageBlock as p, readCms as r, slugify as s, writeCms as w };
|
||||
52
dist/server/chunks/images_DSuHPXn1.mjs
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const uploadDirectory = path.join(process.cwd(), "public", "uploads");
|
||||
const thumbnailDirectory = path.join(uploadDirectory, "thumbs");
|
||||
const ensureUploadDirectories = () => {
|
||||
if (!existsSync(uploadDirectory)) {
|
||||
mkdirSync(uploadDirectory, { recursive: true });
|
||||
}
|
||||
if (!existsSync(thumbnailDirectory)) {
|
||||
mkdirSync(thumbnailDirectory, { recursive: true });
|
||||
}
|
||||
};
|
||||
const storeImageBuffer = async (sourceBuffer) => {
|
||||
ensureUploadDirectories();
|
||||
const id = randomUUID();
|
||||
const imageName = `${id}.webp`;
|
||||
const thumbnailName = `${id}-thumb.webp`;
|
||||
const imagePath = path.join(uploadDirectory, imageName);
|
||||
const thumbnailPath = path.join(thumbnailDirectory, thumbnailName);
|
||||
await sharp(sourceBuffer).rotate().resize({ width: 1400, withoutEnlargement: true }).webp({ quality: 82 }).toFile(imagePath);
|
||||
await sharp(sourceBuffer).rotate().resize(420, 260, { fit: "cover" }).webp({ quality: 76 }).toFile(thumbnailPath);
|
||||
return {
|
||||
image: `/uploads/${imageName}`,
|
||||
thumbnail: `/uploads/thumbs/${thumbnailName}`
|
||||
};
|
||||
};
|
||||
const downloadAndStoreImage = async (url) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
||||
accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||
referer: "https://tass.ru/"
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.startsWith("image/")) {
|
||||
return null;
|
||||
}
|
||||
return storeImageBuffer(Buffer.from(await response.arrayBuffer()));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { downloadAndStoreImage as d, storeImageBuffer as s };
|
||||
41
dist/server/chunks/publisher_Dj_UvF8J.mjs
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import { r as readCms, s as slugify, w as writeCms } from './cms_CRTVymfB.mjs';
|
||||
|
||||
const publishPost = async (input) => {
|
||||
const cms = await readCms();
|
||||
const title = input.title.trim();
|
||||
const slug = slugify(input.slug || title);
|
||||
if (!title || !slug) {
|
||||
throw new Error("title is required");
|
||||
}
|
||||
const body = Array.isArray(input.body) ? input.body : String(input.body ?? "").split(/\n\s*\n/).map((item) => item.trim()).filter(Boolean);
|
||||
const existingIndex = cms.posts.findIndex((item) => item.slug === slug);
|
||||
const existing = existingIndex >= 0 ? cms.posts[existingIndex] : void 0;
|
||||
const post = {
|
||||
slug,
|
||||
title,
|
||||
category: input.category || cms.categories[0] || "عمومی",
|
||||
time: input.time || "هماکنون",
|
||||
readTime: input.readTime || "۳ دقیقه",
|
||||
excerpt: input.excerpt || body[0] || "",
|
||||
body,
|
||||
status: input.status || "pending",
|
||||
image: input.image || void 0,
|
||||
thumbnail: input.thumbnail || void 0,
|
||||
urgent: input.urgent ?? false,
|
||||
sidebar: input.sidebar ?? false,
|
||||
sourceUrl: input.sourceUrl || void 0,
|
||||
publishedAt: input.publishedAt || existing?.publishedAt || (/* @__PURE__ */ new Date()).toISOString()
|
||||
};
|
||||
if (existingIndex >= 0) {
|
||||
cms.posts[existingIndex] = post;
|
||||
} else {
|
||||
cms.posts.unshift(post);
|
||||
}
|
||||
if (!cms.categories.includes(post.category)) {
|
||||
cms.categories.push(post.category);
|
||||
}
|
||||
await writeCms(cms);
|
||||
return post;
|
||||
};
|
||||
|
||||
export { publishPost as p };
|
||||
454
dist/server/chunks/tassAgent_jxZgSz7r.mjs
vendored
Normal file
@@ -0,0 +1,454 @@
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import OpenAI from 'openai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { d as downloadAndStoreImage } from './images_DSuHPXn1.mjs';
|
||||
import { p as publishPost } from './publisher_Dj_UvF8J.mjs';
|
||||
import { r as readCms, s as slugify, w as writeCms } from './cms_CRTVymfB.mjs';
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
let browserInstance = null;
|
||||
const getChromePath = () => {
|
||||
const platform = process.platform;
|
||||
if (platform === "darwin") {
|
||||
const paths = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
||||
];
|
||||
for (const p of paths) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
}
|
||||
if (platform === "linux") {
|
||||
const paths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/snap/bin/chromium"
|
||||
];
|
||||
for (const p of paths) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
}
|
||||
if (platform === "win32") {
|
||||
const paths = [
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
|
||||
];
|
||||
for (const p of paths) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const getBrowser$1 = async () => {
|
||||
if (browserInstance && browserInstance.connected) {
|
||||
return browserInstance;
|
||||
}
|
||||
const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH || getChromePath();
|
||||
if (!chromePath) {
|
||||
throw new Error("Chrome/Chromium not found. Install Chrome or set PUPPETEER_EXECUTABLE_PATH.");
|
||||
}
|
||||
browserInstance = await puppeteer.launch({
|
||||
executablePath: chromePath,
|
||||
headless: true,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--window-size=1920,1080"
|
||||
]
|
||||
});
|
||||
return browserInstance;
|
||||
};
|
||||
const closeBrowser = async () => {
|
||||
if (browserInstance) {
|
||||
await browserInstance.close();
|
||||
browserInstance = null;
|
||||
}
|
||||
};
|
||||
const scrapeArticle = async (url) => {
|
||||
const browser = await getBrowser$1();
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
await page.setUserAgent(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||
);
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", (req) => {
|
||||
const type = req.resourceType();
|
||||
if (["font", "media"].includes(type)) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 6e4
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 2e3));
|
||||
const data = await page.evaluate(() => {
|
||||
const title = document.querySelector('meta[property="og:title"]')?.getAttribute("content") || document.querySelector("h1")?.textContent?.trim() || document.title;
|
||||
const description = document.querySelector('meta[property="og:description"]')?.getAttribute("content") || document.querySelector('meta[name="description"]')?.getAttribute("content") || "";
|
||||
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content") || "";
|
||||
const allImages = [];
|
||||
const imageBlacklist = ["logo", "icon", "avatar", "counter", "yadro", "pixel", "track", "1x1", "badge"];
|
||||
document.querySelectorAll("img").forEach((img) => {
|
||||
const src = img.getAttribute("src") || img.getAttribute("data-src") || "";
|
||||
if (!src) return;
|
||||
const lowerSrc = src.toLowerCase();
|
||||
if (imageBlacklist.some((b) => lowerSrc.includes(b))) return;
|
||||
const width = parseInt(img.getAttribute("width") || "0");
|
||||
const height = parseInt(img.getAttribute("height") || "0");
|
||||
if (width > 0 && width < 50 || height > 0 && height < 50) return;
|
||||
try {
|
||||
allImages.push(new URL(src, window.location.origin).href);
|
||||
} catch {
|
||||
}
|
||||
});
|
||||
const contentContainer = document.querySelector('[class*="ContentPageContainer_content"]') || document.querySelector("article") || document.querySelector("[data-content]");
|
||||
const articleImage = contentContainer?.querySelector("img")?.getAttribute("src") || "";
|
||||
const imageUrl = ogImage || articleImage || "";
|
||||
const paragraphs = [];
|
||||
const footerTexts = /* @__PURE__ */ new Set();
|
||||
document.querySelectorAll('[class*="Legal"], [class*="legal"], [class*="footer"], [class*="Footer"], [class*="Copyright"], [class*="copyright"], [class*="sidebar"], [class*="Sidebar"]').forEach((el) => {
|
||||
el.querySelectorAll("p, li").forEach((p) => {
|
||||
const t = p.textContent?.trim().replace(/\s+/g, " ") || "";
|
||||
if (t) footerTexts.add(t);
|
||||
});
|
||||
});
|
||||
const selectors = [
|
||||
'[class*="ContentPageContainer_content"] p',
|
||||
'[class*="article"] p',
|
||||
'[class*="Article"] p',
|
||||
'[class*="news-text"] p',
|
||||
'[class*="detail"] p',
|
||||
'[class*="topic"] p',
|
||||
"article p",
|
||||
"[data-content] p",
|
||||
".text-content p",
|
||||
".article-body p"
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
document.querySelectorAll(selector).forEach((el) => {
|
||||
const text = el.textContent?.trim().replace(/\s+/g, " ") || "";
|
||||
if (text.length > 20 && !paragraphs.includes(text) && !footerTexts.has(text)) {
|
||||
paragraphs.push(text);
|
||||
}
|
||||
});
|
||||
if (paragraphs.length > 0) break;
|
||||
}
|
||||
if (paragraphs.length < 2) {
|
||||
document.querySelectorAll("main p, .content p, .post-content p").forEach((el) => {
|
||||
const text = el.textContent?.trim().replace(/\s+/g, " ") || "";
|
||||
if (text.length > 30 && !paragraphs.includes(text) && !footerTexts.has(text)) {
|
||||
paragraphs.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (paragraphs.length < 2) {
|
||||
document.querySelectorAll("p").forEach((el) => {
|
||||
const text = el.textContent?.trim().replace(/\s+/g, " ") || "";
|
||||
if (text.length > 60 && !paragraphs.includes(text) && !footerTexts.has(text)) {
|
||||
paragraphs.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
title: title?.trim() || "",
|
||||
description: description?.trim() || "",
|
||||
imageUrl,
|
||||
text: paragraphs.slice(0, 14).join("\n\n"),
|
||||
allImages
|
||||
};
|
||||
});
|
||||
const resolvedImages = data.allImages.map((img) => {
|
||||
try {
|
||||
return new URL(img, url).href;
|
||||
} catch {
|
||||
return img;
|
||||
}
|
||||
});
|
||||
return {
|
||||
...data,
|
||||
allImages: resolvedImages,
|
||||
imageUrl: data.imageUrl ? (() => {
|
||||
try {
|
||||
return new URL(data.imageUrl, url).href;
|
||||
} catch {
|
||||
return data.imageUrl;
|
||||
}
|
||||
})() : ""
|
||||
};
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
};
|
||||
|
||||
const rssUrl = "https://tass.ru/rss/v2.xml";
|
||||
const getOpenAIClient = () => new OpenAI({
|
||||
apiKey: "sk-C9kUNuxFPA6Jvpi2stzikxzZMJzPuYy5TIoJEJiVR89oeiD6",
|
||||
baseURL: "https://api.gapgpt.app/v1"
|
||||
});
|
||||
const getModel = () => "gpt-5-nano";
|
||||
const fetchText = async (url) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
||||
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"accept-language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
referer: "https://tass.ru/"
|
||||
},
|
||||
signal: AbortSignal.timeout(8e3)
|
||||
});
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
const browser = await getBrowser();
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
||||
await new Promise((r) => setTimeout(r, 1e3));
|
||||
const content = await page.evaluate(() => {
|
||||
const pre = document.querySelector("pre");
|
||||
if (pre) return pre.textContent || "";
|
||||
return document.body.innerText || document.body.textContent || "";
|
||||
});
|
||||
return content;
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
};
|
||||
const getTassFeedLinks = async (limit = 50) => {
|
||||
const xml = await fetchText(rssUrl);
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(xml);
|
||||
const rawItems = parsed?.rss?.channel?.item ?? [];
|
||||
const items = Array.isArray(rawItems) ? rawItems : [rawItems];
|
||||
return items.slice(0, limit).map((item) => item.link).filter(Boolean);
|
||||
};
|
||||
const getItems = async (limit) => {
|
||||
const xml = await fetchText(rssUrl);
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(xml);
|
||||
const rawItems = parsed?.rss?.channel?.item ?? [];
|
||||
const items = Array.isArray(rawItems) ? rawItems : [rawItems];
|
||||
return items.slice(0, Math.max(limit * 10, 30));
|
||||
};
|
||||
const stripHtml = (value = "") => {
|
||||
if (!value) return "";
|
||||
const $ = cheerio.load(value);
|
||||
return $.text().replace(/\s+/g, " ").trim();
|
||||
};
|
||||
const getRssImage = (item) => {
|
||||
const candidates = [
|
||||
item.enclosure?.["@_type"]?.startsWith("image/") ? item.enclosure?.["@_url"] : "",
|
||||
item["media:content"]?.["@_type"]?.startsWith("image/") ? item["media:content"]?.["@_url"] : "",
|
||||
item["media:thumbnail"]?.["@_url"]
|
||||
].filter(Boolean);
|
||||
return candidates.find((candidate) => candidate.startsWith("http")) ?? "";
|
||||
};
|
||||
const sortImageItemsFirst = (items) => [...items].sort((first, second) => {
|
||||
const firstHasImage = getRssImage(first) ? 1 : 0;
|
||||
const secondHasImage = getRssImage(second) ? 1 : 0;
|
||||
return secondHasImage - firstHasImage;
|
||||
});
|
||||
const getFallbackArticle = (item) => {
|
||||
const description = stripHtml(item.description);
|
||||
return {
|
||||
title: item.title ?? "",
|
||||
description,
|
||||
imageUrl: getRssImage(item),
|
||||
text: description,
|
||||
usedFallback: true
|
||||
};
|
||||
};
|
||||
const toAbsoluteUrl = (value, baseUrl) => {
|
||||
try {
|
||||
return new URL(value, baseUrl).toString();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const extractArticleCheerio = async (url) => {
|
||||
const html = await fetchText(url);
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('meta[property="og:title"]').attr("content") || $("h1").first().text().replace(/\s+/g, " ").trim();
|
||||
const description = $('meta[property="og:description"]').attr("content") || "";
|
||||
const imageCandidates = [
|
||||
$('meta[property="og:image"]').attr("content"),
|
||||
$("article img").first().attr("src"),
|
||||
$("img").first().attr("src")
|
||||
].filter(Boolean);
|
||||
const imageUrl = imageCandidates.map((c) => toAbsoluteUrl(c, url)).find(Boolean) ?? "";
|
||||
const paragraphs = [];
|
||||
$("article p, [data-content] p, .text-content p, p").each((_, el) => {
|
||||
const pText = $(el).text().replace(/\s+/g, " ").trim();
|
||||
if (pText.length > 80) {
|
||||
paragraphs.push(pText);
|
||||
}
|
||||
});
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
imageUrl,
|
||||
text: paragraphs.slice(0, 14).join("\n\n")
|
||||
};
|
||||
};
|
||||
const extractArticle = async (url) => {
|
||||
try {
|
||||
const scraped = await scrapeArticle(url);
|
||||
if (scraped.text || scraped.title) {
|
||||
return {
|
||||
title: scraped.title,
|
||||
description: scraped.description,
|
||||
imageUrl: scraped.imageUrl,
|
||||
text: scraped.text,
|
||||
usedFallback: false
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
const fallback = await extractArticleCheerio(url);
|
||||
return {
|
||||
...fallback,
|
||||
usedFallback: true
|
||||
};
|
||||
};
|
||||
const translateArticle = async (article) => {
|
||||
const client = getOpenAIClient();
|
||||
if (!client.apiKey) {
|
||||
throw new Error("GAPGPT_API_KEY or OPENAI_API_KEY is not configured");
|
||||
}
|
||||
const sourceText = article.text || article.description || article.title;
|
||||
if (!sourceText) {
|
||||
throw new Error("RSS item has no usable text");
|
||||
}
|
||||
const completion = await client.chat.completions.create({
|
||||
model: getModel(),
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "تو سردبیر فارسی یک سایت خبری هستی. متن روسی/انگلیسی خبر را به فارسی روان و خبری تبدیل کن. خروجی فقط JSON معتبر باشد."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
instruction: "ترجمه و بازنویسی خبری فارسی بساز. دسته یکی از اقتصاد، سیاست، انرژی، فناوری، بینالملل باشد. body آرایه پاراگرافها باشد.",
|
||||
source: { ...article, text: sourceText },
|
||||
schema: {
|
||||
title: "string",
|
||||
excerpt: "string",
|
||||
category: "string",
|
||||
readTime: "string",
|
||||
body: ["string"]
|
||||
}
|
||||
})
|
||||
}
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.3
|
||||
});
|
||||
const content = completion.choices[0]?.message?.content ?? "{}";
|
||||
const jsonText = content.match(/\{[\s\S]*\}/)?.[0] ?? content;
|
||||
const parsed = JSON.parse(jsonText);
|
||||
return {
|
||||
title: parsed.title?.trim() || article.title,
|
||||
excerpt: parsed.excerpt?.trim() || article.description,
|
||||
category: parsed.category?.trim() || "بینالملل",
|
||||
readTime: parsed.readTime?.trim() || "۳ دقیقه",
|
||||
body: Array.isArray(parsed.body) ? parsed.body.map(String).filter(Boolean) : [article.text]
|
||||
};
|
||||
};
|
||||
const runTassAgent = async (limit = 3) => {
|
||||
const cms = await readCms();
|
||||
const processedLinks = new Set(cms.settings.agents?.tass?.processedLinks ?? []);
|
||||
const items = sortImageItemsFirst(await getItems(limit));
|
||||
const created = [];
|
||||
const failed = [];
|
||||
const skipped = [];
|
||||
for (const item of items) {
|
||||
if (created.length >= limit) {
|
||||
break;
|
||||
}
|
||||
const link = item.link;
|
||||
if (!link) {
|
||||
continue;
|
||||
}
|
||||
if (processedLinks.has(link)) {
|
||||
skipped.push(link);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
let article = getFallbackArticle(item);
|
||||
let extractionWarning = "";
|
||||
try {
|
||||
const extractedArticle = await extractArticle(link);
|
||||
article = {
|
||||
...article,
|
||||
...extractedArticle,
|
||||
title: extractedArticle.title || article.title,
|
||||
description: extractedArticle.description || article.description,
|
||||
imageUrl: extractedArticle.imageUrl || article.imageUrl,
|
||||
text: extractedArticle.text || article.text
|
||||
};
|
||||
} catch (error) {
|
||||
extractionWarning = error instanceof Error ? error.message : "Article extraction failed";
|
||||
}
|
||||
const translated = await translateArticle({
|
||||
title: article.title || item.title || "",
|
||||
description: article.description || item.description || "",
|
||||
text: article.text || item.description || ""
|
||||
});
|
||||
const storedImage = article.imageUrl ? await downloadAndStoreImage(article.imageUrl) : null;
|
||||
const body = [...translated.body];
|
||||
if (storedImage?.image) {
|
||||
body.splice(1, 0, `[image:${storedImage.image}|${translated.title}]`);
|
||||
}
|
||||
const post = await publishPost({
|
||||
title: translated.title,
|
||||
slug: slugify(`${translated.title}-${Date.now()}`),
|
||||
category: translated.category,
|
||||
readTime: translated.readTime,
|
||||
excerpt: translated.excerpt,
|
||||
body,
|
||||
status: "pending",
|
||||
image: storedImage?.image,
|
||||
thumbnail: storedImage?.thumbnail,
|
||||
sourceUrl: link
|
||||
});
|
||||
processedLinks.add(link);
|
||||
created.push({
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
source: link,
|
||||
sourceImage: article.imageUrl || void 0,
|
||||
image: storedImage?.image,
|
||||
thumbnail: storedImage?.thumbnail,
|
||||
warning: extractionWarning || void 0
|
||||
});
|
||||
} catch (error) {
|
||||
failed.push({ source: link, error: error instanceof Error ? error.message : "Unknown error" });
|
||||
}
|
||||
}
|
||||
const latestCms = await readCms();
|
||||
latestCms.settings.agents = {
|
||||
...latestCms.settings.agents,
|
||||
tass: {
|
||||
processedLinks: Array.from(processedLinks)
|
||||
}
|
||||
};
|
||||
await writeCms(latestCms);
|
||||
await closeBrowser();
|
||||
return { created, failed, skipped };
|
||||
};
|
||||
|
||||
export { getTassFeedLinks as g, runTassAgent as r };
|
||||
62
dist/server/entry.mjs
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import { renderers } from './renderers.mjs';
|
||||
import { c as createExports, s as serverEntrypointModule } from './chunks/_@astrojs-ssr-adapter_BSED7qqF.mjs';
|
||||
import { manifest } from './manifest_Bsmp4TY0.mjs';
|
||||
|
||||
const _page0 = () => import('./pages/_image.astro.mjs');
|
||||
const _page1 = () => import('./pages/admin/login.astro.mjs');
|
||||
const _page2 = () => import('./pages/admin.astro.mjs');
|
||||
const _page3 = () => import('./pages/api/admin/login.astro.mjs');
|
||||
const _page4 = () => import('./pages/api/admin/logout.astro.mjs');
|
||||
const _page5 = () => import('./pages/api/admin/save.astro.mjs');
|
||||
const _page6 = () => import('./pages/api/admin/upload.astro.mjs');
|
||||
const _page7 = () => import('./pages/api/agents/tass.astro.mjs');
|
||||
const _page8 = () => import('./pages/api/posts/publish.astro.mjs');
|
||||
const _page9 = () => import('./pages/categories/_category_.astro.mjs');
|
||||
const _page10 = () => import('./pages/posts/_slug_.astro.mjs');
|
||||
const _page11 = () => import('./pages/posts.astro.mjs');
|
||||
const _page12 = () => import('./pages/search.astro.mjs');
|
||||
const _page13 = () => import('./pages/_page_.astro.mjs');
|
||||
const _page14 = () => import('./pages/index.astro.mjs');
|
||||
|
||||
const pageMap = new Map([
|
||||
["node_modules/astro/dist/assets/endpoint/node.js", _page0],
|
||||
["src/pages/admin/login.astro", _page1],
|
||||
["src/pages/admin/index.astro", _page2],
|
||||
["src/pages/api/admin/login.ts", _page3],
|
||||
["src/pages/api/admin/logout.ts", _page4],
|
||||
["src/pages/api/admin/save.ts", _page5],
|
||||
["src/pages/api/admin/upload.ts", _page6],
|
||||
["src/pages/api/agents/tass.ts", _page7],
|
||||
["src/pages/api/posts/publish.ts", _page8],
|
||||
["src/pages/categories/[category].astro", _page9],
|
||||
["src/pages/posts/[slug].astro", _page10],
|
||||
["src/pages/posts/index.astro", _page11],
|
||||
["src/pages/search/index.astro", _page12],
|
||||
["src/pages/[page].astro", _page13],
|
||||
["src/pages/index.astro", _page14]
|
||||
]);
|
||||
const serverIslandMap = new Map();
|
||||
const _manifest = Object.assign(manifest, {
|
||||
pageMap,
|
||||
serverIslandMap,
|
||||
renderers,
|
||||
middleware: () => import('./_noop-middleware.mjs')
|
||||
});
|
||||
const _args = {
|
||||
"mode": "standalone",
|
||||
"client": "file:///Users/drfelfel/ruseemrooz/dist/client/",
|
||||
"server": "file:///Users/drfelfel/ruseemrooz/dist/server/",
|
||||
"host": false,
|
||||
"port": 4321,
|
||||
"assets": "_astro"
|
||||
};
|
||||
const _exports = createExports(_manifest, _args);
|
||||
const handler = _exports['handler'];
|
||||
const startServer = _exports['startServer'];
|
||||
const options = _exports['options'];
|
||||
const _start = 'start';
|
||||
{
|
||||
serverEntrypointModule[_start](_manifest, _args);
|
||||
}
|
||||
|
||||
export { handler, options, pageMap, startServer };
|
||||
101
dist/server/manifest_Bsmp4TY0.mjs
vendored
Normal file
1151
dist/server/pages/_image.astro.mjs
vendored
Normal file
30
dist/server/pages/_page_.astro.mjs
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead } from '../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$BaseLayout } from '../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { g as getStaticPageBySlug } from '../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$page = createComponent(($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$page;
|
||||
const page = getStaticPageBySlug(Astro2.params.page ?? "");
|
||||
if (!page) {
|
||||
return Astro2.redirect("/");
|
||||
}
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": `${page.title} | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632`, "description": page.description }, { "default": ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-4xl mx-auto px-4 lg:px-8 py-8"> <article class="bg-white rounded-2xl shadow-sm p-5 sm:p-8"> <span class="text-brand text-sm font-bold">روس امروز</span> <h1 class="text-2xl sm:text-4xl font-black leading-snug mt-2">${page.title}</h1> <p class="text-gray-500 leading-relaxed mt-4">${page.description}</p> <div class="space-y-5 text-gray-700 leading-8 text-base border-t border-gray-100 mt-6 pt-6"> ${page.body.map((paragraph) => renderTemplate`<p>${paragraph}</p>`)} </div> </article> </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/[page].astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/[page].astro";
|
||||
const $$url = "/[page]";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$page,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
99
dist/server/pages/admin.astro.mjs
vendored
Normal file
35
dist/server/pages/admin/login.astro.mjs
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import { c as createComponent, e as renderHead, r as renderTemplate, b as createAstro } from '../../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import 'clsx';
|
||||
/* empty css */
|
||||
import { i as isAdminAuthenticated } from '../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Login = createComponent(($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Login;
|
||||
if (isAdminAuthenticated(Astro2)) {
|
||||
return Astro2.redirect("/admin/");
|
||||
}
|
||||
const error = Astro2.url.searchParams.has("error");
|
||||
return renderTemplate`<html lang="fa" dir="rtl"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ورود ادمین | روس امروز</title><link rel="stylesheet" href="https://rc0.ir/vazirmatn/Vazirmatn-font-face.css">${renderHead()}</head> <body class="bg-gray-100 font-vazir text-gray-900 min-h-screen flex items-center justify-center p-4"> <form action="/api/admin/login" method="post" class="bg-white rounded-2xl shadow-sm p-6 w-full max-w-sm space-y-4"> <div> <span class="text-brand text-sm font-bold">روس امروز</span> <h1 class="text-2xl font-black mt-1">ورود ادمین</h1> </div> ${error && renderTemplate`<p class="bg-red-50 text-brand text-sm rounded-xl px-3 py-2">نام کاربری یا رمز عبور اشتباه است.</p>`} <label class="block text-sm font-bold">
|
||||
نام کاربری
|
||||
<input name="username" value="admin" class="mt-2 w-full bg-gray-100 rounded-xl px-4 py-3 outline-none focus:ring-1 focus:ring-brand"> </label> <label class="block text-sm font-bold">
|
||||
رمز عبور
|
||||
<input name="password" type="password" placeholder="admin123" class="mt-2 w-full bg-gray-100 rounded-xl px-4 py-3 outline-none focus:ring-1 focus:ring-brand"> </label> <button class="w-full bg-brand text-white rounded-xl py-3 font-bold hover:bg-red-700 transition-colors">ورود</button> <p class="text-xs text-gray-400 leading-6">پیشفرض: \`admin\` / \`admin123\`؛ برای production از \`ADMIN_USER\` و \`ADMIN_PASSWORD\` استفاده کنید.</p> </form> </body></html>`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/admin/login.astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/admin/login.astro";
|
||||
const $$url = "/admin/login";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$Login,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
22
dist/server/pages/api/admin/login.astro.mjs
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { a as isValidLogin, s as setAdminSession } from '../../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async (context) => {
|
||||
const formData = await context.request.formData();
|
||||
const username = String(formData.get("username") ?? "");
|
||||
const password = String(formData.get("password") ?? "");
|
||||
if (!isValidLogin(username, password)) {
|
||||
return context.redirect("/admin/login/?error=1", 302);
|
||||
}
|
||||
setAdminSession(context);
|
||||
return context.redirect("/admin/", 302);
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
16
dist/server/pages/api/admin/logout.astro.mjs
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { c as clearAdminSession } from '../../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async (context) => {
|
||||
clearAdminSession(context);
|
||||
return context.redirect("/admin/login/", 302);
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
143
dist/server/pages/api/admin/save.astro.mjs
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
import { r as requireAdmin } from '../../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
import { r as readCms, w as writeCms } from '../../../chunks/cms_CRTVymfB.mjs';
|
||||
import { p as publishPost } from '../../../chunks/publisher_Dj_UvF8J.mjs';
|
||||
import { g as getTassFeedLinks } from '../../../chunks/tassAgent_jxZgSz7r.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const toList = (value) => String(value ?? "").split(/\r?\n|,/).map((item) => item.trim()).filter(Boolean);
|
||||
const toParagraphs = (value) => String(value ?? "").split(/\n\s*\n/).map((item) => item.trim()).filter(Boolean);
|
||||
const POST = async (context) => {
|
||||
const unauthorized = requireAdmin(context);
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
const formData = await context.request.formData();
|
||||
const action = String(formData.get("action") ?? "");
|
||||
const cms = await readCms();
|
||||
const tab = String(formData.get("tab") ?? "pending");
|
||||
if (action === "post-status") {
|
||||
const slug = String(formData.get("slug") ?? "");
|
||||
const status = String(formData.get("status") ?? "pending");
|
||||
cms.posts = cms.posts.map((post) => post.slug === slug ? { ...post, status } : post);
|
||||
}
|
||||
if (action === "toggle-post-flag") {
|
||||
const slug = String(formData.get("slug") ?? "");
|
||||
const flag = String(formData.get("flag") ?? "");
|
||||
cms.posts = cms.posts.map((post) => {
|
||||
if (post.slug !== slug) {
|
||||
return post;
|
||||
}
|
||||
if (flag === "urgent") {
|
||||
return { ...post, urgent: !post.urgent };
|
||||
}
|
||||
if (flag === "sidebar") {
|
||||
return { ...post, sidebar: !post.sidebar };
|
||||
}
|
||||
return post;
|
||||
});
|
||||
}
|
||||
if (action === "set-breaking-post") {
|
||||
const slug = String(formData.get("slug") ?? "");
|
||||
const post = cms.posts.find((item) => item.slug === slug);
|
||||
if (post) {
|
||||
cms.settings.breakingNews = {
|
||||
...cms.settings.breakingNews,
|
||||
href: `/posts/${post.slug}/`,
|
||||
items: [post.title],
|
||||
postSlugs: [post.slug]
|
||||
};
|
||||
}
|
||||
}
|
||||
if (action === "set-home-featured") {
|
||||
const slug = String(formData.get("slug") ?? "");
|
||||
cms.settings.homeLayout.featuredSlug = slug;
|
||||
}
|
||||
if (action === "clear-tass-memory") {
|
||||
cms.settings.agents = {
|
||||
...cms.settings.agents,
|
||||
tass: { processedLinks: [] }
|
||||
};
|
||||
}
|
||||
if (action === "fill-tass-memory") {
|
||||
const links = cms.posts.map((post) => post.sourceUrl).filter(Boolean);
|
||||
const feedLinks = await getTassFeedLinks(50);
|
||||
cms.settings.agents = {
|
||||
...cms.settings.agents,
|
||||
tass: { processedLinks: Array.from(/* @__PURE__ */ new Set([...links, ...feedLinks])) }
|
||||
};
|
||||
}
|
||||
if (action === "delete-post") {
|
||||
const slug = String(formData.get("slug") ?? "");
|
||||
const confirm = String(formData.get("confirm") ?? "");
|
||||
if (confirm !== "confirm") {
|
||||
return context.redirect(`/admin/?deleteError=1&tab=${encodeURIComponent(tab)}`, 302);
|
||||
}
|
||||
cms.posts = cms.posts.filter((post) => post.slug !== slug);
|
||||
}
|
||||
if (action === "delete-all-posts") {
|
||||
const confirm = String(formData.get("confirm") ?? "");
|
||||
if (confirm !== "confirm") {
|
||||
return context.redirect(`/admin/?deleteError=1&tab=${encodeURIComponent(tab)}`, 302);
|
||||
}
|
||||
cms.posts = [];
|
||||
}
|
||||
if (action === "save-post") {
|
||||
const originalSlug = String(formData.get("originalSlug") ?? "");
|
||||
const title = String(formData.get("title") ?? "").trim();
|
||||
const post = await publishPost({
|
||||
slug: String(formData.get("slug") ?? "") || title,
|
||||
title,
|
||||
category: String(formData.get("category") ?? cms.categories[0] ?? "عمومی"),
|
||||
time: String(formData.get("time") ?? "").trim() || "هماکنون",
|
||||
readTime: String(formData.get("readTime") ?? "").trim() || "۳ دقیقه",
|
||||
excerpt: String(formData.get("excerpt") ?? "").trim(),
|
||||
status: String(formData.get("status") ?? "pending"),
|
||||
body: toParagraphs(formData.get("body")),
|
||||
image: String(formData.get("image") ?? "").trim() || void 0,
|
||||
thumbnail: String(formData.get("thumbnail") ?? "").trim() || void 0,
|
||||
urgent: formData.get("urgent") === "on",
|
||||
sidebar: formData.get("sidebar") === "on"
|
||||
});
|
||||
if (originalSlug && originalSlug !== post.slug) {
|
||||
const updatedCms = await readCms();
|
||||
updatedCms.posts = updatedCms.posts.filter((post2) => post2.slug !== originalSlug);
|
||||
await writeCms(updatedCms);
|
||||
}
|
||||
return context.redirect(`/admin/?saved=1&tab=${post.status === "approved" ? "approved" : "pending"}`, 302);
|
||||
}
|
||||
if (action === "categories") {
|
||||
cms.categories = toList(formData.get("categories"));
|
||||
}
|
||||
if (action === "settings") {
|
||||
cms.settings.breakingNews = {
|
||||
label: String(formData.get("breakingLabel") ?? "فوری").trim(),
|
||||
href: String(formData.get("breakingHref") ?? "/").trim(),
|
||||
items: toList(formData.get("breakingItems")),
|
||||
postSlugs: toList(formData.get("breakingPostSlugs"))
|
||||
};
|
||||
cms.settings.hotTopics = toList(formData.get("hotTopics"));
|
||||
cms.settings.homeLayout = {
|
||||
featuredSlug: String(formData.get("featuredSlug") ?? "").trim(),
|
||||
sideFeaturedSlugs: toList(formData.get("sideFeaturedSlugs")).slice(0, 2),
|
||||
latestCount: Math.max(1, Number(formData.get("latestCount") ?? 4)),
|
||||
showAd: formData.get("showAd") === "on"
|
||||
};
|
||||
cms.settings.ad = {
|
||||
enabled: formData.get("adEnabled") === "on",
|
||||
label: String(formData.get("adLabel") ?? "آگهی").trim(),
|
||||
href: String(formData.get("adHref") ?? "/advertise/").trim(),
|
||||
text: String(formData.get("adText") ?? "").trim()
|
||||
};
|
||||
}
|
||||
await writeCms(cms);
|
||||
return context.redirect(`/admin/?saved=1&tab=${encodeURIComponent(tab)}`, 302);
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
30
dist/server/pages/api/admin/upload.astro.mjs
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import { r as requireAdmin } from '../../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
import { s as storeImageBuffer } from '../../../chunks/images_DSuHPXn1.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async (context) => {
|
||||
const unauthorized = requireAdmin(context);
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
const formData = await context.request.formData();
|
||||
const file = formData.get("image");
|
||||
if (!(file instanceof File) || file.size === 0) {
|
||||
return context.json({ error: "فایل تصویری ارسال نشده است." }, 400);
|
||||
}
|
||||
try {
|
||||
const storedImage = await storeImageBuffer(Buffer.from(await file.arrayBuffer()));
|
||||
return context.json(storedImage, 200);
|
||||
} catch {
|
||||
return context.json({ error: "پردازش تصویر ناموفق بود." }, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
36
dist/server/pages/api/agents/tass.astro.mjs
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { r as requireApiToken } from '../../../chunks/apiAuth_DcNSM7D5.mjs';
|
||||
import { i as isAdminAuthenticated } from '../../../chunks/adminAuth_CY4OwU1a.mjs';
|
||||
import { r as runTassAgent } from '../../../chunks/tassAgent_jxZgSz7r.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async (context) => {
|
||||
if (!isAdminAuthenticated(context)) {
|
||||
const unauthorized = requireApiToken(context);
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const contentType = context.request.headers.get("content-type") ?? "";
|
||||
const payload = contentType.includes("application/json") ? await context.request.json() : {};
|
||||
const limit = Math.min(Math.max(Number(payload.limit ?? 3), 1), 10);
|
||||
const result = await runTassAgent(limit);
|
||||
return new Response(JSON.stringify({ ok: true, ...result }), {
|
||||
headers: { "content-type": "application/json; charset=utf-8" }
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Agent failed" }), {
|
||||
status: 500,
|
||||
headers: { "content-type": "application/json; charset=utf-8" }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
50
dist/server/pages/api/posts/publish.astro.mjs
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import { r as requireApiToken } from '../../../chunks/apiAuth_DcNSM7D5.mjs';
|
||||
import { p as publishPost } from '../../../chunks/publisher_Dj_UvF8J.mjs';
|
||||
import { d as downloadAndStoreImage } from '../../../chunks/images_DSuHPXn1.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async (context) => {
|
||||
const unauthorized = requireApiToken(context);
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
try {
|
||||
const payload = await context.request.json();
|
||||
let image = payload.image;
|
||||
let thumbnail = payload.thumbnail;
|
||||
if (payload.imageUrl && !image) {
|
||||
const storedImage = await downloadAndStoreImage(String(payload.imageUrl));
|
||||
image = storedImage?.image;
|
||||
thumbnail = storedImage?.thumbnail;
|
||||
}
|
||||
const post = await publishPost({
|
||||
title: String(payload.title ?? ""),
|
||||
slug: payload.slug ? String(payload.slug) : void 0,
|
||||
category: payload.category ? String(payload.category) : void 0,
|
||||
time: payload.time ? String(payload.time) : void 0,
|
||||
readTime: payload.readTime ? String(payload.readTime) : void 0,
|
||||
excerpt: payload.excerpt ? String(payload.excerpt) : void 0,
|
||||
body: Array.isArray(payload.body) ? payload.body.map(String) : String(payload.body ?? ""),
|
||||
status: payload.status === "approved" ? "approved" : "pending",
|
||||
image,
|
||||
thumbnail
|
||||
});
|
||||
return new Response(JSON.stringify({ ok: true, post }), {
|
||||
headers: { "content-type": "application/json; charset=utf-8" }
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Invalid request" }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json; charset=utf-8" }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
33
dist/server/pages/categories/_category_.astro.mjs
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead } from '../../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$BaseLayout } from '../../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { $ as $$PostCard } from '../../chunks/PostCard_D8td2Gog.mjs';
|
||||
import { r as readCms, a as getPostsByCategory, b as getApprovedPosts } from '../../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$category = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$category;
|
||||
const cms = await readCms();
|
||||
const category = decodeURIComponent(Astro2.params.category ?? "");
|
||||
if (!cms.categories.includes(category)) {
|
||||
return Astro2.redirect("/posts/");
|
||||
}
|
||||
const categoryPosts = getPostsByCategory(getApprovedPosts(cms), category);
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": `${category} | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632`, "description": `\u0622\u062E\u0631\u06CC\u0646 \u0627\u062E\u0628\u0627\u0631 \u0648 \u062A\u062D\u0644\u06CC\u0644\u200C\u0647\u0627\u06CC ${category} \u062F\u0631 \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632` }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-7xl mx-auto px-4 lg:px-8 py-8"> <div class="mb-6"> <a href="/posts/" class="text-brand text-sm font-bold">آرشیو اخبار</a> <h1 class="text-2xl font-black mt-1">اخبار ${category}</h1> <p class="text-gray-500 text-sm mt-2">مجموعه خبرها و تحلیلهای منتشر شده در بخش ${category}</p> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> ${categoryPosts.map((post) => renderTemplate`${renderComponent($$result2, "PostCard", $$PostCard, { "post": post })}`)} </div> </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/categories/[category].astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/categories/[category].astro";
|
||||
const $$url = "/categories/[category]";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$category,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
43
dist/server/pages/index.astro.mjs
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead, a as addAttribute } from '../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$BaseLayout } from '../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { $ as $$Pagination } from '../chunks/Pagination_BeoGb-H9.mjs';
|
||||
import { $ as $$PostCard } from '../chunks/PostCard_D8td2Gog.mjs';
|
||||
import { r as readCms, b as getApprovedPosts, c as getPostBySlug, d as getPaginatedPosts, f as formatPostRelativeTime, e as getCategoryPath } from '../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Index;
|
||||
const cms = await readCms();
|
||||
const posts = getApprovedPosts(cms);
|
||||
const { homeLayout, hotTopics, ad } = cms.settings;
|
||||
const featuredPost = getPostBySlug(posts, homeLayout.featuredSlug) ?? posts[0];
|
||||
const pageParam = Number(Astro2.url.searchParams.get("page") ?? "1");
|
||||
const requestedPage = Number.isFinite(pageParam) ? pageParam : 1;
|
||||
const sideFeaturedPosts = [
|
||||
...posts.filter((post) => post.sidebar),
|
||||
...homeLayout.sideFeaturedSlugs.map((slug) => getPostBySlug(posts, slug)).filter(Boolean)
|
||||
].filter((post, index, list) => list.findIndex((item) => item.slug === post.slug) === index).slice(0, 2);
|
||||
const latestSource = posts.filter((post) => post.slug !== featuredPost?.slug);
|
||||
const { currentPage, totalPages, posts: latestPosts } = getPaginatedPosts(requestedPage, homeLayout.latestCount, latestSource);
|
||||
const mostViewedPosts = posts.slice(1, 6);
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, {}, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-7xl mx-auto px-4 lg:px-8 py-6"> ${featuredPost && renderTemplate`<section class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start mb-5 lg:mb-6"> <a${addAttribute(`/posts/${featuredPost.slug}/`, "href")} class="lg:col-span-2 bg-white rounded-2xl overflow-hidden shadow-sm group"> <div class="img-placeholder h-64 sm:h-80 lg:h-[430px] w-full relative overflow-hidden"> ${featuredPost.image ? renderTemplate`<img${addAttribute(featuredPost.image, "src")}${addAttribute(featuredPost.title, "alt")} class="absolute inset-0 w-full h-full object-cover">` : renderTemplate`<div class="absolute inset-0 flex items-center justify-center text-gray-400"> <span class="material-icons text-5xl opacity-30">image</span> </div>`} <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent"></div> <div class="absolute bottom-0 p-4 sm:p-6"> <span class="bg-brand text-white text-xs px-2.5 py-1 rounded-full font-bold">ویژه</span> <h1 class="text-white font-bold text-xl sm:text-2xl mt-2 leading-snug line-clamp-2 group-hover:text-red-200 transition-colors"> ${featuredPost.title} </h1> <p class="text-white/75 text-sm mt-1 hidden sm:block line-clamp-2">${featuredPost.excerpt}</p> <div class="flex items-center gap-3 mt-3 text-white/60 text-xs"> <span class="material-icons text-sm">schedule</span> <span>${formatPostRelativeTime(featuredPost)}</span> <span>·</span> <span>${featuredPost.category}</span> <span>·</span> <span>${featuredPost.readTime} مطالعه</span> </div> </div> </div> </a> <div class="flex flex-col gap-4 lg:col-span-1"> <div class="grid grid-cols-2 lg:grid-cols-1 gap-4"> ${sideFeaturedPosts.map((post) => renderTemplate`<a${addAttribute(`/posts/${post.slug}/`, "href")} class="bg-white rounded-2xl overflow-hidden shadow-sm group"> <div class="img-placeholder h-28 lg:h-36 w-full relative overflow-hidden"> ${post.thumbnail || post.image ? renderTemplate`<img${addAttribute(post.thumbnail || post.image, "src")}${addAttribute(post.title, "alt")} class="w-full h-full object-cover" loading="lazy">` : renderTemplate`<div class="absolute inset-0 flex items-center justify-center text-gray-400"> <span class="material-icons text-3xl opacity-30">image</span> </div>`} </div> <div class="p-3"> <span class="text-brand text-xs font-bold">${post.category}</span> <h2 class="font-semibold text-sm mt-1 leading-snug line-clamp-2 group-hover:text-brand transition-colors">${post.title}</h2> <p class="text-xs text-gray-400 mt-1">${formatPostRelativeTime(post)}</p> </div> </a>`)} </div> </div> </section>`} <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="lg:col-span-2"> <div class="flex items-center justify-between mb-4"> <h2 class="font-bold text-lg">آخرین اخبار</h2> <a href="/posts/" class="text-brand text-sm hover:underline">همه اخبار</a> </div> <div class="space-y-4"> ${latestPosts.map((post) => renderTemplate`${renderComponent($$result2, "PostCard", $$PostCard, { "post": post })}`)} </div> ${renderComponent($$result2, "Pagination", $$Pagination, { "currentPage": currentPage, "totalPages": totalPages, "basePath": "/" })} </div> <aside class="lg:col-span-1 space-y-6"> <div class="bg-white rounded-2xl p-4 shadow-sm"> <h3 class="font-bold text-base mb-4 flex items-center gap-2"> <span class="material-icons text-brand text-xl">trending_up</span>
|
||||
پربازدیدترین
|
||||
</h3> <ol class="space-y-3"> ${mostViewedPosts.map((item, index) => renderTemplate`<li class="flex items-start gap-3 group"> <span class="text-2xl font-black text-gray-100 leading-none flex-shrink-0 w-6 text-center">${index + 1}</span> <div> <a${addAttribute(`/posts/${item.slug}/`, "href")} class="text-sm font-medium leading-snug group-hover:text-brand transition-colors line-clamp-2">${item.title}</a> <a${addAttribute(getCategoryPath(item.category), "href")} class="text-xs text-brand">${item.category}</a> </div> </li>`)} </ol> </div> ${homeLayout.showAd && ad.enabled && renderTemplate`<a${addAttribute(ad.href, "href")} class="img-placeholder rounded-2xl h-48 flex items-center justify-center text-gray-400"> <div class="text-center"> <span class="material-icons text-4xl opacity-30">campaign</span> <p class="text-xs mt-1 opacity-40">${ad.label}</p> <p class="text-sm mt-2 text-gray-500">${ad.text}</p> </div> </a>`} <div class="bg-white rounded-2xl p-4 shadow-sm"> <h3 class="font-bold text-base mb-3">موضوعات داغ</h3> <div class="flex flex-wrap gap-2"> ${hotTopics.map((tag) => renderTemplate`<a${addAttribute(`/search/?q=${encodeURIComponent(tag)}`, "href")} class="bg-gray-100 hover:bg-red-50 hover:text-brand text-gray-600 text-xs px-3 py-1.5 rounded-full transition-colors">#${tag}</a>`)} </div> </div> </aside> </div> </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/index.astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/index.astro";
|
||||
const $$url = "";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$Index,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
32
dist/server/pages/posts.astro.mjs
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead } from '../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$PostCard } from '../chunks/PostCard_D8td2Gog.mjs';
|
||||
import { $ as $$Pagination } from '../chunks/Pagination_BeoGb-H9.mjs';
|
||||
import { $ as $$BaseLayout } from '../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { r as readCms, d as getPaginatedPosts, b as getApprovedPosts } from '../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Index;
|
||||
const cms = await readCms();
|
||||
const pageParam = Number(Astro2.url.searchParams.get("page") ?? "1");
|
||||
const requestedPage = Number.isFinite(pageParam) ? pageParam : 1;
|
||||
const { currentPage, totalPages, posts } = getPaginatedPosts(requestedPage, 4, getApprovedPosts(cms));
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": "\u0647\u0645\u0647 \u0627\u062E\u0628\u0627\u0631 | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632" }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-7xl mx-auto px-4 lg:px-8 py-8"> <div class="mb-6"> <span class="text-brand text-sm font-bold">آرشیو</span> <h1 class="text-2xl font-black mt-1">همه اخبار</h1> <p class="text-gray-500 text-sm mt-2">لیست کامل نوشتهها و خبرهای منتشر شده در روس امروز</p> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> ${posts.map((post) => renderTemplate`${renderComponent($$result2, "PostCard", $$PostCard, { "post": post })}`)} </div> ${renderComponent($$result2, "Pagination", $$Pagination, { "currentPage": currentPage, "totalPages": totalPages })} </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/posts/index.astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/posts/index.astro";
|
||||
const $$url = "/posts";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$Index,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
50
dist/server/pages/posts/_slug_.astro.mjs
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead, a as addAttribute } from '../../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$BaseLayout } from '../../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { $ as $$PostCard } from '../../chunks/PostCard_D8td2Gog.mjs';
|
||||
import { r as readCms, b as getApprovedPosts, c as getPostBySlug, h as formatPostShamsiDate, e as getCategoryPath, p as parseImageBlock } from '../../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$slug = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$slug;
|
||||
const cms = await readCms();
|
||||
const posts = getApprovedPosts(cms);
|
||||
const post = getPostBySlug(posts, Astro2.params.slug ?? "");
|
||||
if (!post) {
|
||||
return Astro2.redirect("/posts/");
|
||||
}
|
||||
const dateLabel = formatPostShamsiDate(post);
|
||||
const relatedPosts = posts.filter((item) => item.slug !== post.slug).slice(0, 3);
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": `${post.title} | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632`, "description": post.excerpt }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-7xl mx-auto px-4 lg:px-8 py-8"> <article class="bg-white rounded-2xl shadow-sm overflow-hidden"> <div class="img-placeholder h-64 sm:h-80 lg:h-96 relative overflow-hidden"> ${post.image ? renderTemplate`<img${addAttribute(post.image, "src")}${addAttribute(post.title, "alt")} class="w-full h-full object-cover">` : renderTemplate`<div class="absolute inset-0 flex items-center justify-center text-gray-400"> <span class="material-icons text-6xl opacity-30">image</span> </div>`} </div> <div class="max-w-3xl mx-auto p-5 sm:p-8"> <a${addAttribute(getCategoryPath(post.category), "href")} class="text-brand text-sm font-bold">${post.category}</a> <h1 class="text-2xl sm:text-4xl font-black leading-snug mt-3">${post.title}</h1> <p class="text-gray-500 leading-relaxed mt-4">${post.excerpt}</p> <div class="flex flex-wrap items-center gap-3 text-xs text-gray-400 border-y border-gray-100 py-4 my-6"> <span class="material-icons text-base">schedule</span> <span>${dateLabel}</span> <span>·</span> <span>${post.readTime} مطالعه</span> <span>·</span> <span>تحریریه روس امروز</span> </div> <div class="space-y-5 text-gray-700 leading-8 text-base"> ${post.body.map((paragraph) => {
|
||||
const imageBlock = parseImageBlock(paragraph);
|
||||
if (imageBlock) {
|
||||
return renderTemplate`<figure class="my-7"> <img${addAttribute(imageBlock.src, "src")}${addAttribute(imageBlock.caption || post.title, "alt")} class="w-full rounded-2xl object-cover" loading="lazy"> ${imageBlock.caption && renderTemplate`<figcaption class="text-xs text-gray-400 mt-2 text-center">${imageBlock.caption}</figcaption>`} </figure>`;
|
||||
}
|
||||
if (paragraph === "---") {
|
||||
return renderTemplate`<hr class="border-gray-100 my-7">`;
|
||||
}
|
||||
if (paragraph.startsWith("## ")) {
|
||||
return renderTemplate`<h2 class="text-xl font-black text-gray-900 mt-8">${paragraph.replace(/^##\s+/, "")}</h2>`;
|
||||
}
|
||||
if (paragraph.startsWith("> ")) {
|
||||
return renderTemplate`<blockquote class="border-r-4 border-brand bg-red-50/60 rounded-l-xl px-4 py-3 text-gray-700">${paragraph.replace(/^>\s+/, "")}</blockquote>`;
|
||||
}
|
||||
return renderTemplate`<p>${paragraph}</p>`;
|
||||
})} </div> </div> </article> <section class="mt-10"> <div class="flex items-center justify-between mb-4"> <h2 class="font-bold text-lg">نوشتههای مرتبط</h2> <a href="/posts/" class="text-brand text-sm hover:underline">همه اخبار</a> </div> <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> ${relatedPosts.map((relatedPost) => renderTemplate`${renderComponent($$result2, "PostCard", $$PostCard, { "post": relatedPost })}`)} </div> </section> </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/posts/[slug].astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/posts/[slug].astro";
|
||||
const $$url = "/posts/[slug]";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$slug,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
36
dist/server/pages/search.astro.mjs
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { c as createComponent, d as renderComponent, r as renderTemplate, b as createAstro, m as maybeRenderHead, a as addAttribute } from '../chunks/astro/server_DrnzQWw3.mjs';
|
||||
import 'kleur/colors';
|
||||
import { $ as $$BaseLayout } from '../chunks/BaseLayout_Bc2tHXHa.mjs';
|
||||
import { $ as $$PostCard } from '../chunks/PostCard_D8td2Gog.mjs';
|
||||
import { r as readCms, i as searchPosts, b as getApprovedPosts } from '../chunks/cms_CRTVymfB.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Index;
|
||||
const cms = await readCms();
|
||||
const query = Astro2.url.searchParams.get("q")?.trim() ?? "";
|
||||
const results = searchPosts(getApprovedPosts(cms), query);
|
||||
const title = query ? `\u062C\u0633\u062A\u062C\u0648: ${query} | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632` : "\u062C\u0633\u062A\u062C\u0648 | \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632";
|
||||
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": title, "description": "\u062C\u0633\u062A\u062C\u0648 \u062F\u0631 \u062E\u0628\u0631\u0647\u0627 \u0648 \u062A\u062D\u0644\u06CC\u0644\u200C\u0647\u0627\u06CC \u0631\u0648\u0633 \u0627\u0645\u0631\u0648\u0632" }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="max-w-7xl mx-auto px-4 lg:px-8 py-8"> <div class="bg-white rounded-2xl shadow-sm p-5 sm:p-6 mb-6"> <span class="text-brand text-sm font-bold">جستجو</span> <h1 class="text-2xl font-black mt-1">جستجو در روس امروز</h1> <form action="/search/" method="get" class="mt-5 flex gap-2"> <input type="search" name="q"${addAttribute(query, "value")} placeholder="عبارت مورد نظر را وارد کنید..." class="flex-1 bg-gray-100 text-gray-700 text-sm rounded-xl px-4 py-3 outline-none focus:ring-1 focus:ring-brand focus:bg-white"> <button type="submit" class="bg-brand text-white text-sm font-bold px-5 py-3 rounded-xl hover:bg-red-700 transition-colors">
|
||||
جستجو
|
||||
</button> </form> </div> ${query ? renderTemplate`<section> <div class="flex items-center justify-between mb-4"> <h2 class="font-bold text-lg">نتایج برای «${query}»</h2> <span class="text-sm text-gray-400">${results.length} نتیجه</span> </div> ${results.length > 0 ? renderTemplate`<div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> ${results.map((post) => renderTemplate`${renderComponent($$result2, "PostCard", $$PostCard, { "post": post })}`)} </div>` : renderTemplate`<div class="bg-white rounded-2xl shadow-sm p-8 text-center text-gray-500">
|
||||
نتیجهای برای این عبارت پیدا نشد.
|
||||
</div>`} </section>` : renderTemplate`<section class="bg-white rounded-2xl shadow-sm p-5"> <h2 class="font-bold text-base mb-3">موضوعات پیشنهادی</h2> <div class="flex flex-wrap gap-2"> ${cms.settings.hotTopics.map((tag) => renderTemplate`<a${addAttribute(`/search/?q=${encodeURIComponent(tag)}`, "href")} class="bg-gray-100 hover:bg-red-50 hover:text-brand text-gray-600 text-xs px-3 py-1.5 rounded-full transition-colors">
|
||||
#${tag} </a>`)} </div> </section>`} </main> ` })}`;
|
||||
}, "/Users/drfelfel/ruseemrooz/src/pages/search/index.astro", void 0);
|
||||
|
||||
const $$file = "/Users/drfelfel/ruseemrooz/src/pages/search/index.astro";
|
||||
const $$url = "/search";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$Index,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
3
dist/server/renderers.mjs
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const renderers = [];
|
||||
|
||||
export { renderers };
|
||||
7897
package-lock.json
generated
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "ruseemrooz",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^8.3.4",
|
||||
"astro": "^4.16.18",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"cheerio": "^1.2.0",
|
||||
"fast-xml-parser": "^5.9.3",
|
||||
"openai": "^6.44.0",
|
||||
"puppeteer-core": "^25.3.0",
|
||||
"sharp": "^0.35.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
BIN
public/assets/logo.png
Normal file
|
After Width: | Height: | Size: 934 KiB |
BIN
public/uploads/0dfaf688-a3f5-4914-b0a5-14a720c212bf.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/uploads/10d5c9bf-849e-4ddd-a7d8-dd5dc6a2e666.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/uploads/15a3a254-12db-4851-a434-cae6e631838d.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/uploads/23ed9cea-411b-4169-a905-a40bd503d8c6.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/uploads/2d77aee7-e390-4ca2-8100-d0e6528c145a.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/uploads/3b043c9d-57bf-46f2-9fb2-9c8be53bbcf5.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/uploads/553d2a2a-b924-401e-988d-1db34a907355.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/uploads/70cc796b-ee3c-49c8-8939-d0fe0dde1554.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/uploads/7c59fcfa-93cc-4d1d-b2be-d491eb482302.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/uploads/9de7af87-a5f2-4299-bc4e-6c3a17ee9f4d.webp
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/uploads/ad228b50-fe89-40a6-b17d-87440c24d6a5.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/uploads/ae9ef03b-14a9-424d-beb2-60bc3073851c.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/uploads/af246b9a-cf64-43c2-934d-edb5d8f0b8ee.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/uploads/d0858db4-fcc3-43b4-b120-53cc41a4473c.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/uploads/d3a18a75-4a6f-48c6-b3a5-c10f6fe798d7.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |