📄 Program.cs
using System;
using System.IO;
using System.Net.Mime;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MSearch.Domain;
using MSearch.SearchProviders.GitHub;
using MSearch.SearchProviders.HackerNews;
using MSearch.SearchProviders.MicrosoftLearn;
using MSearch.SearchProviders.MozillaDeveloperNetwork;
using MSearch.SearchProviders.OpenStreetMap;
using MSearch.SearchProviders.Reddit;
using MSearch.SearchProviders.Spotify;
using MSearch.SearchProviders.StackExchange;
using MSearch.SearchProviders.TheMovieDb;
using MSearch.SearchProviders.Wikipedia;
using MSearch.SearchProviders.YouTube;
using MSearch.Web.Components;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder
    .Services.AddSearchService()
    .AddGitHubSearchProvider()
    .AddHackerNewsSearchProvider()
    .AddMicrosoftLearnSearchProvider()
    .AddMozillaDeveloperNetworkSearchProvider()
    .AddOpenStreetMapSearchProvider()
    .AddRedditSearchProvider()
    .AddSpotifySearchProvider()
    .AddStackExchangeSearchProvider("stackoverflow")
    .AddTheMovieDbSearchProvider()
    .AddWikipediaSearchProvider("en")
    .AddYouTubeSearchProvider();

builder.Services.AddHttpContextAccessor();

builder.Services.AddRazorComponents();

var app = builder.Build();

app.UseStaticFiles();

app.MapGet(
    "/",
    async (
        [FromServices] IServiceProvider serviceProvider,
        [FromServices] ILoggerFactory loggerFactory,
        [FromServices] SearchService searchService,
        HttpResponse response,
        [FromQuery(Name = "q")] string? query = null,
        CancellationToken cancellationToken = default
    ) =>
    {
        response.Headers.ContentType = MediaTypeNames.Text.Html;

        await using var bodyWriter = new StreamWriter(response.Body);

        await bodyWriter.WriteAsync(
            """
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <base href="/" />
                <title>MSearch</title>
                <link rel="stylesheet" href="app.css" />
                <link rel="search" type="application/opensearchdescription+xml" href="opensearch" title="MSearch">
            </head>
            <body>
                <main>
            """.AsMemory(),
            cancellationToken
        );

        await using var htmlRenderer = new StaticHtmlRenderer(serviceProvider, loggerFactory);

        await htmlRenderer.Dispatcher.InvokeAsync(async () =>
        {
            {
                var output = htmlRenderer.BeginRenderingComponent(new LayoutComponent(), ParameterView.Empty);
                await bodyWriter.WriteAsync(output.ToHtmlString().AsMemory(), cancellationToken);
            }

            if (string.IsNullOrWhiteSpace(query))
            {
                return;
            }

            await foreach (var result in searchService.Search(query, cancellationToken))
            {
                var output = htmlRenderer.BeginRenderingComponent(
                    new SearchResultComponent(result),
                    ParameterView.Empty
                );
                await bodyWriter.WriteAsync(output.ToHtmlString().AsMemory(), cancellationToken);
            }
        });
    }
);

app.MapGet("/opensearch", () => new RazorComponentResult<OpenSearchDescriptor>());

app.MapDefaultEndpoints();

app.Run();