From beedf7c3f314abe8d5a294cb0b8418b1af8b4b6a Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Sun, 20 Sep 2026 22:30:04 +0530 Subject: [PATCH] fix(seo): render localized content for search indexing --- assets/seo-metadata.mjs | 21 ++- desktop-app/resources/index.html | 24 +-- desktop-app/resources/js/script.js | 7 +- index.html | 24 +-- package.json | 1 + script.js | 7 +- seo/SEARCH-CONSOLE-2026-09-20.md | 101 +++++++++++ seo/audit-live.mjs | 79 +++++++++ seo/server-render.mjs | 12 +- seo/welcome-content.mjs | 245 +++++++++++++++++++++++++++ sw.js | 2 + tests/e2e/github-import.spec.js | 4 +- tests/e2e/seo.spec.js | 87 +++++++++- tests/helpers/static-build-check.mjs | 18 ++ tests/helpers/static-server.mjs | 30 +++- wiki/Localization.md | 14 +- 16 files changed, 634 insertions(+), 42 deletions(-) create mode 100644 seo/SEARCH-CONSOLE-2026-09-20.md create mode 100644 seo/audit-live.mjs create mode 100644 seo/welcome-content.mjs diff --git a/assets/seo-metadata.mjs b/assets/seo-metadata.mjs index 9ba32ce7..ede55de5 100644 --- a/assets/seo-metadata.mjs +++ b/assets/seo-metadata.mjs @@ -3,6 +3,7 @@ import { getSeoLocale, hasSeoLocale } from '../seo/locales.mjs'; +import { localizedWelcomeMarkdown, renderWelcomeHtml } from '../seo/welcome-content.mjs'; function setAttribute(field, attribute, value) { const element = document.querySelector(`[data-seo-field="${field}"]`); @@ -43,6 +44,20 @@ function applyForLanguage(languageCode) { updateApplicationSchema(locale, canonicalUrl); } -const requestedLanguage = new URLSearchParams(window.location.search).get('lang'); -window.MarkdownViewerSeo = Object.freeze({ applyForLanguage }); -applyForLanguage(requestedLanguage && hasSeoLocale(requestedLanguage) ? requestedLanguage : 'en'); +function languageFromUrl() { + const requestedLanguage = new URLSearchParams(window.location.search).get('lang'); + return requestedLanguage && hasSeoLocale(requestedLanguage) ? requestedLanguage.toLowerCase() : 'en'; +} + +const initialLanguage = languageFromUrl(); +window.MarkdownViewerSeo = Object.freeze({ applyForLanguage, languageFromUrl }); +applyForLanguage(initialLanguage); + +// Static hosts use the same starter as Pages. Do this only before app startup: +// switching the UI language must never replace an open or saved document. +const preview = document.getElementById('welcome-preview'); +if (preview) preview.outerHTML = renderWelcomeHtml(initialLanguage); +const template = document.getElementById('default-markdown'); +if (template && initialLanguage !== 'en') { + template.textContent = localizedWelcomeMarkdown(initialLanguage); +} diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index be3e276f..cd38dfdb 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -1638,18 +1638,18 @@

Quick start

- - + +
+

Welcome to Markdown Viewer

+

Write and preview Markdown in your browser without creating an account. Open a local .md or .markdown file, paste text, or import a document from GitHub. The editor and preview sit side by side so you can check formatting as you type.

+

Start with a document

+

Use New to create a file or open one from your device. Switch between Edit, Split, and Preview in the toolbar. Organize documents into workspaces and folders, and download a Markdown copy to keep a backup.

+

Preview and export

+

Use GitHub-Flavored Markdown for headings, links, tables, and task lists. Preview highlighted code, LaTeX math, and Mermaid diagrams. Export the result as Markdown, HTML, PDF, or PNG; Share Snapshot and Live Share provide optional sharing.

+

Where your content goes

+

Ordinary editing and autosave stay on this device. Sharing, GitHub import, external images, and remote diagram rendering use network services. Keep separate backups: clearing browser storage can remove your locally saved documents.

+
+
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 874748a9..7277c3f2 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -30726,12 +30726,15 @@ ${selector} .arrowheadPath { isApplyingUiTranslations = false; translateUiTree(document.body); - document.title = activeLang === 'en' ? initialDocumentTitle : translateUiString(initialDocumentTitle); + if (window.MarkdownViewerSeo) window.MarkdownViewerSeo.applyForLanguage(lang); + else document.title = activeLang === 'en' ? initialDocumentTitle : translateUiString(initialDocumentTitle); } async function detectAndInitLanguage() { const urlParams = new URLSearchParams(window.location.search); - let lang = urlParams.get('lang'); + // Public web URLs have a stable language, including the English root. + // The desktop build retains saved/browser language detection below. + let lang = window.MarkdownViewerSeo?.languageFromUrl() || urlParams.get('lang'); if (!lang) { const hash = window.location.hash; diff --git a/index.html b/index.html index cd5bce00..4fff702f 100644 --- a/index.html +++ b/index.html @@ -1735,18 +1735,18 @@

Quick start

- - + +
+

Welcome to Markdown Viewer

+

Write and preview Markdown in your browser without creating an account. Open a local .md or .markdown file, paste text, or import a document from GitHub. The editor and preview sit side by side so you can check formatting as you type.

+

Start with a document

+

Use New to create a file or open one from your device. Switch between Edit, Split, and Preview in the toolbar. Organize documents into workspaces and folders, and download a Markdown copy to keep a backup.

+

Preview and export

+

Use GitHub-Flavored Markdown for headings, links, tables, and task lists. Preview highlighted code, LaTeX math, and Mermaid diagrams. Export the result as Markdown, HTML, PDF, or PNG; Share Snapshot and Live Share provide optional sharing.

+

Where your content goes

+

Ordinary editing and autosave stay on this device. Sharing, GitHub import, external images, and remote diagram rendering use network services. Keep separate backups: clearing browser storage can remove your locally saved documents.

+
+
diff --git a/package.json b/package.json index 2df3f852..e3d36ae2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "npm run check:static && npm run check:seo", "check:static": "node tests/helpers/static-build-check.mjs", "check:seo": "node seo/generate-sitemap.mjs --check", + "audit:seo": "node seo/audit-live.mjs", "seo:sitemap": "node seo/generate-sitemap.mjs", "test": "npm run build && npm run test:e2e", "test:install": "playwright install chromium firefox webkit", diff --git a/script.js b/script.js index 874748a9..7277c3f2 100644 --- a/script.js +++ b/script.js @@ -30726,12 +30726,15 @@ ${selector} .arrowheadPath { isApplyingUiTranslations = false; translateUiTree(document.body); - document.title = activeLang === 'en' ? initialDocumentTitle : translateUiString(initialDocumentTitle); + if (window.MarkdownViewerSeo) window.MarkdownViewerSeo.applyForLanguage(lang); + else document.title = activeLang === 'en' ? initialDocumentTitle : translateUiString(initialDocumentTitle); } async function detectAndInitLanguage() { const urlParams = new URLSearchParams(window.location.search); - let lang = urlParams.get('lang'); + // Public web URLs have a stable language, including the English root. + // The desktop build retains saved/browser language detection below. + let lang = window.MarkdownViewerSeo?.languageFromUrl() || urlParams.get('lang'); if (!lang) { const hash = window.location.hash; diff --git a/seo/SEARCH-CONSOLE-2026-09-20.md b/seo/SEARCH-CONSOLE-2026-09-20.md new file mode 100644 index 00000000..14c7cbc4 --- /dev/null +++ b/seo/SEARCH-CONSOLE-2026-09-20.md @@ -0,0 +1,101 @@ +# Search Console investigation — 20 September 2026 + +All six CSV files in the two supplied ZIPs and both screenshots were reviewed. The exported URLs were checked against the live site, and the repository was inspected. The changes below are local project changes; they have not been deployed or submitted to Search Console. + +**The fixable problem is the language pages' content. The two redirecting URLs are intentional aliases. Google decides indexing, so a code change cannot guarantee that every row will become “Passed.”** + +## What the supplied files show + +| Archive | File | Findings | +| --- | --- | --- | +| `https___markdownviewer.pages.dev_-Coverage-Drilldown-2026-09-20.zip` | `Metadata.csv` | Issue: Crawled - currently not indexed. Scope: All known pages. | +| Same archive | `Table.csv` | 14 URLs, all non-English `?lang=` versions. Last crawls: 4–14 September. | +| Same archive | `Chart.csv` | 77 daily observations, 30 June–14 September; no missing dates. Count rose to 4 on 22 August and 14 on 29 August, then remained at 14. | +| `https___markdownviewer.pages.dev_-Coverage-Drilldown-2026-09-20 (1).zip` | `Metadata.csv` | Issue: Page with redirect. Scope: All known pages. | +| Same archive | `Table.csv` | `/?lang=en` and `/tips`, last crawled on 15 and 12 September respectively. | +| Same archive | `Chart.csv` | 77 daily observations, 30 June–14 September; no missing dates. Count increased from 0 to 2 on 29 August and remained at 2. | + +The screenshots show 16 excluded URLs and 1 indexed URL, with failed validations for the two categories. They do not identify the indexed URL. The two categories with zero affected URLs need no corrective work. The export date is newer than the chart's last observation; it is not a real-time index status check. + +## Every affected URL and its solution + +All URLs below use `https://markdownviewer.pages.dev`. + +| Path | Language | Last Google crawl in export | Live HTTP response | Action | +| --- | --- | --- | --- | --- | +| `/?lang=tr` | Turkish | 2026-09-14 | 200 | Deploy translated initial content and starter | +| `/?lang=es` | Spanish | 2026-09-13 | 200 | Same | +| `/?lang=fr` | French | 2026-09-13 | 200 | Same | +| `/?lang=ru` | Russian | 2026-09-13 | 200 | Same | +| `/?lang=tw` | Traditional Chinese | 2026-09-13 | 200 | Same | +| `/?lang=de` | German | 2026-09-13 | 200 | Same | +| `/?lang=pt` | Brazilian Portuguese | 2026-09-12 | 200 | Same | +| `/?lang=uk` | Ukrainian | 2026-09-12 | 200 | Same | +| `/?lang=zh` | Simplified Chinese | 2026-09-12 | 200 | Same | +| `/?lang=it` | Italian | 2026-09-08 | 200 | Same | +| `/?lang=ja` | Japanese | 2026-09-08 | 200 | Same | +| `/?lang=bg` | Bulgarian | 2026-09-06 | 200 | Same | +| `/?lang=pl` | Polish | 2026-09-06 | 200 | Same | +| `/?lang=ko` | Korean | 2026-09-04 | 200 | Same | +| `/?lang=en` | English alias | 2026-09-15 | 308 → `/` | Retain redirect; inspect the destination `/` | +| `/tips` | Retired alias | 2026-09-12 | 301 → `/` | Retain redirect if the editor is the intended replacement | + +The root `/` currently returns 200. Both reported redirects lead directly to it, without a loop. If `/tips` is instead intended to be a separate searchable tutorial, it needs a real, useful tutorial at that address with a 200 response, its own canonical, and internal links. Removing its redirect without providing that page is not a solution. + +## Confirmed technical findings + +1. **All 15 initial page bodies were identical.** The live root and the 14 translated URLs returned different translated titles, descriptions, language headers, and self-canonicals, but the same English body text. After removing script/style/template/textarea blocks, their normalized body text was 9,068 characters with the same SHA-256 prefix, `0b4ed4d4f2cb456b`. +2. **The visible preview initially contained only a skeleton.** Main document content depended on JavaScript. +3. **The first-run welcome document was English for every URL.** The client translated interface labels, while the main sample document remained English. This is a plausible contributor to the indexing outcome, not proof of Google's exact reason. Google determines a page's language from visible content; translated metadata or interface labels alone are insufficient. [Google's multilingual-site guidance](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites) +4. **Startup could overwrite the localized title.** The interface translation code reused an initial title instead of consistently applying the selected locale's SEO metadata. Saved/browser language could also change the root's interface independently of its English URL. +5. **The sitemap and canonical setup already do useful work.** The live sitemap returns 200 and contains the root plus 14 translated URLs. The redirects are absent. Canonicals are self-referencing, language alternates are reciprocal, and the inspected robots rules permit the public pages. No blocking `X-Robots-Tag` was present on the checked pages. + +These observations do not establish a Google penalty, a hosting restriction, or a need to buy a domain. The exports do not contain Google's selected canonical, rendered screenshot, or detailed validation history for individual URLs. Those require authenticated URL Inspection. + +## Changes made in this project + +- Added useful welcome text for all 15 languages: opening/importing documents, editing, preview, exports, sharing, storage, and backups. +- Rendered that content directly in the initial, visible preview. Visitors and crawlers receive the same content; it is not a hidden SEO block or a bot-specific response. +- Added translated starter documents for first-time visits to the 14 non-English URLs, including code, math, and Mermaid examples. The English starter retains the full demonstration. +- Kept saved and edited documents unchanged when switching languages or reloading. +- Made web URL language authoritative. `/` stays English; a supported `?lang=` selects that language. Desktop preference detection remains available. +- Synchronized the title, canonical, schema, and language after interface initialization and language changes, including `pt-BR`. +- Made HEAD responses describe the same language as GET responses. +- Updated the service-worker asset list and synchronized generated desktop resources. +- Updated the development server to exercise the actual Pages SEO middleware. Earlier browser tests could pass through client-side metadata updates without checking production-like HTML. +- Added `npm run audit:seo`, a read-only deployment check for all language pages, redirects, canonical URLs, alternates, and discovery files. This checks responses, not Google's index. + +Relevant implementation: [server rendering](server-render.mjs), [welcome translations](welcome-content.mjs), [browser metadata](../assets/seo-metadata.mjs), and [localization documentation](../wiki/Localization.md). + +## Deployment and Search Console steps + +1. **Deploy these changes through the site's existing Cloudflare Pages process.** The public site must run the Pages Functions middleware in `functions/_middleware.js`; uploading only static HTML will leave initial translated responses dependent on JavaScript. Keep `_routes.json` routing `/` through Functions. No deployment was performed during this investigation. +2. **Verify the deployed responses.** From the repository run `npm run audit:seo`. Before deployment the live site will fail the new translated-content check; after deployment all response checks should pass. A preview can be checked with `npm run audit:seo -- https://YOUR-PREVIEW.pages.dev`. Production canonicals intentionally remain on `markdownviewer.pages.dev`. +3. **Inspect the canonical URLs in Search Console.** Start with `/` and a few affected languages, then cover all 14. Use **Test live URL → View tested page** and inspect both HTML and screenshot. Confirm a successful fetch, indexing allowed, visible translated content, and a canonical matching the inspected language URL. In the indexed inspection results, separately check Google's selected canonical when available; the live test does not predict Google's canonical choice. +4. **Check the submitted sitemap.** In Sitemaps, submit `https://markdownviewer.pages.dev/sitemap.xml` if missing, or verify the existing submission is successful. It already contains the 15 intended URLs. Do not add `/?lang=en` or `/tips`. +5. **Request indexing after the substantive content change.** Use Request indexing for the changed canonical URLs within the available quota. Repeating the same request does not make Google crawl faster. Restart validation for the crawled/not-indexed group after deployment and live checks, if Search Console offers it. Inspect any failed example individually before retrying. +6. **Leave intentional redirects in place.** They can continue to appear under Page with redirect. Google is supposed to index the destination instead. Revalidating unchanged redirects does not turn them into separately indexable pages. [Google's Page indexing report documentation](https://support.google.com/webmasters/answer/7440203?hl=en) +7. **Allow time for recrawling and reassessment.** Compare Google's new crawl date with the deployment date. Google says crawling can take days to weeks and does not guarantee inclusion. There is no reliable deadline for an indexing or validation pass. [Google's recrawl guidance](https://developers.google.com/search/docs/crawling-indexing/ask-google-to-recrawl) + +If a translated page remains excluded after a fresh crawl, inspect the rendered content and Google-selected canonical. Review translation quality and whether the page provides enough value for that audience. A Search Console live-test success only confirms technical accessibility; it does not prove indexing. Server-rendering helps users and crawlers, but quality and selection remain Google's decision. [Google's JavaScript SEO guidance](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) + +## Avoid changes that only move the exclusion elsewhere + +- Do not block the language URLs in robots.txt or mark them noindex when you want them indexed. +- Do not canonicalize genuine translated pages to English merely to reduce an error count. +- Do not remove useful redirects just to make the report green. +- Do not create empty language landing pages or stuff repeated keywords into hidden text. +- There is no need to migrate all query URLs to new paths solely for this report. That would introduce another URL migration without addressing the English content. + +The intended outcome is that the 15 useful canonical language pages are accessible and eligible for indexing, and the two intentional aliases remain redirects. Zero excluded URLs is not the success criterion. + +## Verification + +- `npm run build`: passed, including static checks, every locale's server-rendered content, HEAD handling, and sitemap consistency. +- `tests/e2e/seo.spec.js` on Chromium: 7 passed, covering all 15 initial responses, JavaScript-disabled visibility, static-host fallback, metadata, saved/browser-language consistency, redirects, and document preservation. +- `tests/e2e/smoke.spec.js` on Chromium: 4 passed, including normal startup and release-note behavior. +- The GitHub importer localization regression: 1 passed, exercising all 15 interface languages. +- `npm run audit:seo -- http://127.0.0.1:4173`: all 15 canonical pages passed; English, tips, and index aliases returned the expected permanent redirects. +- French desktop rendering without JavaScript and French mobile rendering were visually checked. The mobile page produced no browser errors or horizontal page overflow. + +Translations are authored product copy, not a claim of native-speaker review. Indexing and validation status remain unverified until deployment and Google's subsequent processing. diff --git a/seo/audit-live.mjs b/seo/audit-live.mjs new file mode 100644 index 00000000..1ce58be7 --- /dev/null +++ b/seo/audit-live.mjs @@ -0,0 +1,79 @@ +import { SEO_LOCALES, SITE_ORIGIN, canonicalPathForLocale, canonicalUrlForLocale, hreflangEntries } from './locales.mjs'; +import { getWelcomeCopy } from './welcome-content.mjs'; + +// Read-only check. Pass a preview origin to inspect a deployment before release. +const origin = process.argv[2] || SITE_ORIGIN; +const failures = []; +const results = []; + +function check(condition, message) { + if (!condition) failures.push(message); +} + +async function get(path) { + return fetch(new URL(path, origin), { redirect: 'manual', signal: AbortSignal.timeout(30_000) }); +} + +for (const locale of SEO_LOCALES) { + const path = canonicalPathForLocale(locale); + try { + const response = await get(path); + const html = await response.text(); + const start = failures.length; + check(response.status === 200, `${path}: expected HTTP 200; got ${response.status}`); + check(!/noindex/i.test(response.headers.get('x-robots-tag') || ''), `${path}: X-Robots-Tag blocks indexing`); + check(!/]*name=["'](?:robots|googlebot)["'][^>]*content=["'][^"']*noindex/i.test(html), `${path}: meta robots blocks indexing`); + check(html.includes(`href="${canonicalUrlForLocale(locale)}" data-seo-field="canonical"`), `${path}: wrong canonical`); + check(response.headers.get('content-language') === locale.htmlLang, `${path}: wrong Content-Language`); + const intro = getWelcomeCopy(locale.code).intro; + check(html.includes(`

${intro}

`), `${path}: translated welcome content missing from initial HTML`); + check(html.includes('id="welcome-preview"'), `${path}: public preview missing`); + for (const alternate of hreflangEntries()) { + check(html.includes(`hreflang="${alternate.hreflang}" href="${alternate.href}"`), `${path}: missing ${alternate.hreflang} alternate`); + } + results.push({ path, status: response.status, checks: failures.length === start ? 'PASS' : 'FAIL' }); + } catch (error) { + failures.push(`${path}: ${error.message}`); + results.push({ path, checks: 'FAIL' }); + } +} + +for (const path of ['/?lang=en', '/tips', '/index.html']) { + try { + const response = await get(path); + const location = response.headers.get('location'); + const destination = location ? new URL(location, origin) : null; + const valid = [301, 308].includes(response.status) && destination?.href === new URL('/', origin).href; + check(valid, `${path}: expected a single permanent redirect to /`); + results.push({ path, status: response.status, checks: valid ? 'EXPECTED REDIRECT' : 'FAIL' }); + } catch (error) { + failures.push(`${path}: ${error.message}`); + } +} + +try { + const sitemap = await get('/sitemap.xml'); + const xml = await sitemap.text(); + check(sitemap.status === 200, 'sitemap.xml: not HTTP 200'); + check((xml.match(//g) || []).length === SEO_LOCALES.length, 'sitemap.xml: wrong canonical URL count'); + for (const locale of SEO_LOCALES) { + check(xml.includes(`${canonicalUrlForLocale(locale)}`), `sitemap.xml: missing ${locale.code}`); + } + check(!/\?lang=en|\/tips|\/index\.html/.test(xml), 'sitemap.xml: contains redirecting aliases'); + const robots = await get('/robots.txt'); + const text = await robots.text(); + check(robots.status === 200 && text.includes(`Sitemap: ${SITE_ORIGIN}/sitemap.xml`), 'robots.txt: missing or wrong sitemap'); + // This project uses one wildcard group. Flag newly introduced homepage/query blocks. + check(!/^Disallow:\s*\/(?:\s*$|\*|\?)/im.test(text), 'robots.txt: review a rule that may block the canonical pages'); +} catch (error) { + failures.push(`Discovery files: ${error.message}`); +} + +console.log(`SEO response audit: ${origin} (${new Date().toISOString()})`); +console.table(results); +if (failures.length) { + failures.forEach(failure => console.error(`FAIL: ${failure}`)); + process.exitCode = 1; +} else { + console.log('All response checks passed. This verifies deployment, not Google indexing or Search Console validation.'); +} diff --git a/seo/server-render.mjs b/seo/server-render.mjs index 61a24d9d..8d054a07 100644 --- a/seo/server-render.mjs +++ b/seo/server-render.mjs @@ -1,4 +1,5 @@ import { canonicalUrlForLocale, getSeoLocale, hasSeoLocale } from './locales.mjs'; +import { localizedWelcomeMarkdown, renderWelcomeHtml } from './welcome-content.mjs'; function escapeHtmlAttribute(value) { return String(value) @@ -65,6 +66,12 @@ export function renderLocalizedSeoHtml(sourceHtml, localeOrCode) { } html = replaceSeoText(html, 'document-title', locale.title); + html = html.replace(/[\s\S]*?/, () => + `\n${renderWelcomeHtml(locale.code)}\n`); + if (locale.code !== 'en') { + html = html.replace(/(]*\bid="default-markdown"[^>]*>)[\s\S]*?(<\/textarea>)/i, + (_, opening, closing) => `${opening}${escapeHtmlText(localizedWelcomeMarkdown(locale.code))}${closing}`); + } return replaceApplicationSchema(html, locale, canonicalUrl); } @@ -99,7 +106,7 @@ export async function handleSeoRequest(context) { const response = await context.next(); const contentType = response.headers.get('content-type') || ''; - if (context.request.method === 'HEAD' || !response.ok || !contentType.includes('text/html')) { + if (!response.ok || !contentType.includes('text/html')) { return response; } @@ -109,7 +116,8 @@ export async function handleSeoRequest(context) { headers.delete('etag'); headers.set('Content-Language', locale.htmlLang); - return new Response(renderLocalizedSeoHtml(await response.text(), locale), { + const body = context.request.method === 'HEAD' ? null : renderLocalizedSeoHtml(await response.text(), locale); + return new Response(body, { status: response.status, statusText: response.statusText, headers diff --git a/seo/welcome-content.mjs b/seo/welcome-content.mjs new file mode 100644 index 00000000..22d86ac1 --- /dev/null +++ b/seo/welcome-content.mjs @@ -0,0 +1,245 @@ +// Public starter content shared by the HTML response and the browser fallback. +// These strings describe the app; saved documents are never translated. +const COPY = { + en: [ + 'Welcome to Markdown Viewer', + 'Write and preview Markdown in your browser without creating an account. Open a local .md or .markdown file, paste text, or import a document from GitHub. The editor and preview sit side by side so you can check formatting as you type.', + 'Start with a document', + 'Use New to create a file or open one from your device. Switch between Edit, Split, and Preview in the toolbar. Organize documents into workspaces and folders, and download a Markdown copy to keep a backup.', + 'Preview and export', + 'Use GitHub-Flavored Markdown for headings, links, tables, and task lists. Preview highlighted code, LaTeX math, and Mermaid diagrams. Export the result as Markdown, HTML, PDF, or PNG; Share Snapshot and Live Share provide optional sharing.', + 'Where your content goes', + 'Ordinary editing and autosave stay on this device. Sharing, GitHub import, external images, and remote diagram rendering use network services. Keep separate backups: clearing browser storage can remove your locally saved documents.', + 'Try a small example', + 'Edit the text on the left and watch the preview update. Use the toolbar to insert a link, table, or diagram.' + ], + zh: [ + '欢迎使用 Markdown Viewer', + '无需创建账户,即可在浏览器中编写和预览 Markdown。打开本地 .md 或 .markdown 文件、粘贴文本,或从 GitHub 导入文档。编辑器与预览并排显示,输入时就能检查排版。', + '开始编辑文档', + '使用“新建”创建文件,或打开设备上的文件。在工具栏中切换编辑、分屏和预览模式。通过工作区和文件夹整理文档,并下载 Markdown 副本作为备份。', + '预览与导出', + '使用 GitHub 风格的 Markdown 编写标题、链接、表格和任务列表,预览代码高亮、LaTeX 公式和 Mermaid 图表。可导出 Markdown、HTML、PDF 或 PNG,也可选择快照分享或实时协作。', + '内容存储在哪里', + '日常编辑和自动保存都在此设备上进行。分享、GitHub 导入、外部图片和远程图表渲染会使用网络服务。请另存备份:清除浏览器存储可能会删除本地文档。', + '尝试一个简单示例', + '修改左侧文本,查看预览如何更新。使用工具栏插入链接、表格或图表。' + ], + ja: [ + 'Markdown Viewer へようこそ', + 'アカウントを作成せずに、ブラウザーで Markdown を編集してプレビューできます。端末の .md や .markdown ファイルを開く、テキストを貼り付ける、GitHub から文書を取り込むことができます。編集欄とプレビューを並べて、入力しながら書式を確認できます。', + '文書を作成する', + '新規作成からファイルを作るか、端末のファイルを開いてください。ツールバーで編集、分割、プレビューを切り替えられます。ワークスペースとフォルダーで文書を整理し、Markdown ファイルをダウンロードしてバックアップを保存できます。', + 'プレビューとエクスポート', + 'GitHub Flavored Markdown の見出し、リンク、表、タスクリストに対応しています。コードの色分け、LaTeX 数式、Mermaid 図を確認し、Markdown、HTML、PDF、PNG として書き出せます。スナップショット共有やライブ共有も利用できます。', + '文書の保存先', + '通常の編集と自動保存はこの端末で行われます。共有、GitHub からの取り込み、外部画像、リモートでの図の描画にはネットワークサービスを使います。ブラウザーの保存データを消すと文書が失われる場合があるため、別途バックアップを残してください。', + '小さな例で試す', + '左側の文章を編集してプレビューの変化を確認してください。ツールバーからリンク、表、図を挿入できます。' + ], + ko: [ + 'Markdown Viewer에 오신 것을 환영합니다', + '계정을 만들지 않고 브라우저에서 Markdown을 작성하고 미리 볼 수 있습니다. 기기의 .md 또는 .markdown 파일을 열거나, 텍스트를 붙여 넣거나, GitHub에서 문서를 가져오세요. 편집기와 미리보기를 나란히 놓고 입력하면서 서식을 확인할 수 있습니다.', + '문서 시작하기', + '새 파일을 만들거나 기기에서 파일을 여세요. 도구 모음에서 편집, 분할, 미리보기 모드를 전환할 수 있습니다. 작업공간과 폴더로 문서를 정리하고 Markdown 사본을 다운로드해 백업하세요.', + '미리보기와 내보내기', + 'GitHub Flavored Markdown의 제목, 링크, 표, 작업 목록을 사용할 수 있습니다. 코드 구문 강조, LaTeX 수식, Mermaid 다이어그램을 미리 보고 Markdown, HTML, PDF, PNG로 내보내세요. 스냅샷 공유와 라이브 공유도 선택해서 사용할 수 있습니다.', + '콘텐츠 저장 위치', + '일반 편집과 자동 저장은 이 기기에서 이루어집니다. 공유, GitHub 가져오기, 외부 이미지, 원격 다이어그램 렌더링은 네트워크 서비스를 사용합니다. 브라우저 저장소를 지우면 로컬 문서가 삭제될 수 있으므로 별도의 백업을 보관하세요.', + '간단한 예제 사용하기', + '왼쪽 텍스트를 수정하면서 미리보기가 바뀌는 모습을 확인하세요. 도구 모음에서 링크, 표, 다이어그램을 삽입할 수 있습니다.' + ], + pt: [ + 'Boas-vindas ao Markdown Viewer', + 'Escreva e visualize Markdown no navegador sem criar uma conta. Abra um arquivo .md ou .markdown do seu dispositivo, cole texto ou importe um documento do GitHub. O editor e a prévia ficam lado a lado para conferir a formatação enquanto você digita.', + 'Comece com um documento', + 'Use Novo para criar um arquivo ou abra um do seu dispositivo. Alterne entre edição, tela dividida e prévia na barra de ferramentas. Organize documentos em espaços de trabalho e pastas e baixe uma cópia em Markdown como backup.', + 'Prévia e exportação', + 'Use Markdown no estilo GitHub para títulos, links, tabelas e listas de tarefas. Visualize código com destaque, fórmulas LaTeX e diagramas Mermaid. Exporte para Markdown, HTML, PDF ou PNG. O compartilhamento de snapshots e a colaboração ao vivo são opcionais.', + 'Onde fica seu conteúdo', + 'A edição comum e o salvamento automático ficam neste dispositivo. Compartilhamento, importação do GitHub, imagens externas e renderização remota de diagramas usam serviços de rede. Guarde backups separados: limpar os dados do navegador pode apagar seus documentos locais.', + 'Experimente um exemplo simples', + 'Edite o texto à esquerda e acompanhe a prévia. Use a barra de ferramentas para inserir um link, uma tabela ou um diagrama.' + ], + es: [ + 'Te damos la bienvenida a Markdown Viewer', + 'Escribe y previsualiza Markdown en el navegador sin crear una cuenta. Abre un archivo .md o .markdown de tu dispositivo, pega texto o importa un documento desde GitHub. El editor y la vista previa aparecen juntos para comprobar el formato mientras escribes.', + 'Empieza con un documento', + 'Usa Nuevo para crear un archivo o abre uno de tu dispositivo. Cambia entre edición, vista dividida y vista previa desde la barra de herramientas. Organiza documentos en espacios de trabajo y carpetas y descarga una copia en Markdown como respaldo.', + 'Vista previa y exportación', + 'Usa Markdown de GitHub para títulos, enlaces, tablas y listas de tareas. Previsualiza código resaltado, fórmulas LaTeX y diagramas Mermaid. Exporta a Markdown, HTML, PDF o PNG. Puedes compartir una instantánea o colaborar en directo si lo necesitas.', + 'Dónde se guarda el contenido', + 'La edición habitual y el guardado automático se realizan en este dispositivo. Compartir, importar desde GitHub, cargar imágenes externas y renderizar diagramas de forma remota utiliza servicios de red. Guarda copias aparte: borrar los datos del navegador puede eliminar tus documentos locales.', + 'Prueba un ejemplo sencillo', + 'Edita el texto de la izquierda y observa cómo cambia la vista previa. Usa la barra de herramientas para insertar un enlace, una tabla o un diagrama.' + ], + fr: [ + 'Bienvenue dans Markdown Viewer', + 'Rédigez et prévisualisez du Markdown dans votre navigateur sans créer de compte. Ouvrez un fichier .md ou .markdown de votre appareil, collez du texte ou importez un document depuis GitHub. L’éditeur et l’aperçu sont côte à côte pour vérifier la mise en forme pendant la saisie.', + 'Commencer un document', + 'Créez un fichier avec Nouveau ou ouvrez un fichier de votre appareil. Passez de l’édition à la vue partagée ou à l’aperçu depuis la barre d’outils. Classez vos documents dans des espaces de travail et des dossiers, puis téléchargez une copie Markdown pour la sauvegarder.', + 'Aperçu et exportation', + 'Utilisez le Markdown de GitHub pour les titres, liens, tableaux et listes de tâches. Prévisualisez le code coloré, les formules LaTeX et les diagrammes Mermaid. Exportez en Markdown, HTML, PDF ou PNG. Le partage d’instantanés et la collaboration en direct sont facultatifs.', + 'Où se trouve votre contenu', + 'L’édition courante et la sauvegarde automatique restent sur cet appareil. Le partage, l’importation GitHub, les images externes et le rendu distant des diagrammes utilisent des services réseau. Gardez des sauvegardes séparées : effacer les données du navigateur peut supprimer vos documents locaux.', + 'Essayer un exemple simple', + 'Modifiez le texte à gauche et observez l’aperçu. Utilisez la barre d’outils pour insérer un lien, un tableau ou un diagramme.' + ], + de: [ + 'Willkommen bei Markdown Viewer', + 'Schreibe Markdown im Browser und sieh dir die Vorschau an, ohne ein Konto anzulegen. Öffne eine lokale .md- oder .markdown-Datei, füge Text ein oder importiere ein Dokument von GitHub. Editor und Vorschau stehen nebeneinander, damit du die Formatierung beim Schreiben prüfen kannst.', + 'Mit einem Dokument beginnen', + 'Erstelle über Neu eine Datei oder öffne eine Datei von deinem Gerät. Wechsle in der Werkzeugleiste zwischen Bearbeiten, geteilter Ansicht und Vorschau. Ordne Dokumente in Arbeitsbereichen und Ordnern und lade eine Markdown-Kopie als Sicherung herunter.', + 'Vorschau und Export', + 'Nutze GitHub Flavored Markdown für Überschriften, Links, Tabellen und Aufgabenlisten. Prüfe hervorgehobenen Code, LaTeX-Formeln und Mermaid-Diagramme. Exportiere als Markdown, HTML, PDF oder PNG. Bei Bedarf kannst du einen Snapshot teilen oder live zusammenarbeiten.', + 'Wo deine Inhalte gespeichert werden', + 'Normales Bearbeiten und automatisches Speichern erfolgen auf diesem Gerät. Teilen, GitHub-Import, externe Bilder und entfernte Diagrammdienste nutzen das Netzwerk. Bewahre zusätzliche Sicherungen auf: Wenn du Browserdaten löschst, können lokal gespeicherte Dokumente verloren gehen.', + 'Ein kleines Beispiel ausprobieren', + 'Bearbeite den Text links und beobachte die Vorschau. Über die Werkzeugleiste kannst du Links, Tabellen und Diagramme einfügen.' + ], + ru: [ + 'Добро пожаловать в Markdown Viewer', + 'Пишите и просматривайте Markdown в браузере без создания аккаунта. Откройте локальный файл .md или .markdown, вставьте текст или импортируйте документ из GitHub. Редактор и предпросмотр расположены рядом, чтобы проверять оформление во время ввода.', + 'Начните с документа', + 'Создайте новый файл или откройте файл с устройства. На панели инструментов переключайтесь между редактированием, разделённым экраном и предпросмотром. Размещайте документы в рабочих пространствах и папках, скачивайте копии Markdown для резервного хранения.', + 'Предпросмотр и экспорт', + 'Используйте Markdown в стиле GitHub для заголовков, ссылок, таблиц и списков задач. Просматривайте код с подсветкой, формулы LaTeX и диаграммы Mermaid. Экспортируйте в Markdown, HTML, PDF или PNG. При необходимости делитесь снимками или работайте совместно в реальном времени.', + 'Где хранятся ваши данные', + 'Обычное редактирование и автосохранение происходят на этом устройстве. Общий доступ, импорт из GitHub, внешние изображения и удалённое построение диаграмм используют сетевые сервисы. Делайте отдельные резервные копии: очистка данных браузера может удалить локальные документы.', + 'Попробуйте простой пример', + 'Изменяйте текст слева и наблюдайте за предпросмотром. С помощью панели инструментов вставляйте ссылки, таблицы и диаграммы.' + ], + it: [ + 'Benvenuto in Markdown Viewer', + 'Scrivi e visualizza Markdown nel browser senza creare un account. Apri un file .md o .markdown dal dispositivo, incolla del testo o importa un documento da GitHub. Editor e anteprima sono affiancati per controllare la formattazione mentre scrivi.', + 'Inizia con un documento', + 'Usa Nuovo per creare un file oppure aprine uno dal dispositivo. Passa tra modifica, vista divisa e anteprima dalla barra degli strumenti. Organizza i documenti in spazi di lavoro e cartelle e scarica una copia Markdown come backup.', + 'Anteprima ed esportazione', + 'Usa Markdown in stile GitHub per titoli, collegamenti, tabelle ed elenchi di attività. Visualizza codice evidenziato, formule LaTeX e diagrammi Mermaid. Esporta in Markdown, HTML, PDF o PNG. La condivisione di snapshot e la collaborazione in tempo reale sono facoltative.', + 'Dove vengono salvati i contenuti', + 'La normale modifica e il salvataggio automatico avvengono su questo dispositivo. Condivisione, importazione da GitHub, immagini esterne e rendering remoto dei diagrammi usano servizi di rete. Conserva backup separati: cancellare i dati del browser può eliminare i documenti locali.', + 'Prova un piccolo esempio', + 'Modifica il testo a sinistra e osserva l’anteprima. Usa la barra degli strumenti per inserire un collegamento, una tabella o un diagramma.' + ], + tr: [ + 'Markdown Viewer’a hoş geldiniz', + 'Hesap oluşturmadan tarayıcınızda Markdown yazın ve önizleyin. Cihazınızdaki .md veya .markdown dosyasını açın, metin yapıştırın ya da GitHub’dan belge aktarın. Düzenleyici ve önizleme yan yana durur; yazarken biçimlendirmeyi kontrol edebilirsiniz.', + 'Bir belgeyle başlayın', + 'Yeni seçeneğiyle dosya oluşturun veya cihazınızdan bir dosya açın. Araç çubuğundan düzenleme, bölünmüş görünüm ve önizleme arasında geçiş yapın. Belgeleri çalışma alanları ve klasörlerle düzenleyin; yedeklemek için Markdown kopyasını indirin.', + 'Önizleme ve dışa aktarma', + 'Başlıklar, bağlantılar, tablolar ve görev listeleri için GitHub tarzı Markdown kullanın. Vurgulanmış kodu, LaTeX formüllerini ve Mermaid diyagramlarını önizleyin. Markdown, HTML, PDF veya PNG olarak dışa aktarın. İsterseniz anlık görüntü paylaşabilir veya canlı olarak birlikte çalışabilirsiniz.', + 'İçeriğiniz nerede saklanır', + 'Normal düzenleme ve otomatik kaydetme bu cihazda gerçekleşir. Paylaşım, GitHub’dan aktarma, harici görseller ve uzaktan diyagram oluşturma ağ hizmetlerini kullanır. Ayrı yedekler tutun: tarayıcı verilerini temizlemek yerel belgelerinizi silebilir.', + 'Küçük bir örnek deneyin', + 'Soldaki metni düzenleyip önizlemenin güncellenmesini izleyin. Araç çubuğuyla bağlantı, tablo veya diyagram ekleyin.' + ], + pl: [ + 'Witaj w Markdown Viewer', + 'Pisz i przeglądaj Markdown w przeglądarce bez zakładania konta. Otwórz lokalny plik .md lub .markdown, wklej tekst albo zaimportuj dokument z GitHub. Edytor i podgląd są obok siebie, więc możesz sprawdzać formatowanie podczas pisania.', + 'Zacznij od dokumentu', + 'Utwórz nowy plik lub otwórz plik z urządzenia. Na pasku narzędzi przełączaj się między edycją, widokiem dzielonym i podglądem. Porządkuj dokumenty w obszarach roboczych i folderach, a kopię Markdown pobierz jako kopię zapasową.', + 'Podgląd i eksport', + 'Używaj Markdown w stylu GitHub do nagłówków, linków, tabel i list zadań. Przeglądaj wyróżniony kod, wzory LaTeX i diagramy Mermaid. Eksportuj do Markdown, HTML, PDF lub PNG. Opcjonalnie udostępniaj migawki lub współpracuj na żywo.', + 'Gdzie trafiają Twoje dane', + 'Zwykła edycja i automatyczny zapis odbywają się na tym urządzeniu. Udostępnianie, import z GitHub, zewnętrzne obrazy i zdalne renderowanie diagramów korzystają z usług sieciowych. Przechowuj osobne kopie zapasowe: wyczyszczenie danych przeglądarki może usunąć lokalne dokumenty.', + 'Wypróbuj prosty przykład', + 'Zmień tekst po lewej i obserwuj podgląd. Użyj paska narzędzi, aby wstawić link, tabelę lub diagram.' + ], + tw: [ + '歡迎使用 Markdown Viewer', + '無須建立帳號,即可在瀏覽器中撰寫和預覽 Markdown。開啟本機 .md 或 .markdown 檔案、貼上文字,或從 GitHub 導入文件。編輯器與預覽並排顯示,輸入時就能檢查排版。', + '開始編輯文件', + '使用「新增」建立檔案,或開啟裝置上的檔案。在工具列切換編輯、分割檢視和預覽模式。透過工作區與資料夾整理文件,並下載 Markdown 副本作為備份。', + '預覽與匯出', + '使用 GitHub 風格的 Markdown 撰寫標題、連結、表格和工作清單,預覽程式碼醒目提示、LaTeX 公式與 Mermaid 圖表。可匯出 Markdown、HTML、PDF 或 PNG,也可選擇快照分享或即時協作。', + '內容儲存在哪裡', + '一般編輯與自動儲存都在此裝置上進行。分享、GitHub 導入、外部圖片與遠端圖表轉譯會使用網路服務。請另外保存備份:清除瀏覽器儲存空間可能會刪除本機文件。', + '試用一個簡單範例', + '修改左側文字,查看預覽如何更新。使用工具列插入連結、表格或圖表。' + ], + uk: [ + 'Ласкаво просимо до Markdown Viewer', + 'Пишіть і переглядайте Markdown у браузері без створення облікового запису. Відкрийте локальний файл .md або .markdown, вставте текст чи імпортуйте документ із GitHub. Редактор і попередній перегляд розташовані поруч, щоб перевіряти оформлення під час введення.', + 'Почніть із документа', + 'Створіть новий файл або відкрийте файл із пристрою. На панелі інструментів перемикайтеся між редагуванням, розділеним виглядом і переглядом. Упорядковуйте документи в робочих просторах і папках, завантажуйте копію Markdown для резервного зберігання.', + 'Перегляд та експорт', + 'Використовуйте Markdown у стилі GitHub для заголовків, посилань, таблиць і списків завдань. Переглядайте підсвічений код, формули LaTeX і діаграми Mermaid. Експортуйте в Markdown, HTML, PDF або PNG. За потреби діліться знімками чи працюйте разом у реальному часі.', + 'Де зберігаються ваші дані', + 'Звичайне редагування й автозбереження відбуваються на цьому пристрої. Спільний доступ, імпорт із GitHub, зовнішні зображення та віддалене створення діаграм використовують мережеві служби. Робіть окремі резервні копії: очищення даних браузера може видалити локальні документи.', + 'Спробуйте простий приклад', + 'Змінюйте текст ліворуч і спостерігайте за переглядом. За допомогою панелі інструментів вставляйте посилання, таблиці та діаграми.' + ], + bg: [ + 'Добре дошли в Markdown Viewer', + 'Пишете и преглеждайте Markdown в браузъра, без да създавате профил. Отворете локален файл .md или .markdown, поставете текст или импортирайте документ от GitHub. Редакторът и прегледът са един до друг, за да проверявате оформлението, докато пишете.', + 'Започнете с документ', + 'Създайте нов файл или отворете файл от устройството си. От лентата с инструменти превключвайте между редактиране, разделен изглед и преглед. Подреждайте документите в работни пространства и папки и изтегляйте Markdown копие за резервно съхранение.', + 'Преглед и експортиране', + 'Използвайте Markdown в стил GitHub за заглавия, връзки, таблици и списъци със задачи. Преглеждайте оцветен код, формули LaTeX и диаграми Mermaid. Експортирайте като Markdown, HTML, PDF или PNG. По желание споделяйте снимки на документи или работете съвместно на живо.', + 'Къде се съхранява съдържанието', + 'Обикновеното редактиране и автоматичното запазване се извършват на това устройство. Споделянето, импортирането от GitHub, външните изображения и отдалеченото изобразяване на диаграми използват мрежови услуги. Пазете отделни резервни копия: изчистването на данните на браузъра може да премахне локалните документи.', + 'Опитайте кратък пример', + 'Редактирайте текста отляво и наблюдавайте прегледа. Използвайте лентата с инструменти, за да вмъкнете връзка, таблица или диаграма.' + ] +}; + +export function getWelcomeCopy(code) { + const strings = COPY[code] || COPY.en; + const [heading, intro, startHeading, start, featuresHeading, features, privacyHeading, privacy, exampleHeading, example] = strings; + return { heading, intro, startHeading, start, featuresHeading, features, privacyHeading, privacy, exampleHeading, example }; +} + +function escapeHtml(value) { + return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); +} + +export function renderWelcomeHtml(code) { + const copy = getWelcomeCopy(code); + return `
+

${escapeHtml(copy.heading)}

+

${escapeHtml(copy.intro)}

+

${escapeHtml(copy.startHeading)}

+

${escapeHtml(copy.start)}

+

${escapeHtml(copy.featuresHeading)}

+

${escapeHtml(copy.features)}

+

${escapeHtml(copy.privacyHeading)}

+

${escapeHtml(copy.privacy)}

+
`; +} + +export function localizedWelcomeMarkdown(code) { + const copy = getWelcomeCopy(code); + return `# ${copy.heading} + +${copy.intro} + +## ${copy.startHeading} + +${copy.start} + +## ${copy.featuresHeading} + +${copy.features} + +## ${copy.exampleHeading} + +${copy.example} + +\`\`\`javascript +const message = "Markdown"; +console.log(message); +\`\`\` + +$$E = mc^2$$ + +\`\`\`mermaid +flowchart LR + Markdown --> HTML + HTML --> PDF +\`\`\` + +## ${copy.privacyHeading} + +${copy.privacy} +`; +} diff --git a/sw.js b/sw.js index 40a43bc4..5d2d8fc6 100644 --- a/sw.js +++ b/sw.js @@ -11,6 +11,7 @@ const CRITICAL_ASSETS = [ './styles.css', './assets/seo-metadata.mjs', './seo/locales.mjs', + './seo/welcome-content.mjs', './assets/lucide-icons.css', './RELEASE_NOTES', './sample.md', @@ -34,6 +35,7 @@ const NETWORK_FIRST_LOCAL_PATHS = new Set([ '/styles.css', '/assets/seo-metadata.mjs', '/seo/locales.mjs', + '/seo/welcome-content.mjs', '/assets/lucide-icons.css', '/sw.js' ]); diff --git a/tests/e2e/github-import.spec.js b/tests/e2e/github-import.spec.js index 25dcd65f..4bc4647e 100644 --- a/tests/e2e/github-import.spec.js +++ b/tests/e2e/github-import.spec.js @@ -395,7 +395,7 @@ test('resolves slash-containing branches and shows the immutable commit beside t test('localizes the GitHub importer in every supported interface language', async ({ page }) => { test.setTimeout(180_000); - const locales = ['en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'tw', 'uk', 'zh']; + const locales = ['en', 'bg', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'tw', 'uk', 'zh']; const catalogs = Object.fromEntries(locales.map(locale => [ locale, locale === 'en' ? null : require(`../../assets/i18n/${locale}.json`) @@ -440,7 +440,7 @@ test('localizes the GitHub importer in every supported interface language', asyn for (const locale of locales) { const translate = source => catalogs[locale]?.[source] || source; - const languageTag = locale === 'zh' ? 'zh-Hans' : (locale === 'tw' ? 'zh-Hant' : locale); + const languageTag = { zh: 'zh-Hans', tw: 'zh-Hant', pt: 'pt-BR' }[locale] || locale; await openApp(page, `/?lang=${locale}`); await expect(page.locator('html')).toHaveAttribute('lang', languageTag); await openGitHubImporter(page); diff --git a/tests/e2e/seo.spec.js b/tests/e2e/seo.spec.js index eb989b6d..2f2e4e83 100644 --- a/tests/e2e/seo.spec.js +++ b/tests/e2e/seo.spec.js @@ -1,8 +1,9 @@ const { test, expect } = require('@playwright/test'); +const { openApp, setEditorContent } = require('../helpers/app'); test.describe('localized search metadata', () => { test('renders a self-canonical Traditional Chinese page', async ({ page }) => { - await page.goto('/?lang=tw'); + await openApp(page, '/?lang=tw'); await expect(page.locator('html')).toHaveAttribute('lang', 'zh-Hant'); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( @@ -15,11 +16,14 @@ test.describe('localized search metadata', () => { const schema = JSON.parse(await page.locator('#application-schema').textContent()); expect(schema.url).toBe('https://markdownviewer.pages.dev/?lang=tw'); expect(schema.inLanguage).toBe('zh-Hant'); + await expect(page.locator('#markdown-preview')).toContainText('歡迎使用 Markdown Viewer'); + await expect(page.locator('#markdown-editor')).not.toHaveValue(/# Welcome to Markdown Viewer/); }); test('keeps language navigation crawlable and updates canonical metadata', async ({ page }) => { - await page.goto('/?lang=tw'); - await page.locator('html[data-app-ready="true"]').waitFor(); + await openApp(page, '/?lang=tw'); + const originalDocument = '# My document\n\nKeep my content unchanged.'; + await setEditorContent(page, originalDocument); const desktopLanguageLinks = page.locator('#languageDropdown + .settings-language-menu .lang-select-item'); await expect(desktopLanguageLinks).toHaveCount(15); @@ -43,5 +47,82 @@ test.describe('localized search metadata', () => { 'https://markdownviewer.pages.dev/?lang=bg' ); await expect(page).toHaveTitle(/Markdown преглед/); + await expect(page.locator('#markdown-editor')).toHaveValue(originalDocument); + await page.reload(); + await page.locator('html[data-app-ready="true"]').waitFor(); + await expect(page.locator('#markdown-editor')).toHaveValue(originalDocument); + }); + + test('serves distinct translated content before JavaScript for every canonical locale', async ({ request }) => { + const { SEO_LOCALES, canonicalUrlForLocale, canonicalPathForLocale } = await import('../../seo/locales.mjs'); + const { getWelcomeCopy } = await import('../../seo/welcome-content.mjs'); + const intros = new Set(); + for (const locale of SEO_LOCALES) { + const response = await request.get(canonicalPathForLocale(locale), { maxRedirects: 0 }); + expect(response.status()).toBe(200); + expect(response.headers()['content-language']).toBe(locale.htmlLang); + const html = await response.text(); + const copy = getWelcomeCopy(locale.code); + expect(html).toContain(`href="${canonicalUrlForLocale(locale)}" data-seo-field="canonical"`); + expect(html).toContain(`

${copy.intro}

`); + expect(html).toContain(`

${copy.privacy}

`); + expect(html).not.toContain('id="welcome-preview" hidden'); + intros.add(copy.intro); + if (locale.code !== 'en') expect(html).not.toContain('# Welcome to Markdown Viewer'); + } + expect(intros.size).toBe(15); + }); + + test('the translated welcome is readable with JavaScript disabled', async ({ browser, baseURL }) => { + const context = await browser.newContext({ baseURL, javaScriptEnabled: false }); + const page = await context.newPage(); + try { + await page.goto('/?lang=fr'); + await expect(page.locator('#welcome-preview')).toBeVisible(); + await expect(page.locator('#welcome-preview h2')).toHaveText('Bienvenue dans Markdown Viewer'); + await expect(page.locator('#welcome-preview')).toContainText('Rédigez et prévisualisez'); + await expect(page.locator('#welcome-preview')).not.toContainText('Write and preview Markdown'); + } finally { + await context.close(); + } + }); + + test('a static host also starts with the localized document', async ({ page }) => { + const { readFile } = require('node:fs/promises'); + const html = await readFile('index.html', 'utf8'); + await page.route(/\/\?lang=ja$/, route => route.fulfill({ contentType: 'text/html; charset=utf-8', body: html })); + const errors = await openApp(page, '/?lang=ja'); + expect(errors).toEqual([]); + await expect(page.locator('#markdown-preview')).toContainText('Markdown Viewer へようこそ'); + await expect(page.locator('#markdown-editor')).not.toHaveValue(/# Welcome to Markdown Viewer/); + await expect(page).toHaveTitle(/Markdown ビューア/); + }); + + test('the root keeps English content and metadata despite saved or browser language', async ({ browser, baseURL }) => { + const context = await browser.newContext({ baseURL, locale: 'fr-FR' }); + const page = await context.newPage(); + try { + await page.addInitScript(() => localStorage.setItem('app-lang', 'fr')); + await openApp(page); + await expect(page.locator('html')).toHaveAttribute('lang', 'en'); + await expect(page).toHaveTitle('Markdown Viewer - Online Markdown Editor with Live Preview'); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', 'https://markdownviewer.pages.dev/'); + await expect(page.locator('#markdown-preview')).toContainText('Welcome to Markdown Viewer'); + } finally { + await context.close(); + } + }); + + test('canonical aliases redirect once and stay out of the sitemap', async ({ request }) => { + for (const path of ['/?lang=en', '/tips', '/index.html']) { + const response = await request.get(path, { maxRedirects: 0 }); + expect([301, 308]).toContain(response.status()); + expect(new URL(response.headers().location, response.url()).pathname).toBe('/'); + expect(new URL(response.headers().location, response.url()).search).toBe(''); + expect((await request.get(response.headers().location)).status()).toBe(200); + } + const sitemap = await (await request.get('/sitemap.xml')).text(); + expect(sitemap.match(//g)).toHaveLength(15); + expect(sitemap).not.toMatch(/\?lang=en|\/tips|\/index\.html/); }); }); diff --git a/tests/helpers/static-build-check.mjs b/tests/helpers/static-build-check.mjs index 65745488..b1e9b0b0 100644 --- a/tests/helpers/static-build-check.mjs +++ b/tests/helpers/static-build-check.mjs @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import { SEO_LOCALES, canonicalUrlForLocale, hreflangEntries } from '../../seo/locales.mjs'; import { buildSitemap } from '../../seo/generate-sitemap.mjs'; import { handleSeoRequest, renderLocalizedSeoHtml } from '../../seo/server-render.mjs'; +import { getWelcomeCopy, localizedWelcomeMarkdown, renderWelcomeHtml } from '../../seo/welcome-content.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, '../..'); @@ -21,6 +22,7 @@ const requiredFiles = [ '_routes.json', 'assets/seo-metadata.mjs', 'seo/locales.mjs', + 'seo/welcome-content.mjs', 'seo/server-render.mjs', 'seo/generate-sitemap.mjs', 'functions/_middleware.js', @@ -44,6 +46,7 @@ const syntaxCheckedFiles = [ 'preview-worker.js', 'sw.js', 'assets/seo-metadata.mjs', + 'seo/welcome-content.mjs', 'desktop-app/resources/js/main.js', 'desktop-app/resources/js/script.js', 'desktop-app/resources/js/workspace-storage.js', @@ -132,6 +135,16 @@ for (const locale of SEO_LOCALES) { throw new Error(`Server-rendered SEO for ${locale.code} is missing: ${marker}`); } } + const welcome = getWelcomeCopy(locale.code); + if (!renderedHtml.includes(renderWelcomeHtml(locale.code))) { + throw new Error(`The ${locale.code} response must include the visible localized welcome content.`); + } + if (locale.code !== 'en') { + const escapedStarter = localizedWelcomeMarkdown(locale.code).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + if (welcome.intro === getWelcomeCopy('en').intro || !renderedHtml.includes(escapedStarter)) { + throw new Error(`The ${locale.code} page must provide a translated starter document, not the English demo.`); + } + } } async function runSeoRequest(url, method = 'GET') { @@ -160,6 +173,11 @@ if (traditionalChineseResponse.headers.has('Content-Length') || traditionalChine throw new Error('SEO middleware retained stale representation headers after rewriting HTML.'); } +const headResponse = await runSeoRequest('https://markdownviewer.pages.dev/?lang=tw', 'HEAD'); +if (headResponse.headers.get('Content-Language') !== 'zh-Hant' || (await headResponse.text()) !== '') { + throw new Error('HEAD must describe the localized GET response without returning a body.'); +} + for (const url of [ 'https://markdownviewer.pages.dev/?lang=en', 'https://markdownviewer.pages.dev/?lang=unsupported' diff --git a/tests/helpers/static-server.mjs b/tests/helpers/static-server.mjs index 19d379a5..2cd784f0 100644 --- a/tests/helpers/static-server.mjs +++ b/tests/helpers/static-server.mjs @@ -1,8 +1,9 @@ import { createReadStream } from 'node:fs'; -import { stat } from 'node:fs/promises'; +import { readFile, stat } from 'node:fs/promises'; import { createServer } from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { handleSeoRequest } from '../../seo/server-render.mjs'; const host = '127.0.0.1'; const port = Number(process.env.MARKDOWN_VIEWER_TEST_PORT || 4173); @@ -25,7 +26,8 @@ const contentTypes = new Map([ ['.txt', 'text/plain; charset=utf-8'], ['.webmanifest', 'application/manifest+json; charset=utf-8'], ['.woff', 'font/woff'], - ['.woff2', 'font/woff2'] + ['.woff2', 'font/woff2'], + ['.xml', 'application/xml; charset=utf-8'] ]); function resolveRequestPath(requestUrl) { @@ -44,6 +46,30 @@ const server = createServer(async (request, response) => { return; } + const url = new URL(request.url, `http://${host}:${port}`); + if (url.pathname === '/tips' || url.pathname === '/index.html') { + response.writeHead(301, { Location: '/' + url.search }); + response.end(); + return; + } + if (url.pathname === '/') { + try { + const result = await handleSeoRequest({ + request: new Request(url, { method: request.method }), + next: async () => new Response(await readFile(path.join(rootDir, 'index.html'), 'utf8'), { + headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } + }) + }); + response.writeHead(result.status, Object.fromEntries(result.headers)); + response.end(request.method === 'HEAD' ? undefined : await result.text()); + } catch (error) { + console.error(error); + response.writeHead(500); + response.end('Unable to render page'); + } + return; + } + const filePath = resolveRequestPath(request.url); if (!filePath) { response.writeHead(403); diff --git a/wiki/Localization.md b/wiki/Localization.md index 6058676e..9014241b 100644 --- a/wiki/Localization.md +++ b/wiki/Localization.md @@ -1,6 +1,6 @@ # Localization and Internationalization -Markdown Viewer translates its interface in the browser. Core labels live in `I18N_DICTS` in `script.js`, while broader static and dynamic interface strings are loaded from `assets/i18n/.json`. User-authored Markdown and filenames are never translated. +Markdown Viewer translates its interface in the browser. Core labels live in `I18N_DICTS` in `script.js`, while broader static and dynamic interface strings are loaded from `assets/i18n/.json`. Cloudflare Pages also renders the localized public welcome content before JavaScript runs. New visitors to a language URL receive a translated starter document. User-authored Markdown and filenames are never translated. The English interface and English documentation are the source text. The approved multilingual terminology tables on this page align documentation with the current interface labels. @@ -38,7 +38,9 @@ Detailed Wiki pages are maintained in English. Localized READMEs label those des ## Selection Order -The app chooses a language in this order: +On the public website, the URL determines the language: `/` is English and `/?lang=fr`, for example, is French. Saved preferences and browser language do not override a public URL. This keeps the initial HTML, rendered content, title, and canonical URL consistent. Choosing a language updates the URL without replacing any open document. + +The desktop app chooses a language in this order: 1. URL query parameter, such as `?lang=pt`. 2. Hash query parameter when present in a shared URL. @@ -48,6 +50,14 @@ The app chooses a language in this order: When a user picks a language from the dropdown, the app saves `app-lang` and updates the URL query parameter. +## Public Search Content + +`seo/locales.mjs` defines canonical URLs, metadata, and reciprocal language alternates. `seo/welcome-content.mjs` contains the public introduction and starter text for all 15 languages. `seo/server-render.mjs` puts that introduction in the visible preview and prepares the translated starter for new visitors. `assets/seo-metadata.mjs` provides the equivalent browser fallback on static hosts and keeps metadata synchronized after a language change. + +The English starter retains the full feature demonstration. The translated starters explain editing, imports, preview, export, sharing, local storage, and backups, with code, math, and Mermaid examples. Changing the interface language never translates or overwrites saved documents. Translations should receive native-speaker review as terminology evolves. + +The development server runs the same SEO middleware as Cloudflare Pages. Run `npm run build` and `npx playwright test tests/e2e/seo.spec.js --project=chromium` to verify metadata, the initial HTML in every language, rendering without JavaScript, redirects, and document preservation. + ## What Gets Translated The catalogs cover: