From da8190aee5611fcb229ad232a7bb95b57c8334a9 Mon Sep 17 00:00:00 2001 From: dany80213 Date: Sun, 18 Oct 2026 18:07:58 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix(support):=20la=20=C3=97=20della=20modal?= =?UTF-8?q?e=20QR=20non=20copre=20pi=C3=B9=20il=20codice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il pannello della modale aveva 30px di padding in alto, ma il bottone Chiudi occupa la fascia 12–44px: il riquadro del QR finiva sotto la ×. Il padding superiore passa a 56px. Co-Authored-By: Claude Opus 5.5 --- src/pages/support/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/support/index.tsx b/src/pages/support/index.tsx index e7267e3..b4e3877 100644 --- a/src/pages/support/index.tsx +++ b/src/pages/support/index.tsx @@ -338,7 +338,9 @@ function QRModal({ background: T.dark ? '#161619' : '#ffffff', border: `1px solid ${T.border}`, borderRadius: 'var(--radius-lg)', - padding: '30px 26px 26px', + /* top ≥ top+altezza del bottone Chiudi (12+32) + respiro: il QR + non deve finire sotto la ×. */ + padding: '56px 26px 26px', textAlign: 'center', boxShadow: '0 24px 60px rgba(0,0,0,0.4)', }} From 500904c760387492033dbeeedefeaeff3f57135f Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Tue, 20 Oct 2026 10:25:13 +0200 Subject: [PATCH 2/4] chore(seo): togli static/robots.txt, il robots.txt vive alla radice del dominio Con baseUrl /python-doesnt-byte/ anche i file di static/ finiscono sotto il baseUrl: il file era servito a /python-doesnt-byte/robots.txt, dove i crawler non lo cercano (RFC 9309 lo legge solo alla radice dell'host). Il commento diceva il contrario e la riga Sitemap puntava a https://www.rainbowbits.cloud/sitemap.xml, che risponde 404. Il robots.txt della radice va nel sito utente marcofarina.github.io, con Sitemap: https://rainbowbits.cloud/python-doesnt-byte/sitemap.xml. Co-Authored-By: Claude Opus 5.5 --- static/robots.txt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 static/robots.txt diff --git a/static/robots.txt b/static/robots.txt deleted file mode 100644 index 001f214..0000000 --- a/static/robots.txt +++ /dev/null @@ -1,14 +0,0 @@ -# robots.txt — Python Doesn't Byte -# Robots Exclusion Protocol (RFC 9309). Servito alla root del dominio: -# https://www.rainbowbits.cloud/robots.txt -# I file in static/ finiscono alla root del build (come CNAME), quindi alla root -# del dominio — esattamente dove i crawler cercano questo file. Il baseUrl -# /python-doesnt-byte/ NON si applica ai file statici di root. - -# Tutto indicizzabile: le poche pagine da escludere (demo/test) portano già il -# proprio meta robots "noindex", che è più preciso di un Disallow qui (un -# Disallow impedirebbe al crawler di leggere il noindex). -User-agent: * -Allow: / - -Sitemap: https://www.rainbowbits.cloud/sitemap.xml From b7da70ce18f66fd9891da67218a73a2397c96de4 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Tue, 20 Oct 2026 10:27:34 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(config):=20trailingSlash=20true,=20nien?= =?UTF-8?q?te=20pi=C3=B9=20404=20sugli=20URL=20con=20la=20barra=20finale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Con trailingSlash: false la build scriveva `pagina.html`. GitHub Pages non trova niente per `/pagina/` e serve 404.html con status 404; l'app nel 404.html poi mostrava la pagina giusta, dopo un lampo di «non trovata». GoatCounter girava dentro 404.html e contava come pagina non trovata le visite arrivate da Classroom, dove il link era condiviso con la barra. Con true ogni pagina è `pagina/index.html`; canonical, og:url, sitemap, link interni e indice di Pagefind usano l'URL con la barra. Su GitHub Pages `/pagina` riceve un 301 verso `/pagina/`: verificato sul sito pubblicato con la radice `/python-doesnt-byte?percorso=abc`, che va a `/python-doesnt-byte/?percorso=abc` conservando la query. Che valga per ogni pagina dopo il deploy si potrà verificare solo dopo la release. - GoatCounter: script inline prima di count.js che toglie la barra finale dal path (non alla home), così lo storico resta una riga per pagina. - /percorso: link condiviso e QR scritti con la barra (useBaseUrl non la aggiunge); lo stesso per il playground aperto dal PyRunner e per il link d'inizio del percorso. - copy-page-md: i .md restano agli stessi percorsi (`lezione.md`), perché doc.permalink non porta la barra; la riga «Fonte» cita l'URL canonico. - llms.txt: URL canonici (senza www, con la barra) e sitemap reale sotto /python-doesnt-byte/; la vecchia puntava a un 404. Non cambia: gli URL `pagina.html`, che oggi Pagefind usa nei risultati, dopo il deploy rispondono 404. Co-Authored-By: Claude Opus 5.5 --- docusaurus.config.ts | 33 ++++++++++++++++++---- plugins/copy-page-md/index.js | 13 +++++++-- src/pages/percorso.tsx | 6 ++-- src/theme/PyRunner/index.tsx | 2 +- static/llms.txt | 52 +++++++++++++++++------------------ 5 files changed, 68 insertions(+), 38 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 04e35fc..358ed28 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -58,7 +58,12 @@ export default async function createConfig(): Promise { organizationName: 'marcofarina', // Usually your GitHub org/username. projectName: 'python-doesnt-byte', // Usually your repo name. deploymentBranch: 'gh-pages', - trailingSlash: false, + // true: ogni pagina esce come `/index.html` e gli URL canonici + // finiscono con `/`. È la forma che GitHub Pages serve da sé: `/pagina/` + // risponde 200 e `/pagina` riceve un 301 verso `/pagina/` conservando la + // query. Con false la build scriveva `pagina.html` e `/pagina/` finiva in + // 404.html: un lampo di «pagina non trovata» prima del redirect client-side. + trailingSlash: true, onBrokenLinks: 'throw', @@ -115,14 +120,30 @@ export default async function createConfig(): Promise { // Analytics privacy-first (GoatCounter): nessun cookie, niente dati personali. // Solo in produzione: in dev (`npm start`) NODE_ENV è 'development', così le // visite locali non gonfiano le statistiche. - scripts: + // + // Con trailingSlash: true i path arrivano come `/pagina/`; fino alla 0.20 + // erano `/pagina`. `goatcounter.path` toglie la barra finale (non quella + // della home, che l'ha sempre avuta) così lo storico resta su una riga sola + // per pagina. count.js lo legge al caricamento: lo script inline deve + // precedere count.js, per questo stanno tutti e due qui, nell'ordine. + headTags: process.env.NODE_ENV === 'production' ? [ { - src: 'https://gc.zgo.at/count.js', - async: true, - 'data-goatcounter': - 'https://pythondoesntbyte.goatcounter.com/count', + tagName: 'script', + attributes: {}, + innerHTML: + 'window.goatcounter={path:function(p){return p.replace(' + + '/^(\\/python-doesnt-byte\\/[^?#]*[^\\/?#])\\/(?=[?#]|$)/,"$1")}};', + }, + { + tagName: 'script', + attributes: { + src: 'https://gc.zgo.at/count.js', + async: 'async', + 'data-goatcounter': + 'https://pythondoesntbyte.goatcounter.com/count', + }, }, ] : [], diff --git a/plugins/copy-page-md/index.js b/plugins/copy-page-md/index.js index 4be137d..6cd8497 100644 --- a/plugins/copy-page-md/index.js +++ b/plugins/copy-page-md/index.js @@ -4,6 +4,7 @@ const { parseMarkdownFile, DEFAULT_PARSE_FRONT_MATTER, } = require('@docusaurus/utils'); +const { applyTrailingSlash } = require('@docusaurus/utils-common'); const PLUGIN_NAME = 'copy-page-md'; @@ -129,8 +130,12 @@ async function toCleanMarkdown(absPath, doc, context, archivio) { removeContentTitle: true, }); - const siteUrl = context.siteConfig.url.replace(/\/$/, ''); - const pageUrl = siteUrl + doc.permalink; + const { url, baseUrl, trailingSlash } = context.siteConfig; + // doc.permalink non porta la barra finale: la aggiunge il router. La «Fonte» + // cita l'URL canonico, quello di sitemap e . + const pageUrl = + url.replace(/\/$/, '') + + applyTrailingSlash(doc.permalink, { trailingSlash, baseUrl }); const body = sanitize(content, context.siteDir, archivio); return ( @@ -142,7 +147,9 @@ async function toCleanMarkdown(absPath, doc, context, archivio) { // Percorso di output del .md, coerente col fetch lato client. Il permalink // include il baseUrl: lo si toglie perché outDir è già la radice servita a -// baseUrl. Permalink con `/` finale (landing/category) → index.md. +// baseUrl. Permalink con `/` finale (landing/category) → index.md. Con +// trailingSlash: true la pagina è `lezione/index.html` e il .md resta accanto +// alla cartella (`lezione.md`): il permalink dei dati non ha la barra finale. function outFile(outDir, permalink, baseUrl) { let rel = permalink.startsWith(baseUrl) ? permalink.slice(baseUrl.length) diff --git a/src/pages/percorso.tsx b/src/pages/percorso.tsx index 0bd47d0..97831ed 100644 --- a/src/pages/percorso.tsx +++ b/src/pages/percorso.tsx @@ -271,7 +271,7 @@ function StudentCard({ const { code, setCode, clear } = useCurriculum(); const [draft, setDraft] = useState(''); const [error, setError] = useState(null); - const startUrl = useBaseUrl('/programmatore'); + const startUrl = useBaseUrl('/programmatore/'); const activate = () => { try { @@ -363,7 +363,9 @@ export default function PercorsoPage(): ReactNode { const data = useCurriculumPluginData(); const { code } = useCurriculum(); const { siteConfig } = useDocusaurusContext(); - const percorsoUrl = useBaseUrl('/percorso'); + // Con la barra finale: è l'URL canonico, così il link condiviso e il QR non + // passano da un 301. + const percorsoUrl = useBaseUrl('/percorso/'); const allKeys = useMemo( () => data.volumes.flatMap((v) => collectKeys(v.tree)), diff --git a/src/theme/PyRunner/index.tsx b/src/theme/PyRunner/index.tsx index e2dd0a5..bd60215 100644 --- a/src/theme/PyRunner/index.tsx +++ b/src/theme/PyRunner/index.tsx @@ -132,7 +132,7 @@ function PyRunnerInner(props: PyRunnerProps) { const editorRef = useRef(null); const cleanupRef = useRef<(() => void) | null>(null); const toastTimerRef = useRef(undefined); - const playgroundUrl = useBaseUrl('/playground'); + const playgroundUrl = useBaseUrl('/playground/'); const maxLines = coerceNumber(props.maxLines, 20); const readonly = coerceBool(props.readonly); diff --git a/static/llms.txt b/static/llms.txt index d07b521..82cc0b8 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -11,61 +11,61 @@ Apprendista). Al momento il volume più completo è il «Manuale dell'Artefice» esempi Python eseguibili direttamente nel browser (runtime Brython) e quiz. Questo file è mantenuto a mano: se una voce non corrisponde più al sito, fa fede -la sitemap (https://www.rainbowbits.cloud/sitemap.xml). +la sitemap (https://rainbowbits.cloud/python-doesnt-byte/sitemap.xml). ## Orientamento -- [Perché Python](https://www.rainbowbits.cloud/python-doesnt-byte/perche-python): il manifesto del progetto — perché questo libro esiste e per chi. -- [Introduzione](https://www.rainbowbits.cloud/python-doesnt-byte/docs/intro): punto di partenza del libro. -- [Come usare questo libro](https://www.rainbowbits.cloud/python-doesnt-byte/docs/come-usare-questo-libro): convenzioni tipografiche, elementi interattivi e suggerimenti. +- [Perché Python](https://rainbowbits.cloud/python-doesnt-byte/perche-python/): il manifesto del progetto — perché questo libro esiste e per chi. +- [Introduzione](https://rainbowbits.cloud/python-doesnt-byte/docs/intro/): punto di partenza del libro. +- [Come usare questo libro](https://rainbowbits.cloud/python-doesnt-byte/docs/come-usare-questo-libro/): convenzioni tipografiche, elementi interattivi e suggerimenti. ## Volume 1 — Manuale del Programmatore > I fondamenti del linguaggio. In costruzione. -- [Manuale del Programmatore](https://www.rainbowbits.cloud/python-doesnt-byte/programmatore): introduzione al volume. -- [Le variabili](https://www.rainbowbits.cloud/python-doesnt-byte/programmatore/variabili): primo contatto con le variabili in Python. +- [Manuale del Programmatore](https://rainbowbits.cloud/python-doesnt-byte/programmatore/): introduzione al volume. +- [Le variabili](https://rainbowbits.cloud/python-doesnt-byte/programmatore/variabili/): primo contatto con le variabili in Python. ## Volume 2 — Manuale dell'Artefice (OOP) > Programmazione a oggetti, costruita passo-passo attorno al lettore musicale «Risonanza». È il volume più completo. -- [Manuale dell'Artefice](https://www.rainbowbits.cloud/python-doesnt-byte/artefice): introduzione al volume. -- [Perché gli oggetti?](https://www.rainbowbits.cloud/python-doesnt-byte/artefice/perche-gli-oggetti): i limiti del codice procedurale e l'idea-madre dell'OOP — tenere insieme dati e comportamento. -- [Classi, istanze e metodi](https://www.rainbowbits.cloud/python-doesnt-byte/artefice/classi-e-istanze): lo stampo e l'esemplare; cosa fa __init__, perché self compare ovunque, attributi contro metodi. -- [Mostrare un oggetto](https://www.rainbowbits.cloud/python-doesnt-byte/artefice/mostrare-un-oggetto): i metodi speciali __str__ e __repr__ — l'oggetto che impara a raccontarsi. -- [Metodi di classe e statici](https://www.rainbowbits.cloud/python-doesnt-byte/artefice/metodi-di-classe-e-statici): attributi di classe, @classmethod (e i costruttori alternativi), @staticmethod. -- [Incapsulamento](https://www.rainbowbits.cloud/python-doesnt-byte/artefice/incapsulamento): interfaccia pubblica contro stato interno, convenzioni _ e __, «tell, don't ask». +- [Manuale dell'Artefice](https://rainbowbits.cloud/python-doesnt-byte/artefice/): introduzione al volume. +- [Perché gli oggetti?](https://rainbowbits.cloud/python-doesnt-byte/artefice/perche-gli-oggetti/): i limiti del codice procedurale e l'idea-madre dell'OOP — tenere insieme dati e comportamento. +- [Classi, istanze e metodi](https://rainbowbits.cloud/python-doesnt-byte/artefice/classi-e-istanze/): lo stampo e l'esemplare; cosa fa __init__, perché self compare ovunque, attributi contro metodi. +- [Mostrare un oggetto](https://rainbowbits.cloud/python-doesnt-byte/artefice/mostrare-un-oggetto/): i metodi speciali __str__ e __repr__ — l'oggetto che impara a raccontarsi. +- [Metodi di classe e statici](https://rainbowbits.cloud/python-doesnt-byte/artefice/metodi-di-classe-e-statici/): attributi di classe, @classmethod (e i costruttori alternativi), @staticmethod. +- [Incapsulamento](https://rainbowbits.cloud/python-doesnt-byte/artefice/incapsulamento/): interfaccia pubblica contro stato interno, convenzioni _ e __, «tell, don't ask». ## Volume 3 — Manuale dell'Archivista > Basi di dati e SQL, con query eseguibili in pagina. In costruzione. -- [Manuale dell'Archivista](https://www.rainbowbits.cloud/python-doesnt-byte/archivista): introduzione al volume. +- [Manuale dell'Archivista](https://rainbowbits.cloud/python-doesnt-byte/archivista/): introduzione al volume. ## Volume 4 — Biblioteca dell'Apprendista > Esercizi assegnati dai volumi precedenti. In costruzione. -- [Biblioteca dell'Apprendista](https://www.rainbowbits.cloud/python-doesnt-byte/apprendista): introduzione al volume. +- [Biblioteca dell'Apprendista](https://rainbowbits.cloud/python-doesnt-byte/apprendista/): introduzione al volume. ## Le basi del linguaggio (materiale fondamenti) -- [Programmare in Python](https://www.rainbowbits.cloud/python-doesnt-byte/docs/fondamenti-di-python/programmare-in-python): cosa significa programmare e come gira il codice Python. -- [Le variabili](https://www.rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/le-variabili) -- [Input](https://www.rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/input) -- [Output](https://www.rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/output) -- [Le stringhe](https://www.rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/le-stringhe) -- [I moduli](https://www.rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/moduli) +- [Programmare in Python](https://rainbowbits.cloud/python-doesnt-byte/docs/fondamenti-di-python/programmare-in-python/): cosa significa programmare e come gira il codice Python. +- [Le variabili](https://rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/le-variabili/) +- [Input](https://rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/input/) +- [Output](https://rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/output/) +- [Le stringhe](https://rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/le-stringhe/) +- [I moduli](https://rainbowbits.cloud/python-doesnt-byte/docs/basi-del-linguaggio/moduli/) ## Strumenti -- [Playground](https://www.rainbowbits.cloud/python-doesnt-byte/playground): editor Python a tutta pagina, esecuzione nel browser. -- [Note di rilascio](https://www.rainbowbits.cloud/python-doesnt-byte/note-di-rilascio): changelog del progetto. +- [Playground](https://rainbowbits.cloud/python-doesnt-byte/playground/): editor Python a tutta pagina, esecuzione nel browser. +- [Note di rilascio](https://rainbowbits.cloud/python-doesnt-byte/note-di-rilascio/): changelog del progetto. ## Optional -- [Sostieni il progetto](https://www.rainbowbits.cloud/python-doesnt-byte/support): donazioni «value for value». -- [Licenza](https://www.rainbowbits.cloud/python-doesnt-byte/legale/licenza): CC BY-NC-SA 4.0. -- [Privacy policy](https://www.rainbowbits.cloud/python-doesnt-byte/legale/privacy-policy) -- [Termini di servizio](https://www.rainbowbits.cloud/python-doesnt-byte/legale/termini-di-servizio) +- [Sostieni il progetto](https://rainbowbits.cloud/python-doesnt-byte/support/): donazioni «value for value». +- [Licenza](https://rainbowbits.cloud/python-doesnt-byte/legale/licenza/): CC BY-NC-SA 4.0. +- [Privacy policy](https://rainbowbits.cloud/python-doesnt-byte/legale/privacy-policy/) +- [Termini di servizio](https://rainbowbits.cloud/python-doesnt-byte/legale/termini-di-servizio/) From 47f167026b73630206718afdcbf6bc6766b9e968 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 14:19:23 +0000 Subject: [PATCH 4/4] fix(copy-page): gli assistenti AI leggono una copia .txt della pagina MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Pages serve i .md come text/markdown e lo strumento di navigazione di ChatGPT li rifiuta. Il plugin copy-page-md scrive ora accanto a ogni .md una copia .txt identica (servita come text/plain; charset=utf-8) e il prompt per gli assistenti AI cita quella. «Copia pagina» resta sul .md. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018GxiuYvbPs4cCJVKY7tHfA --- plugins/copy-page-md/index.js | 8 +++++++- src/components/CopyPageButton/index.tsx | 6 ++++-- src/components/CopyPageButton/providers.ts | 7 ++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/copy-page-md/index.js b/plugins/copy-page-md/index.js index 6cd8497..e79264a 100644 --- a/plugins/copy-page-md/index.js +++ b/plugins/copy-page-md/index.js @@ -206,8 +206,14 @@ module.exports = function copyPageMd(context) { const file = outFile(outDir, permalink, siteConfig.baseUrl); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, md, 'utf-8'); + // Copia .txt per gli assistenti AI: GitHub Pages serve i .md come + // text/markdown, che lo strumento di navigazione di ChatGPT rifiuta; + // i .txt escono come text/plain; charset=utf-8. + fs.writeFileSync(file.replace(/\.md$/, '.txt'), md, 'utf-8'); } - console.log(`[${PLUGIN_NAME}] scritti ${manifest.length} file .md`); + console.log( + `[${PLUGIN_NAME}] scritti ${manifest.length} file .md (+ copia .txt)`, + ); }, }; }; diff --git a/src/components/CopyPageButton/index.tsx b/src/components/CopyPageButton/index.tsx index 2f14327..db84772 100644 --- a/src/components/CopyPageButton/index.tsx +++ b/src/components/CopyPageButton/index.tsx @@ -128,8 +128,10 @@ function CopyPageMenu({ permalink }: { permalink: string }): ReactNode { // siteConfig.url): così punta sempre all'host da cui la pagina è // realmente servita — dominio custom, fallback GitHub Pages o deploy di // preview — invece di una sola URL canonica fissata a build-time. - const mdUrl = window.location.origin + relMdUrl; - const target = url + encodeURIComponent(buildAiPrompt(mdUrl)); + // Si cita la copia .txt (stesso contenuto del .md): GitHub Pages serve i + // .md come text/markdown, che il browsing di ChatGPT rifiuta. + const txtUrl = window.location.origin + relMdUrl.replace(/\.md$/, '.txt'); + const target = url + encodeURIComponent(buildAiPrompt(txtUrl)); window.open(target, '_blank', 'noopener,noreferrer'); setOpen(false); }, diff --git a/src/components/CopyPageButton/providers.ts b/src/components/CopyPageButton/providers.ts index 4c79278..e34474a 100644 --- a/src/components/CopyPageButton/providers.ts +++ b/src/components/CopyPageButton/providers.ts @@ -42,14 +42,15 @@ export const AI_PROVIDERS: readonly AiProvider[] = [ /** * Prompt italiano coerente col tono di `DEFAULT_EXPLAIN_PROMPT` del PyRunner. - * Cita l'URL del `.md` e chiede una spiegazione adatta a uno studente. + * Cita l'URL del testo ripulito (la copia `.txt` del `.md`) e chiede una + * spiegazione adatta a uno studente. */ -export function buildAiPrompt(mdUrl: string): string { +export function buildAiPrompt(pageUrl: string): string { return ( 'Sto studiando dal manuale scolastico «Python Doesn’t Byte». ' + 'Leggi questa pagina e spiegamela in modo semplice, come a uno studente di ' + '15 anni alle prime armi: dammi un riassunto chiaro e nomina i concetti ' + 'chiave. Rispondi in italiano.\n\n' + - mdUrl + pageUrl ); }