|
| 1 | +# apps/publish |
| 2 | + |
| 3 | +The `publish` tool from DESIGN.md's phase 3, presented as **Broadcast**: one |
| 4 | +event (or a whole feed) turned into whatever every other platform wants. Its |
| 5 | +scope is |
| 6 | +[opentechevents-spec#12](https://github.com/OpenTechEvents/opentechevents-spec/issues/12) |
| 7 | +— GitHub directories (prefilled issues and PR drafts), newsletters, contact |
| 8 | +forms, social posts, platforms with no open API — plus |
| 9 | +[spec#11](https://github.com/OpenTechEvents/opentechevents-spec/issues/11), |
| 10 | +the schema.org snippet, which is the first channel that actually works. |
| 11 | + |
| 12 | +Same build shape as `apps/editor`/`apps/preview`: vanilla TypeScript, one |
| 13 | +esbuild entry point, static `index.html`/`styles.css` copied into `dist/` |
| 14 | +**once** at `pnpm dev` startup (re-run the copy after editing either). |
| 15 | + |
| 16 | +Non-goals inherited from spec#12, worth keeping in view because every future |
| 17 | +channel will be tempted by them: no automatic background publishing, no |
| 18 | +direct Meetup/LinkedIn/Eventbrite integrations, no third-party credentials. |
| 19 | +The organizer reviews and submits every output by hand. |
| 20 | + |
| 21 | +## The page is a channel rail plus a stage, and the rail is the pitch |
| 22 | + |
| 23 | +Most channels do not exist yet. The page still shows all twelve, grouped, with |
| 24 | +a status pill each (`ready` / `planned` / `idea`) — because an organizer |
| 25 | +deciding whether OTE is worth adopting is really asking "where can this take |
| 26 | +my events?", and a page showing only the one finished channel answers that |
| 27 | +question wrongly. |
| 28 | + |
| 29 | +That makes the honesty of the placeholders load-bearing. `src/lib/channels.ts` |
| 30 | +is plain data; a `planned` card states what it will produce, what it will |
| 31 | +never do, and offers two real actions (follow the issue, build it). It never |
| 32 | +renders a fake preview or a disabled button that looks live. **If you add a |
| 33 | +channel, add it as data first** — status, summary, `produces`, `accepts` — |
| 34 | +and only give it a panel in `main.ts` when it genuinely produces something. |
| 35 | + |
| 36 | +## Placeholders earn their space with the organizer's own data |
| 37 | + |
| 38 | +Under "What your event already has", a planned channel lists the fields every |
| 39 | +destination asks for and whether this event carries them |
| 40 | +(`src/lib/event-readiness.ts`). That turns a promise into work the organizer |
| 41 | +can do **today**, in the editor, before any channel ships — and it is the |
| 42 | +honest version of a preview: real data, no invented output. |
| 43 | + |
| 44 | +## The widget previews run the real widget, not a picture |
| 45 | + |
| 46 | +The "Embeddable widget" and "Calendar & RSS links" panels mount a live |
| 47 | +`<ote-events>` / `<ote-subscribe>` **loaded from the same versioned asset the |
| 48 | +snippet names** (`src/lib/preview.ts`): the sibling path on this deployment |
| 49 | +first (`deploy-tools.yml` publishes both apps to one Pages site, so it is the |
| 50 | +identical file, same origin), the absolute tools URL second. Deliberately not |
| 51 | +a bundled copy of the widget source — that could drift from what a visitor's |
| 52 | +browser will actually fetch, and "this is exactly what you are embedding" is |
| 53 | +the entire value of the pane. |
| 54 | + |
| 55 | +Both candidates can fail (offline, or a version not deployed yet). The pane |
| 56 | +then says so in words; it never leaves an empty rectangle that reads as a |
| 57 | +broken widget. `pnpm dev` copies `apps/embed/versions/v<version>/` into |
| 58 | +`dist/embed/` so previews work offline — **dev only**, because production |
| 59 | +already serves the canonical `/embed/` assets and a second copy would be one |
| 60 | +more thing to go stale. |
| 61 | + |
| 62 | +The subscribe panel also HEAD-checks `feed.ics` and `feed.xml`. A fork whose |
| 63 | +export step has not run has a perfectly valid `feed.json` and two dead links; |
| 64 | +better found here than by the first person who subscribes. |
| 65 | + |
| 66 | +## The widget panels are a playground, in place |
| 67 | + |
| 68 | +Layout, theme, card width, a cap on how many events, past events — each |
| 69 | +control re-renders **both** the live preview and the snippet, so what an |
| 70 | +organizer tunes is exactly what they copy. Only non-default attributes are |
| 71 | +emitted (`src/lib/site-snippets.ts`): a snippet that spells out every default |
| 72 | +reads as configuration to maintain, and pins behaviour nobody asked to pin. |
| 73 | +The full attribute surface still lives in the embed playground, linked at the |
| 74 | +bottom of the panel — this is the short path, not a second implementation of |
| 75 | +it. |
| 76 | + |
| 77 | +Two traps worth keeping in mind, both found by an organizer on a real feed: |
| 78 | + |
| 79 | +- **`<ote-subscribe>` takes `feed-ics`/`feed-rss`/`feed-json`, not `feed`.** |
| 80 | + It never fetches anything; with no URL attributes there is nothing to link |
| 81 | + to, and the trigger renders an empty menu that looks exactly like a |
| 82 | + disabled button. There is a test for it. |
| 83 | +- **The widget renders a feed, not an event** — unless `event-id` says |
| 84 | + otherwise. With an event selected in the header, the panel offers |
| 85 | + “Only «that event»” (on by default) and emits |
| 86 | + `event-id="<the event's OTE id>"`, added in embed 0.7.0 for exactly this. |
| 87 | + It still points at the feed URL, so the card follows later edits, and the |
| 88 | + widget keeps rendering it after the date passes — which is what an event's |
| 89 | + own page needs. Unchecked, it is the whole feed again and `limit` is the |
| 90 | + answer to "show fewer". |
| 91 | + |
| 92 | +## Meetup vs conference is a hint, never a gate |
| 93 | + |
| 94 | +Directories are picky in exactly this dimension: confs.tech and |
| 95 | +developers.events take conferences, not a monthly meetup. `guessProfile` |
| 96 | +(`src/lib/event-profile.ts`) reads signals from the event itself — a CFP, |
| 97 | +more than one day, paid tickets — since OTE has no `type` field and inventing |
| 98 | +one is not this tool's call. |
| 99 | + |
| 100 | +The guess is **shown with its reasons**, overridable from the header, and only |
| 101 | +ever reorders and annotates: an unfit channel sinks to the bottom of its group |
| 102 | +and says "conferences only", but it is never hidden and never disabled. An |
| 103 | +organizer learning that confs.tech will not take their meetup is useful; a |
| 104 | +channel silently vanishing is not. |
| 105 | + |
| 106 | +## An invalid feed stops everything, before any channel |
| 107 | + |
| 108 | +`main.ts` runs `validateFeed` first and shows the errors *instead of* the |
| 109 | +console. Channels are pure mappings, not validators: broadcasting from a |
| 110 | +broken feed would carry the same errors to every destination at once. Don't |
| 111 | +"improve" this into best-effort output with a warning. |
| 112 | + |
| 113 | +## Online-only events are not eligible for Google rich results — say so |
| 114 | + |
| 115 | +Google requires a physical location for event rich results ("Virtual |
| 116 | +experiences that have no real-world component aren't supported"), so an |
| 117 | +online-only event always comes back from the Rich Results Test as "no |
| 118 | +eligible item", however good the markup is. The note under that link |
| 119 | +(`eligibilityNote` in `src/lib/snippet.ts`, backed by `isOnlineOnly` in |
| 120 | +`@opentechevents/export-jsonld`) exists because the link itself sets an |
| 121 | +expectation this tool would otherwise break — an organizer seeing red there |
| 122 | +reads it as our bug, not as Google policy. Keep the note next to whatever |
| 123 | +sends them to a validator. |
| 124 | + |
| 125 | +## Mapping logic lives in packages, not here |
| 126 | + |
| 127 | +`schema-org` is a thin call into `@opentechevents/export-jsonld`; the widget |
| 128 | +and subscribe snippets are string building in `src/lib/site-snippets.ts` |
| 129 | +against assets `deploy-tools.yml` already publishes. Any question about how |
| 130 | +an OTE field becomes someone else's field belongs in the package that owns |
| 131 | +the mapping — the same code has to serve the SSR renderer (issue #58) and |
| 132 | +every future channel, not just this page. |
| 133 | + |
| 134 | +`site-snippets.ts` pins the widget version from `apps/embed/package.json`, |
| 135 | +injected by `build.mjs` as `__EMBED_VERSION__`. Pinned, never `/latest/`: a |
| 136 | +widget that changes behaviour on someone else's site without them touching |
| 137 | +anything is a bad trade (see `apps/embed/CLAUDE.md`). `vitest.config.ts` |
| 138 | +defines the same global for tests, which don't go through esbuild. |
| 139 | + |
| 140 | +## Feed source: `?repo=` first, `?feed=` as the escape hatch |
| 141 | + |
| 142 | +`?repo=owner/name` is the DESIGN.md convention every central tool follows |
| 143 | +(the fork's dashboard already links here that way, at |
| 144 | +`/publish?repo=…`). For a repo, the fork's GitHub Pages URL is tried first |
| 145 | +and `raw.githubusercontent.com` on the default branch second — Pages may not |
| 146 | +be enabled yet while `feed.json` is already committed. `?feed=<url>` covers |
| 147 | +feeds published anywhere else; non-http(s) URLs are rejected. |
0 commit comments