mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(web): sitemap and llms.txt list public pages at their canonical /page/ URL (#166)
This commit is contained in:
committed by
GitHub
parent
d36c9a659d
commit
ec3eaa6437
@@ -57,7 +57,7 @@ export const ABOUT_FAQ: AboutFaqItem[] = [
|
||||
{
|
||||
question: "Is Autopilot Monitor free and open source?",
|
||||
answer:
|
||||
"Yes. The Community plan is free and stays free, includes the complete current feature set, and is meant for production fleets, not just labs. The source code is on GitHub: the agent under the MIT license, the backend, portal, and MCP server under AGPL-3.0. A commercial Pro plan is coming for organizations that need reliability commitments, priority support, longer data retention, and delegated administration across customer tenants.",
|
||||
"Yes. The Community plan is free, stays free, and is meant for production fleets, not just labs: live monitoring, the full rules engine including custom rules, fleet analytics, notifications, diagnostics, and AI integration through MCP within usage limits, with 90-day data retention. The source code is on GitHub: the agent under the MIT license, the backend, portal, and MCP server under AGPL-3.0. The optional Pro plan, sold through Microsoft Marketplace and Cleverbridge, adds 365-day retention, higher API and AI usage limits, delegated administration across customer tenants, OOBE bootstrap sessions, reliability commitments, and priority support; a tenant administrator can try it free for 30 days.",
|
||||
},
|
||||
{
|
||||
question: "How is the agent deployed, and does it stay on the device?",
|
||||
|
||||
@@ -18,7 +18,7 @@ import AppInsightsInit from "../components/AppInsightsInit";
|
||||
import ChunkReloadRecovery from "../components/ChunkReloadRecovery";
|
||||
import { HostRoutingGuard } from "../components/HostRoutingGuard";
|
||||
import { LegacyPathRedirect } from "../components/LegacyPathRedirect";
|
||||
import { API_BASE_URL, DOCS_URL, SITE_URL } from "@/utils/config";
|
||||
import { API_BASE_URL, DOCS_URL, SITE_URL, sitePageUrl } from "@/utils/config";
|
||||
import { AUTH_HINT_INLINE_SCRIPT } from "@/lib/authHint";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
@@ -145,7 +145,7 @@ const jsonLd = {
|
||||
name: "Community plan",
|
||||
price: "0",
|
||||
priceCurrency: "EUR",
|
||||
url: `${SITE_URL}/plans`,
|
||||
url: sitePageUrl("/plans"),
|
||||
},
|
||||
author: { "@id": PERSON_ID },
|
||||
creator: { "@id": PERSON_ID },
|
||||
|
||||
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import { isProViaMsp, missingContactProfileParts, trialDaysLeft } from "@/lib/edition";
|
||||
import { PlanCards } from "@/components/plans/PlanCards";
|
||||
import { SITE_URL } from "@/utils/config";
|
||||
import { sitePageUrl } from "@/utils/config";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
@@ -108,7 +108,7 @@ export function SectionPlan() {
|
||||
<p className="mt-3 text-xs text-gray-600">
|
||||
Ready to buy? Pro is sold through Microsoft Marketplace and Cleverbridge —{" "}
|
||||
<a
|
||||
href={`${SITE_URL}/buy`}
|
||||
href={sitePageUrl("/buy")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-purple-700 hover:underline dark:text-purple-300"
|
||||
|
||||
@@ -3,9 +3,7 @@ export const dynamic = "force-static";
|
||||
|
||||
import type { MetadataRoute } from "next";
|
||||
import { PAGE_LASTMOD } from "@/utils/page-lastmod.generated";
|
||||
import { SITE_URL } from "@/utils/config";
|
||||
|
||||
const BASE_URL = SITE_URL;
|
||||
import { sitePageUrl } from "@/utils/config";
|
||||
|
||||
// Build-time only (static export): a URL missing from PAGE_LASTMOD means
|
||||
// scripts/generate-lastmod.js drifted from this list -- fail the build rather
|
||||
@@ -24,49 +22,49 @@ function lastmod(urlPath: string): Date {
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: `${BASE_URL}/`,
|
||||
url: sitePageUrl("/"),
|
||||
lastModified: lastmod("/"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/about`,
|
||||
url: sitePageUrl("/about"),
|
||||
lastModified: lastmod("/about"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/get-started`,
|
||||
url: sitePageUrl("/get-started"),
|
||||
lastModified: lastmod("/get-started"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/plans`,
|
||||
url: sitePageUrl("/plans"),
|
||||
lastModified: lastmod("/plans"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/buy`,
|
||||
url: sitePageUrl("/buy"),
|
||||
lastModified: lastmod("/buy"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/help`,
|
||||
url: sitePageUrl("/help"),
|
||||
lastModified: lastmod("/help"),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/privacy`,
|
||||
url: sitePageUrl("/privacy"),
|
||||
lastModified: lastmod("/privacy"),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/terms`,
|
||||
url: sitePageUrl("/terms"),
|
||||
lastModified: lastmod("/terms"),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.3,
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
> Free, open-source real-time monitoring and troubleshooting for Windows Autopilot enrollments managed through Microsoft Intune. A lightweight, temporary agent streams enrollment events to a web portal, analyze rules flag failure patterns automatically, and a Model Context Protocol (MCP) server lets AI assistants query the data. Created and maintained by Oliver Kieselbach (Microsoft MVP); the hosted service is operated by glueckkanja AG and runs in Azure Germany West Central.
|
||||
|
||||
Autopilot Monitor is for IT administrators, helpdesk engineers, and managed service providers who deploy Windows devices with Intune and Autopilot. The Community plan is free and covers the full current feature set; a Pro plan with reliability commitments, priority support, longer retention, and delegated administration across customer tenants is coming.
|
||||
Autopilot Monitor is for IT administrators, helpdesk engineers, and managed service providers who deploy Windows devices with Intune and Autopilot. The Community plan is free and stays free: live monitoring, the full rules engine, fleet analytics, and AI integration (MCP) within usage limits. The optional Pro plan adds 365-day instead of 90-day retention, higher usage limits, delegated administration across customer tenants, OOBE bootstrap sessions, reliability commitments, and priority support; it is sold through Microsoft Marketplace and Cleverbridge, and a tenant administrator can try it free for 30 days.
|
||||
|
||||
## Portal
|
||||
|
||||
- [Home](https://www.autopilotmonitor.com/): product overview
|
||||
- [About](https://www.autopilotmonitor.com/about): what it is, how it works, common questions
|
||||
- [Get started](https://www.autopilotmonitor.com/get-started): first steps for a new tenant
|
||||
- [Plans](https://www.autopilotmonitor.com/plans): Community vs. Pro
|
||||
- [Help](https://www.autopilotmonitor.com/help): support channels
|
||||
- [Privacy Policy](https://www.autopilotmonitor.com/privacy)
|
||||
- [Terms of Use](https://www.autopilotmonitor.com/terms)
|
||||
- [About](https://www.autopilotmonitor.com/about/): what it is, how it works, common questions
|
||||
- [Get started](https://www.autopilotmonitor.com/get-started/): first steps for a new tenant
|
||||
- [Plans](https://www.autopilotmonitor.com/plans/): Community vs. Pro
|
||||
- [Get Pro](https://www.autopilotmonitor.com/buy/): purchase options and the free 30-day trial
|
||||
- [Help](https://www.autopilotmonitor.com/help/): support channels
|
||||
- [Privacy Policy](https://www.autopilotmonitor.com/privacy/)
|
||||
- [Terms of Use](https://www.autopilotmonitor.com/terms/)
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import nextConfig from "../../next.config";
|
||||
import sitemap from "../../app/sitemap";
|
||||
import { SITE_URL, sitePageUrl } from "../config";
|
||||
|
||||
/**
|
||||
* Public pages are served at /page/ only: the static export writes
|
||||
* page/index.html (next.config trailingSlash: true), SWA's trailingSlash "auto"
|
||||
* 301-redirects /page to /page/, and Next emits /page/ as the canonical URL.
|
||||
* Page URLs handed to crawlers outside Next metadata (sitemap, llms.txt) must
|
||||
* use that form, or each one is a redirect to a different canonical URL
|
||||
* (Search Console: "Page with redirect").
|
||||
*/
|
||||
|
||||
const WEB_ROOT = join(__dirname, "..", "..");
|
||||
|
||||
/** Served form: SITE_URL origin, path "/" or "/segment/.../". */
|
||||
function expectServedForm(url: string): void {
|
||||
const parsed = new URL(url);
|
||||
expect(parsed.origin, url).toBe(SITE_URL);
|
||||
expect(parsed.pathname, url).toMatch(/^\/(?:[^/]+\/)*$/);
|
||||
}
|
||||
|
||||
describe("public page URLs use the served /page/ form", () => {
|
||||
it("the export and SWA serve public pages at /page/", () => {
|
||||
expect(nextConfig.trailingSlash).toBe(true);
|
||||
const swa = JSON.parse(readFileSync(join(WEB_ROOT, "staticwebapp.config.json"), "utf-8"));
|
||||
expect(swa.trailingSlash).toBe("auto");
|
||||
});
|
||||
|
||||
it("sitePageUrl appends the trailing slash exactly once", () => {
|
||||
expect(sitePageUrl("/")).toBe(`${SITE_URL}/`);
|
||||
expect(sitePageUrl("/plans")).toBe(`${SITE_URL}/plans/`);
|
||||
expect(sitePageUrl("/plans/")).toBe(`${SITE_URL}/plans/`);
|
||||
});
|
||||
|
||||
it("sitemap lists every page in the served form", () => {
|
||||
const urls = sitemap().map((entry) => entry.url);
|
||||
expect(urls.length).toBeGreaterThan(0);
|
||||
for (const url of urls) expectServedForm(url);
|
||||
});
|
||||
|
||||
it("llms.txt links site pages in the served form", () => {
|
||||
const llms = readFileSync(join(WEB_ROOT, "public", "llms.txt"), "utf-8");
|
||||
const links = [...llms.matchAll(/\]\(([^)\s]+)\)/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((url) => new URL(url).origin === SITE_URL);
|
||||
expect(links.length).toBeGreaterThan(0);
|
||||
for (const url of links) expectServedForm(url);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,19 @@ export const DOCS_URL = "https://docs.autopilotmonitor.com";
|
||||
/** Public marketing/product website (also feeds metadataBase/sitemap/robots). */
|
||||
export const SITE_URL = "https://www.autopilotmonitor.com";
|
||||
|
||||
/**
|
||||
* Absolute URL of a public page in the /page/ form the site serves: the static
|
||||
* export writes page/index.html (next.config trailingSlash: true) and SWA
|
||||
* 301-redirects /page to /page/. Next applies that form to metadata URLs
|
||||
* (canonical, og:url) itself; every page URL built outside metadata (sitemap,
|
||||
* JSON-LD, absolute links) goes through here, so crawlers get the canonical URL
|
||||
* instead of a redirect. sitePageUrl.guard.test.ts pins the coupling.
|
||||
*/
|
||||
export function sitePageUrl(path: `/${string}`): string {
|
||||
const slashed = path.endsWith("/") ? path : `${path}/`;
|
||||
return `${SITE_URL}${slashed}`;
|
||||
}
|
||||
|
||||
/** Customer portal (deep links rendered into generated bootstrap scripts). */
|
||||
export const PORTAL_URL = "https://portal.autopilotmonitor.com";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user