Skip to content

Commit e22cdee

Browse files
committed
copilot instructions
1 parent c2813fd commit e22cdee

4 files changed

Lines changed: 339 additions & 0 deletions

File tree

‎.github/copilot-instructions.md‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# adaptTo() Website – Copilot Instructions
2+
3+
adaptTo() conference website built on **Adobe Edge Delivery Services (EDS / AEM Franklin)**.
4+
Content is authored in Microsoft SharePoint/Google Docs and served as semantic HTML, then
5+
progressively enhanced client-side by vanilla ES modules. There is **no build step** and **no
6+
framework** – plain JavaScript (ES modules), CSS, and the AEM `aem.js` library.
7+
8+
## Big picture
9+
10+
- **Authors** edit documents (one document = one page) and spreadsheets (`.xlsx`). EDS publishes
11+
them as HTML pages plus JSON endpoints (`query-index.json`, yearly `schedule-data.json`).
12+
- **The browser** loads a page, and `scripts/scripts.js` decorates the DOM: it builds sections,
13+
auto-blocks, and loads block JS/CSS on demand.
14+
- **Blocks** (`blocks/<name>/<name>.js` + `.css`) are the UI components. Each exports a default
15+
`decorate(block)` function that transforms its DOM subtree and may fetch data.
16+
- **Services** (`scripts/services/`) encapsulate all **business logic / data access** – reading the
17+
query index and schedule sheets, resolving speakers/talks, filtering the archive. Blocks should
18+
delegate data work to services, not reimplement it.
19+
- **Utils** (`scripts/utils/`) are small stateless helpers (DOM, dates, paths, metadata parsing).
20+
21+
```
22+
content (Docs + Sheets)
23+
│ published by EDS
24+
▼
25+
query-index.json / schedule-data.json / HTML pages
26+
│ fetched by
27+
▼
28+
scripts/services/* ──► blocks/*/*.js ──► decorated DOM
29+
▲ ▲
30+
└── scripts/utils/* ──────┘
31+
```
32+
33+
## Yearly editions & content model
34+
35+
- The site hosts one edition per year. **URL convention is the source of truth**:
36+
- `/<year>/` – site root of an edition (e.g. `/2024/`), matched by `/^\/\d\d\d\d\/$/`.
37+
- `/<year>/schedule/<talk>` – a main talk detail page.
38+
- `/<year>/schedule/<talk>/<lightning>` – a lightning talk (one level deeper).
39+
- `/speakers/<name>` – speaker pages, which live **outside** the yearly tree and are shared
40+
across editions. Use a `#<year>` hash to bind a speaker page to an edition.
41+
- Speaker metadata can change over the years; `uptoyear` and `speaker-alias` fields let a speaker
42+
have multiple variants. Resolution logic lives in `QueryIndex` (see services instructions).
43+
- Page behaviour is driven by **metadata** (`getMetadata(name)` from `aem.js`): `template`,
44+
`theme`, `include-aside-bar`, `include-teaser-bar`, `affiliation`, `video`, `article:tag`, etc.
45+
- Pre-2024 editions use a legacy design: `loadEager` adds the `design-2023` body class and loads
46+
`styles/styles-design-2023.css` when `getYearFromPath(...) < 2024`.
47+
48+
## Page lifecycle (`scripts/scripts.js`)
49+
50+
Three phases, mirroring the EDS pattern:
51+
1. **`loadEager`** – language, legacy-design detection, fullscreen handling, static header,
52+
template/theme auto-detection, `decorateMain`, render first section (LCP).
53+
2. **`loadLazy`** – header/footer, remaining sections, hash scroll, lazy CSS, consent management.
54+
3. **`loadDelayed`** – everything deferrable (loaded after 3s via `delayed.js`).
55+
56+
`decorateMain` runs `buildAutoBlocks`, which **synthesises blocks** based on page type:
57+
- Speaker pages → `speaker-detail` block (see `isSpeakerDetailPath`).
58+
- `theme === 'talk-detail'` pages → inserts `talk-detail-before-outline`,
59+
`talk-detail-after-outline`, `talk-detail-footer`, and relocates `talk-qa`.
60+
- Always extracts a `stage-header` section and appends `teaser-bar` / `aside-bar` fragments unless
61+
disabled via metadata.
62+
63+
When adding page-type behaviour, prefer extending `buildAutoBlocks` + a dedicated block over
64+
inlining logic in `scripts.js`.
65+
66+
## Conventions
67+
68+
- **ES modules only.** Imports must include the `.js` extension (enforced by ESLint
69+
`import/extensions`). Unix linebreaks. Style is `airbnb-base`.
70+
- **Build DOM with `scripts/utils/dom.js`** (`append`, `prepend`) instead of verbose
71+
`createElement` + `append` chains. Use the `html` tagged template (`utils/htmlTemplateTag.js`)
72+
for larger markup – it auto-escapes interpolations (prefix with `$` to opt out).
73+
- **All fetches go through `utils/fetch.js`** cache helpers so a browser reload force-refreshes data.
74+
- **Never hardcode `/2024/` style paths.** Derive them with `utils/site.js` / `utils/path.js`
75+
helpers (`getSiteRootPath`, `getYearFromPath`, `getSchedulePath`, …).
76+
- Use JSDoc with `@typedef`/`@param`/`@returns`; the codebase is typed via JSDoc, not TypeScript.
77+
- Keep data/business logic in `scripts/services/`; keep blocks focused on presentation.
78+
79+
## Developer workflow
80+
81+
```sh
82+
npm install
83+
npm test # web-test-runner, files: test/**/*.test.js (with coverage)
84+
npm run test:watch
85+
npm run lint # lint:js (eslint) + lint:css (stylelint)
86+
```
87+
88+
Local preview uses the AEM CLI: `npm install -g @adobe/aem-cli` then `aem up` (serves at
89+
`http://localhost:3000`). CI runs build + tests and reports coverage to SonarCloud.
90+
91+
## Where to look
92+
93+
- Architecture / page lifecycle → `scripts/scripts.js`, this file.
94+
- **Talk & speaker data model and business logic** → `.github/instructions/data-services.instructions.md`.
95+
- Authoring blocks and how they consume data → `.github/instructions/blocks.instructions.md`.
96+
- Tests → `.github/instructions/testing.instructions.md`.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
applyTo: "blocks/**"
3+
---
4+
5+
# Blocks – authoring & consuming data
6+
7+
Blocks are the UI components of the site. Each block is a folder `blocks/<name>/` containing
8+
`<name>.js` and `<name>.css`. EDS loads a block's JS/CSS lazily when the block appears on a page.
9+
10+
## Anatomy
11+
12+
```js
13+
import { getQueryIndex } from '../../scripts/services/QueryIndex.js';
14+
import { append } from '../../scripts/utils/dom.js';
15+
16+
/**
17+
* @param {Element} block The block's root element
18+
*/
19+
export default async function decorate(block) {
20+
// transform block's DOM subtree in place
21+
}
22+
```
23+
24+
- Export a **default `decorate(block)`** function. It may be `async`.
25+
- It receives the block's root element (already in the DOM) and mutates that subtree.
26+
- Authored content arrives as nested `div` rows/cells; read it, then usually replace it
27+
(`block.textContent = ''` / `block.innerHTML = ...`) with the final markup.
28+
- Read block configuration (key/value rows) with `readBlockConfig(block)` from `aem.js`
29+
(e.g. `speaker-gallery` reads a `speakers` list this way).
30+
31+
## Data flow: always go through services
32+
33+
Blocks must get talk/speaker/schedule data from `scripts/services/`, never by fetching or parsing
34+
JSON directly. Typical patterns:
35+
36+
- **Query index**: `const queryIndex = await getQueryIndex();` then use methods like
37+
`getItem`, `getSpeaker`, `getTalksForSpeaker`, `getTalkSpeakerNames`,
38+
`getLightningTalkSpeakerNames` (see the data-services instructions).
39+
- **Schedule**: `const data = await getScheduleData(\`${siteRoot}schedule-data.json\`, forceReload);`
40+
then `data.getDays()` / `data.getTalkEntry(path)`.
41+
- **Archive**: `const archive = await getTalkArchive();` then `applyFilter` + `getFilteredTalks*`.
42+
43+
Resolve paths/years with `utils/site.js` and `utils/path.js`
44+
(`getSiteRootPath`, `getSiteRootPathAlsoForSpeakerPath`, `getYearFromPath`,
45+
`getSpeakerDetailPath`, `getArchivePath`, …). **Never hardcode a year or path.**
46+
47+
### Speaker rendering rules (when building speaker UI)
48+
49+
- Resolve a speaker for the current edition with `queryIndex.getSpeaker(name, siteRoot)` so the
50+
correct yearly variant (`uptoyear`) is chosen.
51+
- Link to a speaker detail page using `getSpeakerDetailPath(speakerItem, siteRoot)` — it appends
52+
the `#<year>` hash that binds the shared speaker page to the edition.
53+
- Use `createOptimizedPicture` (from `aem.js`) for speaker images; fall back to
54+
`/resources/img/speaker_placeholder.svg` when `speakerItem.image` is absent.
55+
- Use eager image loading only for the first few speakers (see `speaker-gallery`).
56+
57+
### Schedule / talk-detail rules
58+
59+
- Talk detail pages are driven by metadata (`theme: talk-detail`); the auto-blocks in
60+
`scripts.js` inject `talk-detail-before-outline`, `talk-detail-after-outline`,
61+
`talk-detail-footer`. Add talk-detail UI by editing those blocks.
62+
- Get a talk's time/duration via `scheduleData.getTalkEntry(document.location.pathname)`.
63+
- The `schedule` block owns parallel-track grouping (`buildGroupedEntries`) and day-tab
64+
navigation; active day comes from the `#day-<n>` hash or today's date.
65+
66+
## Markup & DOM conventions
67+
68+
- Build elements with `append`/`prepend` from `utils/dom.js` (the optional rest args are class
69+
names) rather than manual `createElement` chains.
70+
- For larger fragments use the `html` tagged template (`utils/htmlTemplateTag.js`): interpolations
71+
are **HTML-escaped by default**; prefix the placeholder with `$` only for trusted pre-escaped
72+
content.
73+
- React to in-page edition/day/filter changes via the `hashchange` event (several blocks reload or
74+
re-render on hash change).
75+
- Keep imports extension-explicit (`../../scripts/...js`) — ESLint enforces it.
76+
- Styling belongs in the block's own `.css`; class names are scoped under the block root.
77+
78+
## Adding a new block
79+
80+
1. Create `blocks/<name>/<name>.js` (default `decorate`) and `blocks/<name>/<name>.css`.
81+
2. Put any data/domain logic in a service under `scripts/services/`, not in the block.
82+
3. If the block should be auto-injected for a page type, wire it into `buildAutoBlocks`
83+
in `scripts/scripts.js`.
84+
4. Add a test under `test/blocks/<name>/` using the test harness.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
applyTo: "scripts/services/**/*.js"
3+
---
4+
5+
# Talk & speaker data – business logic
6+
7+
All data access and domain logic lives in `scripts/services/`. Blocks consume these services and
8+
should **not** parse the raw JSON themselves. There are two data sources, both published by EDS:
9+
10+
- **`/query-index.json`** – flat list of every published page with its metadata (one row per page).
11+
- **`/<year>/schedule-data.json`** – per-edition spreadsheet export describing the timetable.
12+
13+
## Query index (`QueryIndex.js`, `QueryIndexItem.js`)
14+
15+
`getQueryIndex()` fetches `/query-index.json` once and **caches the promise** (singleton). Tests
16+
reset it with `clearQueryIndexCache()`. Each raw row is mapped onto a `QueryIndexItem`
17+
(`Object.assign(new QueryIndexItem(), row)`), and the placeholder `default-meta-image.png` is
18+
nulled out.
19+
20+
`QueryIndexItem` is the canonical page/metadata record. Fields are strings as stored; helper
21+
methods parse them:
22+
- `getKeywords()` / `getRobots()` → `parseCSVArray` (comma-separated).
23+
- `getTags()` → `parseJsonArray` (JSON array, falls back to CSV).
24+
- `getSpeakers()` → `parseCSVArray` of the `speakers` field.
25+
26+
Speaker-specific fields: `affiliation`, `twitter`, `speaker-alias`, `uptoyear`.
27+
Talk-specific field: `speakers` (speaker **names** or speaker **document-names**).
28+
29+
### Page-type detection is path-based
30+
31+
`QueryIndex` classifies items purely by their `path` using regexes:
32+
- site root: `/^\/\d\d\d\d\/$/`
33+
- speaker page: `/^\/speakers\/.*$/`
34+
- talk page: `/^\/\d\d\d\d\/schedule\/.+$/`
35+
36+
Key methods:
37+
- `getItem(path)` – exact path lookup.
38+
- `getAllSiteRoots()` – yearly editions, newest first.
39+
- `getAllTalks()` – all talk pages, sorted year-desc then title-asc.
40+
- `getTalkSpeakerNames(siteRoot)` – distinct sorted speakers of **main** talks (`schedule/<x>`).
41+
- `getLightningTalkSpeakerNames(siteRoot)` – speakers of **lightning** talks
42+
(`schedule/<x>/<y>`), **minus** anyone already in the main-talk list.
43+
- `getTalksForSpeaker(speakerItem)` – talks whose `speakers` include the speaker's title or
44+
document-name.
45+
46+
### Speaker variant resolution (important domain rule)
47+
48+
A speaker can appear at the same name across years with different affiliation/details.
49+
`getSpeaker(pathOrName, siteRootPath)`:
50+
1. If given a URL/path under `/speakers/`, returns that exact item.
51+
2. Otherwise matches speaker items whose `title` **or** document-name equals the input.
52+
3. Disambiguates multiple matches via `getMatchingSpeakerVariant`: sorts by `uptoyear` (items
53+
**without** `uptoyear` sort last = "current"), and picks the first variant whose `uptoyear`
54+
is absent or `>= requested year`.
55+
56+
When you need the right speaker for a given edition, always pass the `siteRootPath` so this logic
57+
applies — don't pick the first match yourself.
58+
59+
## Schedule data (`ScheduleData.js`, `ScheduleDay.js`, `ScheduleEntry.js`)
60+
61+
`getScheduleData(url, forceReload)` fetches a yearly `schedule-data.json` and **joins it with the
62+
query index** to produce `ScheduleDay[]` → `ScheduleEntry[]`.
63+
64+
Spreadsheet columns → `ScheduleEntry`: `Day`, `Track`, `Start`, `End`, `Entry` (title),
65+
`Duration`, `FAQ` (Q&A minutes), `Type`, `Speakers`.
66+
- Valid `Type`s: `day`, `talk`, `break`, `other`, `other_rating`. `day` rows are dropped from
67+
entries (only used implicitly).
68+
- `Start`/`End` are Excel/Sheets serial numbers → real `Date`s via
69+
`convertSheetDateValue` (UTC). Times are formatted UTC (`utils/datetime.js`).
70+
- A row is **invalid and skipped** if day/start/end/title/duration are missing/zero or the type is
71+
not recognised.
72+
73+
### Talk rows resolve against the query index
74+
75+
For `Type === 'talk'`, the `Entry` value is a reference to the talk detail page:
76+
- absolute URL/path → used directly; otherwise treated as a document-name under
77+
`/<year>/schedule/<ref>`.
78+
- If no matching query-index item exists, **the entry is dropped**.
79+
- The entry's `title` is taken from the index item (with `removeTitleSuffix`), and if the sheet
80+
has no `Speakers`, speakers are inherited from the index item.
81+
82+
`ScheduleData.getTalkEntry(path)` finds the entry for a talk detail page (used by talk-detail
83+
blocks to show time/duration).
84+
85+
`ScheduleDay` aggregates its entries' min `start` / max `end`. Parallel tracks are represented by
86+
the `track` number (track 1..n share a start time); grouping into parallel rows is done in the
87+
`schedule` block, not here.
88+
89+
## Talk archive (`TalkArchive*.js`)
90+
91+
`getTalkArchive()` builds a `TalkArchive` from the query index. It projects talks into lightweight
92+
`TalkArchiveItem`s (arrays already parsed) and **drops talks with no speakers**.
93+
94+
- `TalkArchiveFilter` – `tags` / `years` / `speakers` (AND across categories, OR within a
95+
category). Serialised to/from the URL hash via `buildHash()` / `getFilterFromHash(hash)` using
96+
`category=val1,val2/...`. Only `tags`, `years`, `speakers` are valid categories.
97+
- `TalkArchive.applyFilter(filter)` recomputes `filteredTalks` and invalidates the full-text index.
98+
- `getFilteredTalksFullTextSearch(text)` lazily builds a `TalkArchiveFullTextIndex` over the
99+
currently filtered talks. The index is deliberately simplistic: it concatenates
100+
title/description/keywords/tags/speakers, lowercases, and does substring matching.
101+
- Filter option lists: `getTagFilterOptions()` / `getSpeakerFilterOptions()` (asc),
102+
`getYearFilterOptions()` (desc) — all distinct & sorted.
103+
104+
## Link handling (`Link.js`, `LinkHandler.js`)
105+
106+
`rewriteUrl` / `decorateAnchor` strip the host from internal adaptTo() URLs (so preview links stay
107+
on preview and live stays on live), open external links in a new tab, and mark `.pdf`/`.zip` links
108+
as downloads. `decorateAnchors(container)` is applied during `decorateMain`.
109+
110+
## When extending the data layer
111+
112+
- Add new domain logic here as a service/method with JSDoc, not inline in a block.
113+
- Reuse `utils/path.js` (`isUrlOrPath`, `getPathName`, `getDocumentName`, `getYearFromPath`) and
114+
`utils/metadata.js` (`parseCSVArray`, `parseJsonArray`, `removeTitleSuffix`) rather than new regexes.
115+
- Fetch via `utils/fetch.js` cache helpers.
116+
- Add a matching test under `test/scripts/services/` with sample JSON in `test/test-data/`.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
applyTo: "test/**"
3+
---
4+
5+
# Tests
6+
7+
Tests run with **@web/test-runner** in a real browser, using Chai (`@esm-bundle/chai`) and Sinon.
8+
9+
```sh
10+
npm test # all test/**/*.test.js with coverage
11+
npm run test:watch
12+
```
13+
14+
## Layout
15+
16+
- `test/scripts/services/` – unit tests for the data/business-logic services.
17+
- `test/blocks/<name>/` – block tests (decorate a fixture DOM, assert resulting markup).
18+
- `test/test-data/` – sample fixtures: `query-index-*.json`, `schedule-data-*.json` (+ source
19+
`.xlsx`), images.
20+
- `test/scripts/test-utils.js` – shared helpers.
21+
22+
## Conventions & helpers
23+
24+
- Files are ES modules and use the global `describe`/`it` (declared via
25+
`/* global describe it */`). Import `expect` from `@esm-bundle/chai`.
26+
- **Stub network with `stubFetchUrlMap(map)`** from `test-utils.js` — it redirects requested URLs
27+
(e.g. `/query-index.json`) to a local fixture under `test/test-data/`. Example:
28+
```js
29+
stubFetchUrlMap({ '/query-index.json': '/test/test-data/query-index-sample.json' });
30+
```
31+
- The query index is a cached singleton: call `clearQueryIndexCache()` when a test needs fresh
32+
data with a different stub.
33+
- Control the current URL with `setWindowLocationHref(href)` (path-based logic depends on
34+
`window.location`).
35+
- Wait for async EDS loading with `sectionLoaded` / `blockLoaded`; use `sleep(ms)` sparingly.
36+
37+
## When changing behaviour
38+
39+
- Add/adjust a fixture in `test/test-data/` and a test mirroring the source file's location
40+
(a service in `scripts/services/Foo.js` → `test/scripts/services/Foo.test.js`).
41+
- Prefer asserting domain outcomes (resolved speaker variant, dropped invalid schedule rows,
42+
filter/full-text results) over incidental markup details.
43+
- Run `npm test` and `npm run lint` before finishing.

0 commit comments

Comments
 (0)