diff --git a/app/Attributes/SeoDataAttribute.php b/app/Attributes/SeoDataAttribute.php new file mode 100644 index 0000000..d86ce66 --- /dev/null +++ b/app/Attributes/SeoDataAttribute.php @@ -0,0 +1,49 @@ + new SEOData( + title: __('Meetups - Übersicht'), + description: __('Entdecke alle verfügbaren Meetups in deiner Region.'), + ), + // Add more as needed + 'default' => new SEOData( + title: __('Willkommen'), + description: __('Toximalistisches Infotainment für bullische Bitcoiner.'), + ), + ]; + } + + // Static method to get SEO data by key as SEOData instance + public static function getData(string $key): SEOData + { + if (empty(self::$seoDefinitions)) { + self::initDefinitions(); + } + return self::$seoDefinitions[$key] ?? self::$seoDefinitions['default']; + } + + // If direct SEOData is provided, return it; else fetch by key as SEOData + public function resolve(): SEOData + { + if ($this->key) { + return self::getData($this->key); + } + return self::getData('default'); // Fallback + } +} diff --git a/app/Traits/SeoTrait.php b/app/Traits/SeoTrait.php new file mode 100644 index 0000000..fa34025 --- /dev/null +++ b/app/Traits/SeoTrait.php @@ -0,0 +1,31 @@ +setupSeo(); + } + + /** + * Setup SEO data from attributes and share it globally. + */ + protected function setupSeo(): void + { + $reflection = new \ReflectionClass($this); + $attributes = $reflection->getAttributes(SeoDataAttribute::class); + + if (!empty($attributes)) { + $seoDataAttribute = $attributes[0]->newInstance(); + $seoData = $seoDataAttribute->resolve(); + } else { + $seoData = SeoDataAttribute::getData('default'); + } + + View::share('SEOData', $seoData); + } +} diff --git a/config/seo.php b/config/seo.php new file mode 100644 index 0000000..bc3bca5 --- /dev/null +++ b/config/seo.php @@ -0,0 +1,121 @@ + SEO::class, + + /** + * Use this setting to specify the site name that will be used in OpenGraph tags. + */ + 'site_name' => env('APP_NAME'), + + /** + * Use this setting to specify the path to the sitemap of your website. This exact path will outputted, so + * you can use both a hardcoded url and a relative path. We recommend the later. + * + * Example: '/storage/sitemap.xml' + * Do not forget the slash at the start. This will tell the search engine that the path is relative + * to the root domain and not relative to the current URL. The `spatie/laraavel-sitemap` package + * is a great package to generate sitemaps for your application. + */ + 'sitemap' => null, + + /** + * Use this setting to specify whether you want self-referencing `` tags to + * be added to the head of every page. There has been some debate whether this a good practice, but experts + * from Google and Yoast say that this is the best strategy. + * See https://yoast.com/rel-canonical/. + */ + 'canonical_link' => true, + + 'robots' => [ + /** + * Use this setting to specify the default value of the robots meta tag. `` + * Overwrite it with the robots attribute of the SEOData object. `SEOData->robots = 'noindex, nofollow'` + * "max-snippet:-1" Use n chars (-1: Search engine chooses) as a search result snippet. + * "max-image-preview:large" Max size of a preview in search results. + * "max-video-preview:-1" Use max seconds (-1: There is no limit) as a video snippet in search results. + * See https://developers.google.com/search/docs/advanced/robots/robots_meta_tag + * Default: 'max-snippet:-1, max-image-preview:large, max-video-preview:-1' + */ + 'default' => 'max-snippet:-1,max-image-preview:large,max-video-preview:-1', + + /** + * Force set the robots `default` value and make it impossible to overwrite it. (e.g. via SEOData->robots) + * Use case: You need to set `noindex, nofollow` for the entire website without exception. + * Default: false + */ + 'force_default' => false, + ], + + /** + * Use this setting to specify the path to the favicon for your website. The url to it will be generated using the `secure_url()` function, + * so make sure to make the favicon accessibly from the `public` folder. + * + * You can use the following filetypes: ico, png, gif, jpeg, svg. + */ + 'favicon' => '/img/favicon.svg', + + 'title' => [ + /** + * Use this setting to let the package automatically infer a title from the url, if no other title + * was given. This will be very useful on pages where you don't have an Eloquent model for, or where you + * don't want to hardcode the title. + * + * For example, if you have a with the url '/foo/about-me', we'll automatically set the title to 'About me' and append the site suffix. + */ + 'infer_title_from_url' => true, + + /** + * Use this setting to provide a suffix that will be added after the title on each page. + * If you don't want a suffix, you should specify an empty string. + */ + 'suffix' => ' - ' . env('APP_NAME'), + + /** + * Use this setting to provide a custom title for the homepage. We will not use the suffix on the homepage, + * so you'll need to add the suffix manually if you want that. If set to null, we'll determine the title + * just like the other pages. + */ + 'homepage_title' => null, + ], + + 'description' => [ + /** + * Use this setting to specify a fallback description, which will be used on places + * where we don't have a description set via an associated ->seo model or via + * the ->getDynamicSEOData() method. + */ + 'fallback' => 'Toximalist infotainment for bullish bitcoiners.', + ], + + 'image' => [ + /** + * Use this setting to specify a fallback image, which will be used on places where you + * don't have an image set via an associated ->seo model or via the ->getDynamicSEOData() method. + * This should be a path to an image. The url to the path is generated using the `secure_url()` function (`secure_url($yourProvidedPath)`). + */ + 'fallback' => '/img/social.jpg', + ], + + 'author' => [ + /** + * Use this setting to specify a fallback author, which will be used on places where you + * don't have an author set via an associated ->seo model or via the ->getDynamicSEOData() method. + */ + 'fallback' => 'einundzwanzig', + ], + + 'twitter' => [ + /** + * Use this setting to enter your username and include that with the Twitter Card tags. + * Enter the username like 'yourUserName', so without the '@'. + */ + '@username' => '_einundzwanzig_', + ], +]; diff --git a/public/img/social.jpg b/public/img/social.jpg new file mode 100644 index 0000000..1256c1b Binary files /dev/null and b/public/img/social.jpg differ diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index ca8fa8d..834b46e 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -3,6 +3,7 @@ use App\Models\LoginKey; use App\Models\User; use App\Notifications\ModelCreatedNotification; +use App\Traits\SeoTrait; use Illuminate\Auth\Events\Lockout; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\RateLimiter; @@ -20,6 +21,8 @@ use eza\lnurl; new #[Layout('components.layouts.auth')] class extends Component { + use SeoTrait; + #[Validate('required|string|email')] public string $email = ''; diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 63f2b66..d7f96c9 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -2,10 +2,13 @@ use App\Models\Meetup; use App\Models\MeetupEvent; +use App\Traits\SeoTrait; use Livewire\Volt\Component; use Flux\Flux; new class extends Component { + use SeoTrait; + public $selectedMeetupId = null; public $country = 'de'; diff --git a/resources/views/livewire/meetups/index.blade.php b/resources/views/livewire/meetups/index.blade.php index 4530963..f936160 100644 --- a/resources/views/livewire/meetups/index.blade.php +++ b/resources/views/livewire/meetups/index.blade.php @@ -1,11 +1,16 @@ Meetup::with(['city.country', 'createdBy']) ->withExists([ - 'meetupEvents as has_future_events' => fn($query) => $query->where('start', '>=', now()) + 'meetupEvents as has_future_events' => fn($query) => $query->where('start', '>=', now()), ]) ->leftJoin('meetup_events', function ($join) { - $join->on('meetups.id', '=', 'meetup_events.meetup_id') + $join + ->on('meetups.id', '=', 'meetup_events.meetup_id') ->where('meetup_events.start', '>=', now()); }) ->selectRaw('meetups.*, MIN(meetup_events.start) as next_event_start') @@ -43,14 +49,16 @@ new class extends Component {
{{ __('Meetups') }}
- {{ __('Kalender-Stream-URL kopieren') }} + {{ __('Kalender-Stream-URL kopieren') }} @auth - + {{ __('Meetup erstellen') }} @endauth @@ -70,9 +78,10 @@ new class extends Component { @foreach ($meetups as $meetup) - +
@if($meetup->city) @@ -107,12 +116,14 @@ new class extends Component { diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index 3cb30ee..e37338a 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,7 +1,7 @@ -{{ $title ?? config('app.name') }} +{!! seo($SEOData) !!}