Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Gapwise is one application serving dedicated university editions via host-based
| **University of Guelph** | Guelph Campus | WebAdvisor (text paste or `.ics`) | [guelph.gapwise.ca](https://guelph.gapwise.ca) |
| **University of Ottawa** | Downtown Campus | uoCampus (text paste or `.ics`) | [uottawa.gapwise.ca](https://uottawa.gapwise.ca) |
| **Brock University** | St. Catharines Campus | BrockDB / Student Portal (text paste or `.ics`) | [brock.gapwise.ca](https://brock.gapwise.ca) |
| **University of British Columbia** | Vancouver / Point Grey Campus | Workday View My Courses (table copy/paste) | [ubc.gapwise.ca](https://ubc.gapwise.ca) |
| **University of Waterloo** | Main Campus | Quest Class Schedule (list-view copy/paste) | [waterloo.gapwise.ca](https://waterloo.gapwise.ca) |

---

Expand Down
20 changes: 10 additions & 10 deletions docs/ECOSYSTEM_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ Gapwise is one product ecosystem implemented across seven product repositories a

## Repository graph

| Repository | Owns | Consumes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `data` | Canonical multi-university campus facts and geometry across 13 universities; routing graphs and reviewed entrances | Reviewed public/field evidence; core consumer tests |
| `gapwise` | Web/PWA across 11 university editions, timetable/gap/routing semantics, encrypted private state, API v1, OpenAPI and both SDKs | Validated build-time Data mirror; explicit AI delegation |
| `android` | Kotlin/Compose UX, Keystore persistence, native import and encrypted-sync adapter (focuses on U of T) | Core HTTP/crypto contracts and public campus APIs |
| `ios` | SwiftUI UX and portable local timetable foundation (focuses on U of T) | Campus-scoped identity; broader native campus/account integrations remain pending |
| `ai` | OAuth/MCP authorization, minimized snapshots and bounded preference actions | Core public API and browser-authoritative delegated snapshots |
| `docs` | Public developer documentation | Released producer contracts, including AI's generated MCP manifest |
| `status` | Independent probes and incident presentation | Public service endpoints and operator-confirmed facts |
| `.github` | Organization contributor, support and security guidance | Ownership boundaries above |
| Repository | Owns | Consumes |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `data` | Canonical multi-university campus facts and geometry across 13 universities; routing graphs and reviewed entrances | Reviewed public/field evidence; core consumer tests |
| `gapwise` | Web/PWA across 13 university editions and 15 campus models, timetable/gap/routing semantics, encrypted private state, API v1, OpenAPI and both SDKs | Validated build-time Data mirror; explicit AI delegation |
| `android` | Kotlin/Compose UX, Keystore persistence, native import and encrypted-sync adapter (focuses on U of T) | Core HTTP/crypto contracts and public campus APIs |
| `ios` | SwiftUI UX and portable local timetable foundation (focuses on U of T) | Campus-scoped identity; broader native campus/account integrations remain pending |
| `ai` | OAuth/MCP authorization, minimized snapshots and bounded preference actions | Core public API and browser-authoritative delegated snapshots |
| `docs` | Public developer documentation | Released producer contracts, including AI's generated MCP manifest |
| `status` | Independent probes and incident presentation | Public service endpoints and operator-confirmed facts |
| `.github` | Organization contributor, support and security guidance | Ownership boundaries above |

## Public developer platform

Expand Down
2 changes: 2 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://gapwise.ca/</loc></url>
<url><loc>https://gapwise.ca/about</loc></url>
<url><loc>https://gapwise.ca/universities</loc></url>
<url><loc>https://gapwise.ca/open-source</loc></url>
<url><loc>https://gapwise.ca/utm-timetable</loc></url>
<url><loc>https://gapwise.ca/campus-map</loc></url>
<url><loc>https://gapwise.ca/gap-planner</loc></url>
Expand Down
82 changes: 48 additions & 34 deletions scripts/build-seo-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ const SITE_ORIGIN = "https://gapwise.ca";
const GITHUB_ORGANIZATION = "https://github.com/GapwiseHQ";
const GITHUB_CORE = `${GITHUB_ORGANIZATION}/gapwise`;

const universitiesManifest = JSON.parse(await readFile("universities.json", "utf8")) as {
universities: Array<{
id: string;
name: string;
shortName: string;
campusScope: string;
hosts: string[];
campuses: string[];
enabledFeatures?: { routing?: boolean };
}>;
};
const supportedUniversityCount = universitiesManifest.universities.length;
const supportedCampusCount = universitiesManifest.universities.reduce(
(count, university) => count + university.campuses.length,
0,
);

export type UniversityContext = {
id: string;
name: string;
Expand Down Expand Up @@ -90,8 +107,7 @@ const PAGES: readonly SeoPage[] = [
description:
"Gapwise is a free and open-source timetable, campus navigation, and student planning platform for students across multiple Canadian universities.",
heading: "Make the time between classes count.",
detail:
"Import your timetable in your browser, explore source-backed campus maps, understand the usable time between classes, and plan your campus day across Canadian universities. The default edition supports University of Toronto (UTM, UTSG, UTSC) with ACORN import and pedestrian routing for UTM. Dedicated editions are available for Carleton, TMU, Queen's, Laurier, York, McMaster, Western, Guelph, uOttawa, and Brock.",
detail: `Import your timetable in your browser, explore source-backed campus maps, understand the usable time between classes, and plan your campus day across ${supportedUniversityCount} Canadian universities and ${supportedCampusCount} campus models.`,
sections: [
{
title: "Your timetable, connected to campus context",
Expand Down Expand Up @@ -152,8 +168,7 @@ const PAGES: readonly SeoPage[] = [
{
path: "/developers",
title: "Gapwise API & SDKs — Developers",
description:
"Build with the Gapwise public campus building, place, routing, and deterministic gap-planning API, OpenAPI contract, and official SDKs across all 12 supported Canadian universities.",
description: `Build with the Gapwise public campus building, place, routing, and deterministic gap-planning API, OpenAPI contract, and official SDKs across all ${supportedUniversityCount} supported Canadian universities.`,
heading: "Deterministic campus intelligence for developers.",
detail:
"Gapwise publishes a bounded public API for campus buildings, places, routing, and gap planning across supported universities, with OpenAPI plus JavaScript/TypeScript and Python SDK documentation.",
Expand Down Expand Up @@ -251,10 +266,10 @@ function outputPath(path: string) {

function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
const origin = uniContext?.origin ?? SITE_ORIGIN;
const organizationId = `${origin}/#organization`;
const organizationId = `${SITE_ORIGIN}/#organization`;
const websiteId = `${origin}/#website`;
const appId = `${origin}/#app`;
const founderId = `${origin}/#andrew-muratov`;
const founderId = `${SITE_ORIGIN}/#andrew-muratov`;
const isDedicatedTenant = Boolean(
uniContext && (uniContext.id !== "uoft" || uniContext.campusName),
);
Expand All @@ -266,13 +281,12 @@ function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
"@type": "Organization",
"@id": organizationId,
name: "Gapwise",
url: `${origin}/`,
description: uniName
? `Privacy-first timetable intelligence, campus maps, and day planning for ${uniName} students.`
: "Privacy-first timetable intelligence, campus maps, and day planning for university students across Canada.",
url: `${SITE_ORIGIN}/`,
description:
"Gapwise is the free and open-source university timetable, campus navigation, routing, and student-planning platform for Canadian universities.",
logo: {
"@type": "ImageObject",
url: `${origin}/icon-512.png`,
url: `${SITE_ORIGIN}/icon-512.png`,
width: 512,
height: 512,
},
Expand All @@ -298,16 +312,17 @@ function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
{
"@type": "WebSite",
"@id": websiteId,
name: uniName ? `Gapwise — ${uniName}` : "Gapwise",
name: uniName ? `Gapwise for ${uniName}` : "Gapwise",
url: `${origin}/`,
description: page.description,
inLanguage: "en-CA",
publisher: { "@id": organizationId },
...(origin !== SITE_ORIGIN ? { isPartOf: { "@id": `${SITE_ORIGIN}/#website` } } : {}),
},
{
"@type": "WebApplication",
"@type": ["WebApplication", "SoftwareApplication"],
"@id": appId,
name: uniName ? `Gapwise — ${uniName}` : "Gapwise",
name: uniName ? `Gapwise for ${uniName}` : "Gapwise",
url: `${origin}/`,
description: page.description,
applicationCategory: "EducationalApplication",
Expand All @@ -316,6 +331,7 @@ function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
inLanguage: "en-CA",
creator: { "@id": founderId },
publisher: { "@id": organizationId },
isPartOf: { "@id": websiteId },
offers: {
"@type": "Offer",
price: "0",
Expand All @@ -336,10 +352,10 @@ function homepageStructuredData(page: SeoPage, uniContext?: UniversityContext) {
"Optional encrypted private sync",
]
: [
"Browser-local timetable import (ACORN .ics, Carleton Central, TMU, Queen's, Laurier, York, McMaster, Western, Guelph, uOttawa, Brock schedule formats)",
"Mississauga, St. George, Scarborough, Carleton, TMU, Queen's, Laurier, York, McMaster, Western, Guelph, uOttawa, and Brock campus timetable identity",
"Source-backed building maps for all 12 supported universities",
"Source-backed UTM pedestrian routing",
`Browser-local timetable import across ${supportedUniversityCount} supported Canadian universities`,
`University-specific timetable identity across ${supportedCampusCount} campus models`,
`Source-backed building maps for all ${supportedUniversityCount} supported universities`,
"Campus pedestrian routing with source-classified entrances and explicit uncertainty",
"Optional encrypted private sync",
],
sameAs: [GITHUB_CORE],
Expand Down Expand Up @@ -393,6 +409,8 @@ function fallback(page: SeoPage, uniContext?: UniversityContext) {
? ([
["/", "Gapwise home"],
["/about", "About Gapwise"],
["/universities", "Supported universities"],
["/open-source", "Open source"],
["/campus-map", "Campus map"],
["/gap-planner", "Gap planner"],
["/campus-routing", "Campus routing"],
Expand All @@ -407,6 +425,8 @@ function fallback(page: SeoPage, uniContext?: UniversityContext) {
: ([
["/", "Gapwise home"],
["/about", "About Gapwise"],
["/universities", "Supported universities"],
["/open-source", "Open source"],
["/utm-timetable", "UTM timetable"],
["/gap-planner", "Gap planner"],
["/campus-map", "Campus map"],
Expand Down Expand Up @@ -447,20 +467,13 @@ function fallback(page: SeoPage, uniContext?: UniversityContext) {
page.path === "/" && !isDedicatedTenant
? `<section aria-labelledby="supported-universities-heading">
<h2 id="supported-universities-heading">Supported Canadian Universities</h2>
<p>Gapwise provides dedicated editions with university-specific timetable import, campus data, and verified destinations:</p>
<ul>
<li><a href="https://gapwise.ca">University of Toronto</a> — Mississauga, St. George, and Scarborough</li>
<li><a href="https://carleton.gapwise.ca">Carleton University</a> — Ottawa campus</li>
<li><a href="https://tmu.gapwise.ca">Toronto Metropolitan University</a> — Downtown Toronto campus</li>
<li><a href="https://queens.gapwise.ca">Queen's University</a> — Kingston campus</li>
<li><a href="https://laurier.gapwise.ca">Wilfrid Laurier University</a> — Waterloo campus</li>
<li><a href="https://york.gapwise.ca">York University</a> — Keele campus</li>
<li><a href="https://mcmaster.gapwise.ca">McMaster University</a> — Hamilton campus</li>
<li><a href="https://western.gapwise.ca">Western University</a> — London campus</li>
<li><a href="https://guelph.gapwise.ca">University of Guelph</a> — Guelph campus</li>
<li><a href="https://uottawa.gapwise.ca">University of Ottawa</a> — Downtown Ottawa campus</li>
<li><a href="https://brock.gapwise.ca">Brock University</a> — St. Catharines campus</li>
</ul>
<p>Gapwise provides dedicated editions with university-specific timetable import and source-backed campus data:</p>
<ul>${universitiesManifest.universities
.map(
(university) =>
`<li><a href="https://${university.hosts[0]}">${escapeHtml(university.name)}</a> — ${escapeHtml(university.campusScope)}</li>`,
)
.join("")}</ul>
</section>`
: "";

Expand Down Expand Up @@ -518,6 +531,8 @@ function renderDocument(baseHtml: string, page: SeoPage, uniContext?: University
const UNIVERSITY_COMMON_PATHS = new Set([
"/",
"/about",
"/universities",
"/open-source",
"/campus-map",
"/gap-planner",
"/campus-routing",
Expand All @@ -534,7 +549,7 @@ function getUniversityPage(page: SeoPage, uni: UniversityContext): SeoPage {
if (page.path === "/") {
return {
path: "/",
title: `Gapwise — ${uni.name}`,
title: `Gapwise for ${uni.name} — Timetable & Campus Navigation`,
description: `Gapwise is a free and open-source timetable, campus navigation, and student planning platform for ${uni.name} students.`,
heading: "Make the time between classes count.",
detail:
Expand Down Expand Up @@ -651,7 +666,6 @@ for (const { campusId, context } of UOFT_CAMPUS_EDITIONS) {
await writeFile(join(campusDir, "robots.txt"), renderRobotsTxt(`${context.origin}/sitemap.xml`));
}

const universitiesManifest = JSON.parse(await readFile("universities.json", "utf8"));
let universityCount = 0;
const universityPageCounts: Record<string, number> = {};

Expand Down
Loading
Loading