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) => {