From 8a4578051a874e2b1ec1160a9f549a8884d7ab19 Mon Sep 17 00:00:00 2001 From: Bahman026 Date: Wed, 24 Jun 2026 21:42:49 +0330 Subject: [PATCH 1/2] feat(shop): foundation - SEO, footer data, icons, error pages - Wire shop to the shared admin-owned Postgres via a read-only Setting model; render the global footer (links, contact, socials, about, copyright) from autoloaded settings through Inertia shared props. - Add SEO scaffolding: AppHead component, shared meta defaults, canonical, Open Graph/Twitter, robots.txt. - Add useFormat composable (Persian digits, Jalali date, money). - Add Persian RTL 404/500 error pages. - Add self-hosted FontAwesome behind a shared Icon component and make it the required icon pattern in AGENTS.md. Co-authored-by: Cursor --- shop/AGENTS.md | 3 + .../Http/Middleware/HandleInertiaRequests.php | 75 ++++++++++++- shop/app/Models/Setting.php | 37 ++++++ shop/docs/CACHE.md | 1 + shop/docs/STOREFRONT_IMPLEMENTATION.md | 11 +- shop/package-lock.json | 59 ++++++++++ shop/package.json | 4 + shop/public/robots.txt | 2 + shop/resources/js/Components/AppHead.vue | 106 ++++++++++++++++++ .../js/Components/Footer/AppFooter.vue | 11 ++ .../js/Components/Footer/FooterSocial.vue | 23 +++- shop/resources/js/Components/Icon.vue | 14 +++ shop/resources/js/Layouts/AppLayout.vue | 12 +- shop/resources/js/Pages/Home.vue | 9 +- shop/resources/js/app.js | 1 + shop/resources/js/composables/useFormat.js | 71 ++++++++++++ shop/resources/js/fontawesome.js | 15 +++ shop/resources/js/ssr.js | 2 + shop/resources/views/errors/404.blade.php | 5 + shop/resources/views/errors/500.blade.php | 5 + shop/resources/views/errors/layout.blade.php | 73 ++++++++++++ 21 files changed, 523 insertions(+), 16 deletions(-) create mode 100644 shop/app/Models/Setting.php create mode 100644 shop/resources/js/Components/AppHead.vue create mode 100644 shop/resources/js/Components/Icon.vue create mode 100644 shop/resources/js/composables/useFormat.js create mode 100644 shop/resources/js/fontawesome.js create mode 100644 shop/resources/views/errors/404.blade.php create mode 100644 shop/resources/views/errors/500.blade.php create mode 100644 shop/resources/views/errors/layout.blade.php diff --git a/shop/AGENTS.md b/shop/AGENTS.md index dc6ce94f..cd942d7c 100644 --- a/shop/AGENTS.md +++ b/shop/AGENTS.md @@ -199,6 +199,9 @@ The shop UI uses **Inertia + Vue 3** (SSR enabled). Clean, readable code is a ha - Extract repeated logic into composables under `resources/js/composables/` (e.g. `useCart.ts`). - Style with Tailwind utility classes, RTL-first (see fonts/RTL section). No inline styles, no copy-pasted markup; reuse components. - **Brand color is `#ff8615`.** It is registered in `resources/css/app.css` as `--color-brand`, so use the Tailwind `brand` utilities (`bg-brand`, `text-brand`, `border-brand`, ...) for primary actions and accents. Do not hardcode the hex in components. +- **Icons: FontAwesome only, for a uniform icon set.** Always render icons through the shared `` component (`resources/js/Components/Icon.vue`). Never use raw SVGs, emoji, or another icon library, and do not place `` directly in components. + - Register every icon as an object in `resources/js/fontawesome.js` (solid from `@fortawesome/free-solid-svg-icons`, brands from `@fortawesome/free-brands-svg-icons`) and pass the imported icon object: ``. + - Do NOT use string names with `library.add` (e.g. `['fab','instagram']`). Inertia turns FontAwesome's missing-icon `console.error` into an SSR exception, so string lookups break SSR. Passing icon objects is the SSR-safe, tree-shakeable pattern. - Pages must use Inertia's `` for SEO tags (see SEO section) and render meaningful content server-side. ## Language, RTL & fonts diff --git a/shop/app/Http/Middleware/HandleInertiaRequests.php b/shop/app/Http/Middleware/HandleInertiaRequests.php index b9701985..fbf692e8 100644 --- a/shop/app/Http/Middleware/HandleInertiaRequests.php +++ b/shop/app/Http/Middleware/HandleInertiaRequests.php @@ -4,7 +4,9 @@ namespace App\Http\Middleware; +use App\Models\Setting; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Inertia\Middleware; class HandleInertiaRequests extends Middleware @@ -37,9 +39,80 @@ public function version(Request $request): ?string */ public function share(Request $request): array { + $settings = $this->autoloadedSettings(); + return [ ...parent::share($request), - // + 'seo' => [ + 'siteName' => (string) config('app.name'), + 'url' => $request->url(), + 'locale' => 'fa_IR', + ], + 'footer' => [ + 'about' => $this->value($settings, 'footer_about'), + 'columns' => [ + ['title' => 'فروشگاه', 'links' => $this->json($settings, 'footer_links_shop')], + ['title' => 'پشتیبانی', 'links' => $this->json($settings, 'footer_links_support')], + ], + 'contact' => [ + 'title' => 'ارتباط با ما', + 'phone' => $this->value($settings, 'site_phone'), + 'email' => $this->value($settings, 'site_email'), + 'hours' => $this->json($settings, 'site_working_hours'), + 'address' => $this->value($settings, 'site_address'), + ], + 'socials' => $this->json($settings, 'footer_socials'), + 'copyright' => $this->value($settings, 'footer_copyright'), + ], ]; } + + /** + * Load the globally autoloaded settings as a key => content map. + * + * Guarded with rescue() so the storefront still renders in environments + * where the admin-owned settings table is absent (e.g. the test database). + * + * @return Collection + */ + private function autoloadedSettings(): Collection + { + /** @var Collection $settings */ + $settings = rescue( + fn (): Collection => Setting::query()->autoloaded()->pluck('content', 'key'), + collect(), + false, + ); + + return $settings; + } + + /** + * @param Collection $settings + */ + private function value(Collection $settings, string $key): ?string + { + $value = $settings->get($key); + + return is_string($value) ? $value : null; + } + + /** + * Decode a JSON setting value into an array. + * + * @param Collection $settings + * @return array + */ + private function json(Collection $settings, string $key): array + { + $value = $this->value($settings, $key); + + if ($value === null || $value === '') { + return []; + } + + $decoded = json_decode($value, true); + + return is_array($decoded) ? $decoded : []; + } } diff --git a/shop/app/Models/Setting.php b/shop/app/Models/Setting.php new file mode 100644 index 00000000..bd0ebdb0 --- /dev/null +++ b/shop/app/Models/Setting.php @@ -0,0 +1,37 @@ + 'boolean', + ]; + + public function scopeAutoloaded(Builder $query): Builder + { + return $query->where('autoload', true); + } +} diff --git a/shop/docs/CACHE.md b/shop/docs/CACHE.md index e6d18529..7bec45cb 100644 --- a/shop/docs/CACHE.md +++ b/shop/docs/CACHE.md @@ -23,6 +23,7 @@ Legend: `[ ]` not started, `[x]` implemented. | 7 | `varieties.product.{product_id}` | All varieties for a product (price, inventory, status) | 15 min | Variety saved / deleted | | 9 | `pages.{slug}` | Single published page record | 1 hour | Page saved / deleted | | 10 | `faqs.{position}` | FAQs for a given position (null = main FAQ page) | 1 hour | FAQ saved / deleted | +| 11 | `settings.autoload` | Autoloaded site settings (key => content), used for footer/contact | 1 hour | Setting saved / deleted (in admin) | --- diff --git a/shop/docs/STOREFRONT_IMPLEMENTATION.md b/shop/docs/STOREFRONT_IMPLEMENTATION.md index 5915a4bb..3e6f3f61 100644 --- a/shop/docs/STOREFRONT_IMPLEMENTATION.md +++ b/shop/docs/STOREFRONT_IMPLEMENTATION.md @@ -23,10 +23,13 @@ A storefront feature is "done" when it has: read/write Eloquent models for the s - [x] `A Iranian Sans` font + brand color `#ff8615` - [x] Base `AppLayout` + sample `Home` page - [x] Eloquent models mapping shared tables (read-focused): `Category`, `Brand`, `Product`, `Variety`, `Attribute`, `Image`, `Banner`, `Slider`/`Slide`, `Menu`/`MenuItem`, `Page`, `Faq`, `Review` (+ status enums and `HasOptions` trait). Relations to not-yet-created models (`AttributeGroup`, `Coupon`, `Discount`) are deferred to their phases. -- [ ] Shared UI kit components: `BaseButton`, `PriceTag`, `ProductCard`, `QuantityInput`, `Breadcrumbs`, `Pagination`, `RatingStars`, `EmptyState` -- [ ] Helpers/composables: Persian digits, Jalali date, money formatting (`useFormat`) -- [ ] SEO scaffolding: per-page `` pattern, shared meta defaults, canonical URL, Open Graph, `robots.txt` -- [ ] Error pages (404 / 500) in Persian, RTL +- [~] Shared UI kit components: `BaseButton` + `AppLink` + `Icon` done; `PriceTag`, `ProductCard`, `QuantityInput`, `Breadcrumbs`, `Pagination`, `RatingStars`, `EmptyState` pending +- [x] Helpers/composables: Persian digits, Jalali date, money formatting (`useFormat`) +- [x] SEO scaffolding: per-page `` via `AppHead` component, shared meta defaults (Inertia `seo` shared prop), canonical URL, Open Graph + Twitter, `robots.txt` +- [x] Error pages (404 / 500) in Persian, RTL (self-contained Blade, brand color, IranSans, no Vite/SSR dependency) +- [x] Shared database connection: shop reads the admin-owned Postgres (`shop_flow_db`); file/sync session/cache/queue so shop owns no tables (`.env.example` stays SQLite for CI/tests) +- [x] Icon system: FontAwesome (self-hosted via npm) behind the shared `Icon` component, icon-object pattern (SSR-safe). See `AGENTS.md` +- [x] Global footer wired from `settings`: read-only `Setting` model + Inertia shared `footer` (link columns, contact, socials, about, copyright) rendered by `AppFooter`/`Footer/*` components ## Phase 1 - Catalog browsing (highest SEO value, build first) diff --git a/shop/package-lock.json b/shop/package-lock.json index 1279b516..0f53d400 100644 --- a/shop/package-lock.json +++ b/shop/package-lock.json @@ -5,6 +5,10 @@ "packages": { "": { "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/vue-fontawesome": "^3.2.0", "@inertiajs/vue3": "^3.4.0", "@vitejs/plugin-vue": "^6.0.7", "@vue/server-renderer": "^3.5.38", @@ -95,6 +99,61 @@ "tslib": "^2.4.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", + "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.2.0.tgz", + "integrity": "sha512-7BwGjTZn8QDvVEIu8fvkHhsDRRv//tq7jtsldaDhF3dE1fyWLIQcEg3zvIzy33su7kcppWsZZ6XRYP5wp3UCgQ==", + "license": "MIT", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "vue": ">= 3.0.0 < 4" + } + }, "node_modules/@inertiajs/core": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.4.0.tgz", diff --git a/shop/package.json b/shop/package.json index 2d613f2b..b6dc2b5c 100644 --- a/shop/package.json +++ b/shop/package.json @@ -14,6 +14,10 @@ "vite": "^8.0.0" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/vue-fontawesome": "^3.2.0", "@inertiajs/vue3": "^3.4.0", "@vitejs/plugin-vue": "^6.0.7", "@vue/server-renderer": "^3.5.38", diff --git a/shop/public/robots.txt b/shop/public/robots.txt index eb053628..b098cdc7 100644 --- a/shop/public/robots.txt +++ b/shop/public/robots.txt @@ -1,2 +1,4 @@ User-agent: * Disallow: + +Sitemap: /sitemap.xml diff --git a/shop/resources/js/Components/AppHead.vue b/shop/resources/js/Components/AppHead.vue new file mode 100644 index 00000000..3396fb18 --- /dev/null +++ b/shop/resources/js/Components/AppHead.vue @@ -0,0 +1,106 @@ + + + diff --git a/shop/resources/js/Components/Footer/AppFooter.vue b/shop/resources/js/Components/Footer/AppFooter.vue index df6733c8..f9510cca 100644 --- a/shop/resources/js/Components/Footer/AppFooter.vue +++ b/shop/resources/js/Components/Footer/AppFooter.vue @@ -6,6 +6,10 @@ import FooterNewsletter from '@/Components/Footer/FooterNewsletter.vue'; import FooterCopyright from '@/Components/Footer/FooterCopyright.vue'; defineProps({ + about: { + type: String, + default: '', + }, columns: { type: Array, default: () => [], @@ -34,6 +38,13 @@ const emit = defineEmits(['subscribe']);