{"id":25871721,"url":"https://github.com/crcrc/toodle.pageoptimizer","last_synced_at":"2026-05-01T21:34:31.815Z","repository":{"id":278451272,"uuid":"935662576","full_name":"crcrc/Toodle.PageOptimizer","owner":"crcrc","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-27T20:22:45.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-28T05:05:48.770Z","etag":null,"topics":["blazor","dotnet","mvc"],"latest_commit_sha":null,"homepage":"https://www.toodle.uk/opensource","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crcrc.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-19T20:09:36.000Z","updated_at":"2025-02-27T20:21:08.000Z","dependencies_parsed_at":"2025-02-19T21:25:39.899Z","dependency_job_id":"aa530046-5d5c-4b7f-a684-d5edfaf35a3f","html_url":"https://github.com/crcrc/Toodle.PageOptimizer","commit_stats":null,"previous_names":["crcrc/toodle.pageoptimizer"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crcrc%2FToodle.PageOptimizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crcrc%2FToodle.PageOptimizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crcrc%2FToodle.PageOptimizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crcrc%2FToodle.PageOptimizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crcrc","download_url":"https://codeload.github.com/crcrc/Toodle.PageOptimizer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241472225,"owners_count":19968351,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blazor","dotnet","mvc"],"created_at":"2025-03-02T07:23:28.117Z","updated_at":"2026-05-01T21:34:31.783Z","avatar_url":"https://github.com/crcrc.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# Toodle.PageOptimizer\n![NuGet Version](https://img.shields.io/nuget/v/Toodle.PageOptimizer)\n![NuGet Downloads](https://img.shields.io/nuget/dt/Toodle.PageOptimizer)\n\nA high-performance metadata, breadcrumb, and resource optimization manager for ASP.NET Core. It streamlines SEO best practices and improves Core Web Vitals (LCP/FCP) by automating resource hinting and header management.\n\nSee [https://www.pricewatchdog.co.uk](https://www.pricewatchdog.co.uk) and [https://www.competitions-whale.co.uk](https://www.competitions-whale.co.uk) as examples of websites using this library.\n## Features\n- Fluent SEO API: Easily manage Meta Titles, Descriptions, Canonical URLs, og:type (with typed article/product/profile objects), and Twitter Card types.\n- Social Sharing: Full Open Graph and Twitter Card support including per-page and global default images.\n- HTTP Link Headers: Injects rel=preload and rel=preconnect headers into the HTTP response for faster asset discovery.\n- Static File Caching: Granular control over Cache-Control headers for specific file extensions or paths.\n- HTTPS Compression: One-click setup for Brotli and Gzip.\n- Structured Data: Automatic generation of JSON-LD BreadcrumbList for Google Search results.\n- Flexible Sitemaps: Register dynamic sitemap sources from databases or static lists.\n- robots.txt: Serve a generated robots.txt with optional custom rules. Sitemap URL included automatically.\n- Localization: Integrated support for RequestCulture and og:locale tags.\n\n## Installation\n```Bash\ndotnet add package Toodle.PageOptimizer\n```\n\n### Service Configuration\nRegister the service in Program.cs. This is where you define your compression and sitemap logic.\n\n```c#\nbuilder.Services.AddPageOptimizer(options =\u003e\n{\n    options.EnableHttpsCompression = true; // Enables Brotli/Gzip for HTTPS\n    options.UseRequestCulture = new RequestCulture(\"en-GB\");\n})\n.AddSitemapSource(async (serviceProvider) =\u003e\n{\n    // Example: Fetching dynamic product links for the sitemap\n    using var scope = serviceProvider.CreateScope();\n    var db = scope.ServiceProvider.GetRequiredService\u003cApplicationDbContext\u003e();\n    var products = await db.Products.ToListAsync();\n\n    return products.Select(p =\u003e new SitemapUrl {\n        Location = $\"/products/{p.Slug}\",\n        Priority = 0.8m,\n        ChangeFrequency = ChangeFrequency.Weekly\n    });\n});\n```\n\n### Middleware \u0026 App Configuration\nConfigure your global site defaults. This locks the global configuration to prevent accidental runtime changes.\n\n```C#\napp.ConfigurePageOptimizer()\n    .WithBaseTitle(\"Price Watchdog\", \"|\")\n    .WithBaseUrl(\"https://www.pricewatchdog.co.uk\")\n    .WithDefaultImage(\"/images/default-share.jpg\") // resolved to absolute using base URL\n    .AddDefaultPreconnect(\"https://res.cloudinary.com\")\n    // Adds Link: \u003c/js/bundle.js\u003e; rel=preload; as=script to every GET response header\n    .AddDefaultPreload(\"/js/bundle.min.js\", AssetType.Script) \n    .AddDefaultBreadcrumb(\"Home\", \"/\")\n    .AddStaticFileCacheHeaders(opt =\u003e\n    {\n        opt.IsPublic = true;\n        opt.MaxAge = TimeSpan.FromDays(7);\n        opt.FileExtensions = new[] { \".js\", \".css\", \".ico\", \".webp\" };\n    })\n    .ServeSitemap(opt =\u003e\n    {\n        opt.Path = \"/sitemap.xml\";\n        opt.CacheDuration = TimeSpan.FromHours(4);\n    })\n    .ServeRobotsTxt(opt =\u003e\n    {\n        opt.AdditionalRules = new[]\n        {\n            \"Disallow: /admin/\",\n            \"\",\n            \"# Block AI scrapers\",\n            \"User-agent: GPTBot\",\n            \"Disallow: /\"\n        };\n    });\n\napp.UsePageOptimizer(); // Enables header injection and SEO middleware\n```\n\n### Rendering Meta Tags \u0026 Breadcrumbs\nAdd the following to your _ViewImports.cshtml:\n\n```\n@addTagHelper *, Toodle.PageOptimizer\n```\n\nIn Layout (_Layout.cshtml)\n\nThe \u003cpage-optimizer /\u003e tag helper handles the title, meta, link rel=\"canonical\", and the JSON-LD Breadcrumb script.\n\n```html\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\" /\u003e\n    \u003cpage-optimizer /\u003e\n\u003c/head\u003e\n```\n\n### UI Breadcrumbs (Partial View)\n\nCreate a partial view (e.g., _Breadcrumbs.cshtml) to render the visual navigation using the injected service.\n\n```Razor CSHTML\n@inject Toodle.PageOptimizer.IPageOptimizerService pageOptimizerService\n@{\n    var breadcrumbs = pageOptimizerService.GetBreadCrumbs();\n}\n\n@if (breadcrumbs.Any())\n{\n    \u003cnav aria-label=\"breadcrumb\"\u003e\n        \u003col class=\"breadcrumb\"\u003e\n            @foreach (var crumb in breadcrumbs)\n            {\n                var isLast = breadcrumbs.Last() == crumb;\n                \u003cli class=\"breadcrumb-item @(isLast ? \"active\" : \"\")\"\u003e\n                    @if (isLast)\n                    {\n                        \u003cspan aria-current=\"page\"\u003e@crumb.Title\u003c/span\u003e\n                    }\n                    else\n                    {\n                        \u003ca href=\"@crumb.Url\"\u003e@crumb.Title\u003c/a\u003e\n                    }\n                \u003c/li\u003e\n            }\n        \u003c/ol\u003e\n    \u003c/nav\u003e\n}\n```\n\n### Controller Usage\nUpdate page metadata and breadcrumbs dynamically within your actions.\n\n```C#\n\npublic class ProductController : Controller\n{\n    private readonly IPageOptimizerService _optimizer;\n\n    public ProductController(IPageOptimizerService optimizer)\n    {\n        _optimizer = optimizer;\n    }\n\n    public IActionResult Details(string slug)\n    {\n        var product = _db.Products.Find(slug);\n\n        _optimizer\n            .SetMetaTitle(product.Name)\n            .SetMetaDescription(product.Summary)\n            .SetCanonicalUrl($\"/products/{product.Slug}\") // relative or absolute\n            .SetMetaImage(product.ImageUrl, width: 1200, height: 630, alt: product.Name)\n            .SetOgType(new OgTypeProduct\n            {\n                PriceAmount = product.Price,\n                PriceCurrency = \"GBP\",\n                Availability = \"instock\"\n            })\n            .SetTwitterCard(TwitterCard.SummaryLargeImage)\n            .AddBreadCrumb(\"Products\", \"/products\")\n            .AddBreadCrumb(product.Name); // Current page (no URL)\n\n        // Prevent indexing for specific conditions\n        if (product.IsDiscontinued) _optimizer.SetNoIndex(); // shortcut for SetRobots(\"noindex\")\n\n        // Or use the full robots string for more control\n        // _optimizer.SetRobots(\"noindex, nofollow\");\n\n        return View(product);\n    }\n}\n```\n\n## Advanced Mechanics\n### Resource Hinting (Link Headers)\nThe library automatically appends Link headers to the HTTP response for all preconnect and preload resources defined in configuration. This triggers \"Early Hints\" in supported browsers and CDNs, allowing assets to begin downloading while the server is still processing the HTML.\n\n### Response Compression\nWhen EnableHttpsCompression is set to true, the library automatically configures:\n\n- Brotli \u0026 Gzip at Optimal compression levels.\n- Support for image/svg+xml and other standard MIME types.\n\n### Social Sharing (og:image / twitter:image)\nSet a global default share image in `ConfigurePageOptimizer()` using `WithDefaultImage()`. Individual pages can override it with `SetMetaImage()`. Both methods accept relative paths (resolved against the base URL) or absolute URLs, making it easy to serve images from a CDN on a different domain.\n\n### Robots Meta Tag\n`SetNoIndex()` is a shortcut for the common case. `SetRobots()` accepts any valid robots directives string for full control:\n\n```csharp\n_optimizer.SetNoIndex();                          // \u003cmeta name=\"robots\" content=\"noindex\"\u003e\n_optimizer.SetRobots(\"noindex, nofollow\");        // \u003cmeta name=\"robots\" content=\"noindex, nofollow\"\u003e\n_optimizer.SetRobots(\"noarchive\");               // \u003cmeta name=\"robots\" content=\"noarchive\"\u003e\n```\n\nThe tag is not rendered unless one of these methods is called.\n\n### og:type\n`SetOgType()` accepts a typed object that sets the `og:type` tag and automatically renders the matching type-specific properties:\n\n| Type | og:type | Extra tags rendered |\n|---|---|---|\n| `OgTypeWebsite` | `website` | none |\n| `OgTypeArticle` | `article` | `article:published_time`, `article:modified_time`, `article:author`, `article:section`, `article:tag` |\n| `OgTypeProduct` | `product` | `product:price:amount`, `product:price:currency`, `product:availability` |\n| `OgTypeProfile` | `profile` | `profile:first_name`, `profile:last_name`, `profile:username` |\n\nAll properties on each type are optional. Not rendered unless `SetOgType()` is called. Custom types can be created by extending the `OgType` base class and overriding `RenderTags()`.\n\n### Twitter Card\n`SetTwitterCard()` accepts a `TwitterCard` enum value (`Summary`, `SummaryLargeImage`, `App`, `Player`). Not rendered unless explicitly set.\n\n### robots.txt\n`ServeRobotsTxt()` generates and serves a `robots.txt` at `/robots.txt` (configurable). The default output is:\n\n```\nUser-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml\n```\n\nThe `Sitemap:` line is added automatically if `ServeSitemap()` has been called, and omitted if not. Additional rules are appended after the default block using `AdditionalRules` — each string is one line, empty strings produce blank lines for spacing between blocks. This supports any valid robots.txt syntax including comments, multiple `User-agent` blocks, and `Crawl-delay`.\n\n### Static File Cache Middleware\nThe StaticFileCacheHeaderMiddleware intercepts requests for static assets and applies Cache-Control headers based on your FileExtensions and Paths configuration, ensuring high cache hit ratios. Headers are applied just before the response is flushed, so they take precedence over any headers set by UseStaticFiles.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrcrc%2Ftoodle.pageoptimizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrcrc%2Ftoodle.pageoptimizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrcrc%2Ftoodle.pageoptimizer/lists"}