diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0a7840 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# SourceVortex + +Front-end do [sourcevortex.com.br](https://sourcevortex.com.br) em Laravel + Inertia + Vue. + +Este projeto **não tem banco próprio**. Ele lê, em modo somente leitura, as tabelas de uma +instalação existente do WordPress (`wp_posts`, `wp_postmeta`, `wp_terms`, `wp_term_taxonomy`, +`wp_term_relationships`, `wp_users`, `wp_usermeta`) através de models Eloquent. O WordPress +continua sendo onde o conteúdo é escrito e administrado; aqui só se lê e se renderiza. + +## Stack + +| | | +|---|---| +| Back-end | Laravel 12, PHP 8.2+ | +| Front-end | Vue 3, TypeScript, Inertia 2 | +| Build | Vite 7, Tailwind 4 | +| Componentes | reka-ui (shadcn-vue) | +| Testes | Pest 3 | + +## Requisitos + +- PHP 8.2 ou superior +- Composer +- Node 22 ou superior +- MySQL **com as tabelas do WordPress já populadas** + +## Instalação + +```bash +composer install +npm ci +cp .env.example .env +php artisan key:generate +``` + +Aponte o `.env` para o banco do WordPress: + +```dotenv +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=nome_do_banco_wordpress +DB_USERNAME=usuario +DB_PASSWORD=senha + +# URL da instalação do WordPress, usada para montar os links do painel +WP_ADMIN_URL=https://exemplo.com.br +``` + +> **Não rode `php artisan migrate` contra o banco do WordPress** sem entender o efeito. +> As migrations do esqueleto do Laravel criam tabelas (`users`, `cache`, `jobs`) dentro +> do mesmo schema. Se `CACHE_STORE` ou `SESSION_DRIVER` estiverem em `database`, o Laravel +> passa a escrever no banco do WordPress. Prefira `redis` ou `file`. + +Para desenvolver: + +```bash +composer dev +``` + +Sobe servidor, fila, logs e Vite em paralelo. + +## Testes + +```bash +php artisan test +``` + +Os testes **não precisam** do banco do WordPress. `tests/Support/WordPressSchema.php` +cria as tabelas `wp_*` em sqlite na memória, e `tests/Support/Wp.php` traz construtores +para as linhas. Como o app não tem migration para essas tabelas, esse é o único lugar +onde o schema esperado está descrito — vale mantê-lo em dia com a instalação real. + +## Qualidade + +```bash +vendor/bin/pint --test # estilo PHP +npm run format:check # formatação do front +npm run lint:check # eslint +npm run type-check # vue-tsc +``` + +Os quatro rodam no CI e reprovam o build. + +## Convenções que vêm do WordPress + +Algumas regras de negócio moram no conteúdo, não no código: + +| Convenção | Efeito | +|---|---| +| Categoria `destaques` | Posts nela aparecem no carrossel da home e **saem** da listagem principal | +| Menu chamado `Menu` | É o menu principal do site | +| Página com slug `politica-de-privacidade` | Alimenta a rota `/politica-de-privacidade` | + +As URLs seguem o permalink padrão do WordPress (`/%year%/%monthnum%/%postname%`), o que +mantém os endereços do site antigo válidos. + +## ⚠️ Dependência de plugin: Advanced Custom Fields + +O subtítulo exibido na capa do post vem de `wp_postmeta` com `meta_key = 'subtitle'`. +Esse campo é criado pelo **Advanced Custom Fields** no WordPress — o tema V1 o lia com +`get_field("subtitle")`. + +Isso significa que: + +- o ACF precisa continuar instalado e o campo `subtitle` precisa continuar existindo; +- se o campo for movido para dentro de um *group* ou *repeater*, o `meta_key` muda de + formato e o subtítulo **some silenciosamente** aqui, sem erro nenhum. + +Não há como este projeto detectar essa quebra sozinho. Se o subtítulo sumir do site, +comece a investigação pela configuração do ACF. + +## Diferenças em relação ao tema antigo + +O tema WordPress que este projeto substitui tinha alguns recursos que não foram portados: + +- **Comentários** — descontinuados por decisão de produto. +- **Anúncios (AdSense)** — o tema tinha quatro posições. Não estão aqui. +- **AMP, post formats e página de biografia** — não portados. diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 0dd251b..ff0b8ea 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -4,7 +4,6 @@ use App\Services\WpAuthService; use App\Services\WpMenuService; -use Illuminate\Foundation\Inspiring; use Illuminate\Http\Request; use Inertia\Middleware; use Tighten\Ziggy\Ziggy; @@ -39,14 +38,11 @@ public function version(Request $request): ?string */ public function share(Request $request): array { - [$message, $author] = str(Inspiring::quotes()->random())->explode('-'); - $wpUser = WpAuthService::getLoggedInWpUser(); return [ ...parent::share($request), 'name' => config('app.name'), - 'quote' => ['message' => trim($message), 'author' => trim($author)], 'auth' => [ 'user' => $request->user(), ], diff --git a/app/Models/WpPost.php b/app/Models/WpPost.php index 0ae7b32..30c6706 100644 --- a/app/Models/WpPost.php +++ b/app/Models/WpPost.php @@ -90,8 +90,14 @@ public function useSearchTerm(Builder $query, ?string $searchTerm = null): void return; } - $query->where('post_title', 'LIKE', "%{$searchTerm}%") - ->orWhere('post_content', 'LIKE', "%{$searchTerm}%"); + // Grouped explicitly. Laravel currently nests the wheres a named scope + // adds, so the OR does not leak today — but that is a property of how + // the scope is invoked, not of this code, and it breaks the moment the + // condition is inlined into a query. + $query->where(function (Builder $query) use ($searchTerm) { + $query->where('post_title', 'LIKE', "%{$searchTerm}%") + ->orWhere('post_content', 'LIKE', "%{$searchTerm}%"); + }); } #[Scope] diff --git a/app/Services/WpCategoryService.php b/app/Services/WpCategoryService.php index 53e2f6b..1fc9dea 100644 --- a/app/Services/WpCategoryService.php +++ b/app/Services/WpCategoryService.php @@ -13,7 +13,11 @@ public static function getCategoryBySlug($slug) return cache()->remember($cacheKey, $cacheTTL, function () use ($slug) { return WpTerm::query() - ->where('slug', $slug) + // Slugs are only unique within a taxonomy: without this filter a + // tag sharing a category's slug could be returned instead. + ->join('wp_term_taxonomy', 'wp_term_taxonomy.term_id', '=', 'wp_terms.term_id') + ->where('wp_term_taxonomy.taxonomy', 'category') + ->where('wp_terms.slug', $slug) ->first(); }); } diff --git a/bootstrap/app.php b/bootstrap/app.php index a827a7f..14455db 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ + // Reads the appearance cookie the theme switcher writes and shares + // it with the root view. It existed but was never registered, so + // the cookie was written and never read, and anyone who picked a + // theme explicitly got a flash of the wrong one on every full load. + HandleAppearance::class, HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, ]); diff --git a/config/inertia.php b/config/inertia.php index d15cad9..575d918 100644 --- a/config/inertia.php +++ b/config/inertia.php @@ -15,9 +15,12 @@ | */ + // Off by default: when this is true and no SSR process is listening, every + // request pays for a failed connection to 127.0.0.1:13714 before falling + // back to client rendering. `composer dev` does not start that process. 'ssr' => [ - 'enabled' => true, - 'url' => 'http://127.0.0.1:13714', + 'enabled' => env('INERTIA_SSR_ENABLED', false), + 'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'), // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), ], diff --git a/phpunit.xml b/phpunit.xml index c09b5bc..61c031c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,8 @@ - + + diff --git a/resources/js/components/Carousel.vue b/resources/js/components/Carousel.vue index 5386bec..15c76b4 100644 --- a/resources/js/components/Carousel.vue +++ b/resources/js/components/Carousel.vue @@ -45,13 +45,15 @@ watchOnce(api, (api) => { :opts="{ loop: true, }" - :plugins="[Autoplay({ - delay: 5000, - active: true, - stopOnFocusIn: false, - stopOnInteraction: false, - stopOnMouseEnter: false, - })]" + :plugins="[ + Autoplay({ + delay: 5000, + active: true, + stopOnFocusIn: false, + stopOnInteraction: false, + stopOnMouseEnter: false, + }), + ]" > @@ -62,7 +64,7 @@ watchOnce(api, (api) => {