json([ 'data' => [ 'locales' => collect(config('locales.supported')) ->map(fn (array $meta, string $code) => [ 'code' => $code, 'name' => $meta['native'] ?? $code, 'flag' => $meta['flag'] ?? null, 'is_default' => $code === config('locales.default'), ]) ->values(), 'default_locale' => config('locales.default'), /* * A number that moves whenever published content does. Keep it * as part of your cache key and everything downstream becomes * stale the moment the site really changed, instead of when a * timer says so. */ 'content_version' => ContentVersion::current(), // So the frontend hides its forms when the inbox is closed, // rather than rendering one that would be refused on submit. 'submissions_enabled' => (bool) config('site.submissions.enabled', true), 'stats' => [ 'projects' => Project::query()->published()->count(), 'active_projects' => Project::query()->published()->where('status', 'active')->count(), 'members' => Member::query()->public()->current()->count(), 'open_calls' => Call::query()->open()->count(), 'services' => Service::query()->published()->count(), 'clients' => Client::query()->public()->whereHas('projects', fn ($q) => $q->published())->count(), 'good_first_issues' => RepositoryIssue::query() ->where('is_good_first_issue', true) ->whereNull('assignee') ->whereHas('repository', fn ($q) => $q->publiclyVisible()) ->count(), 'stars' => (int) Repository::query()->publiclyVisible()->sum('stars'), ], ], ]); } /** * GET /api/public/v1/slugs * * Every published URL, grouped by type and locale. This is what the * frontend builds its sitemap and static paths from — one request instead * of paging through every collection. */ public function slugs(): JsonResponse { $entries = []; foreach (Page::query()->published()->get(['id', 'slug', 'updated_at']) as $page) { $entries['pages'][] = $this->entry($page, 'page'); } foreach (Project::query()->published()->get(['id', 'slug', 'updated_at']) as $project) { $entries['projects'][] = $this->entry($project, 'project'); } foreach (Call::query()->open()->get(['id', 'slug', 'updated_at']) as $call) { $entries['calls'][] = $this->entry($call, 'call'); } foreach (Service::query()->published()->get(['id', 'slug', 'updated_at']) as $service) { $entries['services'][] = $this->entry($service, 'service'); } foreach (Member::query()->public()->current()->get(['id', 'slug', 'updated_at']) as $member) { $entries['members'][] = [ 'id' => $member->id, 'updated_at' => $member->updated_at?->toIso8601String(), 'alternates' => collect(array_keys(config('locales.supported'))) ->mapWithKeys(fn (string $l) => [$l => SiteUrls::path('member', $l, $member->slug)]) ->all(), ]; } return response()->json(['data' => $entries]); } /** * GET /api/public/v1/redirects * * Every URL that used to work and where it goes now, so the frontend can * answer a 301 instead of a 404. Most of these were written by the panel * itself the moment somebody renamed a slug — the only moment at which the * old address was still known. * * Returned whole rather than looked up one at a time: there are never many, * and an SSR frontend would otherwise ask this API on every 404 it serves. */ public function redirects(): JsonResponse { $redirects = Redirect::query() ->enabled() ->orderBy('from') ->get(['from', 'to', 'status', 'locale']); return response()->json([ 'data' => $redirects->map(fn (Redirect $redirect) => [ 'from' => $redirect->from, 'to' => $redirect->to, 'status' => $redirect->status, // Null means it applies whatever language the visitor is // browsing in. 'locale' => $redirect->locale, ])->all(), ]); } /** * GET /api/public/v1/radar * * The tech radar: what the collective adopts, is trialling, is assessing * and is holding off on. Highly shareable and cheap to maintain. */ public function radar(): AnonymousResourceCollection { $technologies = Technology::query() ->onRadar() ->with('logoMedia') ->withCount(['projects', 'members']) ->orderBy('category') ->orderBy('position') ->orderBy('name') ->get(); return PublicTechnologyResource::collection($technologies); } /** * GET /api/public/v1/technologies */ public function technologies(Request $request): AnonymousResourceCollection { $technologies = Technology::query() ->with('logoMedia') ->withCount(['projects', 'members']) ->when($request->filled('category'), fn ($q) => $q ->whereIn('category', (array) $request->input('category'))) // By default only technologies actually in use, so the list is not // padded with tags nobody applied. ->when($request->boolean('in_use', true), fn ($q) => $q->has('projects')) ->orderBy('position') ->orderBy('name') ->get(); return PublicTechnologyResource::collection($technologies); } /** * GET /api/public/v1/good-first-issues * * The single most effective thing on a collective's site: it turns a * visitor who was only browsing into a contributor in one click. */ public function goodFirstIssues(Request $request): AnonymousResourceCollection { $issues = RepositoryIssue::query() ->with(['repository.project']) ->where(fn ($q) => $q ->where('is_good_first_issue', true) ->orWhere('is_help_wanted', $request->boolean('include_help_wanted', true))) // An issue somebody already took is not an invitation. ->available() ->whereHas('repository', fn ($q) => $q->publiclyVisible()->where('is_archived', false)) ->when($request->filled('language'), fn ($q) => $q ->whereHas('repository', fn ($r) => $r ->where('primary_language', $request->string('language')))) ->orderByDesc('issue_created_at') ->limit(min($request->integer('limit', 20), 100)) ->get(); return PublicIssueResource::collection($issues); } /** * @return array */ protected function entry(Model $record, string $type): array { return [ 'id' => $record->id, 'updated_at' => $record->updated_at?->toIso8601String(), 'alternates' => SiteUrls::alternates($record, $type), ]; } }