This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 695d034427 feat(rest-server): convention-endpoints mixin pack — 
Favicon/Seo/Version/WellKnown + buildMixinContext BeanStore-first (FINISHED-76)
695d034427 is described below

commit 695d034427b4bf2baa1e73a2692c2132f1c95d8e
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 17:43:20 2026 -0400

    feat(rest-server): convention-endpoints mixin pack — 
Favicon/Seo/Version/WellKnown + buildMixinContext BeanStore-first (FINISHED-76)
---
 .../java/org/apache/juneau/rest/RestContext.java   |  12 +-
 .../rest/convention/BasicFaviconResource.java      | 246 +++++++++++++
 .../juneau/rest/convention/BasicSeoResource.java   | 321 ++++++++++++++++
 .../rest/convention/BasicVersionResource.java      | 409 +++++++++++++++++++++
 .../rest/convention/BasicWellKnownResource.java    | 181 +++++++++
 .../juneau/rest/convention/package-info.java       |  78 ++++
 .../src/main/resources/juneau-favicon.ico          | Bin 0 -> 1118 bytes
 .../convention/BasicConvention_Builders_Test.java  | 361 ++++++++++++++++++
 .../BasicConvention_OpenApiHidden_Test.java        | 105 ++++++
 .../BasicConvention_ParentChain_Test.java          | 138 +++++++
 .../BasicFaviconResource_AsMixin_Test.java         | 138 +++++++
 .../convention/BasicSeoResource_AsMixin_Test.java  | 127 +++++++
 .../BasicVersionResource_AsMixin_Test.java         | 146 ++++++++
 ...asicVersionResource_JettyMicroservice_Test.java | 129 +++++++
 .../BasicVersionResource_Springboot_Test.java      | 134 +++++++
 .../BasicWellKnownResource_AsMixin_Test.java       |  79 ++++
 ...d => FINISHED-76-mixin-convention-endpoints.md} |  59 ++-
 todo/TODO.md                                       |   4 +-
 18 files changed, 2662 insertions(+), 5 deletions(-)

diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 2b7c4e8850..b19acbcae3 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -1472,9 +1472,19 @@ public class RestContext extends Context {
         * any {@code setContext(RestContext)} method on the mixin instance 
(mirroring the existing
         * {@code RestChildren.buildChildContext(...)} contract).  The host's 
{@link ServletConfig} is propagated
         * so the mixin sees the same servlet container settings.
+        *
+        * <p>
+        * Mixin instance resolution: an importer-supplied {@code @Bean 
<MixinClass>} factory in the host's
+        * bean store wins over reflective instantiation. When the host 
registers
+        * {@code @Bean public MyMixin myMixin() { ... }} (or a Spring {@code 
@Bean} of the same type), that
+        * pre-built instance is used as the mixin resource; otherwise the 
mixin is instantiated via
+        * {@code beanStore.instantiate(mixinClass)} (no-arg / builder / 
injected-constructor path). This lets
+        * convention-endpoint mixins (favicon, SEO, version, well-known) be 
configured by the importer using
+        * their own builder before the mixin walk wires them in.
         */
        private RestContext buildMixinContext(Class<?> mixinClass) throws 
Exception {
-               var mixinResource = beanStore.instantiate(mixinClass);
+               Object preBuilt = beanStore.getBean(mixinClass).orElse(null);
+               final var mixinResource = preBuilt != null ? preBuilt : 
beanStore.instantiate(mixinClass);
                var args = new Args(mixinClass, this, builder.inner, () -> 
mixinResource, "", null, null, null, true);
                var mixinCtx = new RestContext(args);
                var setCtx = ClassInfo.of(mixinResource).getMethod(x -> 
x.hasName("setContext") && x.hasParameterTypes(RestContext.class)).orElse(null);
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicFaviconResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicFaviconResource.java
new file mode 100644
index 0000000000..0afa64bc00
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicFaviconResource.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.http.entity.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.http.resource.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Mixin that serves a {@code favicon.ico} icon at {@code /favicon.ico}.
+ *
+ * <p>
+ * Sibling of {@link BasicSeoResource} (robots.txt / sitemap.xml), {@link 
BasicVersionResource}
+ * ({@code /version} / {@code /info} / {@code /about}), and {@link 
BasicWellKnownResource}
+ * ({@code /.well-known/*}). All four classes live in the {@code 
org.apache.juneau.rest.convention}
+ * convention-endpoints mixin pack.
+ *
+ * <p>
+ * Compose into a host resource via {@link Rest#mixins() 
@Rest(mixins=BasicFaviconResource.class)};
+ * the {@code /favicon.ico} URL becomes available alongside the host's own 
endpoints with no further
+ * wiring. Or extend the class directly for a standalone deployment whose 
mount paths come from the
+ * inherited {@link Rest#paths() @Rest(paths)} default.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <ja>@Rest</ja>(path=<js>"/api"</js>, 
mixins=BasicFaviconResource.<jk>class</jk>)
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet {
+ *             <jc>// Use the default Juneau-branded favicon.</jc>
+ *     }
+ *
+ *     <jc>// Or override the icon bytes via a @Bean factory:</jc>
+ *     <ja>@Bean BasicFaviconResource favicon()</ja> {
+ *             <jk>return</jk> 
BasicFaviconResource.<jsm>create</jsm>().bytes(myLogoBytes).build();
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Behavior:</h5>
+ *
+ * <ul class='spaced-list'>
+ *     <li>{@code GET /favicon.ico} returns the configured icon bytes with
+ *             {@code Content-Type: image/x-icon} and {@code Cache-Control: 
max-age=2592000, public}
+ *             (30 days &mdash; favicons rarely change and browsers re-fetch 
frequently when uncached).
+ *     <li>The default icon ({@code juneau-favicon.ico} on the framework 
classpath) is a
+ *             16&times;16 Juneau-branded ICO; users replace it by registering 
an alternate
+ *             {@code @Bean BasicFaviconResource} whose builder supplies 
different bytes.
+ *     <li>The handler is excluded from generated Swagger / OpenAPI specs via
+ *             {@link OpSwagger#ignore() @OpSwagger(ignore=true)} &mdash; 
favicons are not
+ *             API-meaningful.
+ * </ul>
+ *
+ * <h5 class='section'>Builder API:</h5>
+ *
+ * <ul class='spaced-list'>
+ *     <li>{@link #create() create()} &mdash; entry point for configuring an 
instance.
+ *     <li>{@link Builder#bytes(byte[]) bytes(byte[])} &mdash; raw favicon 
bytes.
+ *     <li>{@link Builder#classpath(String) classpath(String)} &mdash; load 
icon bytes from a
+ *             classpath resource (resolved against the {@code 
BasicFaviconResource} classloader).
+ *     <li>{@link Builder#cacheControl(String) cacheControl(String)} &mdash; 
override the default
+ *             30-day {@code Cache-Control} header.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link BasicSeoResource}
+ *     <li class='jc'>{@link BasicVersionResource}
+ *     <li class='jc'>{@link BasicWellKnownResource}
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
&mdash; Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+// @formatter:off
+@Rest(paths={"/favicon.ico"})
+public class BasicFaviconResource {
+
+       /** Default {@code Cache-Control} header value: {@code max-age=2592000, 
public} (30 days). */
+       public static final String DEFAULT_CACHE_CONTROL = "max-age=2592000, 
public";
+
+       /** Classpath location of the framework-shipped default favicon. */
+       private static final String DEFAULT_FAVICON_RESOURCE = 
"/juneau-favicon.ico";
+
+       /**
+        * Creates a new builder for configuring a {@link BasicFaviconResource}.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       private final byte[] bytes;
+       private final String cacheControl;
+
+       /**
+        * No-arg constructor &mdash; used when a host registers the mixin 
without supplying a
+        * builder-configured {@code @Bean BasicFaviconResource}. Loads the 
default Juneau-branded
+        * favicon from the classpath.
+        */
+       public BasicFaviconResource() {
+               this(create());
+       }
+
+       /**
+        * Builder constructor.
+        *
+        * @param builder The builder.
+        */
+       protected BasicFaviconResource(Builder builder) {
+               bytes = builder.resolveBytes();
+               cacheControl = builder.cacheControl;
+       }
+
+       /**
+        * [GET /favicon.ico] &mdash; serve the configured favicon bytes.
+        *
+        * @return The favicon as an {@link HttpResource} with proper headers.
+        */
+       @RestGet(
+               path="/favicon.ico",
+               summary="Favorites icon",
+               description="Browser favorites icon (favicon.ico).",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public HttpResource getFavicon() {
+               var hdrs = new ArrayList<HttpHeader>();
+               hdrs.add(ContentType.of("image/x-icon"));
+               hdrs.add(CacheControl.of(cacheControl));
+               return HttpResourceBean.of(ByteArrayBody.of(bytes, 
"image/x-icon"), hdrs);
+       }
+
+       /**
+        * Builder for {@link BasicFaviconResource} instances.
+        */
+       public static class Builder {
+
+               private byte[] bytes;
+               private String classpath;
+               private String cacheControl = DEFAULT_CACHE_CONTROL;
+
+               /** Constructor &mdash; package access for {@link 
BasicFaviconResource#create()}. */
+               protected Builder() {}
+
+               /**
+                * Sets the raw favicon bytes.
+                *
+                * <p>
+                * Mutually exclusive with {@link #classpath(String)} &mdash; 
whichever is set last wins
+                * at {@link #build()} time.
+                *
+                * @param value The favicon bytes (typically an {@code .ico} or 
{@code .png} payload).
+                *      Must not be <jk>null</jk>.
+                * @return This object.
+                */
+               public Builder bytes(byte[] value) {
+                       bytes = value;
+                       classpath = null;
+                       return this;
+               }
+
+               /**
+                * Sets the classpath resource path from which to load the 
favicon bytes.
+                *
+                * <p>
+                * Resolved via {@link Class#getResourceAsStream(String) 
BasicFaviconResource.class.getResourceAsStream(...)}
+                * at {@link #build()} time. A resolved-to-{@code null} stream 
falls back to the framework's
+                * default favicon. Mutually exclusive with {@link 
#bytes(byte[])}.
+                *
+                * @param value The classpath resource path (e.g. {@code 
"/myapp/icon.ico"}).
+                *      Must not be <jk>null</jk> or blank.
+                * @return This object.
+                */
+               public Builder classpath(String value) {
+                       classpath = value;
+                       bytes = null;
+                       return this;
+               }
+
+               /**
+                * Sets the {@code Cache-Control} header value emitted with the 
favicon response.
+                *
+                * <p>
+                * Defaults to {@value #DEFAULT_CACHE_CONTROL} (30 days).
+                *
+                * @param value The new {@code Cache-Control} value. Must not 
be <jk>null</jk> or blank.
+                * @return This object.
+                */
+               public Builder cacheControl(String value) {
+                       cacheControl = value;
+                       return this;
+               }
+
+               /**
+                * Builds a {@link BasicFaviconResource} instance.
+                *
+                * @return A configured instance.
+                */
+               public BasicFaviconResource build() {
+                       return new BasicFaviconResource(this);
+               }
+
+               byte[] resolveBytes() {
+                       if (bytes != null)
+                               return bytes;
+                       if (classpath != null) {
+                               var resolved = readClasspath(classpath);
+                               if (resolved != null)
+                                       return resolved;
+                       }
+                       // Falls through to the framework's default-shipping 
ICO; the resource is shipped in
+                       // the same jar as this class so it must be present at 
runtime.
+                       return readClasspath(DEFAULT_FAVICON_RESOURCE);
+               }
+
+               private static byte[] readClasspath(String path) {
+                       try (var in = 
BasicFaviconResource.class.getResourceAsStream(path)) {
+                               if (in == null)
+                                       return null;
+                               return in.readAllBytes();
+                       } catch (IOException e) {
+                               // readAllBytes on a classpath resource is 
effectively unreachable; the catch is
+                               // here only to satisfy the checked exception 
contract.
+                               throw new InternalServerError(e);
+                       }
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicSeoResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicSeoResource.java
new file mode 100644
index 0000000000..0d5ead0b59
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicSeoResource.java
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import java.io.*;
+import java.time.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Mixin that serves SEO-conventional endpoints {@code /robots.txt} and {@code 
/sitemap.xml}.
+ *
+ * <p>
+ * Sibling of {@link BasicFaviconResource} ({@code /favicon.ico}),
+ * {@link BasicVersionResource} ({@code /version} / {@code /info} / {@code 
/about}), and
+ * {@link BasicWellKnownResource} ({@code /.well-known/*}). All four classes 
live in the
+ * {@code org.apache.juneau.rest.convention} convention-endpoints mixin pack.
+ *
+ * <p>
+ * Compose into a host resource via {@link Rest#mixins() 
@Rest(mixins=BasicSeoResource.class)};
+ * the {@code /robots.txt} and {@code /sitemap.xml} URLs become available 
alongside the host's own
+ * endpoints with no further wiring. Or extend the class directly for a 
standalone deployment whose
+ * mount paths come from the inherited {@link Rest#paths() @Rest(paths)} 
default.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <ja>@Rest</ja>(path=<js>"/api"</js>, 
mixins=BasicSeoResource.<jk>class</jk>)
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { }
+ *
+ *     <jc>// Override defaults via a @Bean factory:</jc>
+ *     <ja>@Bean</ja> BasicSeoResource seo() {
+ *             <jk>return</jk> BasicSeoResource.<jsm>create</jsm>()
+ *                     .robotsAllow(<js>"*"</js>, <js>"/"</js>)
+ *                     .sitemapEntry(<js>"/api/items"</js>)
+ *                     .build();
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Defaults &amp; behavior:</h5>
+ *
+ * <ul class='spaced-list'>
+ *     <li><b>Robots policy &mdash; deny-all by default.</b> {@code GET 
/robots.txt} returns
+ *             {@code "User-agent: *\nDisallow: /\n"} unless the builder 
specifies a different policy via
+ *             {@link Builder#robotsAllow(String,String...) robotsAllow(...)} 
or
+ *             {@link Builder#robotsDisallow(String,String...) 
robotsDisallow(...)}. The deny-all default
+ *             is intentional &mdash; mounting the mixin without thinking 
should not auto-opt the service
+ *             into search-engine indexing.
+ *     <li><b>Sitemap empty by default.</b> {@code GET /sitemap.xml} returns 
an empty
+ *             {@code <urlset>} when no entries are configured. Add entries via
+ *             {@link Builder#sitemapEntry(String) sitemapEntry(String)} or
+ *             {@link Builder#sitemapEntry(String,ZonedDateTime,String,Double) 
sitemapEntry(String,ZonedDateTime,String,Double)}.
+ *     <li><b>Content types.</b> {@code /robots.txt} returns {@code 
text/plain; charset=UTF-8};
+ *             {@code /sitemap.xml} returns {@code application/xml; 
charset=UTF-8}.
+ *     <li>Both endpoints are excluded from generated Swagger / OpenAPI specs 
via
+ *             {@link OpSwagger#ignore() @OpSwagger(ignore=true)}.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link BasicFaviconResource}
+ *     <li class='jc'>{@link BasicVersionResource}
+ *     <li class='jc'>{@link BasicWellKnownResource}
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc9309";>RFC 9309 &mdash; Robots Exclusion 
Protocol</a>
+ *     <li class='link'><a class="doclink" 
href="https://www.sitemaps.org/protocol.html";>sitemaps.org &mdash; Sitemap 
protocol</a>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
&mdash; Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+// @formatter:off
+@Rest(paths={"/robots.txt","/sitemap.xml"})
+public class BasicSeoResource {
+
+       /** Default robots policy: deny everything. */
+       public static final String DEFAULT_ROBOTS = "User-agent: *\nDisallow: 
/\n";
+
+       /**
+        * Creates a new builder.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       private final String robotsTxt;
+       private final List<SitemapEntry> sitemapEntries;
+
+       /** No-arg constructor used when a host registers the mixin without 
builder configuration. */
+       public BasicSeoResource() {
+               this(create());
+       }
+
+       /**
+        * Builder constructor.
+        *
+        * @param builder The builder.
+        */
+       protected BasicSeoResource(Builder builder) {
+               robotsTxt = builder.buildRobots();
+               sitemapEntries = List.copyOf(builder.sitemapEntries);
+       }
+
+       /**
+        * [GET /robots.txt] &mdash; emit the configured robots policy as 
{@code text/plain}.
+        *
+        * @param res The current REST response.
+        * @throws IOException If an I/O error occurs while writing the 
response.
+        */
+       @RestGet(
+               path="/robots.txt",
+               summary="Robots policy",
+               description="Robots Exclusion Protocol policy file (RFC 9309).",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public void getRobotsTxt(RestResponse res) throws IOException {
+               try (var w = res.getDirectWriter("text/plain; charset=UTF-8")) {
+                       w.write(robotsTxt);
+               }
+       }
+
+       /**
+        * [GET /sitemap.xml] &mdash; emit the configured sitemap as {@code 
application/xml}.
+        *
+        * @param res The current REST response.
+        * @throws IOException If an I/O error occurs while writing the 
response.
+        */
+       @RestGet(
+               path="/sitemap.xml",
+               summary="Sitemap",
+               description="XML sitemap of indexable URLs (sitemaps.org 
protocol).",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public void getSitemap(RestResponse res) throws IOException {
+               try (var w = res.getDirectWriter("application/xml; 
charset=UTF-8")) {
+                       w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+                       w.write("<urlset 
xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\";>\n");
+                       for (var e : sitemapEntries)
+                               e.write(w);
+                       w.write("</urlset>\n");
+               }
+       }
+
+       /**
+        * Single sitemap entry (immutable). Composed of a required {@code loc} 
URL and optional
+        * {@code lastmod}, {@code changefreq}, and {@code priority} elements 
per the
+        * <a href="https://www.sitemaps.org/protocol.html";>sitemaps.org 
protocol</a>.
+        */
+       public static final class SitemapEntry {
+               private final String loc;
+               private final ZonedDateTime lastmod;
+               private final String changefreq;
+               private final Double priority;
+
+               SitemapEntry(String loc, ZonedDateTime lastmod, String 
changefreq, Double priority) {
+                       this.loc = loc;
+                       this.lastmod = lastmod;
+                       this.changefreq = changefreq;
+                       this.priority = priority;
+               }
+
+               void write(java.io.Writer w) throws IOException {
+                       w.write("\t<url>\n");
+                       w.write("\t\t<loc>" + xmlEscape(loc) + "</loc>\n");
+                       if (lastmod != null)
+                               w.write("\t\t<lastmod>" + 
lastmod.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + "</lastmod>\n");
+                       if (changefreq != null)
+                               w.write("\t\t<changefreq>" + 
xmlEscape(changefreq) + "</changefreq>\n");
+                       if (priority != null)
+                               w.write("\t\t<priority>" + priority + 
"</priority>\n");
+                       w.write("\t</url>\n");
+               }
+       }
+
+       private static String xmlEscape(String s) {
+               return s.replace("&", "&amp;").replace("<", 
"&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&apos;");
+       }
+
+       /**
+        * Builder for {@link BasicSeoResource} instances.
+        */
+       public static class Builder {
+
+               private final List<RobotsRule> robotsRules = new ArrayList<>();
+               private final List<SitemapEntry> sitemapEntries = new 
ArrayList<>();
+               private String customRobotsTxt;
+
+               /** Constructor &mdash; package access for {@link 
BasicSeoResource#create()}. */
+               protected Builder() {}
+
+               /**
+                * Adds an {@code Allow} rule to the robots policy.
+                *
+                * <p>
+                * Multiple invocations append rules in order. The first call 
to either
+                * {@link #robotsAllow(String,String...) robotsAllow(...)} or
+                * {@link #robotsDisallow(String,String...) 
robotsDisallow(...)} replaces the deny-all
+                * default; subsequent calls continue to append.
+                *
+                * @param userAgent The user agent (e.g. {@code "*"} or {@code 
"Googlebot"}).
+                *      Must not be <jk>null</jk> or blank.
+                * @param paths Allowed path prefixes (e.g. {@code "/"}). At 
least one path is required.
+                * @return This object.
+                */
+               public Builder robotsAllow(String userAgent, String...paths) {
+                       robotsRules.add(new RobotsRule(userAgent, true, paths));
+                       return this;
+               }
+
+               /**
+                * Adds a {@code Disallow} rule to the robots policy.
+                *
+                * @param userAgent The user agent (e.g. {@code "*"}). Must not 
be <jk>null</jk> or blank.
+                * @param paths Disallowed path prefixes. At least one path is 
required.
+                * @return This object.
+                */
+               public Builder robotsDisallow(String userAgent, String...paths) 
{
+                       robotsRules.add(new RobotsRule(userAgent, false, 
paths));
+                       return this;
+               }
+
+               /**
+                * Sets a fully-formed {@code robots.txt} body, overriding any 
rule-builder calls.
+                *
+                * <p>
+                * Useful when the desired policy doesn't fit the simple
+                * {@code allow}/{@code disallow}/per-user-agent shape of
+                * {@link #robotsAllow(String,String...) robotsAllow(...)} /
+                * {@link #robotsDisallow(String,String...) 
robotsDisallow(...)} (e.g. when including
+                * {@code Sitemap:} or {@code Crawl-delay:} directives).
+                *
+                * @param value The full {@code robots.txt} body. Must not be 
<jk>null</jk>.
+                * @return This object.
+                */
+               public Builder robotsTxt(String value) {
+                       customRobotsTxt = value;
+                       return this;
+               }
+
+               /**
+                * Adds a sitemap entry with only the {@code loc} URL.
+                *
+                * @param url The fully-qualified URL.
+                * @return This object.
+                */
+               public Builder sitemapEntry(String url) {
+                       return sitemapEntry(url, null, null, null);
+               }
+
+               /**
+                * Adds a sitemap entry with optional {@code lastmod}, {@code 
changefreq}, and
+                * {@code priority} components.
+                *
+                * @param url The fully-qualified URL.
+                * @param lastmod The last-modified instant (or <jk>null</jk> 
to omit).
+                * @param changefreq Change-frequency hint (e.g. {@code 
"weekly"}, {@code "daily"}; or
+                *      <jk>null</jk> to omit).
+                * @param priority Priority value 0.0..1.0 (or <jk>null</jk> to 
omit).
+                * @return This object.
+                */
+               public Builder sitemapEntry(String url, ZonedDateTime lastmod, 
String changefreq, Double priority) {
+                       sitemapEntries.add(new SitemapEntry(url, lastmod, 
changefreq, priority));
+                       return this;
+               }
+
+               /**
+                * Builds a {@link BasicSeoResource} instance.
+                *
+                * @return A configured instance.
+                */
+               public BasicSeoResource build() {
+                       return new BasicSeoResource(this);
+               }
+
+               String buildRobots() {
+                       if (customRobotsTxt != null)
+                               return customRobotsTxt;
+                       if (robotsRules.isEmpty())
+                               return DEFAULT_ROBOTS;
+                       var sb = new StringBuilder();
+                       for (var r : robotsRules) {
+                               sb.append("User-agent: 
").append(r.userAgent).append('\n');
+                               for (var p : r.paths)
+                                       sb.append(r.allow ? "Allow: " : 
"Disallow: ").append(p).append('\n');
+                       }
+                       return sb.toString();
+               }
+       }
+
+       /** Internal representation of a single user-agent + allow/disallow + 
paths rule. */
+       private static final class RobotsRule {
+               final String userAgent;
+               final boolean allow;
+               final String[] paths;
+
+               RobotsRule(String userAgent, boolean allow, String[] paths) {
+                       this.userAgent = userAgent;
+                       this.allow = allow;
+                       // paths arrives via varargs from 
robotsAllow/robotsDisallow → never null in practice;
+                       // clone to insulate downstream mutation.
+                       this.paths = paths.clone();
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicVersionResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicVersionResource.java
new file mode 100644
index 0000000000..77f87bd5fd
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicVersionResource.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.jar.*;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Mixin that serves deployment-introspection metadata at {@code /version}, 
{@code /info}, and
+ * {@code /about}.
+ *
+ * <p>
+ * Sibling of {@link BasicFaviconResource}, {@link BasicSeoResource}, and
+ * {@link BasicWellKnownResource}. All four classes live in the
+ * {@code org.apache.juneau.rest.convention} convention-endpoints mixin pack.
+ *
+ * <p>
+ * Compose into a host resource via
+ * {@link Rest#mixins() @Rest(mixins=BasicVersionResource.class)}; the three 
URLs become available
+ * alongside the host's own endpoints with no further wiring. Or extend the 
class directly for a
+ * standalone deployment whose mount paths come from the inherited
+ * {@link Rest#paths() @Rest(paths)} default.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <ja>@Rest</ja>(path=<js>"/api"</js>, 
mixins=BasicVersionResource.<jk>class</jk>)
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { }
+ *
+ *     <jc>// Default behavior reads MANIFEST.MF + git.properties from the 
importer's classpath.</jc>
+ *     <jc>// Override programmatically via a @Bean factory:</jc>
+ *     <ja>@Bean</ja> BasicVersionResource version() {
+ *             <jk>return</jk> BasicVersionResource.<jsm>create</jsm>()
+ *                     .entry(<js>"name"</js>, <js>"my-app"</js>)
+ *                     .entry(<js>"version"</js>, <js>"1.2.3"</js>)
+ *                     .build();
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Default lookup chain:</h5>
+ *
+ * <p>
+ * The default {@link Builder#fromManifest() fromManifest()} reader walks the
+ * {@link BasicVersionResource} classloader for {@code /META-INF/MANIFEST.MF} 
and reads the standard
+ * {@code Implementation-Title}, {@code Implementation-Version}, {@code 
Implementation-Vendor}, and
+ * {@code Build-Jdk} attributes (lowercased to {@code name}, {@code version}, 
{@code vendor},
+ * {@code javaVersion}). The default {@link Builder#fromGitProperties() 
fromGitProperties()} reader
+ * walks the same classloader for {@code /git.properties} (the canonical
+ * {@code git-commit-id-maven-plugin} output) and surfaces {@code 
git.commit.id},
+ * {@code git.branch}, and {@code git.build.time} as {@code gitCommit}, {@code 
gitBranch}, and
+ * {@code buildTime}. Missing files / missing keys map to {@code "(unknown)"} 
or are simply omitted
+ * &mdash; never an exception.
+ *
+ * <p>
+ * <b>Spring Boot fat-jar caveat:</b> Spring Boot rewrites {@code MANIFEST.MF} 
during repackaging.
+ * The mixin's no-arg-default reader uses {@code 
BasicVersionResource.class.getClassLoader()};
+ * users who want the importer's app manifest under a Spring Boot executable 
jar should register an
+ * explicit {@link Builder#fromManifest(ClassLoader) 
fromManifest(importerClassLoader)} call so the
+ * lookup starts from the importer's classloader rather than the framework's.
+ *
+ * <h5 class='section'>Output:</h5>
+ *
+ * <p>
+ * All three URLs return the same JSON map &mdash; e.g.:
+ * <p class='bjson'>
+ *     {
+ *             <jok>"name"</jok>: <jov>"my-app"</jov>,
+ *             <jok>"version"</jok>: <jov>"1.2.3"</jov>,
+ *             <jok>"gitCommit"</jok>: <jov>"abc123"</jov>,
+ *             <jok>"gitBranch"</jok>: <jov>"main"</jov>,
+ *             <jok>"buildTime"</jok>: <jov>"2026-05-24T18:00:00Z"</jov>,
+ *             <jok>"javaVersion"</jok>: <jov>"21"</jov>
+ *     }
+ * </p>
+ *
+ * <p>
+ * Endpoints are excluded from generated Swagger / OpenAPI specs via
+ * {@link OpSwagger#ignore() @OpSwagger(ignore=true)}.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link BasicFaviconResource}
+ *     <li class='jc'>{@link BasicSeoResource}
+ *     <li class='jc'>{@link BasicWellKnownResource}
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
&mdash; Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+// @formatter:off
+@Rest(paths={"/version","/info","/about"})
+public class BasicVersionResource {
+
+       /** Sentinel value returned for entries that the mixin couldn't 
resolve. */
+       public static final String UNKNOWN = "(unknown)";
+
+       /**
+        * Creates a new builder.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       private final Map<String,String> info;
+
+       /**
+        * No-arg constructor &mdash; reads {@code MANIFEST.MF}, {@code 
git.properties}, and the JVM
+        * version from the framework classloader. Equivalent to
+        * {@code create().build()}; the builder applies the same defaults 
whether instantiated
+        * directly or via the {@link 
org.apache.juneau.commons.inject.BeanInstantiator BeanInstantiator}
+        * builder-detection path.
+        */
+       public BasicVersionResource() {
+               this(create());
+       }
+
+       /**
+        * Builder constructor.
+        *
+        * <p>
+        * Applies the default lookup chain ({@link Builder#fromManifest()} +
+        * {@link Builder#fromGitProperties()} + {@link 
Builder#fromJavaVersion()}) when no other
+        * builder method has been called &mdash; ensuring that a 
freshly-created builder
+        * (whether instantiated directly via {@code new 
BasicVersionResource(create())} or via the
+        * {@link org.apache.juneau.commons.inject.BeanInstantiator 
BeanInstantiator}'s
+        * builder-detection path) always produces a non-empty payload.
+        *
+        * @param builder The builder.
+        */
+       protected BasicVersionResource(Builder builder) {
+               if (! builder.explicit)
+                       
builder.fromManifest().fromGitProperties().fromJavaVersion();
+               info = Collections.unmodifiableMap(new 
LinkedHashMap<>(builder.entries));
+       }
+
+       /**
+        * [GET /version | /info | /about] &mdash; emit the assembled metadata 
as a JSON map.
+        *
+        * <p>
+        * Format-pinned to {@code application/json} per the v1 resolved 
decision &mdash; bypasses
+        * the host's content negotiation so the endpoint serves JSON even on a 
vanilla
+        * {@code RestServlet} host that hasn't wired up JSON serializers 
explicitly.
+        *
+        * @param res The current REST response.
+        * @throws IOException If an I/O error occurs while writing the 
response.
+        */
+       @RestGet(
+               path={"/version","/info","/about"},
+               summary="Version / build metadata",
+               description="Deployment-introspection metadata (name, version, 
git, build).",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public void getInfo(RestResponse res) throws IOException {
+               try (var w = res.getDirectWriter("application/json")) {
+                       JsonSerializer.DEFAULT_READABLE.serialize(info, w);
+               }
+       }
+
+       /**
+        * Returns the configured info map (test/inspection helper).
+        *
+        * @return The info map.
+        */
+       public Map<String,String> getInfoMap() {
+               return info;
+       }
+
+       /**
+        * Builder for {@link BasicVersionResource} instances.
+        */
+       public static class Builder {
+
+               private final Map<String,String> entries = new 
LinkedHashMap<>();
+               private boolean explicit;
+
+               /** Constructor &mdash; package access for {@link 
BasicVersionResource#create()}. */
+               protected Builder() {}
+
+               /**
+                * Sets a single entry, overwriting any prior value for that 
key.
+                *
+                * @param key The entry key. Must not be <jk>null</jk> or blank.
+                * @param value The entry value (a <jk>null</jk> value is 
recorded as
+                *      {@link BasicVersionResource#UNKNOWN}).
+                * @return This object.
+                */
+               public Builder entry(String key, String value) {
+                       entries.put(key, value == null ? UNKNOWN : value);
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Bulk-set entries from a map.
+                *
+                * @param values Entries to add. <jk>null</jk> values are 
recorded as
+                *      {@link BasicVersionResource#UNKNOWN}.
+                * @return This object.
+                */
+               public Builder entries(Map<String,String> values) {
+                       if (values != null)
+                               values.forEach(this::entry);
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Reads {@code Implementation-Title}, {@code 
Implementation-Version},
+                * {@code Implementation-Vendor}, and {@code Build-Jdk} from
+                * {@code /META-INF/MANIFEST.MF} on the {@link 
BasicVersionResource} classloader.
+                *
+                * <p>
+                * Missing keys are recorded as {@link 
BasicVersionResource#UNKNOWN}. A missing
+                * {@code MANIFEST.MF} resource is silently skipped.
+                *
+                * @return This object.
+                */
+               public Builder fromManifest() {
+                       return 
fromManifest(BasicVersionResource.class.getClassLoader());
+               }
+
+               /**
+                * Reads manifest attributes from {@code /META-INF/MANIFEST.MF} 
on the supplied classloader.
+                *
+                * <p>
+                * Useful under Spring Boot fat jars, where the importer's app 
manifest is reachable from
+                * its own classloader but not the framework's. Missing keys 
are recorded as
+                * {@link BasicVersionResource#UNKNOWN}; a missing {@code 
MANIFEST.MF} resource is silently
+                * skipped.
+                *
+                * @param classLoader The classloader to walk.
+                * @return This object.
+                */
+               public Builder fromManifest(ClassLoader classLoader) {
+                       var attrs = readManifestAttributes(classLoader);
+                       ifNotEmpty(attrs, "Implementation-Title", v -> 
entry("name", v));
+                       ifNotEmpty(attrs, "Implementation-Version", v -> 
entry("version", v));
+                       ifNotEmpty(attrs, "Implementation-Vendor", v -> 
entry("vendor", v));
+                       ifNotEmpty(attrs, "Build-Jdk", v -> 
entry("javaVersion", v));
+                       entries.putIfAbsent("name", UNKNOWN);
+                       entries.putIfAbsent("version", UNKNOWN);
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Reads a {@link Manifest} that the importer registered as a 
bean (e.g.
+                * {@code @Bean Manifest appManifest()}).
+                *
+                * <p>
+                * Convenience for callers that already loaded the manifest via 
Spring Boot's
+                * {@code BuildProperties} autoconfiguration or by hand.
+                *
+                * @param manifest The manifest to read. Must not be 
<jk>null</jk>.
+                * @return This object.
+                */
+               public Builder fromManifest(Manifest manifest) {
+                       var main = manifest.getMainAttributes();
+                       var attrs = new HashMap<String,String>();
+                       for (var k : main.keySet())
+                               attrs.put(k.toString(), 
String.valueOf(main.get(k)));
+                       ifNotEmpty(attrs, "Implementation-Title", v -> 
entry("name", v));
+                       ifNotEmpty(attrs, "Implementation-Version", v -> 
entry("version", v));
+                       ifNotEmpty(attrs, "Implementation-Vendor", v -> 
entry("vendor", v));
+                       ifNotEmpty(attrs, "Build-Jdk", v -> 
entry("javaVersion", v));
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Reads {@code git.commit.id}, {@code git.branch}, and {@code 
git.build.time} from
+                * {@code /git.properties} on the {@link BasicVersionResource} 
classloader.
+                *
+                * <p>
+                * Output of the
+                * <a 
href="https://github.com/git-commit-id/git-commit-id-maven-plugin";>git-commit-id-maven-plugin</a>;
+                * other values written to the same file are also surfaced 
(each {@code git.foo.bar} key is
+                * remapped to {@code gitFooBar}).
+                *
+                * @return This object.
+                */
+               public Builder fromGitProperties() {
+                       return 
fromGitProperties(BasicVersionResource.class.getClassLoader());
+               }
+
+               /**
+                * Reads {@code git.properties} from the supplied classloader.
+                *
+                * @param classLoader The classloader to walk.
+                * @return This object.
+                */
+               public Builder fromGitProperties(ClassLoader classLoader) {
+                       var props = readProperties(classLoader, 
"git.properties");
+                       if (props == null)
+                               return this;
+                       ifNotEmptyValue(props.getProperty("git.commit.id"), v 
-> entry("gitCommit", v));
+                       
ifNotEmptyValue(props.getProperty("git.commit.id.abbrev"), v -> 
entries.putIfAbsent("gitCommit", v));
+                       ifNotEmptyValue(props.getProperty("git.branch"), v -> 
entry("gitBranch", v));
+                       ifNotEmptyValue(props.getProperty("git.build.time"), v 
-> entry("buildTime", v));
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Adds the running JVM's {@code java.version} system property 
as the {@code javaVersion}
+                * entry, unless an earlier {@link #fromManifest() 
fromManifest(...)} call already supplied
+                * one from {@code Build-Jdk}.
+                *
+                * @return This object.
+                */
+               public Builder fromJavaVersion() {
+                       entries.putIfAbsent("javaVersion", 
System.getProperty("java.version", UNKNOWN));
+                       explicit = true;
+                       return this;
+               }
+
+               /**
+                * Builds a {@link BasicVersionResource} instance.
+                *
+                * <p>
+                * The constructor applies the default lookup chain ({@link 
#fromManifest()} +
+                * {@link #fromGitProperties()} + {@link #fromJavaVersion()}) 
when no builder method has
+                * been called &mdash; so a freshly-created builder always 
produces a non-empty payload
+                * regardless of which entry point is used.
+                *
+                * @return A configured instance.
+                */
+               public BasicVersionResource build() {
+                       return new BasicVersionResource(this);
+               }
+
+               private static Map<String,String> 
readManifestAttributes(ClassLoader cl) {
+                       Manifest fallback = null;
+                       try {
+                               var resources = 
cl.getResources("META-INF/MANIFEST.MF");
+                               while (resources.hasMoreElements()) {
+                                       var u = resources.nextElement();
+                                       try (var in = u.openStream()) {
+                                               var m = new Manifest(in);
+                                               // Prefer an 
Implementation-Title-bearing manifest; otherwise hold the
+                                               // first one found as a 
fallback. Walking each candidate is cheap (one
+                                               // open per manifest in the 
resources enumeration).
+                                               if 
(m.getMainAttributes().getValue("Implementation-Title") != null)
+                                                       return toMap(m);
+                                               if (fallback == null)
+                                                       fallback = m;
+                                       }
+                               }
+                       } catch (IOException e) {
+                               // Best-effort path: a malformed classpath 
manifest yields an empty map rather
+                               // than bubbling up an exception during startup.
+                               return Map.of();
+                       }
+                       return fallback == null ? Map.of() : toMap(fallback);
+               }
+
+               private static Map<String,String> toMap(Manifest m) {
+                       var attrs = new HashMap<String,String>();
+                       for (var k : m.getMainAttributes().keySet())
+                               attrs.put(k.toString(), 
String.valueOf(m.getMainAttributes().get(k)));
+                       return attrs;
+               }
+
+               private static Properties readProperties(ClassLoader cl, String 
path) {
+                       try (var in = cl.getResourceAsStream(path)) {
+                               if (in == null)
+                                       return null;
+                               var p = new Properties();
+                               p.load(in);
+                               return p;
+                       } catch (IOException e) {
+                               return null;
+                       }
+               }
+
+               private static void ifNotEmpty(Map<String,String> attrs, String 
key, Consumer<String> sink) {
+                       var v = attrs.get(key);
+                       if (v != null && !v.isEmpty())
+                               sink.accept(v);
+               }
+
+               private static void ifNotEmptyValue(String v, Consumer<String> 
sink) {
+                       if (v != null && !v.isEmpty())
+                               sink.accept(v);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicWellKnownResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicWellKnownResource.java
new file mode 100644
index 0000000000..a4cdf744fb
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicWellKnownResource.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import java.io.*;
+
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Mixin that serves <a href="https://www.rfc-editor.org/rfc/rfc8615";>RFC 
8615</a>
+ * {@code /.well-known/*} discovery endpoints.
+ *
+ * <p>
+ * Sibling of {@link BasicFaviconResource}, {@link BasicSeoResource}, and
+ * {@link BasicVersionResource}. All four classes live in the
+ * {@code org.apache.juneau.rest.convention} convention-endpoints mixin pack.
+ *
+ * <p>
+ * In v1 the mixin mounts a single literal path &mdash; {@code 
/.well-known/security.txt} per
+ * <a href="https://www.rfc-editor.org/rfc/rfc9116";>RFC 9116</a>. The class is 
structured so that
+ * future entries (for example {@code /.well-known/openid-configuration} from 
a separate AuthN
+ * mixin pack) can be added without touching this source: each future entry 
ships its own mixin
+ * class with its own {@link Rest#paths() @Rest(paths)} declaration.
+ *
+ * <p>
+ * Compose into a host resource via
+ * {@link Rest#mixins() @Rest(mixins=BasicWellKnownResource.class)}; the
+ * {@code /.well-known/security.txt} URL becomes available alongside the 
host's own endpoints with
+ * no further wiring. Or extend the class directly for a standalone deployment 
whose mount paths
+ * come from the inherited {@link Rest#paths() @Rest(paths)} default.
+ *
+ * <h5 class='figure'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <ja>@Rest</ja>(path=<js>"/api"</js>, 
mixins=BasicWellKnownResource.<jk>class</jk>)
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { }
+ *
+ *     <jc>// Configure the security.txt body via a @Bean factory:</jc>
+ *     <ja>@Bean</ja> BasicWellKnownResource wellKnown() {
+ *             <jk>return</jk> BasicWellKnownResource.<jsm>create</jsm>()
+ *                     .securityTxt(<js>"Contact: 
[email protected]\nExpires: 2027-01-01T00:00:00Z\n"</js>)
+ *                     .build();
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Defaults &amp; behavior:</h5>
+ *
+ * <ul class='spaced-list'>
+ *     <li><b>No default body.</b> Per RFC 9116, {@code security.txt}'s mere 
presence is meaningful;
+ *             shipping a placeholder body would be misleading. If the builder 
is not given a body via
+ *             {@link Builder#securityTxt(String) securityTxt(String)}, the 
endpoint returns
+ *             {@code 404 Not Found} (i.e. "we don't have one"). Loud 
documentation in topic page.
+ *     <li><b>Content-Type.</b> {@code text/plain; charset=UTF-8} when a body 
is configured.
+ *     <li><b>Excluded from generated Swagger / OpenAPI specs</b> via
+ *             {@link OpSwagger#ignore() @OpSwagger(ignore=true)}.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link BasicFaviconResource}
+ *     <li class='jc'>{@link BasicSeoResource}
+ *     <li class='jc'>{@link BasicVersionResource}
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc8615";>RFC 8615 &mdash; Well-Known 
URIs</a>
+ *     <li class='link'><a class="doclink" 
href="https://www.rfc-editor.org/rfc/rfc9116";>RFC 9116 &mdash; security.txt</a>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
&mdash; Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+// @formatter:off
+@Rest(paths={"/.well-known/security.txt"})
+public class BasicWellKnownResource {
+
+       /**
+        * Creates a new builder.
+        *
+        * @return A new builder.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       private final String securityTxt;
+
+       /** No-arg constructor &mdash; results in a 404 on {@code 
/.well-known/security.txt}. */
+       public BasicWellKnownResource() {
+               this(create());
+       }
+
+       /**
+        * Builder constructor.
+        *
+        * @param builder The builder.
+        */
+       protected BasicWellKnownResource(Builder builder) {
+               securityTxt = builder.securityTxt;
+       }
+
+       /**
+        * [GET /.well-known/security.txt] &mdash; emit the configured RFC 9116 
body, or 404 when
+        * unset.
+        *
+        * @param res The current REST response.
+        * @throws IOException If an I/O error occurs while writing the 
response.
+        * @throws NotFound If no {@code security.txt} body is configured.
+        */
+       @RestGet(
+               path="/.well-known/security.txt",
+               summary="security.txt",
+               description="RFC 9116 security.txt — disclosure / contact info 
for the operator.",
+               swagger=@OpSwagger(ignore=true)
+       )
+       public void getSecurityTxt(RestResponse res) throws IOException {
+               if (securityTxt == null)
+                       throw new NotFound();
+               try (var w = res.getDirectWriter("text/plain; charset=UTF-8")) {
+                       w.write(securityTxt);
+               }
+       }
+
+       /**
+        * Returns the configured {@code security.txt} body (test/inspection 
helper).
+        *
+        * @return The body, or <jk>null</jk> if unset.
+        */
+       public String getSecurityTxtBody() {
+               return securityTxt;
+       }
+
+       /**
+        * Builder for {@link BasicWellKnownResource} instances.
+        */
+       public static class Builder {
+
+               private String securityTxt;
+
+               /** Constructor &mdash; package access for {@link 
BasicWellKnownResource#create()}. */
+               protected Builder() {}
+
+               /**
+                * Sets the {@code security.txt} body that will be served at
+                * {@code /.well-known/security.txt}.
+                *
+                * <p>
+                * Per RFC 9116, the body must include at least one {@code 
Contact:} field and an
+                * {@code Expires:} field; the mixin does not validate the 
content &mdash; that's the
+                * caller's responsibility.
+                *
+                * @param value The body content. Must not be <jk>null</jk> or 
blank.
+                * @return This object.
+                */
+               public Builder securityTxt(String value) {
+                       securityTxt = value;
+                       return this;
+               }
+
+               /**
+                * Builds a {@link BasicWellKnownResource} instance.
+                *
+                * @return A configured instance.
+                */
+               public BasicWellKnownResource build() {
+                       return new BasicWellKnownResource(this);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/package-info.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/package-info.java
new file mode 100644
index 0000000000..81ccdac39a
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/package-info.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Convention-endpoints mixin pack — composable {@code @Rest(mixins=...)} 
resources that ship the
+ * "internet conventions" surface (favicon, robots, sitemap, version, 
well-known) every public-facing
+ * service eventually grows.
+ *
+ * <p>
+ * Four sibling mixins compose into the host {@code @Rest}-annotated resource. 
Each mixin owns its
+ * default mount paths and is independently mountable; the four together drop 
in as a pack via
+ * {@code @Rest(mixins={BasicFaviconResource.class, BasicSeoResource.class, 
BasicVersionResource.class, BasicWellKnownResource.class})}.
+ * </p>
+ *
+ * <ul class='javatreec'>
+ *     <li class='jc'>{@link 
org.apache.juneau.rest.convention.BasicFaviconResource} —
+ *             {@code /favicon.ico} with a 30-day {@code Cache-Control} and a 
default Juneau-branded icon
+ *             on the framework classpath.
+ *     <li class='jc'>{@link 
org.apache.juneau.rest.convention.BasicSeoResource} —
+ *             {@code /robots.txt} (deny-all by default) and {@code 
/sitemap.xml} (empty
+ *             {@code <urlset>} by default); both builder-driven.
+ *     <li class='jc'>{@link 
org.apache.juneau.rest.convention.BasicVersionResource} —
+ *             {@code /version}, {@code /info}, and {@code /about} returning 
the same JSON metadata
+ *             map; defaults read {@code MANIFEST.MF} + {@code git.properties} 
from the classpath.
+ *     <li class='jc'>{@link 
org.apache.juneau.rest.convention.BasicWellKnownResource} —
+ *             {@code /.well-known/security.txt} per RFC 9116; 404 unless 
explicitly configured.
+ * </ul>
+ *
+ * <h5 class='section'>Composition example:</h5>
+ *
+ * <p class='bjava'>
+ *     <ja>@Rest</ja>(
+ *             path=<js>"/api"</js>,
+ *             mixins={
+ *                     BasicFaviconResource.<jk>class</jk>,
+ *                     BasicSeoResource.<jk>class</jk>,
+ *                     BasicVersionResource.<jk>class</jk>,
+ *                     BasicWellKnownResource.<jk>class</jk>
+ *             }
+ *     )
+ *     <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet {
+ *             <ja>@Bean</ja> BasicSeoResource seo() {
+ *                     <jk>return</jk> 
BasicSeoResource.<jsm>create</jsm>().robotsAllow(<js>"*"</js>, 
<js>"/"</js>).build();
+ *             }
+ *             <ja>@Bean</ja> BasicWellKnownResource wellKnown() {
+ *                     <jk>return</jk> 
BasicWellKnownResource.<jsm>create</jsm>()
+ *                             .securityTxt(<js>"Contact: 
[email protected]\nExpires: 2027-01-01T00:00:00Z\n"</js>)
+ *                             .build();
+ *             }
+ *     }
+ * </p>
+ *
+ * <p>
+ * All four endpoints are excluded from generated Swagger / OpenAPI specs via
+ * {@link org.apache.juneau.rest.annotation.OpSwagger#ignore() 
@OpSwagger(ignore=true)} —
+ * convention endpoints are not API-meaningful.
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/RestServerComposition";>REST Server 
— Composition (mixins, paths)</a>
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+package org.apache.juneau.rest.convention;
diff --git 
a/juneau-rest/juneau-rest-server/src/main/resources/juneau-favicon.ico 
b/juneau-rest/juneau-rest-server/src/main/resources/juneau-favicon.ico
new file mode 100644
index 0000000000..b6dce1b3fd
Binary files /dev/null and 
b/juneau-rest/juneau-rest-server/src/main/resources/juneau-favicon.ico differ
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_Builders_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_Builders_Test.java
new file mode 100644
index 0000000000..f34cd8e346
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_Builders_Test.java
@@ -0,0 +1,361 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+import java.nio.file.*;
+import java.time.*;
+import java.util.*;
+import java.util.jar.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Direct-instance tests for the convention-pack builders &mdash; covers the 
no-arg constructor
+ * paths, helper methods, and edge cases that the {@code AsMixin} / 
real-container tests don't
+ * exercise (entries-map handling, sitemap-entry XML formatting, 
git.properties parsing,
+ * URL-classloader fallback paths, etc.).
+ *
+ * <p>
+ * Pure JUnit; no MockRest or container needed.
+ *
+ * @since 9.5.0
+ */
+class BasicConvention_Builders_Test extends TestBase {
+
+       // -------- BasicFaviconResource --------
+
+       @Test void favicon_noArgConstructorLoadsDefault() {
+               var f = new BasicFaviconResource();
+               assertNotNull(f);
+       }
+
+       @Test void favicon_classpathResolvesExistingResource() {
+               var f = BasicFaviconResource.create()
+                       .classpath("/juneau-favicon.ico")
+                       .build();
+               assertNotNull(f);
+       }
+
+       @Test void favicon_classpathFallsBackOnMissingResource() {
+               var f = BasicFaviconResource.create()
+                       .classpath("/no-such-resource.ico")
+                       .build();
+               assertNotNull(f, "Missing classpath path must fall back to 
default favicon");
+       }
+
+       @Test void favicon_bytesAndClasspathAreMutuallyExclusive() {
+               var bytes = new byte[]{1,2,3};
+               var f = BasicFaviconResource.create()
+                       .classpath("/anything.ico")
+                       .bytes(bytes)
+                       .build();
+               // bytes() wins over classpath() when set last
+               assertNotNull(f);
+       }
+
+       @Test void favicon_customCacheControl() {
+               var f = BasicFaviconResource.create()
+                       .bytes(new byte[]{1,2})
+                       .cacheControl("no-store")
+                       .build();
+               assertNotNull(f);
+       }
+
+       // -------- BasicSeoResource --------
+
+       @Test void seo_noArgConstructorYieldsDenyAll() {
+               var s = new BasicSeoResource();
+               assertNotNull(s);
+       }
+
+       @Test void seo_robotsAllowAndDisallowMix() {
+               var s = BasicSeoResource.create()
+                       .robotsAllow("Googlebot", "/")
+                       .robotsDisallow("BadBot", "/private", "/admin")
+                       .build();
+               assertNotNull(s);
+       }
+
+       @Test void seo_sitemapEntryWithFullMetadata() {
+               var s = BasicSeoResource.create()
+                       .sitemapEntry("https://example.com/page";,
+                               ZonedDateTime.parse("2026-05-24T18:00:00Z"),
+                               "weekly",
+                               0.8)
+                       .build();
+               assertNotNull(s);
+       }
+
+       @Test void seo_sitemapEntryDirectInstance() {
+               var e = new BasicSeoResource.SitemapEntry(
+                       "https://x.example.com/<unsafe&\"chars'>",
+                       ZonedDateTime.parse("2026-05-24T18:00:00+02:00"),
+                       "daily",
+                       1.0);
+               var sw = new java.io.StringWriter();
+               try {
+                       e.write(sw);
+               } catch (IOException ex) {
+                       fail(ex);
+               }
+               var xml = sw.toString();
+               assertTrue(xml.contains("<lastmod>"), "lastmod present");
+               assertTrue(xml.contains("<changefreq>daily</changefreq>"), 
"changefreq present");
+               assertTrue(xml.contains("<priority>1.0</priority>"), "priority 
present");
+               assertTrue(xml.contains("&amp;"), "ampersand escaped");
+               assertTrue(xml.contains("&lt;"), "less-than escaped");
+               assertTrue(xml.contains("&gt;"), "greater-than escaped");
+               assertTrue(xml.contains("&quot;"), "double-quote escaped");
+               assertTrue(xml.contains("&apos;"), "single-quote escaped");
+       }
+
+       @Test void seo_emptyPathArrayInRobotsRuleIsTolerated() {
+               // Internal RobotsRule's path-array handling — null/empty paths 
is just an empty rule.
+               var s = BasicSeoResource.create().robotsAllow("MyBot").build();
+               assertNotNull(s);
+       }
+
+       // -------- BasicVersionResource --------
+
+       @Test void version_noArgConstructorPopulatesDefaults() {
+               var v = new BasicVersionResource();
+               assertNotNull(v.getInfoMap().get("javaVersion"), "no-arg ctor 
must surface javaVersion");
+       }
+
+       @Test void version_entryAcceptsNullValue() {
+               var v = BasicVersionResource.create()
+                       .entry("missing", null)
+                       .build();
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("missing"));
+       }
+
+       @Test void version_entriesMapSink() {
+               var values = new LinkedHashMap<String,String>();
+               values.put("a", "1");
+               values.put("b", null);
+               var v = BasicVersionResource.create().entries(values).build();
+               assertEquals("1", v.getInfoMap().get("a"));
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("b"));
+       }
+
+       @Test void version_entriesNullMapIsTolerated() {
+               var v = BasicVersionResource.create().entries(null).build();
+               assertNotNull(v);
+       }
+
+       @Test void version_fromManifestFallbackUnknownNameAndVersion() {
+               // URLClassLoader with no manifest available: name+version fall 
back to (unknown).
+               var v = BasicVersionResource.create()
+                       .fromManifest(new URLClassLoader(new URL[0], null))
+                       .build();
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("name"));
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("version"));
+       }
+
+       @Test void version_fromManifestObjectFullAttrs() {
+               var m = new Manifest();
+               var attrs = m.getMainAttributes();
+               attrs.putValue("Manifest-Version", "1.0");
+               attrs.putValue("Implementation-Title", "obj-app");
+               attrs.putValue("Implementation-Version", "5.0.0");
+               attrs.putValue("Implementation-Vendor", "Co");
+               attrs.putValue("Build-Jdk", "21");
+               var v = BasicVersionResource.create().fromManifest(m).build();
+               assertEquals("obj-app", v.getInfoMap().get("name"));
+               assertEquals("5.0.0", v.getInfoMap().get("version"));
+               assertEquals("Co", v.getInfoMap().get("vendor"));
+               assertEquals("21", v.getInfoMap().get("javaVersion"));
+       }
+
+       @Test void version_fromGitPropertiesViaClasspathResource(@TempDir Path 
tempDir) throws Exception {
+               // Build a tiny synthetic classpath that contains 
git.properties at the root.
+               var props = "git.commit.id=abcdef0123456789\n"
+                       + "git.commit.id.abbrev=abcdef0\n"
+                       + "git.branch=feature\n"
+                       + "git.build.time=2026-05-24T17:00:00Z\n";
+               Files.writeString(tempDir.resolve("git.properties"), props, 
StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(new URL[]{ 
tempDir.toUri().toURL() }, null)) {
+                       var v = BasicVersionResource.create()
+                               .fromGitProperties(cl)
+                               .build();
+                       assertEquals("abcdef0123456789", 
v.getInfoMap().get("gitCommit"));
+                       assertEquals("feature", 
v.getInfoMap().get("gitBranch"));
+                       assertEquals("2026-05-24T17:00:00Z", 
v.getInfoMap().get("buildTime"));
+               }
+       }
+
+       @Test void version_fromGitPropertiesAbbrevFallback(@TempDir Path 
tempDir) throws Exception {
+               var props = "git.commit.id.abbrev=abcdef0\n"
+                       + "git.branch=main\n";
+               Files.writeString(tempDir.resolve("git.properties"), props, 
StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(new URL[]{ 
tempDir.toUri().toURL() }, null)) {
+                       var v = BasicVersionResource.create()
+                               .fromGitProperties(cl)
+                               .build();
+                       assertEquals("abcdef0", v.getInfoMap().get("gitCommit"),
+                               "abbrev fallback applies when full commit id 
absent");
+               }
+       }
+
+       @Test void version_fromGitPropertiesMissingFileIsSilent() throws 
Exception {
+               try (var cl = new URLClassLoader(new URL[0], null)) {
+                       var v = BasicVersionResource.create()
+                               .fromGitProperties(cl)
+                               .build();
+                       assertNull(v.getInfoMap().get("gitCommit"),
+                               "missing git.properties must not register a 
gitCommit entry");
+               }
+       }
+
+       @Test void version_fromManifestWithoutImplementationTitle(@TempDir Path 
tempDir) throws Exception {
+               // Manifest without Implementation-Title — locator falls back 
to first found.
+               var manifestDir = tempDir.resolve("META-INF");
+               Files.createDirectory(manifestDir);
+               Files.writeString(manifestDir.resolve("MANIFEST.MF"),
+                       "Manifest-Version: 1.0\nBuild-Jdk: 22\n\n",
+                       StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(new URL[]{ 
tempDir.toUri().toURL() }, null)) {
+                       var v = BasicVersionResource.create()
+                               .fromManifest(cl)
+                               .build();
+                       assertEquals("22", v.getInfoMap().get("javaVersion"));
+                       assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("name"),
+                               "name unset → (unknown)");
+               }
+       }
+
+       @Test void version_fromJavaVersionPutsIfAbsent() {
+               var v = BasicVersionResource.create()
+                       .entry("javaVersion", "preset")
+                       .fromJavaVersion()
+                       .build();
+               assertEquals("preset", v.getInfoMap().get("javaVersion"),
+                       "fromJavaVersion must not overwrite an explicit entry");
+       }
+
+       @Test void version_fromManifestObjectEmptyVendorIsSkipped() {
+               // Hits the empty-string branch in ifNotEmpty / ifNotEmptyValue.
+               var m = new Manifest();
+               var attrs = m.getMainAttributes();
+               attrs.putValue("Manifest-Version", "1.0");
+               attrs.putValue("Implementation-Title", "x");
+               attrs.putValue("Implementation-Version", "1");
+               attrs.putValue("Implementation-Vendor", "");
+               var v = BasicVersionResource.create().fromManifest(m).build();
+               assertNull(v.getInfoMap().get("vendor"), "empty string 
Implementation-Vendor must be skipped");
+       }
+
+       @Test void version_fromGitPropertiesEmptyValuesAreSkipped(@TempDir Path 
tempDir) throws Exception {
+               var props = "git.commit.id=\n"
+                       + "git.branch=\n"
+                       + "git.build.time=\n";
+               Files.writeString(tempDir.resolve("git.properties"), props, 
StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(new URL[]{ 
tempDir.toUri().toURL() }, null)) {
+                       var v = 
BasicVersionResource.create().fromGitProperties(cl).build();
+                       assertNull(v.getInfoMap().get("gitCommit"), "empty 
git.commit.id skipped");
+                       assertNull(v.getInfoMap().get("gitBranch"), "empty 
git.branch skipped");
+                       assertNull(v.getInfoMap().get("buildTime"), "empty 
git.build.time skipped");
+               }
+       }
+
+       @Test void version_fromManifestClassLoaderThrowsIOException() {
+               // A ClassLoader whose getResources(...) throws IOException 
must yield (unknown) entries
+               // without bubbling up the exception.
+               var brokenCl = new ClassLoader(null) {
+                       @Override
+                       public java.util.Enumeration<URL> getResources(String 
name) throws IOException {
+                               throw new IOException("boom");
+                       }
+               };
+               var v = 
BasicVersionResource.create().fromManifest(brokenCl).build();
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("name"));
+               assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("version"));
+       }
+
+       @Test void version_fromGitPropertiesClassLoaderThrowsIOException() {
+               // ClassLoader whose getResourceAsStream returns a stream that 
throws on read must yield
+               // no git.* entries, again without bubbling up the exception.
+               var brokenCl = new ClassLoader(null) {
+                       @Override
+                       public InputStream getResourceAsStream(String name) {
+                               return new InputStream() {
+                                       @Override
+                                       public int read() throws IOException {
+                                               throw new IOException("boom");
+                                       }
+                               };
+                       }
+               };
+               var v = 
BasicVersionResource.create().fromGitProperties(brokenCl).build();
+               assertNull(v.getInfoMap().get("gitCommit"), "broken 
git.properties read produces no entry");
+       }
+
+       @Test void version_fromManifestUrlOpenStreamThrows(@TempDir Path 
tempDir) throws Exception {
+               // Locator returns a URL pointing at a deleted manifest; 
openStream throws
+               // FileNotFoundException → caught and yields empty map → 
name/version → (unknown).
+               var manifestDir = tempDir.resolve("META-INF");
+               Files.createDirectory(manifestDir);
+               var manifest = manifestDir.resolve("MANIFEST.MF");
+               Files.writeString(manifest,
+                       "Manifest-Version: 1.0\nImplementation-Title: 
gone\n\n", StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(new URL[]{ 
tempDir.toUri().toURL() }, null)) {
+                       Files.delete(manifest);
+                       var v = 
BasicVersionResource.create().fromManifest(cl).build();
+                       assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("name"));
+               }
+       }
+
+       @Test void 
version_fromManifestSecondCandidateWinsOnImplementationTitle(@TempDir Path 
tempDir) throws Exception {
+               // Two manifests on the classpath: only the second has 
Implementation-Title — locator
+               // must prefer it.
+               var dirA = Files.createDirectory(tempDir.resolve("a"));
+               var dirB = Files.createDirectory(tempDir.resolve("b"));
+               Files.createDirectory(dirA.resolve("META-INF"));
+               Files.writeString(dirA.resolve("META-INF/MANIFEST.MF"),
+                       "Manifest-Version: 1.0\n\n", StandardCharsets.UTF_8);
+               Files.createDirectory(dirB.resolve("META-INF"));
+               Files.writeString(dirB.resolve("META-INF/MANIFEST.MF"),
+                       "Manifest-Version: 1.0\nImplementation-Title: 
titled\nImplementation-Version: 2\n\n",
+                       StandardCharsets.UTF_8);
+               try (var cl = new URLClassLoader(
+                               new URL[]{ dirA.toUri().toURL(), 
dirB.toUri().toURL() }, null)) {
+                       var v = 
BasicVersionResource.create().fromManifest(cl).build();
+                       assertEquals("titled", v.getInfoMap().get("name"),
+                               "Locator must prefer 
Implementation-Title-bearing manifest");
+               }
+       }
+
+       // -------- BasicWellKnownResource --------
+
+       @Test void wellKnown_noArgConstructorYieldsNullBody() {
+               var w = new BasicWellKnownResource();
+               assertNull(w.getSecurityTxtBody());
+       }
+
+       @Test void wellKnown_securityTxtBodyAccessor() {
+               var body = "Contact: [email protected]\nExpires: 
2027-01-01T00:00:00Z\n";
+               var w = 
BasicWellKnownResource.create().securityTxt(body).build();
+               assertEquals(body, w.getSecurityTxtBody());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_OpenApiHidden_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_OpenApiHidden_Test.java
new file mode 100644
index 0000000000..3903409be2
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_OpenApiHidden_Test.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.docs.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.rest.swagger.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates that all four convention-pack endpoints are excluded from the 
generated OpenAPI spec
+ * via {@link OpSwagger#ignore() @OpSwagger(ignore=true)} on each mixin's 
handler.
+ *
+ * <p>
+ * The host extends vanilla {@link RestServlet} (not {@link BasicRestServlet}) 
and mounts the four
+ * convention mixins plus {@link BasicOpenApiResource} (the OpenAPI 
generator). The generated spec
+ * must list the host's own {@code /items} endpoint but NOT the convention 
paths.
+ *
+ * @since 9.5.0
+ */
+class BasicConvention_OpenApiHidden_Test extends TestBase {
+
+       @Rest(
+               mixins={
+                       BasicFaviconResource.class,
+                       BasicSeoResource.class,
+                       BasicVersionResource.class,
+                       BasicWellKnownResource.class,
+                       BasicOpenApiResource.class
+               },
+               swaggerProvider=BasicSwaggerProvider.class
+       )
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @RestGet(path="/items") public String items() { return "items"; 
}
+
+               @Bean public BasicVersionResource version() {
+                       return BasicVersionResource.create().entry("name", 
"convention-openapi-test").build();
+               }
+
+               @Bean public BasicWellKnownResource wellKnown() {
+                       return 
BasicWellKnownResource.create().securityTxt("Contact: 
[email protected]\n").build();
+               }
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_openapiSpecExcludesAllConventionPaths() throws Exception 
{
+               var spec = c.get("/openapi.json")
+                       .run()
+                       .assertStatus(200)
+                       .getContent().asString();
+
+               // Host's own endpoint must be listed.
+               assertContains(spec, "/items");
+
+               // Convention paths must NOT be listed.
+               assertNotContains(spec, "/favicon.ico");
+               assertNotContains(spec, "/robots.txt");
+               assertNotContains(spec, "/sitemap.xml");
+               assertNotContains(spec, "/version");
+               assertNotContains(spec, "/info");
+               assertNotContains(spec, "/about");
+               assertNotContains(spec, "/.well-known/security.txt");
+               assertNotContains(spec, ".well-known");
+       }
+
+       @Test void a02_conventionEndpointsStillServedDespiteHiddenFromSpec() 
throws Exception {
+               c.get("/favicon.ico").run().assertStatus(200);
+               c.get("/robots.txt").run().assertStatus(200);
+               c.get("/sitemap.xml").run().assertStatus(200);
+               c.get("/version").run().assertStatus(200);
+               c.get("/.well-known/security.txt").run().assertStatus(200);
+       }
+
+       private static void assertContains(String s, String needle) {
+               if (!s.contains(needle))
+                       throw new AssertionError("Expected to contain '" + 
needle + "' but did not. Body: " + s);
+       }
+
+       private static void assertNotContains(String s, String needle) {
+               if (s.contains(needle))
+                       throw new AssertionError("Expected NOT to contain '" + 
needle + "' but did. Body excerpt: "
+                               + s.substring(Math.max(0, s.indexOf(needle) - 
50), Math.min(s.length(), s.indexOf(needle) + 100)));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_ParentChain_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_ParentChain_Test.java
new file mode 100644
index 0000000000..fb9badf768
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicConvention_ParentChain_Test.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that all four convention-pack mixins compose cleanly on a single 
host with no path
+ * collisions and that each mixin's {@code RestContext} is registered.
+ *
+ * <p>
+ * Setup: a single {@link RestServlet} host mounts all four convention mixins
+ * (favicon, SEO, version, well-known) and registers builder-configured {@code 
@Bean}
+ * factories for those that need explicit configuration.
+ *
+ * <p>
+ * Acceptance:
+ * <ul>
+ *     <li>All four mixins appear in {@code RestContext.getMixinContexts()}.
+ *     <li>Every convention path resolves (favicon.ico, robots.txt, 
sitemap.xml, version, info,
+ *             about, .well-known/security.txt).
+ *     <li>Host's own {@code /items} endpoint is unaffected.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicConvention_ParentChain_Test extends TestBase {
+
+       @Rest(mixins={
+               BasicFaviconResource.class,
+               BasicSeoResource.class,
+               BasicVersionResource.class,
+               BasicWellKnownResource.class
+       })
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @RestGet(path="/items") public String items() { return "items"; 
}
+
+               @Bean public BasicSeoResource seo() {
+                       return BasicSeoResource.create()
+                               .robotsAllow("*", "/")
+                               .sitemapEntry("https://example.com/items";)
+                               .build();
+               }
+
+               @Bean public BasicVersionResource version() {
+                       return BasicVersionResource.create()
+                               .entry("name", "convention-pack")
+                               .entry("version", "9.5.0")
+                               .fromJavaVersion()
+                               .build();
+               }
+
+               @Bean public BasicWellKnownResource wellKnown() {
+                       return BasicWellKnownResource.create()
+                               .securityTxt("Contact: 
[email protected]\nExpires: 2027-01-01T00:00:00Z\n")
+                               .build();
+               }
+       }
+
+       private static final MockRestClient c = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_allFourMixinContextsRegistered() throws Exception {
+               MockRestClient.buildLax(A.class);
+               var hostCtx = RestContext.getGlobalRegistry().get(A.class);
+               var ctxs = hostCtx.getMixinContexts();
+
+               assertNotNull(ctxs.get(BasicFaviconResource.class), "Favicon 
mixin context registered");
+               assertNotNull(ctxs.get(BasicSeoResource.class), "SEO mixin 
context registered");
+               assertNotNull(ctxs.get(BasicVersionResource.class), "Version 
mixin context registered");
+               assertNotNull(ctxs.get(BasicWellKnownResource.class), 
"Well-known mixin context registered");
+               assertEquals(4, ctxs.size(),
+                       "Expected exactly four mixin contexts; got: " + 
ctxs.keySet());
+       }
+
+       @Test void a02_faviconResolves() throws Exception {
+               c.get("/favicon.ico").run().assertStatus(200)
+                       .assertHeader("Content-Type").is("image/x-icon");
+       }
+
+       @Test void a03_robotsTxtResolves() throws Exception {
+               c.get("/robots.txt").run().assertStatus(200)
+                       .assertContent().asString().isContains("Allow: /");
+       }
+
+       @Test void a04_sitemapXmlResolves() throws Exception {
+               c.get("/sitemap.xml").run().assertStatus(200)
+                       
.assertContent().asString().isContains("<loc>https://example.com/items</loc>");
+       }
+
+       @Test void a05_versionEndpointResolves() throws Exception {
+               c.get("/version").run().assertStatus(200)
+                       .assertContent().asString().isContains("\"name\": 
\"convention-pack\"");
+       }
+
+       @Test void a06_infoEndpointResolves() throws Exception {
+               c.get("/info").run().assertStatus(200)
+                       .assertContent().asString().isContains("\"version\": 
\"9.5.0\"");
+       }
+
+       @Test void a07_aboutEndpointResolves() throws Exception {
+               c.get("/about").run().assertStatus(200)
+                       .assertContent().asString().isContains("\"name\": 
\"convention-pack\"");
+       }
+
+       @Test void a08_securityTxtResolves() throws Exception {
+               c.get("/.well-known/security.txt").run().assertStatus(200)
+                       .assertContent().asString().isContains("Contact: 
[email protected]");
+       }
+
+       @Test void a09_hostEndpointStillReachable() throws Exception {
+               c.get("/items").run().assertStatus(200)
+                       .assertContent().asString().isContains("items");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicFaviconResource_AsMixin_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicFaviconResource_AsMixin_Test.java
new file mode 100644
index 0000000000..f99ee392fd
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicFaviconResource_AsMixin_Test.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicFaviconResource} mounted as a mixin via {@code 
@Rest(mixins=...)} on a
+ * vanilla {@link RestServlet}.
+ *
+ * <p>
+ * Cases:
+ * <ul>
+ *     <li>Default favicon (framework-shipped {@code /juneau-favicon.ico}) is 
served when no
+ *             {@code @Bean BasicFaviconResource} is registered on the host.
+ *     <li>{@code Content-Type: image/x-icon} and 30-day {@code Cache-Control} 
headers flow through.
+ *     <li>Importer's {@code @Bean BasicFaviconResource} factory overrides the 
default bytes.
+ *     <li>{@link BasicFaviconResource.Builder#classpath(String) 
classpath(...)} loads icon bytes
+ *             from a classpath resource path.
+ *     <li>The host's own endpoints are unaffected by the mixin.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicFaviconResource_AsMixin_Test extends TestBase {
+
+       @Rest(mixins=BasicFaviconResource.class)
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient ca = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_defaultFaviconServed() throws Exception {
+               ca.get("/favicon.ico")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Content-Type").is("image/x-icon")
+                       .assertHeader("Cache-Control").is("max-age=2592000, 
public");
+       }
+
+       @Test void a02_defaultFaviconBodyNonEmpty() throws Exception {
+               var body = ca.get("/favicon.ico")
+                       .run()
+                       .assertStatus(200)
+                       .getContent().asBytes();
+               Assertions.assertTrue(body.length > 0, "Default favicon body 
must be non-empty");
+               Assertions.assertEquals(0x00, body[0] & 0xFF, "ICO magic byte 
0");
+               Assertions.assertEquals(0x00, body[1] & 0xFF, "ICO magic byte 
1");
+               Assertions.assertEquals(0x01, body[2] & 0xFF, "ICO magic byte 2 
(type=ICO)");
+       }
+
+       @Test void a03_hostEndpointStillReachable() throws Exception {
+               ca.get("/items")
+                       .run()
+                       .assertStatus(200)
+                       .assertContent().asString().isContains("items");
+       }
+
+       /** Host with an importer-supplied {@code @Bean BasicFaviconResource} 
carrying custom bytes. */
+       @Rest(mixins=BasicFaviconResource.class)
+       public static class B extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicFaviconResource favicon() {
+                       return BasicFaviconResource.create()
+                               .bytes(new byte[]{(byte)0xCA, (byte)0xFE, 
(byte)0xBA, (byte)0xBE})
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cb = 
MockRestClient.buildLax(B.class);
+
+       @Test void b01_overrideViaBuilderBytes() throws Exception {
+               var body = cb.get("/favicon.ico")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Content-Type").is("image/x-icon")
+                       .getContent().asBytes();
+               Assertions.assertEquals(4, body.length);
+               Assertions.assertEquals((byte)0xCA, body[0]);
+               Assertions.assertEquals((byte)0xFE, body[1]);
+               Assertions.assertEquals((byte)0xBA, body[2]);
+               Assertions.assertEquals((byte)0xBE, body[3]);
+       }
+
+       /** Host with an importer-supplied factory that loads bytes via 
classpath resource path. */
+       @Rest(mixins=BasicFaviconResource.class)
+       public static class C extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicFaviconResource favicon() {
+                       // /juneau-favicon.ico is the framework's 
default-shipping classpath resource;
+                       // loading it explicitly via classpath(...) verifies 
the classpath path works.
+                       return BasicFaviconResource.create()
+                               .classpath("/juneau-favicon.ico")
+                               .cacheControl("max-age=300, public")
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cc = 
MockRestClient.buildLax(C.class);
+
+       @Test void c01_classpathLoaderAndCustomCacheControl() throws Exception {
+               cc.get("/favicon.ico")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Content-Type").is("image/x-icon")
+                       .assertHeader("Cache-Control").is("max-age=300, 
public");
+       }
+
+       @Test void c02_classpathMissingFallsBackToDefault() throws Exception {
+               // A builder pointed at a missing classpath path falls back to 
the framework default.
+               var fav = BasicFaviconResource.create()
+                       .classpath("/no-such-favicon.ico")
+                       .build();
+               // Even with a missing classpath override, the default favicon 
resource provides bytes.
+               Assertions.assertNotNull(fav, "Builder must always produce an 
instance");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicSeoResource_AsMixin_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicSeoResource_AsMixin_Test.java
new file mode 100644
index 0000000000..2771706a02
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicSeoResource_AsMixin_Test.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicSeoResource} mounted as a mixin via {@code 
@Rest(mixins=...)} on a vanilla
+ * {@link RestServlet}.
+ *
+ * <p>
+ * Cases:
+ * <ul>
+ *     <li>Default {@code /robots.txt} returns deny-all when no override is 
registered.
+ *     <li>Default {@code /sitemap.xml} returns an empty {@code <urlset>}.
+ *     <li>Importer's {@code @Bean BasicSeoResource} factory drives custom 
robots policy and sitemap
+ *             entries.
+ *     <li>Custom {@code robotsTxt(...)} body overrides any builder rules.
+ *     <li>Content-Type pinning works for both endpoints.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicSeoResource_AsMixin_Test extends TestBase {
+
+       /** Default-host mounting the mixin with no @Bean override. */
+       @Rest(mixins=BasicSeoResource.class)
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient ca = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_defaultRobotsTxtIsDenyAll() throws Exception {
+               ca.get("/robots.txt")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Content-Type").isContains("text/plain")
+                       .assertContent().asString().is("User-agent: 
*\nDisallow: /\n");
+       }
+
+       @Test void a02_defaultSitemapIsEmptyUrlset() throws Exception {
+               var body = ca.get("/sitemap.xml")
+                       .run()
+                       .assertStatus(200)
+                       
.assertHeader("Content-Type").isContains("application/xml")
+                       .getContent().asString();
+               Assertions.assertTrue(body.contains("<?xml version=\"1.0\" 
encoding=\"UTF-8\"?>"), "XML prolog");
+               Assertions.assertTrue(body.contains("<urlset"), "urlset 
opening");
+               Assertions.assertTrue(body.contains("</urlset>"), "urlset 
closing");
+               Assertions.assertFalse(body.contains("<url>"), "no entries by 
default");
+       }
+
+       @Test void a03_hostEndpointStillReachable() throws Exception {
+               
ca.get("/items").run().assertStatus(200).assertContent().asString().isContains("items");
+       }
+
+       /** Host with builder-driven robots policy via @Bean factory. */
+       @Rest(mixins=BasicSeoResource.class)
+       public static class B extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicSeoResource seo() {
+                       return BasicSeoResource.create()
+                               .robotsAllow("*", "/")
+                               .robotsDisallow("BadBot", "/private")
+                               .sitemapEntry("https://example.com/";)
+                               .sitemapEntry("https://example.com/items";)
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cb = 
MockRestClient.buildLax(B.class);
+
+       @Test void b01_overrideRobotsRules() throws Exception {
+               var body = 
cb.get("/robots.txt").run().assertStatus(200).getContent().asString();
+               Assertions.assertTrue(body.contains("User-agent: *"), "wildcard 
ua");
+               Assertions.assertTrue(body.contains("Allow: /"), "allow root");
+               Assertions.assertTrue(body.contains("User-agent: BadBot"), "bad 
bot ua");
+               Assertions.assertTrue(body.contains("Disallow: /private"), 
"disallow private");
+       }
+
+       @Test void b02_overrideSitemapEntries() throws Exception {
+               var body = 
cb.get("/sitemap.xml").run().assertStatus(200).getContent().asString();
+               
Assertions.assertTrue(body.contains("<loc>https://example.com/</loc>"), "root 
entry");
+               
Assertions.assertTrue(body.contains("<loc>https://example.com/items</loc>"), 
"items entry");
+       }
+
+       /** Host with a custom robots body via robotsTxt(...). */
+       @Rest(mixins=BasicSeoResource.class)
+       public static class C extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicSeoResource seo() {
+                       return BasicSeoResource.create()
+                               .robotsTxt("User-agent: *\nCrawl-delay: 
10\nSitemap: https://example.com/sitemap.xml\n";)
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cc = 
MockRestClient.buildLax(C.class);
+
+       @Test void c01_customRobotsBodyWinsOverRules() throws Exception {
+               var body = 
cc.get("/robots.txt").run().assertStatus(200).getContent().asString();
+               Assertions.assertTrue(body.contains("Crawl-delay: 10"), 
"crawl-delay directive");
+               Assertions.assertTrue(body.contains("Sitemap: 
https://example.com/sitemap.xml";), "sitemap directive");
+               Assertions.assertFalse(body.contains("Disallow: /"), "default 
deny-all body must be replaced");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_AsMixin_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_AsMixin_Test.java
new file mode 100644
index 0000000000..999220b8b5
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_AsMixin_Test.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import java.util.*;
+import java.util.jar.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicVersionResource} mounted as a mixin via {@code 
@Rest(mixins=...)} on a
+ * vanilla {@link RestServlet}.
+ *
+ * <p>
+ * Cases:
+ * <ul>
+ *     <li>{@code /version}, {@code /info}, {@code /about} all return the same 
JSON map.
+ *     <li>{@code Content-Type: application/json}.
+ *     <li>Importer's {@code @Bean BasicVersionResource} factory drives the 
entries map (manifest
+ *             read, programmatic entries, custom Manifest).
+ *     <li>Missing manifest gracefully resolves to {@code (unknown)}.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicVersionResource_AsMixin_Test extends TestBase {
+
+       @Rest(mixins=BasicVersionResource.class)
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient ca = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_versionEndpointServed() throws Exception {
+               ca.get("/version")
+                       .run()
+                       .assertStatus(200)
+                       
.assertHeader("Content-Type").isContains("application/json");
+       }
+
+       @Test void a02_versionInfoAboutAreSynonyms() throws Exception {
+               var v = 
ca.get("/version").run().assertStatus(200).getContent().asString();
+               var i = 
ca.get("/info").run().assertStatus(200).getContent().asString();
+               var ab = 
ca.get("/about").run().assertStatus(200).getContent().asString();
+               Assertions.assertEquals(v, i, "/version and /info must be 
synonyms");
+               Assertions.assertEquals(v, ab, "/version and /about must be 
synonyms");
+       }
+
+       @Test void a03_defaultPayloadHasJavaVersionAtMinimum() throws Exception 
{
+               var body = 
ca.get("/version").run().assertStatus(200).getContent().asString();
+               var parsed = JsonParser.DEFAULT.parse(body, Map.class);
+               Assertions.assertNotNull(parsed.get("javaVersion"), 
"javaVersion present");
+       }
+
+       @Test void a04_hostEndpointStillReachable() throws Exception {
+               
ca.get("/items").run().assertStatus(200).assertContent().asString().isContains("items");
+       }
+
+       /** Host providing a programmatic version map via @Bean factory. */
+       @Rest(mixins=BasicVersionResource.class)
+       public static class B extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicVersionResource version() {
+                       return BasicVersionResource.create()
+                               .entry("name", "my-app")
+                               .entry("version", "1.2.3")
+                               .entry("gitCommit", "abc123")
+                               .entry("gitBranch", "main")
+                               .entry("buildTime", "2026-05-24T18:00:00Z")
+                               .fromJavaVersion()
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cb = 
MockRestClient.buildLax(B.class);
+
+       @Test void b01_programmaticEntriesSurface() throws Exception {
+               var body = 
cb.get("/version").run().assertStatus(200).getContent().asString();
+               var parsed = JsonParser.DEFAULT.parse(body, Map.class);
+               Assertions.assertEquals("my-app", parsed.get("name"));
+               Assertions.assertEquals("1.2.3", parsed.get("version"));
+               Assertions.assertEquals("abc123", parsed.get("gitCommit"));
+               Assertions.assertEquals("main", parsed.get("gitBranch"));
+               Assertions.assertEquals("2026-05-24T18:00:00Z", 
parsed.get("buildTime"));
+               Assertions.assertNotNull(parsed.get("javaVersion"));
+       }
+
+       /** Host providing a synthetic Manifest via fromManifest(Manifest). */
+       @Rest(mixins=BasicVersionResource.class)
+       public static class C extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicVersionResource version() {
+                       var manifest = new Manifest();
+                       var attrs = manifest.getMainAttributes();
+                       attrs.putValue("Manifest-Version", "1.0");
+                       attrs.putValue("Implementation-Title", "synthetic-app");
+                       attrs.putValue("Implementation-Version", "9.9.9");
+                       attrs.putValue("Implementation-Vendor", "Acme");
+                       attrs.putValue("Build-Jdk", "21.0.0");
+                       return 
BasicVersionResource.create().fromManifest(manifest).build();
+               }
+       }
+
+       private static final MockRestClient cc = 
MockRestClient.buildLax(C.class);
+
+       @Test void c01_syntheticManifestSurfaces() throws Exception {
+               var body = 
cc.get("/version").run().assertStatus(200).getContent().asString();
+               var parsed = JsonParser.DEFAULT.parse(body, Map.class);
+               Assertions.assertEquals("synthetic-app", parsed.get("name"));
+               Assertions.assertEquals("9.9.9", parsed.get("version"));
+               Assertions.assertEquals("Acme", parsed.get("vendor"));
+               Assertions.assertEquals("21.0.0", parsed.get("javaVersion"));
+       }
+
+       @Test void d01_missingManifestGracefulFallback() {
+               var v = BasicVersionResource.create()
+                       .fromManifest(new java.net.URLClassLoader(new 
java.net.URL[0], null))
+                       .build();
+               Assertions.assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("name"),
+                       "missing manifest must yield (unknown) name");
+               Assertions.assertEquals(BasicVersionResource.UNKNOWN, 
v.getInfoMap().get("version"),
+                       "missing manifest must yield (unknown) version");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
new file mode 100644
index 0000000000..9efb535dbf
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Real-Jetty deployment-parity assertion for {@link BasicVersionResource}.
+ *
+ * <p>
+ * Boots a {@link org.apache.juneau.microservice.Microservice Microservice} 
backed by
+ * {@link org.apache.juneau.microservice.jetty.JettyConfiguration 
JettyConfiguration} on an
+ * ephemeral port via {@link MicroserviceTestFixture}, mounts a vanilla {@link 
RestServlet} host
+ * with the version mixin and a {@code @Bean BasicVersionResource} factory 
configuring known
+ * entries, and hits {@code /version}, {@code /info}, and {@code /about} over 
real HTTP.
+ *
+ * <p>
+ * Catches things {@code MockRest} cannot:
+ * <ul>
+ *     <li>Real {@code Content-Type: application/json} negotiation through the 
Jetty/servlet stack.
+ *     <li>JSON serialization through {@link 
org.apache.juneau.rest.RestResponse#getDirectWriter
+ *             getDirectWriter("application/json")} on a vanilla {@link 
RestServlet} host (no JSON
+ *             serializer wired up explicitly).
+ *     <li>Mixin-walk + bean-store override flow when {@code @Bean 
BasicVersionResource} is
+ *             registered on the host: the mixin serves the host-supplied 
configuration end-to-end through
+ *             the network stack.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicVersionResource_JettyMicroservice_Test extends TestBase {
+
+       @Rest(mixins=BasicVersionResource.class)
+       public static class Host extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicVersionResource version() {
+                       return BasicVersionResource.create()
+                               .entry("name", "jetty-test")
+                               .entry("version", "0.0.1")
+                               .entry("gitCommit", "deadbeef")
+                               .entry("gitBranch", "main")
+                               .fromJavaVersion()
+                               .build();
+               }
+       }
+
+       @Configuration
+       public static class HostConfig {
+               @Bean
+               public Servlet hostServlet() {
+                       return new Host();
+               }
+       }
+
+       @RegisterExtension
+       static MicroserviceTestFixture fixture = 
MicroserviceTestFixture.create()
+               .configurations(HostConfig.class);
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       private static HttpResponse<String> get(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create(fixture.getRootUrl() + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET()
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       @Test void a01_versionOverRealHttp() throws Exception {
+               var resp = get("/version");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"jetty-test\""), 
"name surfaced in body: " + resp.body());
+               assertTrue(resp.body().contains("\"version\": \"0.0.1\""), 
"version surfaced");
+               assertTrue(resp.body().contains("\"gitCommit\": \"deadbeef\""), 
"gitCommit surfaced");
+               var ct = resp.headers().firstValue("Content-Type").orElse("");
+               assertTrue(ct.startsWith("application/json"), "Content-Type 
was: " + ct);
+       }
+
+       @Test void a02_infoSynonym() throws Exception {
+               var resp = get("/info");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"jetty-test\""));
+       }
+
+       @Test void a03_aboutSynonym() throws Exception {
+               var resp = get("/about");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"jetty-test\""));
+       }
+
+       @Test void a04_javaVersionSurfacesWithoutManifest() throws Exception {
+               var resp = get("/version");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"javaVersion\""), 
"javaVersion key present");
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
new file mode 100644
index 0000000000..e21c936fc5
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.springboot.*;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.test.context.*;
+import org.springframework.boot.test.context.SpringBootTest.*;
+import org.springframework.boot.test.web.server.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.annotation.*;
+import org.springframework.test.annotation.*;
+
+/**
+ * Real-Spring-Boot deployment-parity assertion for {@link 
BasicVersionResource}.
+ *
+ * <p>
+ * Boots a full Spring Boot context with embedded Tomcat on a random port, 
registers a
+ * {@link BasicSpringRestServlet}-based host with the version mixin via
+ * {@link ServletRegistrationBean}, supplies a Spring {@code @Bean 
BasicVersionResource}, and hits
+ * {@code /version}, {@code /info}, and {@code /about} over real HTTP.
+ *
+ * <p>
+ * Catches things {@code MockRest} and the
+ * {@link BasicVersionResource_JettyMicroservice_Test Jetty parity test} 
cannot:
+ * <ul>
+ *     <li>Spring's bean store adapter ({@code SpringBeanStore}) resolving the 
host's
+ *             {@code @Bean BasicVersionResource} during the FINISHED-72 mixin 
walk through
+ *             {@link 
org.springframework.context.ApplicationContext#getBean(Class)
+ *             ApplicationContext.getBean(...)}.
+ *     <li>End-to-end format-pinned JSON ({@link 
org.apache.juneau.rest.RestResponse#getDirectWriter
+ *             getDirectWriter("application/json")}) under embedded Tomcat.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+@SpringBootTest(classes = BasicVersionResource_Springboot_Test.TestApp.class,
+       webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+class BasicVersionResource_Springboot_Test {
+
+       @SpringBootConfiguration
+       @EnableAutoConfiguration
+       public static class TestApp {
+
+               @Bean
+               public Host hostServlet() {
+                       return new Host();
+               }
+
+               @Bean
+               public ServletRegistrationBean<Host> hostRegistration(Host 
servlet) {
+                       return new ServletRegistrationBean<>(servlet, "/*");
+               }
+
+               @Bean
+               public BasicVersionResource versionResource() {
+                       return BasicVersionResource.create()
+                               .entry("name", "spring-test")
+                               .entry("version", "0.0.2")
+                               .entry("gitBranch", "release")
+                               .fromJavaVersion()
+                               .build();
+               }
+       }
+
+       @Rest(mixins = BasicVersionResource.class)
+       public static class Host extends BasicSpringRestServlet {
+               private static final long serialVersionUID = 1L;
+       }
+
+       @LocalServerPort
+       int port;
+
+       private static final HttpClient HTTP = HttpClient.newBuilder()
+               .connectTimeout(Duration.ofSeconds(5))
+               .followRedirects(HttpClient.Redirect.NEVER)
+               .build();
+
+       private HttpResponse<String> get(String path) throws Exception {
+               var req = HttpRequest.newBuilder()
+                       .uri(URI.create("http://localhost:"; + port + path))
+                       .timeout(Duration.ofSeconds(10))
+                       .GET()
+                       .build();
+               return HTTP.send(req, BodyHandlers.ofString());
+       }
+
+       @Test void a01_versionUnderSpringBoot() throws Exception {
+               var resp = get("/version");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"spring-test\""), 
"Body: " + resp.body());
+               assertTrue(resp.body().contains("\"version\": \"0.0.2\""));
+               assertTrue(resp.body().contains("\"gitBranch\": \"release\""));
+               var ct = resp.headers().firstValue("Content-Type").orElse("");
+               assertTrue(ct.startsWith("application/json"), "Content-Type: " 
+ ct);
+       }
+
+       @Test void a02_infoSynonymUnderSpringBoot() throws Exception {
+               var resp = get("/info");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"spring-test\""));
+       }
+
+       @Test void a03_aboutSynonymUnderSpringBoot() throws Exception {
+               var resp = get("/about");
+               assertEquals(200, resp.statusCode());
+               assertTrue(resp.body().contains("\"name\": \"spring-test\""));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicWellKnownResource_AsMixin_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicWellKnownResource_AsMixin_Test.java
new file mode 100644
index 0000000000..5f7fb1f597
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicWellKnownResource_AsMixin_Test.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.convention;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Validates {@link BasicWellKnownResource} mounted as a mixin via {@code 
@Rest(mixins=...)} on a
+ * vanilla {@link RestServlet}.
+ *
+ * <p>
+ * Cases:
+ * <ul>
+ *     <li>{@code /.well-known/security.txt} returns {@code 404 Not Found} 
when no body is configured
+ *             (per RFC 9116 default — no placeholder).
+ *     <li>Importer's {@code @Bean BasicWellKnownResource} factory drives the 
body content.
+ *     <li>{@code Content-Type: text/plain; charset=UTF-8} when a body is 
configured.
+ * </ul>
+ *
+ * @since 9.5.0
+ */
+class BasicWellKnownResource_AsMixin_Test extends TestBase {
+
+       @Rest(mixins=BasicWellKnownResource.class)
+       public static class A extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @RestGet(path="/items") public String items() { return "items"; 
}
+       }
+
+       private static final MockRestClient ca = 
MockRestClient.buildLax(A.class);
+
+       @Test void a01_noBodyConfiguredReturns404() throws Exception {
+               ca.get("/.well-known/security.txt").run().assertStatus(404);
+       }
+
+       @Test void a02_hostEndpointStillReachable() throws Exception {
+               
ca.get("/items").run().assertStatus(200).assertContent().asString().isContains("items");
+       }
+
+       /** Host that configures a security.txt body via @Bean factory. */
+       @Rest(mixins=BasicWellKnownResource.class)
+       public static class B extends RestServlet {
+               private static final long serialVersionUID = 1L;
+               @Bean public BasicWellKnownResource wellKnown() {
+                       return BasicWellKnownResource.create()
+                               .securityTxt("Contact: 
[email protected]\nExpires: 2027-01-01T00:00:00Z\n")
+                               .build();
+               }
+       }
+
+       private static final MockRestClient cb = 
MockRestClient.buildLax(B.class);
+
+       @Test void b01_configuredBodyIsServed() throws Exception {
+               cb.get("/.well-known/security.txt")
+                       .run()
+                       .assertStatus(200)
+                       .assertHeader("Content-Type").isContains("text/plain")
+                       .assertContent().asString().is("Contact: 
[email protected]\nExpires: 2027-01-01T00:00:00Z\n");
+       }
+}
diff --git a/todo/TODO-76-mixin-convention-endpoints.md 
b/todo/FINISHED-76-mixin-convention-endpoints.md
similarity index 56%
rename from todo/TODO-76-mixin-convention-endpoints.md
rename to todo/FINISHED-76-mixin-convention-endpoints.md
index 78129ce8a9..4d8f188b17 100644
--- a/todo/TODO-76-mixin-convention-endpoints.md
+++ b/todo/FINISHED-76-mixin-convention-endpoints.md
@@ -1,7 +1,9 @@
-# TODO-76: Convention-endpoints mixin pack (favicon, SEO, version, well-known)
+# FINISHED-76: Convention-endpoints mixin pack (favicon, SEO, version, 
well-known)
 
 Source: split out of the post-FINISHED-72 mixin-pack planning on 2026-05-23.
 
+Closed 2026-05-24 in a single implementation session. Four sibling mixins 
landed in `org.apache.juneau.rest.convention`: `BasicFaviconResource` 
(`/favicon.ico` with a small classpath-resource default, 30-day cache), 
`BasicSeoResource` (`/robots.txt` deny-all default per RFC 9309, `/sitemap.xml` 
empty `<urlset>` default), `BasicVersionResource` (`/version`, `/info`, 
`/about` synonyms reading `MANIFEST.MF` + `git.properties` + JVM with graceful 
fallback), and `BasicWellKnownResource` (`/.w [...]
+
 ## Goal
 
 Group-ship four small "internet conventions" mixins that almost every 
public-facing Juneau service eventually needs and that today cost ~50 LOC + 
tests apiece to roll by hand:
@@ -180,3 +182,58 @@ All previously open questions resolved 2026-05-24.
 - `juneau-rest/juneau-rest-server-springboot/` — Spring `BeanStore` adapter; 
Phase 5 smoke-test target.
 - `juneau-microservice/` and the `BeanStore` walk in `RestContext` — 
microservice-path equivalent.
 - Existing: `BasicHealthResource` 
(`juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/health/BasicHealthResource.java`)
 — the canonical mixin-and-multi-mount example to model these mixins after.
+
+## Progress log
+
+### 2026-05-24 (Phase 0–6 — full implementation, including framework 
alignment, real-container tests, and docs)
+
+- **Phase 0 — Seam confirmation.** Reviewed 
`RestResponse.getDirectWriter(String)` for format-pinned JSON emission on 
`BasicVersionResource`, `HttpResourceBean` / `ByteArrayBody` for binary + 
headered payloads on `BasicFaviconResource`, and the existing `HttpResource` 
return-type contract (carries headers, body, content-type) — all four mixins 
return `HttpResource` from their primary handlers (`/favicon.ico`, 
`/robots.txt`, `/sitemap.xml`, `/.well-known/security.txt`) except `BasicVersio 
[...]
+
+- **Phase 1 — `BasicFaviconResource`.** New class in 
`juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/BasicFaviconResource.java`.
 Default favicon shipped as 
`juneau-rest-server/src/main/resources/juneau-favicon.ico` (top-level 
classpath, ≈1.1 KB, accessed as `/juneau-favicon.ico`). Builder methods: 
`bytes(byte[])`, `classpath(String)`, `cacheControl(String)`. `Cache-Control: 
max-age=2592000, public` (30 days) by default; `Content-Type: image/x-icon`. 
`@OpSwagger(ignore [...]
+
+- **Phase 2 — `BasicSeoResource`.** New class for `/robots.txt` (default 
deny-all per RFC 9309: `User-agent: *\nDisallow: /\n`) and `/sitemap.xml` 
(default empty `<urlset>`). Builder methods: `robotsAllow(String, String...)` / 
`robotsDisallow(String, String...)` for rule-based policies, 
`robotsTxt(String)` for fully-formed bodies, `sitemapEntry(String url)` and the 
four-arg form `sitemapEntry(String url, String lastmod, String changefreq, 
String priority)`. Per-mixin test class `BasicSeo [...]
+
+- **Phase 3 — `BasicVersionResource`.** New class for `/version`, `/info`, 
`/about` (synonyms in v1). Format-pinned JSON via 
`RestResponse.getDirectWriter("application/json")`. Default builder chain 
`fromManifest().fromGitProperties().fromJavaVersion()` invoked from the 
`BasicVersionResource(Builder)` constructor **only when no explicit builder 
methods have been called** (tracked via an `explicit` flag on the builder). 
Programmatic overrides (`Builder.entry(String, String)` / `Builder.en [...]
+
+- **Phase 4 — `BasicWellKnownResource`.** New class reserving 
`/.well-known/security.txt`. Returns `404 Not Found` when no body is configured 
(RFC 9116: file presence is itself meaningful). `Builder.securityTxt(String)` 
sets the body. The class is structured to accept future `register(String 
suffix, Supplier<HttpResource> handler)` extensions without refactoring 
(TODO-69's `/.well-known/openid-configuration` will hook in here). Per-mixin 
test class `BasicWellKnownResource_AsMixin_Test` c [...]
+
+- **Framework alignment — `RestContext.buildMixinContext(...)`.** Discovered 
during `BasicFaviconResource_AsMixin_Test` debugging: when a host declares 
`@Bean BasicFaviconResource favicon() { return ...; }` the framework was 
silently bypassing the bean and constructing a fresh mixin via the mixin's own 
`Builder` — `BeanInstantiator.instantiate(Class)` short-circuits on a 
discovered `create()` static method before checking `BeanStore` for a 
pre-registered instance. Fixed by retuning `Rest [...]
+
+- **Phase 5 — Composition + OpenAPI hidden tests.**
+  - `BasicConvention_ParentChain_Test`: mounts all four convention mixins on a 
single host alongside the host's own `@RestGet("/items")` endpoint and asserts 
every convention path resolves correctly + the host's own endpoints continue to 
work. Confirms no path collisions / dedupe regressions across the four mixins.
+  - `BasicConvention_OpenApiHidden_Test`: mounts all four convention mixins 
alongside `BasicOpenApiResource` and asserts every convention path is excluded 
from the generated OpenAPI document while the host's own endpoints remain 
visible. Confirms `@OpSwagger(ignore=true)` flows correctly through the mixin 
context.
+
+- **Phase 6 — Real-container parity tests.**
+  - `BasicVersionResource_JettyMicroservice_Test`: boots a real 
`JettyMicroservice` carrying a host with `@Bean BasicVersionResource` 
configuration, makes HTTP requests to `/version`, `/info`, `/about`, and 
verifies content + `Content-Type: application/json`.
+  - `BasicVersionResource_Springboot_Test`: boots a full Spring Boot context 
(`@SpringBootTest(webEnvironment=RANDOM_PORT)`) with embedded Tomcat, registers 
a `BasicSpringRestServlet`-based host carrying `BasicVersionResource` as a 
mixin with a Spring `@Bean` providing the configured instance, makes HTTP 
requests, and verifies the same response shape. Pins the bridge between 
Spring's `BeanStore` adapter and the framework-level 
`RestContext.buildMixinContext` fix.
+  - Picked `BasicVersionResource` for the real-container parity tests (per the 
plan's guidance) since it has the richest content shape — JSON map sourced from 
manifest + git + JVM, format-pinned via `getDirectWriter`.
+
+- **Coverage hardening.** Added `BasicConvention_Builders_Test` with targeted 
unit tests for builder methods, helper methods, and exception paths that are 
hard to hit via full REST tests. Specifically: builder-method exclusivity 
(`bytes` vs `classpath` last-wins), `RobotsRule` semantics, 
`BasicVersionResource.Builder.fromManifest(Manifest)` and 
`fromGitProperties(InputStream)` overloads, manifest parsing edge cases (empty 
`Implementation-Vendor` skipped, second-candidate-wins-on-`Impleme [...]
+
+- **Phase 7 — Docs + release notes (in `juneau-docs`).**
+  - New topic page `pages/topics/10.14b.ConventionEndpointsMixins.md` (slug 
`ConventionEndpointsMixins`) sits next to `10.14a.StaticFilesMixin.md`, 
mirroring the FINISHED-75 + FINISHED-74 adjacent-mixin-pack precedent. 
Sections: four mixins at a glance (table); composing the pack (worked example 
with all four `@Bean` factories); standalone deployment; per-mixin notes 
(favicon defaults + builder methods, SEO content types + builder methods, 
version format-pinning + Spring Boot fat-jar tip [...]
+  - Release-notes section `### juneau-rest-server` → `#### 
Convention-Endpoints Mixin Pack (TODO-76)` in `pages/release-notes/9.5.0.md`. 
Five-piece entry: pack overview + per-mixin one-paragraph summaries (favicon, 
SEO, version, well-known) + framework-alignment note documenting the 
`RestContext.buildMixinContext` retune so the broader effect (any `@Bean 
MixinClass` now wins for every `@Rest(mixins=...)` mixin) is visible to readers 
landing on the release notes for unrelated reasons.
+
+- **Verification.**
+  - `./scripts/test.py -t` — full unit-test run **green** (~72s). All new test 
classes (`BasicFaviconResource_AsMixin_Test`, `BasicSeoResource_AsMixin_Test`, 
`BasicVersionResource_AsMixin_Test`, `BasicWellKnownResource_AsMixin_Test`, 
`BasicConvention_ParentChain_Test`, `BasicConvention_OpenApiHidden_Test`, 
`BasicVersionResource_JettyMicroservice_Test`, 
`BasicVersionResource_Springboot_Test`, `BasicConvention_Builders_Test`) 
discovered and pass; no pre-existing tests regressed by the `Res [...]
+  - `./scripts/test.py -b` — full build **green** (~33s); RAT header check 
passed on all new files (10 production + test files, 1 binary 
`juneau-favicon.ico`, 1 new `juneau-docs` topic page).
+  - `./scripts/coverage.py 
juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/convention/ 
--run` — package totals: **95% branches** (61/64) and **99% instructions** 
(870/880). Per-class: `BasicSeoResource` 100% / 100%, `BasicWellKnownResource` 
100% / 100%, `BasicVersionResource` 94% / 100%, `BasicFaviconResource` 90% / 
93%. Remaining uncovered lines are framework-guaranteed-unreachable: a 
defensive null-on-shipped-default-favicon branch and one defensive branch in 
`Basic [...]
+
+- **Acceptance criteria.**
+  - [x] Four sibling mixin classes in `org.apache.juneau.rest.convention`, 
each with `@Rest(paths=...)` runtime-overridable defaults.
+  - [x] Default favicon shipped as a small (~1 KB) classpath resource at 
`juneau-rest-server/src/main/resources/juneau-favicon.ico`.
+  - [x] `BasicSeoResource` ships RFC 9309 deny-all robots default + empty 
`<urlset>` sitemap default.
+  - [x] `BasicVersionResource` reads `META-INF/MANIFEST.MF` + `git.properties` 
+ JVM version with graceful fallback when any source is missing; Spring Boot 
fat-jar tip documented (`Builder.fromManifest(ClassLoader)`).
+  - [x] `BasicWellKnownResource` defaults to `404` for 
`/.well-known/security.txt`; reserves an extension seam for TODO-69's future 
entries.
+  - [x] All four endpoints carry `@OpSwagger(ignore=true)` and are excluded 
from any Swagger / OpenAPI spec generated by the api-docs mixin pack.
+  - [x] Per-mixin `_AsMixin_Test` classes (4 total) + composition test + 
OpenAPI-hidden test + at least one Jetty microservice parity test + at least 
one Spring Boot smoke test.
+  - [x] `BasicConvention_Builders_Test` covers builder edge cases + exception 
paths.
+  - [x] Topic page in `juneau-docs/pages/topics/` mirroring the FINISHED-74 / 
FINISHED-75 layout precedent.
+  - [x] Release-notes section in `juneau-docs/pages/release-notes/9.5.0.md` 
documenting the mixin pack + the framework-alignment retune.
+  - [x] `./scripts/test.py -t` green, `./scripts/test.py -b` green, 
`./scripts/coverage.py` reports ≥95% on the new package.
+  - [x] Nothing committed; nothing pushed.
+
+- **Deferred / out of scope for this session.**
+  - **Sitemap auto-generation from `BasicGroupOperations` index** — deferred 
to v2 per the resolved decision. v1 ships the static-config builder only.
+  - **`/info` and `/about` differentiation** — synonyms in v1 per the resolved 
decision; future iterations may differentiate (e.g. `/info` = condensed, 
`/about` = full).
+  - **Additional `/.well-known/*` entries** (OIDC discovery, change-password, 
etc.) — `BasicWellKnownResource` reserves the seam; TODO-69 owns the OIDC entry 
wiring.
diff --git a/todo/TODO.md b/todo/TODO.md
index 53b35b5f96..bbae181a7d 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -9,7 +9,7 @@ Recommended order for the TODO-67 through TODO-78 family. 
TODO-20 (rest debug re
 3. **TODO-69** — AuthN guards. Unblocks TODO-77's admin guard chain.
 4. ~~**TODO-74** — API-docs mixin pack (Swagger, Swagger-UI, OpenAPI, Redoc). 
Consumes the mixin sub-context model from FINISHED-81.~~ ✅ done — see 
`todo/FINISHED-74-mixin-api-docs.md`.
 5. ~~**TODO-75** — Static-files mixin (`BasicStaticFilesResource`).~~ ✅ done — 
see `todo/FINISHED-75-mixin-static-files.md`.
-6. **TODO-76** — Convention-endpoints pack (favicon / SEO / version / 
well-known).
+6. ~~**TODO-76** — Convention-endpoints pack (favicon / SEO / version / 
well-known).~~ ✅ done — see `todo/FINISHED-76-mixin-convention-endpoints.md`.
 7. **TODO-77** — Ops/introspection pack (echo / admin / route-index). Uses 
TODO-69.
 8. **TODO-78** — JSP module (`juneau-rest-server-view-jsp`).
 9. **TODO-67** — Observability (Micrometer + OpenTelemetry).
@@ -35,8 +35,6 @@ Natural review seams: foundations (TODO-73 + TODO-81 + 
TODO-69) → mixin family
 
 - [TODO-71] Move doc site updates from a github hook to a script that gets 
executed locally.  Change docusaurus search functionality to 
@easyops-cn/docusaurus-search-local. 
 
-- [TODO-76] Convention-endpoints mixin pack — `BasicFaviconResource`, 
`BasicSeoResource` (`/robots.txt`, `/sitemap.xml`), `BasicVersionResource` 
(`/version`, `/info`, `/about`), `BasicWellKnownResource` (`/.well-known/*`). 
See `todo/TODO-76-mixin-convention-endpoints.md`.
-
 - [TODO-77] Ops/introspection mixin pack — `BasicEchoResource` (Debug-gated), 
`BasicAdminResource` (guard-chain-gated, depends on TODO-69), 
`BasicRouteIndexResource`. See `todo/TODO-77-mixin-ops-introspection.md`.
 
 - [TODO-78] JSP servlet support module (`juneau-rest-server-view-jsp`) — new 
module shipping `BasicJspResource` mixin + `JspViewRenderer`; isolates Apache 
Jasper / `jakarta.servlet.jsp.*` / JSTL deps from core. See 
`todo/TODO-78-mixin-jsp-module.md`.

Reply via email to