iframe 경고와 페이지네이션 강조 수정
This commit is contained in:
@@ -241,7 +241,6 @@ watch(mastodonEmbedUrl, () => {
|
||||
:allow="customIframe.allow"
|
||||
:height="customIframe.height || 480"
|
||||
:width="customIframe.width || undefined"
|
||||
:allowfullscreen="customIframe.allowFullscreen"
|
||||
loading="lazy"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-presentation"
|
||||
/>
|
||||
|
||||
@@ -119,7 +119,7 @@ const getPageLinkTo = (page) => ({
|
||||
<NuxtLink
|
||||
v-else
|
||||
class="site-pagination__page inline-flex min-w-9 items-center justify-center rounded-full px-2 py-2 text-base font-semibold text-[var(--site-muted)] transition hover:text-[var(--site-text)]"
|
||||
:class="item.page === currentPage ? 'site-pagination__page--current pointer-events-none text-[var(--site-text)]' : ''"
|
||||
:class="item.page === currentPage ? 'site-pagination__page--current pointer-events-none !bg-[var(--site-accent)] !text-white shadow-sm' : ''"
|
||||
:to="getPageLinkTo(item.page)"
|
||||
:aria-current="item.page === currentPage ? 'page' : undefined"
|
||||
:aria-label="`${item.page}페이지로 이동`"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
||||
| lib/markdown-block-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 |
|
||||
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
||||
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출 |
|
||||
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |
|
||||
| lib/markdown-html-block.js | 게시물 본문 HTML 블록의 parser 기반 허용 태그·속성 sanitizer, 실행 URL·인라인 style 제거와 새 창 링크 rel 보정 |
|
||||
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
||||
| lib/markdown-fenced-meta.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 북마크·회원가입·미디어 메타 파싱 |
|
||||
@@ -196,7 +196,7 @@
|
||||
| components/site/HomeHero.vue | 홈 상단 720px 커버 배너, 라이트·다크 테마별 이미지 교체, 왼쪽 하단 오버레이 제목·본문 |
|
||||
| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 색상 배지 메타에 표시하고 태그 페이지 링크 제공, 오른쪽 아래 게시물 링크 화살표 |
|
||||
| components/site/PostCardMedia.vue | 게시물 카드 썸네일(`featuredImageThumbnail` 우선, 없으면 대표 이미지 원본 사용. 대표 이미지 없으면 모바일 3줄·`sm` 이상 4줄 제목 placeholder 표시, 이미지 유무와 관계없이 썸네일 영역 크기 고정) |
|
||||
| components/site/SitePagination.vue | 공개 목록 공용 번호형 페이지네이션, 이전·다음 화살표와 첫/끝/현재 주변 페이지 직접 이동, 많은 페이지의 `...` 접기, 모바일 가로 스크롤 |
|
||||
| components/site/SitePagination.vue | 공개 목록 공용 번호형 페이지네이션, 이전·다음 화살표와 첫/끝/현재 주변 페이지 직접 이동, 현재 페이지 브랜드 포인트 강조, 많은 페이지의 `...` 접기, 모바일 가로 스크롤 |
|
||||
| components/site/TagHeader.vue | 태그 페이지 헤더 |
|
||||
| components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시, slug 변경 시 입력·답글·오류·좋아요 상태 초기화와 지연 응답 폐기, 기본 정렬 최신순(최신순·인기순·오래된순 선택), 입력값 기반 등록 버튼 활성화, 작성자 썸네일/좋아요/상대시간 표시 |
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
|
||||
### 공개 URL 구조
|
||||
|
||||
- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다. 같은 path에서 `page` query만 바뀌는 이동은 Nuxt 페이지 컴포넌트가 재사용될 수 있으므로 전역 전환 베일을 켜지 않는다.
|
||||
- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다. 현재 페이지는 브랜드 포인트 배경색으로 표시한다. 같은 path에서 `page` query만 바뀌는 이동은 Nuxt 페이지 컴포넌트가 재사용될 수 있으므로 전역 전환 베일을 켜지 않는다.
|
||||
- `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR과 클라이언트 내부 이동 모두 본문을 먼저 조회한 뒤 완성된 상세 화면으로 진입한다. 이동 시작 시 불투명 전환 베일을 즉시 표시하고, `page:finish` 이후 베일 뒤에서 스크롤을 최상단으로 맞춘 뒤 베일을 해제한다. 뒤로가기는 저장 스크롤 위치를 베일 뒤에서 복원한다. `history.scrollRestoration`은 `manual`로 두어 브라우저 자동 복원과 충돌하지 않게 한다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다.
|
||||
- `/` - 홈. 클라이언트 복귀 시에도 주요 데이터를 블로킹 조회한 뒤 전환한다.
|
||||
- `/sitemap.xml` - 공개 sitemap XML. 홈, 게시물 목록, 태그 목록, 공개 발행글, 공개 고정 페이지, 글이 있는 태그 URL을 요청 시점 DB 기준으로 자동 생성한다. 멤버십·비공개·초안·예약 대기 글과 `noindex` 글은 포함하지 않는다.
|
||||
@@ -848,7 +848,7 @@ components/content/
|
||||
- 콜아웃 배경 프리셋은 인용 블록과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`를 지원한다.
|
||||
- 토글 블록은 `:::toggle 제목` fenced block 안에 펼침 본문을 저장한다. 라이브 모드에서는 저장된 기본 펼침·닫힘 상태로 처음 표시한 뒤 제목·본문을 인라인 편집하며, 공개 화면에서는 chevron 또는 제목 클릭으로 펼침·접힘 시 본문이 애니메이션된다.
|
||||
- 링크 블록은 `:::link` fenced block 안에 URL을 저장하고 `ProseEmbed`로 렌더링한다. 이미지 파일 URL은 링크 블록이 아니라 이미지 블록으로 저장한다.
|
||||
- iframe 블록은 `:::iframe` fenced block 안에 iframe HTML을 저장한다. 렌더링 시 `src`, `title`, `allow`, `width`, `height`, `allowfullscreen` 등 제한된 속성만 추출해 sandbox iframe으로 표시한다.
|
||||
- iframe 블록은 `:::iframe` fenced block 안에 iframe HTML을 저장한다. 렌더링 시 `src`, `title`, `allow`, `width`, `height` 등 제한된 속성만 추출해 sandbox iframe으로 표시한다. `allowfullscreen`은 DOM 속성으로 중복 출력하지 않고 `allow`의 `fullscreen` 권한으로 정규화한다.
|
||||
- HTML 블록은 `:::html` fenced block 안에 사용자 HTML을 저장하고 `ProseHtml`로 렌더링한다. 렌더링 직전에 `sanitize-html` parser의 허용 목록으로 표·문단·제목(h2~h6)·목록·인라인 서식·기본 이미지와 안전한 링크 속성만 남긴다. script, style, iframe, SVG, object, form 계열 태그와 이벤트 속성, 인라인 style, `javascript:` 등 비허용 URL scheme을 제거하며, `target="_blank"` 링크에는 `noopener noreferrer`를 추가한다. iframe은 `/iframe` 블록을 사용한다. 고정 페이지의 `html_document`는 별도의 완전한 HTML 문서 기능이므로 이 본문 블록 정책을 적용하지 않는다.
|
||||
- 관리자 Markdown-first 에디터의 라이브/스타일 모드에서 링크·iframe·HTML 블록은 입력 카드 없이 즉시 실제 프리뷰로 표시된다. 링크·iframe·HTML·비디오·오디오·파일 프리뷰 카드는 hover/focus 시 우측 상단 삭제 버튼을 표시한다. 블록 래퍼에 포커스한 상태에서 `Backspace`·`Delete`·`Ctrl/Cmd+Shift+K`로 삭제하고, `Enter`로 아래 빈 줄을 추가하며, `ArrowUp`·`ArrowDown`은 브라우저 스크롤 대신 이전/다음 편집 줄로 이동한다.
|
||||
- 라이브/스타일 모드에서 제목 블록 Enter는 현재 제목 내용을 저장한 뒤 바로 아래 빈 문단을 추가하고, 원문 마크다운 편집 상태로 전환하지 않는다.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.59
|
||||
|
||||
- 사용자 iframe 렌더링에서 `allowfullscreen`을 DOM 속성으로 중복 출력하지 않고 `allow`의 `fullscreen` 권한으로 정규화해 브라우저 경고를 줄임.
|
||||
- 공개 번호형 페이지네이션의 현재 페이지를 브랜드 포인트 배경색으로 표시해 활성 페이지 구분을 강화.
|
||||
- iframe 속성 정규화 단위 테스트를 추가.
|
||||
|
||||
## v1.11.58
|
||||
|
||||
- 공개 전환 베일 시작 조건을 정리해 `/posts?page=2`에서 `/posts?page=3`처럼 같은 path의 query만 바뀌는 페이지네이션 이동에서는 베일을 켜지 않도록 수정.
|
||||
|
||||
@@ -49,6 +49,22 @@ const sanitizeAllowValue = (value) => String(value || '')
|
||||
.replace(/[^\w\s;*.:/-]/g, '')
|
||||
.trim()
|
||||
|
||||
/**
|
||||
* iframe allow 속성과 fullscreen 권한을 정리한다.
|
||||
* @param {Record<string, string>} attributes - iframe 속성 객체
|
||||
* @param {string} openerAttributes - iframe 여는 태그 원본 속성 문자열
|
||||
* @returns {string} 정리된 allow 값
|
||||
*/
|
||||
const normalizeAllowValue = (attributes, openerAttributes) => {
|
||||
const allow = sanitizeAllowValue(attributes.allow || 'fullscreen; picture-in-picture; encrypted-media; web-share')
|
||||
|
||||
if (!/\ballowfullscreen\b/i.test(openerAttributes) || /\bfullscreen\b/i.test(allow)) {
|
||||
return allow
|
||||
}
|
||||
|
||||
return [allow, 'fullscreen'].filter(Boolean).join('; ')
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe HTML 코드인지 확인한다.
|
||||
* @param {string} value - 검사할 문자열
|
||||
@@ -59,7 +75,7 @@ export const isIframeEmbedMarkup = (value) => /^<iframe\b[\s\S]*?(?:<\/iframe>\s
|
||||
/**
|
||||
* iframe HTML 코드에서 렌더링 가능한 정보만 추출한다.
|
||||
* @param {string} value - iframe HTML 코드
|
||||
* @returns {{ src: string, title: string, allow: string, width: string, height: string, allowFullscreen: boolean }|null} iframe 정보
|
||||
* @returns {{ src: string, title: string, allow: string, width: string, height: string }|null} iframe 정보
|
||||
*/
|
||||
export const parseIframeEmbedMarkup = (value) => {
|
||||
const source = String(value || '').trim()
|
||||
@@ -79,9 +95,8 @@ export const parseIframeEmbedMarkup = (value) => {
|
||||
return {
|
||||
src,
|
||||
title: attributes.title || 'Embedded content',
|
||||
allow: sanitizeAllowValue(attributes.allow || 'fullscreen; picture-in-picture; encrypted-media; web-share'),
|
||||
allow: normalizeAllowValue(attributes, opener[1]),
|
||||
width: sanitizeSizeValue(attributes.width),
|
||||
height: sanitizeSizeValue(attributes.height),
|
||||
allowFullscreen: /\ballowfullscreen\b/i.test(opener[1])
|
||||
height: sanitizeSizeValue(attributes.height)
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.58",
|
||||
"version": "1.11.59",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.58",
|
||||
"version": "1.11.59",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.58",
|
||||
"version": "1.11.59",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
17
tests/markdown-iframe-embed.test.js
Normal file
17
tests/markdown-iframe-embed.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { parseIframeEmbedMarkup } from '../lib/markdown-iframe-embed.js'
|
||||
|
||||
test('iframe allowfullscreen은 allow fullscreen 권한으로 정규화한다', () => {
|
||||
const parsed = parseIframeEmbedMarkup('<iframe src="https://example.com/embed" allowfullscreen></iframe>')
|
||||
|
||||
assert.equal(parsed.src, 'https://example.com/embed')
|
||||
assert.match(parsed.allow, /\bfullscreen\b/)
|
||||
assert.equal(Object.hasOwn(parsed, 'allowFullscreen'), false)
|
||||
})
|
||||
|
||||
test('iframe allow 속성은 안전한 문자만 보존한다', () => {
|
||||
const parsed = parseIframeEmbedMarkup('<iframe src="https://example.com/embed" allow="fullscreen; autoplay; bad()"></iframe>')
|
||||
|
||||
assert.equal(parsed.allow, 'fullscreen; autoplay; bad')
|
||||
})
|
||||
Reference in New Issue
Block a user