Kept the prototype out of search engines in the Website service.

This commit is contained in:
javier committed 2026-09-21 17:36:39 +02:00
1 parent 359fe0d6ac
commit 326299fc35
12 files changed
+232 -44

No files matched your search

@@ -64,6 +64,7 @@ func application(
cacheControl: reader.cacheControl,
compressionMinResponseSize: reader.compressionMinResponseSize,
httpsRedirect: reader.httpsRedirect,
isIndexable: reader.isIndexable,
rateLimit: reader.rateLimit,
securityHeaders: reader.securityHeaders,
// An unset origin leaves the pages without canonical URLs and language alternates, rather than
@@ -181,6 +182,7 @@ private func logger(
/// - analytics: the analytics tracker both pages embed, or `nil` to omit it.
/// - cacheControl: the cache-control directives applied to the served static files.
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
/// - isIndexable: whether search engines may index the site; while `false`, every response is marked `noindex`.
/// - httpsRedirect: the origin plain-HTTP requests are redirected to, and whether the forwarded-protocol header is trusted.
/// - rateLimit: the rate limit configuration, currently applied to no route.
/// - securityHeaders: the security headers applied to every response.
@@ -201,6 +203,7 @@ private func router(
cacheControl: CacheControl,
compressionMinResponseSize: Int,
httpsRedirect: HTTPSRedirectMiddleware<AppRequestContext>.Configuration,
isIndexable: Bool,
rateLimit: RateLimitMiddleware<AppRequestContext>.Configuration,
securityHeaders: SecurityHeadersMiddleware<AppRequestContext>.Configuration,
siteOrigin: String?,
@@ -241,6 +244,11 @@ private func router(
await home.refresh(with: .init(from: listings))
}
// Outermost, so the mark reaches redirects, errors, and served files alike.
if !isIndexable {
router.add(middleware: NoIndexMiddleware())
}
router.addMiddleware {
LogRequestsMiddleware(logLevel)
SecurityHeadersMiddleware(
@@ -264,6 +264,16 @@ package extension ConfigReader {
)
}
/// Whether search engines may index the site, read from the `site.indexable` key.
///
/// Off by default: the prototype holds demo data, and a sign-in anyone can use. While off, every response is marked `noindex`.
var isIndexable: Bool {
bool(
forKey: .Site.indexable,
default: false
)
}
/// The origin passkeys are bound to, read from the `passkeys.origin` key.
var passkeysOrigin: String {
string(
@@ -82,6 +82,11 @@ extension AbsoluteConfigKey {
/// The absolute configuration key for the directory the static files are served from.
public static let staticFiles: AbsoluteConfigKey = .init(.Path.staticFiles)
}
/// A namespace for the site configuration keys, as absolute keys.
public enum Site {
/// The absolute configuration key for letting search engines index the site.
public static let indexable: AbsoluteConfigKey = .init(.Site.indexable)
}
/// A namespace for the security headers configuration keys, as absolute keys.
public enum Security {
/// The absolute configuration key for the `Content-Security-Policy` header value.
@@ -107,5 +107,7 @@ extension ConfigKey {
public enum Site {
/// The configuration key for the public origin the site is served at (scheme and host, no trailing slash).
public static let origin: ConfigKey = "site.origin"
/// The configuration key for letting search engines index the site.
public static let indexable: ConfigKey = "site.indexable"
}
}
@@ -0,0 +1,43 @@
import Hummingbird
import Infrastructure
/// Keeps the site out of search engines: every response — pages, files, redirects, errors — is marked `X-Robots-Tag: noindex, nofollow`.
///
/// A header rather than `robots.txt`, which stays open on purpose: a crawler barred from a page never reads its `noindex`, and may still
/// list the bare URL from links elsewhere. A header also covers what carries no markup, such as the photos sellers upload.
public struct NoIndexMiddleware<Context: RequestContext>: RouterMiddleware {
// MARK: Constants
/// The directive stamped on every response.
public static var directive: String { "noindex, nofollow" }
// MARK: Initializers
/// Creates the middleware.
public init() {}
// MARK: Methods
public func handle(
_ request: Request,
context: Context,
next: (Request, Context) async throws -> Response
) async throws -> Response {
do {
var response = try await next(request, context)
response.headers[.robotsTag] = Self.directive
return response
} catch let error as HTTPError {
// An error thrown past this point becomes a response further out: the mark travels with it.
var error = error
error.headers[.robotsTag] = Self.directive
throw error
}
}
}