` container with `data-growi-is-content-rendering="true"` initially
- Sets attribute to `"false"` via `onLoad` and `onError` handlers on the `
![]()
` element
- The plantuml remark plugin (`plantuml.ts`) is updated to output a custom `
` HAST element instead of a plain `
`. This allows the renderer to map the `plantuml` element to the `PlantUmlViewer` React component.
- `sanitizeOption` is exported from the plantuml service and merged in `renderer.tsx` (same pattern as drawio and mermaid)
- `PlantUmlViewer` is registered as `components.plantuml` in all view option generators (`generateViewOptions`, `generateSimpleViewOptions`, `generatePreviewOptions`)
---
### remark-lsx Modification
#### Lsx (modification)
| Field | Detail |
|-------|--------|
| Intent | Add rendering-status attribute lifecycle to async SWR page list fetching |
| Requirements | 4.3, 4.4, 4.7 |
**Implementation Notes**
- Set `data-growi-is-content-rendering="true"` on the outermost container element while `isLoading === true` (SWR fetch in progress)
- Set attribute to `"false"` when data arrives — whether success, error, or empty result
- Use declarative attribute binding via the existing `isLoading` state (no imperative DOM manipulation needed)
- File: `packages/remark-lsx/src/client/components/Lsx.tsx`
- The lsx remark plugin sanitize options must be updated to include the new attribute name
- `@growi/core` must be added as a dependency of `remark-lsx` (same pattern as `remark-drawio`)
- **SWR cache hit behavior**: When SWR returns a cached result immediately (`isLoading=false` on first render), the attribute starts at `"false"` and no re-scroll is triggered. This is correct: a cached result means the list renders without a layout shift, so no compensation is needed. The re-scroll mechanism only activates when `isLoading` starts as `"true"` (no cache) and transitions to `"false"` after the fetch completes.
---
### SearchResultContent Integration
#### SearchResultContent (modification)
| Field | Detail |
|-------|--------|
| Intent | Integrate rendering-watch into SearchResultContent's keyword scroll so that layout shifts from async renderers are compensated |
| Requirements | 5.1, 5.4, 5.5, 6.1 |
**Background**: `SearchResultContent` renders page content inside a div with `overflow-y-scroll` (`#search-result-content-body-container`). The keyword-highlight scroll mechanism was originally inlined as a `useEffect` with no dependency array and no cleanup.
**Post-Implementation Correction**: The initial design (tasks 6.1–6.3) attempted to integrate `useContentAutoScroll` (hash-based) into SearchResultContent. This was architecturally incorrect — search pages use `/search?q=foo` with no URL hash, so the hash-driven hook would never activate. See `research.md` "Post-Implementation Finding" for details.
**Final Architecture**: The keyword scroll effect was extracted into a dedicated `useKeywordRescroll` hook (co-located with SearchResultContent), which directly integrates `watchRenderingAndReScroll` for rendering compensation. No hash-based scroll is used in SearchResultContent.
**Hook Call Site**
```typescript
useKeywordRescroll({ scrollElementRef, key: page._id });
```
- `scrollElementRef` is the existing React ref pointing to the scroll container
- `key: page._id` triggers re-execution when the selected page changes
- The hook internally handles MutationObserver setup, debounced keyword scroll, rendering watch, and full cleanup
**File**: `apps/app/src/features/search/client/components/SearchPage/SearchResultContent.tsx`
---
## Error Handling
### Error Strategy
This feature operates entirely in the browser DOM layer with no server interaction. Errors are limited to DOM state mismatches.
### Error Categories and Responses
**Target Not Found** (2.3): If the hash target never appears within 10s, the observer disconnects silently. No error is surfaced to the user — this matches browser-native behavior for invalid hash links.
**Container Not Found** (1.5): If the container element ID does not resolve, the hook returns immediately with no side effects.
**Rendering Watch Timeout** (3.6): After 10s, all observers and timers are cleaned up regardless of remaining rendering elements. This prevents resource leaks from components that fail to signal completion.