# Phase 6 — Public-facing website
**Date:** 23 August 2026
**Baseline:** `0595769` (Phase 5.5)
> **No institute facts, student records, results, testimonials, fees, batches,
> contact details, achievements, or other real-world claims were invented
> during Phase 6.**
>
> Everything the site states is traceable to `src/config/institute.ts`, the
> logo artwork, or a row an administrator entered. Everything else is either
> absent or handled by an empty state that says so plainly.
---
## 1. Routes created
| Route | Rendering | Data |
| --- | --- | --- |
| `/about` | Static | Config only |
| `/courses` | ISR 1h | Config + batches |
| `/courses/[slug]` ×5 | SSG + ISR 1h | Config + batches |
| `/results` | Dynamic | Published + consented results |
| `/stories` | ISR 1h | Published + consented stories |
| `/announcements` | ISR 15m | Announcements inside their window |
## 2. Routes modified
`/` (rebuilt with real data bands), `src/app/sitemap.ts` (8 public routes),
`src/config/nav.ts` (dead links removed).
## 3. Components created
`src/components/domain/public-cards.tsx` — `ResultCard`, `StoryCard`,
`BatchCard`, `CourseCard`, `AnnouncementCard`, plus the shared `Portrait` /
`Monogram` pair that renders initials wherever a photograph is not authorised.
## 4. Database queries added
All in `src/lib/public-data.ts`: `getPublishedResults`, `getPublishedStories`,
`getUpcomingBatches`, `getActiveAnnouncements`, `getTopAnnouncement`.
## 5. Public data exposure rules
**Consent metadata never leaves the server.** Every query selects the consent
columns, runs `present()` on the server, and returns **only the resolved
presentation**. The type a component receives has no `consentRef`, no
`consentPhoto`, no `studentName`.
A component cannot leak a field it was never handed.
| Content | Database-side filter |
| --- | --- |
| Results | `published AND consentResult AND consentRef IS NOT NULL` |
| Stories | `published AND consentStory AND consentRef IS NOT NULL` |
| Batches | `published AND startsAt >= now()` |
| Announcements | `published AND startsAt <= now() AND endsAt >= now()` |
Filtering is in SQL, never in JavaScript. `?programme=` is narrowed against the
enum, so an unknown value becomes "no filter" rather than reaching Prisma.
## 6. Consent handling
- A result grant does not publish a name; a name grant does not publish a photo.
- A **story grant does not publish a photograph** — verified twice, once
through the rendered page and once by the database refusing the row.
- Where a name is not authorised, the card leads with the achievement and shows
a monogram tile.
## 7. Security decisions
- `src/lib/public-data.ts` imports `server-only`.
- `/admin` absent from navigation, sitemap, and every public HTML payload.
- `Course` structured data carries name and provider only — **no price, no
duration, no rating**. A fabricated `offers` block is what earns a manual
action.
- Announcement links are restricted to on-site paths (validated in Phase 5).
## 8. Accessibility
Checked on all 8 public pages: **exactly one `
` each**, **zero images
missing alt text**, `lang="en-IN"`, skip-to-content link present. Status is
never colour-only. Filters and pagination are real links, so they work by
keyboard and without JavaScript.
## 9. SEO
Per-route metadata and canonicals; `EducationalOrganization` on the homepage
and `Course` per course page; sitemap lists all 8 public routes and excludes
`/admin`. **No `AggregateRating`, no `Review`, no founding date, no invented
telephone or coordinates.** `robots.txt` still disallows everything — the site
is pre-launch, and that flips in Phase 7.
## 10. Performance
Server-first throughout. **Every public page added this phase is a server
component**; no client component was introduced. Static or ISR wherever
possible — only `/results` is dynamic, because it reads `searchParams`.
Responsive audit: **0 fixed widths above 320px** across all 8 pages. The
`max-w-[1200px]` container is a *max* width and shrinks correctly.
## 11–12. Tests added, and results
New: `scripts/verify-public-isolation.mjs` — **50 assertions** against real
PostgreSQL and real HTTP, run in three phases (seed → start app → assert) so
ISR caching cannot mask a failure.
| Suite | Result |
| --- | --- |
| Public data isolation | **50 / 50 PASS** |
| Consent constraints | 35 / 35 PASS |
| End-to-end admin + enquiry | 62 / 62 PASS |
| Unit tests | 67 / 67 PASS |
| **Total** | **214 automated checks** |
Covering every case requested:
- unpublished result / story / batch / announcement **does not appear**
- published + consented **appears**
- name without name consent **does not leak** the name
- photo attached to an unpublished record **never renders**
- database **refuses** a published photo without photo consent
- expired batch **does not appear as upcoming**; expired and future-dated
announcements **do not appear**
- `consentRef`, `consentPhoto`, `consentName`, `consentResult`, `consentStory`,
`publishedAt`, `displayNameMode` — **all absent from public HTML**
- every internal link on the homepage resolves (11 checked, 0 dead)
## 13–16. Verification
| Check | Result |
| --- | --- |
| Typecheck | clean |
| Lint | 0 errors, 0 warnings |
| Build | 40 routes |
| Dependency audit | **0 vulnerabilities** |
| Secret scan | **1,035 KB** across 9 public pages + 11 assets — clean |
The scan found no `DATABASE_URL`, no secrets, no `PrismaClient`, no
`passwordHash`, no `requireAdmin`, no `ipHash`, no consent fields, no
`studentName`, and no `/admin` reference in any public payload.
## 17. Two real bugs found and fixed
**1. The navigation pointed at four pages that did not exist.**
`/faculty`, `/reviews`, `/videos` and `/gallery` were all 404s served from the
most prominent element on the site. They are removed until their pages exist
and the content behind them is confirmed. A comment in `nav.ts` states the rule:
*a route appears here only if the page exists.*
**2. Publishing did not update the public site.**
Admin actions called `revalidatePath` on `/admin/*` only — **never on any
public route**. A teacher would publish an announcement, watch the website not
change for up to an hour, and reasonably conclude the admin was broken. Fixed
with `src/lib/revalidate-public.ts`, wired into all four action files.
Also corrected: three flaws in my own test harness — a photo fixture that
collided with the site logo, assertions that counted `next/image` `srcSet`
entries as separate photos, and seeding after the server had already cached
the pages. All were test bugs, not application bugs, and are reported here
rather than quietly fixed.
## 18. Files changed
Created: `src/lib/public-data.ts`, `src/lib/revalidate-public.ts`,
`src/components/domain/public-cards.tsx`, `src/app/about/page.tsx`,
`src/app/courses/page.tsx`, `src/app/courses/[slug]/page.tsx`,
`src/app/results/page.tsx`, `src/app/stories/page.tsx`,
`src/app/announcements/page.tsx`, `scripts/verify-public-isolation.mjs`,
`docs/PHASE-6-REPORT.md`.
Modified: `src/app/page.tsx`, `src/app/sitemap.ts`, `src/config/nav.ts`,
`src/config/institute.ts` (courses published), the four admin action files
(public revalidation), `README.md`, `docs/README.md`, `package.json`.
## 19. Intentionally not implemented
| Not built | Why |
| --- | --- |
| `/faculty` | No verified names, credentials or portraits. Inventing faculty is fabrication. |
| `/reviews` | Review Engine not activated — needs Google Business Profile API access. |
| `/videos` | No YouTube channel ID supplied. |
| `/gallery` | No photography supplied. |
| Course syllabus / fees / timings | Not supplied. The pages say so and offer a way to ask. |
| Individual story pages | Two published stories do not justify a route; the cards carry the full text. |
| Map embed | No Place ID or coordinates. |
## 20. Blocked by missing manual setup
Hosted database (Neon account), professional email + sending domain — both
unchanged from Phase 5.5 and both outside this phase's scope.
## 21. Institute information still missing
Faculty names and credentials · founding year and story · course syllabus,
fees, duration and timings · verified NAP and opening hours · Place ID and
coordinates · professional email · social profiles · photography · real
results, stories and consent records · transparent/vector logo.
**Every one of these is absent from the site rather than guessed at.**
## 22. Recommended next phase
**Phase 7 — content collection and launch.** The build is now ahead of the
content by a wide margin, and has been for four phases. The highest-value work
is no longer engineering:
1. Provision the Neon database (Phase 5.5 §1) and run the migration.
2. Collect the blocking content in §21 — especially faculty, the founder's
story, and course details, which unblock three pages at once.
3. Flip `robots.ts` and the root `noindex` at launch.
I would not add more features. The site does what it should; what it needs is
true information to put in it.
---
## The content-integrity statement
Every claim on the public site traces to one of three sources:
1. **The logo artwork** — the institute's name and the tagline *"Exclusive
Institute for Commerce Education"*, both read verbatim.
2. **`src/config/institute.ts`** — locality, address, phone numbers (marked
`unverified` and pending client confirmation) and the programme list.
3. **A database row an administrator entered** — results, stories, batches,
announcements, each gated on consent.
Nothing else is stated. No founding year, no student count, no pass rate, no
faculty, no fees, no achievements, no ratings, no testimonials. Where a visitor
would expect one of those, they find a short sentence explaining it will be
published, and a way to ask a person instead.
**No fake student data was inserted.** All test fixtures were prefixed `ZZDEMO`
with deliberately non-human names, and every one was deleted — the database
ends this phase with **0 rows** across every content table.