Repository navigation
Add disambiguation pages with editor templates and reader notices #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /** Kept in the document so revisions, Git and MCP preserve the page type together. */ | ||
| export const DISAMBIGUATION_MARKER = '<!-- vireowiki:disambiguation -->'; | ||
| const prefix = /^(?:\uFEFF)?[ \t\r\n]*<!-- vireowiki:disambiguation -->[ \t]*(?:\r?\n|$)/; | ||
| export function isDisambiguation(content: unknown): boolean { | ||
| return typeof content === 'string' && prefix.test(content); | ||
| } | ||
| export function stripDisambiguation(content: unknown): string { | ||
| return typeof content === 'string' ? content.replace(prefix, '') : ''; | ||
| } | ||
| export function setDisambiguation(content: string, enabled: boolean): string { | ||
| const body = stripDisambiguation(content); | ||
| return enabled ? DISAMBIGUATION_MARKER + '\n' + body : body; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| * - 다른 모듈이 read/write 하는 state(slug, editor, sectionMode 등)는 types.ts 에 | ||
| * 선언된 window 프로퍼티를 직접 read/write 한다 — 모듈 내부 로컬 미러는 두지 않는다. | ||
| */ | ||
| import {isDisambiguation,setDisambiguation,stripDisambiguation} from '../../../packages/wiki-shared/src/markup/disambiguation'; | ||
| import { ui } from '../../../packages/wiki-shared/src/i18n/client'; | ||
| import './types'; | ||
| import { escapeHtml } from '../utils/html'; | ||
|
|
@@ -1258,6 +1259,8 @@ document.addEventListener('DOMContentLoaded', async () => { | |
| // 문서 변경 감지 리스너 | ||
| const updateListener = EditorView.updateListener.of((update) => { | ||
| if (update.docChanged) { | ||
| const typeInput = document.getElementById('disambiguationType') as HTMLInputElement | null; | ||
| if (typeInput) typeInput.checked = isDisambiguation(update.state.doc.toString()); | ||
| editorEventHandlers.change.forEach(cb => cb()); | ||
| window.updateEditorTextCounterFromDoc(update.state.doc); | ||
| if (_findFeatureOnDocChange) _findFeatureOnDocChange(update); | ||
|
|
@@ -2748,6 +2751,28 @@ document.addEventListener('DOMContentLoaded', async () => { | |
| // 넘겨주기(redirect) 변경 시 편집 요약 자동 갱신 | ||
| // input: 매 키 입력마다 디바운스로 갱신 (입력 중간 prefix 가 길어졌다 짧아졌다 반복하는 것 완화) | ||
| // change: blur 직후 즉시 확정 | ||
| const redirectField = document.getElementById('redirectInput')?.closest('.mb-3'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This condition also adds the disambiguation controls when Useful? React with 👍 / 👎. |
||
| if (redirectField && !document.getElementById('disambiguationControls')) { | ||
| const controls = document.createElement('div'); controls.id = 'disambiguationControls'; controls.className = 'mb-3'; | ||
| const label = document.createElement('label'); label.className = 'form-check-label fw-bold'; | ||
| const checkbox = document.createElement('input'); checkbox.id = 'disambiguationType'; checkbox.type = 'checkbox'; checkbox.className = 'form-check-input me-2'; | ||
| label.append(checkbox, document.createTextNode(ui('disambiguation.type'))); | ||
| const hint = document.createElement('div'); hint.className = 'form-text text-muted'; hint.textContent = ui('disambiguation.hint'); | ||
| const template = document.createElement('button'); template.type = 'button'; template.className = 'btn btn-sm btn-wiki-outline mt-2'; template.textContent = ui('disambiguation.insertTemplate'); | ||
| checkbox.onchange = () => { | ||
| if (!editor) return; | ||
| editor.setMarkdown(setDisambiguation(editor.getMarkdown(), checkbox.checked)); | ||
| }; | ||
| template.onclick = async () => { | ||
| if (!editor) return; | ||
| if (stripDisambiguation(editor.getMarkdown()).trim() && !(await window.Swal.fire({text:ui('disambiguation.replaceConfirm'),showCancelButton:true})).isConfirmed) return; | ||
| const name = (document.getElementById('titleInput') as HTMLInputElement)?.value.trim().replace(/[\[\]|\r\n]/g,'') || ui('disambiguation.exampleTitle'); | ||
| editor.setMarkdown(setDisambiguation(ui('disambiguation.template',[name]),true)); | ||
| }; | ||
| checkbox.checked = !!editor && isDisambiguation(editor.getMarkdown()); | ||
| controls.style.display = sectionMode ? 'none' : ''; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an edit opens with Useful? React with 👍 / 👎. |
||
| controls.append(label,hint,template); redirectField.after(controls); | ||
| } | ||
| const redirectInputEl = document.getElementById('redirectInput'); | ||
| if (redirectInputEl) { | ||
| let redirectDebounce = null; | ||
|
|
@@ -3251,6 +3276,7 @@ function applySectionEditModeUI(range: SectionRange, fullContent: string): void | |
| document.getElementById('alternateTitleInput'), | ||
| document.getElementById('categoryInput'), | ||
| document.getElementById('redirectInput'), | ||
| document.getElementById('disambiguationType'), | ||
| ]; | ||
| lockedContainers.forEach(el => { | ||
| if (el) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import {isDisambiguation} from '../../packages/wiki-shared/src/markup/disambiguation'; | ||
| import { trackPageView } from '../utils/analytics'; | ||
| import { ui } from '../i18n/server'; | ||
| import { Hono, type Context } from 'hono'; | ||
|
|
@@ -1742,7 +1743,7 @@ wiki.get('/w/:slug', async (c, next) => { | |
| console.error('loadPalettesForPage failed:', e); | ||
| } | ||
|
|
||
| const result = safeJSON({ ...page, redirected_from: redirectedFrom, used_palettes: usedPalettes }); | ||
| const result = safeJSON({ ...page, is_disambiguation: isDisambiguation(page.content), redirected_from: redirectedFrom, used_palettes: usedPalettes }); | ||
|
|
||
| // 편집 메모(editor_note)는 편집기 로딩(for_edit=true) 시에만 wiki:edit 권한자에게 노출한다. | ||
| // 일반 열람·SPA 네비게이션·검색 등에서는 응답에서 제거한다. | ||
|
|
@@ -2173,6 +2174,8 @@ wiki.put('/w/:slug', requireAuth, async (c) => { | |
| return c.json({ error: ui("m_8dd4e38868d9e2ba") }, 400); | ||
| } | ||
|
|
||
| if (isDisambiguation(body.content) && body.redirect_to) return c.json({error:ui('disambiguation.redirectConflict')},400); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This guard only covers direct Useful? React with 👍 / 👎. |
||
|
|
||
| // CRLF/CR → LF 정규화. 클라이언트 환경(Windows 클립보드, 외부 임포트 등)에서 | ||
| // \r 가 섞여 들어오면 렌더 파이프라인의 펜스/`:::`/폴드 정규식이 깨진다. | ||
| // 저장 시점에 한 번만 정규화하면 이후 모든 읽기 경로가 안전해진다. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every marked page passes this HTML comment unchanged to
extractMetaDescription(page.content)insrc/index.ts:784-790. That extractor removes ordinary HTML tags but not comments, so the generated description begins with the literal<!-- vireowiki:disambiguation -->; it is then used for both standard and Open Graph description metadata and for crawler responses. Strip the marker before description extraction so search and link previews expose the actual article text rather than internal metadata.Useful? React with 👍 / 👎.