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..e79264a 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) @@ -199,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 ); } 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/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)', }} 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/) 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