The newsroom · How PrompTugal.news is made, in public
Code and data
Where PrompTugal.news mixes code with its words and its data, measured at every release, and the plan to separate them: every word a reader reads in markdown, every fact, label and list in JSON, and code that only loads, extracts, transforms and saves.
On this page
Why
Dinis Cruz, 6 November 2026: "we really need to move to a point where all of the content is either in JSON or markdown files. So they are easy to maintain, and you have a mode where the website can be maintained quite nicely." This page is the map of where we are not there yet, the rule the gate now holds us to, and the order of the work. It is kept by dev@PrompTugal.dinis.
What the estate already does
- One press, several sites, data apart (php.oct.2026.newsroom.sgit.ai, its split of 25 October 2026): the site's words are markdown in each site's
content/; every data file is JSON in its own repository'sfiles/, each with a schema and immutable versions; the build, the gate and the assets live in a "press" that holds neither words nor facts, andsites.jsonsays which site owns which pages. Each repository has its own gate, in seconds. - Single source of truth, by structure (coding.sgit.ai): a figure on a page is written in by a generator from the data file that holds it, and the release fails if one has drifted; the rule pages are generated from
rules.json. - Generate or date every number (nfrs.sgit.ai): no figure is typed by hand; each is computed, or carries the date it was measured.
- LETS (sgit.ai): Load, Extract, Transform, Save. State is files; each pass loads what it needs, extracts a slice, transforms it in a disposable engine and saves the answer as new files that the next pass, or the reader's browser, starts from.
Where we are
| Code measured | 121 files, 1114.1 KB (newsroom/build/, newsroom/tools/, assets/; vendored code left out) |
| Prose inside code | 124.8 KB, 11% of the code's bytes, in 85 files |
| HTML tags inside code | 554, in 26 files |
| The baseline | 114.6 KB of prose and 548 tags, measured 2026-11-06T03:23Z at v0.1.83: the gate refuses a file that goes above its line |
| Measured | at this build, v0.1.291 |
Our build already has the right shape in places. The prose pages (content/*.md) carry fill markers (an HTML comment naming a feature's filler) that features fill with what only the data can say: the front page, the companies index and the agenda work this way. The data is JSON with a schema for every kind of file (data/schemas/, checked by the gate), and derived files are made by one tool. What is left is the prose and the lookup tables that grew inside the code.
The files that hold the most prose
| File | Prose | Tags | Baseline | First prose literal |
|---|---|---|---|---|
assets/obs/obs-view.js | 16.9 KB | 0 | 16.8 KB, 0 | line 68: "files, bytes of ciphertext, all decrypted here with Web Crypto.…" |
newsroom/build/features/graph.mjs | 12.3 KB | 103 | 12.1 KB, 103 | line 50: "The whole graph (api/v1/graph/whole.json), compact, for the graph explorer at /graph/ and…" |
newsroom/build/features/newsroom.mjs | 11.9 KB | 94 | 11.9 KB, 94 | line 22: "<p class="nr-label"><a href="@@UP@@newsroom/index.html">The newsroom</a> · How PrompTugal.…" |
newsroom/build/features/account.mjs | 7.8 KB | 8 | 7.8 KB, 8 | line 20: "<div id="nr-" class="nr-mount" data-view=""><div class="nr-noscript"></div></div>…" |
newsroom/build/features/api.mjs | 4.9 KB | 0 | 4.9 KB, 0 | line 52: "Every node and edge of the newsroom's data. Never render it whole: render the answer to a…" |
newsroom/build/features/observability.mjs | 4.6 KB | 9 | 4.6 KB, 9 | line 29: ": "" needs vault_id and a read_key of the form sgit_public_read_<64 hex> (never a vault ke…" |
assets/chat/chat.js | 4.5 KB | 83 | 4.5 KB, 83 | line 25: "<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-w…" |
newsroom/build/lib/shell.mjs | 4.4 KB | 61 | 4.6 KB, 67 | line 28: "<img class="wordmark wm-light" src="assets/brand/promptugal-wordmark-light.svg" alt="Promp…" |
assets/vault/vault.js | 3.4 KB | 0 | 3.4 KB, 0 | line 173: "Your vault: your reading account, carried between your devices…" |
newsroom/build/features/companies.mjs | 3.1 KB | 0 | 2.9 KB, 0 | line 24: "- A session page in its sources that is not in our programme: . The programme desk found i…" |
newsroom/build/features/billing.mjs | 2.7 KB | 0 | 2.7 KB, 0 | line 49: "The reading meter's config for , made by newsroom/build/features/billing.mjs from and . sg…" |
newsroom/build/features/chat.mjs | 2.6 KB | 3 | 2.6 KB, 3 | line 18: "A chat that answers questions about Web Summit Lisbon 2026 from this site's own data, with…" |
newsroom/build/features/search.mjs | 2.2 KB | 3 | 2.2 KB, 3 | line 109: "The newsroom's search index, built at release from every listed page: one row per page, [i…" |
assets/graph/commands.js | 2.1 KB | 0 | 2.1 KB, 0 | line 34: "Open this address (or call again with open: true) to see it.…" |
assets/core/tools.js | 2.1 KB | 0 | 2.1 KB, 0 | line 78: "One entity of the newsroom's data: a company, event (session, side event), person, organis…" |
assets/search/search.js | 2.0 KB | 46 | 2.0 KB, 46 | line 14: "<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap…" |
newsroom/build/features/portugal.mjs | 1.7 KB | 0 | 1.5 KB, 0 | line 34: "Portugal's tech scene in the week of Web Summit Lisbon: what the research desk has found,…" |
newsroom/build/features/newsroom-draw.mjs | 1.5 KB | 18 | 1.5 KB, 18 | line 82: "<figure class="adm-fig"><div class="adm-scroll"></div></figure>…" |
assets/meter/meter.js | 1.5 KB | 0 | 1.5 KB, 0 | line 86: ".lead, .body p, .body li, .body h2, .body h3, .body pre, .body blockquote, .body tr, .body…" |
newsroom/build/tests/test_billing_browser.mjs | 1.5 KB | 0 | 1.5 KB, 0 | line 32: "FAIL: build the site first (node newsroom/build/build.mjs)…" |
assets/chat/chat-core.js | 1.5 KB | 0 | 1.5 KB, 0 | line 41: "You are "Ask the newsroom", the assistant on (): the independent newsroom at Web Summit Li…" |
newsroom/build/features/events.mjs | 1.3 KB | 0 | 1.3 KB, 0 | line 50: "::: nvn## Speakers {#speakers}nnAs the event's page lists them: name, role, organisation.n…" |
newsroom/build/lib/seo.mjs | 1.2 KB | 0 | 1.2 KB, 0 | line 10: "PrompTugal.news: technology and AI, in context. Mapping the universe of AI in Portugal.…" |
assets/login/login.js | 1.2 KB | 0 | 1.0 KB, 0 | line 53: "this browser cannot hold a key here (no IndexedDB or Web Crypto)…" |
newsroom/build/tests/test_vault.mjs | 1.2 KB | 0 | 1.2 KB, 0 | line 39: "the twelve vendored library files hash to what v0.2.3/VENDOR.md records…" |
assets/graph/explorer.js | 1.1 KB | 70 | 1.1 KB, 70 | line 67: "(whole, industry, summit, people, companies/<slug>, events/<slug>, people/<slug>)…" |
newsroom/tools/old-host.mjs | 1.0 KB | 4 | 1.0 KB, 4 | line 26: "no .generated.txt: run node newsroom/build/build.mjs first…" |
newsroom/build/features/topics.mjs | 1.0 KB | 0 | 1.0 KB, 0 | line 8: "The industry fields the team's company map files each company under.…" |
newsroom/tools/algarve-graph.mjs | 1.0 KB | 1 | new | line 22: "<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 " role="img" aria-labelledby="t d"><t…" |
newsroom/build/features/tools.mjs | 1.0 KB | 0 | 1.0 KB, 0 | line 14: "Every page publishes its commands on window.__tool (assets/core/tool-api.js; contract in…" |
And 55 more files, each with less; all of them in the baseline.
What kinds of mixing there are
- Page prose inside build features. Whole sections of text are template strings in
newsroom/build/features/: the newsroom section, the graph explorer's page, the account and billing pages, the API page, the chat and search pages, observability, and the fixed blocks of every company page (our questions for them, about PrompTugal.news). These belong incontent/as markdown, with a fill marker where a number or a list goes. - Lookup tables inside code, some of them twice. Labels for kinds of event (in the agenda, events and search features, three times), organisers (twice), the plural of each entity type (once for the build, once for the browser), the newsroom's tabs, pipeline stages and people, beats, topic kinds, the chat's models and suggested questions, the search's hand list of synonyms, and the summit's four days in the live strip, which
data/site/site.jsonalready knows. These belong in one data file each, with a schema, read by the build and served to the browser from the same place. - Words of the browser components. The observability view, the chat, the vault and the sign-in panel carry their messages in their scripts. Their markup is the component's job; their words can be a JSON file beside the data the component reads.
- Who-made-this as prose. Each article ends with a paragraph naming its desk, its reviewer, its storyline and its checks. Those are facts about the article: as front matter (
story,desk,reviewed_by,review,draft), the build can write the paragraph and the graph can use them. - The shell's markup (
newsroom/build/lib/shell.mjs) is the template, and markup is what a template is for; its words already come fromdata/site/site.json. It stays.
The target
| Where | What it holds | What it never holds |
|---|---|---|
content/ | every word a reader reads, as markdown with front matter; fill markers where the data speaks; layout hooks (::: name) where the design needs a box | numbers typed by hand, lists that the data already has |
data/ | every fact, label, list and setting, as JSON with a schema in data/schemas/ | prose a reader reads as a page |
newsroom/build/, newsroom/tools/ | code that loads, extracts, transforms and saves; the gate | sentences longer than a label; facts |
assets/ | components: behaviour and markup | words a reader reads, beyond a button's label |
generated (api/, the pages, their twins) | what the build saved, never committed | anything a person edits |
Every feature becomes a LETS cycle: load its data and content, extract what it needs, transform it into one model, save the model as an API file, and render the page from that same model, so the page and the API cannot disagree. What the desks are doing is the first feature written this way from the start (its words are content/newsroom/desks.md; newsroom/build/features/desks.mjs only fills the markers and saves the model). The sign-in page is the first moved: its words left login.mjs for content/login.md.
The day the site splits the way the PHP site did (one press, a site per audience, a data repository), this separation is what makes it a move of folders, not a rewrite.
The rule the gate now holds
The gate measures every code file at every release (newsroom/build/lib/codedata.mjs: the string literals of eight words or more, and the HTML tags in strings) and refuses a file that holds more than its line in newsroom/build/quality-baseline.json. A new file may hold almost none. The lines only go down: after moving words out of a file, node newsroom/tools/quality-baseline.mjs lowers them to what the files hold now. Raising one is possible, in the open: --accept <file> --why "<reason>" keeps the reason in the baseline, for a reviewer to read in the commit.
The order of the work
| # | Work | Who |
|---|---|---|
| 1 | The newsroom section's words to content/newsroom/ (the largest builder of pages), each page a markdown file with markers | dev@ |
| 2 | The graph, account, billing, API, chat and search pages' words to content/; the company page's fixed blocks to content/companies/ | dev@ |
| 3 | One labels file (data/site/labels.json, with its schema): kinds, organisers, plurals, beats, stages, tabs; the build and the browser's tools read it, and the copies go | ontologist@ (the schema), dev@ (the readers) |
| 4 | The browser components' words to JSON beside their data; who may drain from the key register (a role on the person's key), not a list in the script | dev@, observability@ |
| 5 | The live strip's days from data/site/site.json | dev@ |
| 6 | Who made each article as front matter, the paragraph written by the build | journalist@ (the fields), dev@ (the paragraph) |
| 7 | Each feature as a LETS cycle with its model saved under api/v1/ | dev@ |
| 8 | When Dinis says so: the split into a press, the sites and a data repository | Dinis, then dev@ |
Progress shows above at every release: the totals fall as the work lands, and the gate keeps them from rising again.