Skip to content

Perf/next image media - #82

Merged
sphildreth merged 7 commits into
mainfrom
perf/next-image-media
Sep 12, 2026
Merged

sphildreth merged 7 commits into
mainfrom
perf/next-image-media

Conversation

@sphildreth

Copy link
Copy Markdown
Owner

This pull request introduces a comprehensive upgrade to the project's linting, dependency management, and documentation. ESLint is now an enforced quality gate with a new workspace-wide configuration, and several dependency versions are updated across the board. The documentation and configuration files have been updated to reflect these changes, and some performance, security, and cleanup improvements are included.

Linting and Quality Gates

  • Added a new workspace-wide eslint.config.mjs using eslint-config-next (core-web-vitals and TypeScript rules), and enabled ESLint as a required gate in local and CI workflows. Warnings now fail the build, and autofix support is available (pnpm lint, pnpm lint:fix). All rule relaxations are scoped and documented as technical debt. Deferred work and dependency pin rationale are recorded in docs/DEVELOPMENT.md. [1] [2] [3] [4] [5] [6]
  • Added and documented the lint commands in package.json and updated precheck scripts and guides to include linting as a fail-fast step. [1] [2] [3] [4]

Dependency Updates

  • Upgraded major dependencies: Next.js to 16.3.5, React to 19.3, Tiptap to 3.31.3, Mermaid to 12, Zod to 4.6.2, lucide-react to 1.45, dompurify to 3.4.15, jszip to 3.10.2, AWS SDK to 3.1131, marked to 18.0.12, Playwright to 1.63, Puppeteer to 25.10, Vitest to 5, Vite to 8.3, and related types and plugins. [1] [2] [3]
  • Documented load-bearing dependency pins for ESLint (stays on 9.x) and TypeScript (stays on 6.x) due to ecosystem compatibility, with explanations in CHANGELOG.md and docs/DEVELOPMENT.md. [1] [2]

Performance and Security

  • Improved avatar and banner image performance by switching to next/image and broadening Google OAuth avatar host matching to *.googleusercontent.com. [1] [2]
  • Addressed security advisories: pinned/overrode lodash-es, postcss, and qs to secure versions, ensuring no known vulnerabilities in production or dev dependencies.

Documentation and Chores

  • Updated README badges and documentation to reflect new versions, linting gate status, and standard commands. [1] [2]
  • Added and updated tests to cover new behaviors, removed dead code and unused imports, and bumped the app version to 1.0.4. [1] [2]

Other Notable Changes

  • Fixed code blocks to follow theme changes, improved reaction anti-abuse logic, and made minor bug fixes in API routes and preview effects.

These changes collectively improve code quality, developer experience, and site performance, while keeping the dependency stack secure and well-documented.

- pnpm update --latest across 37 minor/patch packages (Next 16.3.5, React 19.3,
  vitest 5, mermaid 12, @simplewebauthn 14, AWS SDK 3.1131, tiptap 3.31, zod 4.6)
- pnpm-workspace.yaml: bump the postcss override to 8.5.28, add a qs >=6.16.0
  override to clear the two dev-only express advisories, retarget the
  @simplewebauthn peer allowance at v14
- TypeScript stays on 6.0.3: typescript-eslint hard-errors on the TS 7 API
Follow-up to the dependency refresh review: ESLint is now enforced, and the
remaining dependency work (newest releases, advisories) is applied.

Dependencies
- eslint@9 + eslint-config-next@16.3.5, plus `pnpm update --latest` across the
  workspace: Next 16.3.5, React 19.3, React 19.3 types, Vitest 5, Tiptap 3.23.8,
  @simplewebauthn 14, zod 4.6, @upstash/ratelimit 2, @aws-sdk/client-s3 3.1070.
- pnpm-workspace.yaml: retarget the @simplewebauthn/* peer that
  @simplewebauthn/next@14 declares as ^13 (same React peer problem as before);
  override qs >=6.16.0 and postcss 8.5.28 for the advisories `pnpm audit` flags.

ESLint
- `pnpm lint` is `eslint . --max-warnings=0`, so the gate that had been SKIP for
  this repository now runs and treats warnings as failures.
- eslint.config.mjs keeps eslint-config-next core-web-vitals + TypeScript rules,
  documents every relaxation and scopes the remaining ones to named files.
- Two pins are load-bearing and recorded in the config and docs: ESLint 9.x
  (eslint-plugin-react/jsx-a11y peer ranges stop at ^9; ESLint 10 removed
  `context.getFilename`) and TypeScript 6.x (typescript-eslint hard-errors on the
  TypeScript 7 native API).

Code changes required to reach 0 errors / 0 warnings
- Remove unused imports, variables and catch bindings across src/ and tests/, and
  drop the `catch (_e)` bindings the old code left behind.
- @next/next/no-html-link-for-pages: 7 internal anchors now use next/link (root
  layout, home page, MaintenancePanel, AppearanceManager, CommentNode,
  CommentsSection, TwoFactorProvider).
- Escape 48 literal quotes in JSX text (`"` / `'`).
- react-hooks/prefer-as-const: 8 `useRef<string>("")` -> `useRef<"">("")`.
- react-hooks/immutability: hoist AuditLogSection's fetch above its effect and make
  the placeholder state explicit; PostForm's featured-image effect no longer reads
  the state it owns.
- react-hooks/purity: ReactionButtons takes its anti-abuse mount timestamp in an
  effect (and reports "now" when unset so the server check fails closed); the post
  page keeps its server-side performance.now() timing behind a documented scope.
- react-hooks/component-ref-as-props: rename the Tiptap instance prop so it is no
  longer mistaken for a `ref`.
- Delete dead code the lint run exposed: the empty `runtime` export on the rss-feed
  route and the unused `getSearchResultUrl` helper (with its two unit tests).
- prefer-const runs with ignoreReadBeforeAssign, which matches how timer/socket
  handles are declared in this codebase.

Docs
- README gate table and docs/DEVELOPMENT.md describe the now-active gate, the two
  version pins and the deferred lint work (next/image remotePatterns policy,
  TiptapEditor components declared during render, setState-in-effect syncs).

Verification: `./scripts/do-prechecks.py` reports all 11 gates passing
(typecheck, ESLint, production build, 984 unit tests).
mermaid -> chevrotain pins lodash-es ^4.17.21, which resolves to 4.17.23
(GHSA-f23m-r3pf-42rh, patched in 4.17.24). Override to >=4.17.24, which also
dedupes with the 4.18.1 copy dagre-d3-es already used.

`pnpm audit` now reports no known vulnerabilities for prod and dev deps.
Four `<img>` sites whose box is already fixed by CSS now use next/image, which
removes them from the `@next/next/no-img-element` exception list:

- Banner: `fill` + `priority` in the existing `relative h-64 overflow-hidden`
  wrapper, keeping the configured focal point via objectPosition. `priority`
  preserves today's eager loading (next/image defaults to lazy).
- ArticleCard, UserMenu, UsersManager avatars: explicit width/height + `sizes`,
  matching the avatar markup CommentNode already used, so a 20-36px avatar no
  longer downloads the full-size OAuth photo.

next.config.mjs: allow `*.googleusercontent.com` instead of only lh3. Google
serves account photos from lh3..lh6 and the ggpht mirrors, and `users.image` is
only ever written from the GitHub/Google OAuth profile (src/lib/auth.ts), so the
widening stays inside the two providers this app actually supports. Without it an
lh4 avatar would make the post list throw instead of rendering.

eslint.config.mjs: the no-img-element scope shrinks from 9 files to 5 and the
comment now states the real reason for each survivor - the post featured image
and comment attachments have no stored intrinsic dimensions (uploads table has no
width/height, so next/image would need a crop box and change layout), the
lightbox deliberately shows original bytes of imported media, and the two admin
previews render `blob:` object URLs the optimizer cannot proxy.

tests: UserMenu asserted the raw avatar `src`; it now asserts the original URL
survives in the optimizer URL and that `sizes="36px"` is applied.

Verification: `./scripts/do-prechecks.py` all 11 gates pass (984 tests). Against
the running production server, /_next/image returns 200 for
avatars.githubusercontent.com, fetches lh3.googleusercontent.com through the new
wildcard, and still returns 400 for an unlisted host.
ThemeToggle rewrites `<html data-theme>` in place (no navigation, cookie
persisted through the setTheme action), but CodeBlock sampled that attribute once
in a mount effect. Result: toggling the theme left every already-rendered code
block in the previous palette until a reload.

It is now an external store: MutationObserver on data-theme plus a
matchMedia('(prefers-color-scheme: dark)') listener, with the light palette as the
server snapshot so hydration output is unchanged. jsdom has no matchMedia, so the
lookups stay guarded.

tests/components/CodeBlockTheme.test.tsx asserts the <pre> background goes from
white to the dark palette when data-theme flips without a navigation; it fails
against the previous implementation (verified by stashing the fix).

Drops src/components/CodeBlock.tsx from the react-hooks/set-state-in-effect scope
in eslint.config.mjs and updates the deferred-work notes in docs/DEVELOPMENT.md.

Verification: `./scripts/do-prechecks.py` all 11 gates pass (985 tests, 139 files).
Copilot AI lite review requested due to automatic review settings September 12, 2026 14:18
@sphildreth

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Passkey calls still use the incompatible v14 API, and CI does not run the advertised lint gate.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR strengthens linting, refreshes major dependencies, and applies image, theme, security, and cleanup improvements across the Next.js application and tests.

Changes:

  • Added workspace ESLint configuration, scripts, documentation, and local precheck integration.
  • Upgraded framework, editor, testing, and security dependencies.
  • Updated tests, optimized selected images, and removed unused code.
  • Passkey API call-shape fixes and CI lint wiring remain required.
File summaries
File Summary
vite.config.mts Consolidates Vitest aliases.
tests/wxr/xml-parsing.test.ts Removes unused imports.
tests/wxr/versioning.test.ts Removes an unused type import.
tests/wxr/html-sanitization.test.ts Removes an unused helper import.
tests/wxr/core-functionality.test.ts Removes an unused helper import.
tests/video-shortcode.test.ts Removes an unused mock import.
tests/uploads-serve-security.test.ts Simplifies the filesystem mock.
tests/upload-tracking.test.ts Removes an unused lifecycle import.
tests/transaction-handling.test.ts Removes dead test setup.
tests/taxonomy.test.ts Removes an unused import.
tests/scripts/seed-posts.test.ts Simplifies the mock signature.
tests/rateLimit.test.ts Removes an unused error import.
tests/post-access-control.test.ts Removes unused timestamps.
tests/moderation.test.ts Removes an unused lifecycle import.
tests/migration.test.ts Removes unused query assignments.
tests/mermaid-subgraph-rendering.test.ts Removes dead test fixtures.
tests/local-storage.test.ts Removes unused imports.
tests/import-wxr.test.ts Removes unused test dependencies.
tests/import-wxr-taxonomy.test.ts Removes an unused interface.
tests/import-wxr-offline-media.test.ts Removes an unused spy binding.
tests/import-wxr-media.test.ts Removes an unused spy binding.
tests/import-wxr-integration.test.ts Removes an unused mock import.
tests/import-wxr-enhancements.test.ts Removes an unused filesystem import.
tests/import-wxr-dimension-handling.test.ts Removes an unused result binding.
tests/helpers/normalizeHtml.ts Converts immutable locals to const.
tests/e2e/tiptap-editor.spec.ts Removes placeholder setup.
tests/data-operations.test.ts Removes unused schema imports.
tests/config-getBoolean.test.ts Marks an unused parameter.
tests/components/CodeBlockTheme.test.tsx Adds theme-switching coverage.
tests/components/auth/UserMenu.test.tsx Updates optimized-image assertions.
tests/comment-auto-approval.test.ts Removes unused capture state.
tests/backup-restore.test.ts Removes an unused catch binding.
tests/archives.test.ts Removes unused imports.
tests/api/2fa-webauthn-api.test.ts Matches requestless route handlers.
tests/api/2fa-trusted-devices-api.test.ts Matches requestless route handlers.
tests/api/2fa-totp-api.test.ts Matches requestless route handlers.
tests/api/2fa-status-disable.test.ts Matches requestless route handlers.
tests/api/2fa-recovery-api.test.ts Matches requestless route handlers.
tests/analytics-view-tracking.test.ts Simplifies database mocks.
tests/admin.test.ts Removes unused lifecycle imports.
tests/2fa-trusted-device.test.ts Removes unused fixtures.
src/proxy.ts Removes unused timing state.
src/lib/taxonomy.ts Removes an unused schema import.
src/lib/seo.ts Removes unused catch bindings.
src/lib/redirects.ts Removes an unused catch binding.
src/lib/reactions.ts Removes unused imports.
src/lib/rateLimit.ts Removes dead state and a catch binding.
src/lib/posts.ts Removes unused query state and types.
src/lib/performance.ts Removes unused query identifiers.
src/lib/moderation.ts Removes unused parameters.
src/lib/jobs.ts Marks an unused parameter.
src/lib/comments.ts Makes immutable collections constant.
src/lib/auth.ts Removes an unused catch binding.
src/lib/analytics.ts Removes an unused import.
src/lib/2fa/totp.ts Simplifies TOTP validation API.
src/components/Sidebar.tsx Removes an unused type import.
src/components/reactions/ReactionButtons.tsx Moves timing initialization into an effect.
src/components/editor/TiptapEditor.tsx Removes unused imports and bindings.
src/components/editor/extensions/MermaidNode.ts Removes unused ProseMirror imports.
src/components/comments/CommentThread.tsx Removes an unused prop binding; moderate finding: required limitReplies remains passed but ignored.
src/components/comments/CommentsSection.tsx Uses Next.js internal links.
src/components/comments/actions.ts Removes unused configuration state.
src/components/CodeBlock.tsx Subscribes to live theme changes.
src/components/Banner.tsx Optimizes an unused component; nit: it has no live call site, so the delivered banner is unchanged.
src/components/auth/UserMenu.tsx Optimizes user avatars.
src/components/ArticleCard.tsx Optimizes author images.
src/components/analytics/Sparkline.tsx Removes unused path data.
src/components/admin/users/UsersManager.tsx Optimizes avatars and removes unused state; nit: required page remains unused.
src/components/admin/security/TwoFactorSetupSelector.tsx Escapes JSX text.
src/components/admin/security/TotpSetupFlow.tsx Cleans error handling and JSX text.
src/components/admin/security/RevokeAllDevicesButton.tsx Escapes JSX text.
src/components/admin/security/RegenerateRecoveryCodesButton.tsx Cleans error handling and JSX text.
src/components/admin/security/PasskeySetupFlow.tsx Cleans error handling and JSX text.
src/components/admin/security/AddPasskeyButton.tsx Removes an unused catch binding.
src/components/admin/posts/UnpublishPostButton.tsx Removes an unused catch binding.
src/components/admin/posts/PostsManager.tsx Removes unused transition state; nit: required page remains unused.
src/components/admin/posts/PostForm.tsx Refines preview URL ownership and JSX escaping.
src/components/admin/posts/LockPostButton.tsx Removes an unused catch binding.
src/components/admin/DeletePostButton.tsx Escapes JSX text.
src/app/tiptap-demo/page.tsx Escapes JSX text.
src/app/tags/[slug]/page.tsx Removes unused pagination parameters.
src/app/tags/[slug]/not-found.tsx Escapes JSX text.
src/app/page.tsx Uses Next.js internal links.
src/app/layout.tsx Removes unused metadata variables.
src/app/feed.xml/route.ts Removes unused catch bindings.
src/app/categories/[slug]/not-found.tsx Escapes JSX text.
src/app/api/rum/route.ts Exports the Node.js runtime.
src/app/api/redirects/route.ts Removes an unused catch binding.
src/app/api/r2/complete/route.ts Removes an unused catch binding.
src/app/api/admin/purge/route.ts Removes unused types and variables.
src/app/api/admin/export/[operationId]/route.ts Removes an unused catch binding.
src/app/api/admin/comments/moderate/route.ts Removes an unused type import.
src/app/api/2fa/webauthn/register/options/route.ts Simplifies the route handler signature.
src/app/api/2fa/webauthn/authenticate/options/route.ts Simplifies the route handler signature.
src/app/api/2fa/trusted-devices/route.ts Simplifies the route handler signature.
src/app/api/2fa/totp/init/route.ts Simplifies the route handler signature.
src/app/api/2fa/status/route.ts Removes unused request and query imports.
src/app/api/2fa/recovery/regenerate/route.ts Simplifies the route handler signature.
src/app/actions/import.ts Removes an unused catch binding.
src/app/actions/deletePost.ts Removes an unused redirect import.
src/app/(public)/[slug]/page.tsx Removes Server-Timing plumbing; nit: remaining measureAsync calls discard their timing work.
src/app/(public)/[slug]/not-found.tsx Escapes JSX text.
src/app/(admin)/admin/users/actions.ts Removes unused schema and ORM imports.
src/app/(admin)/admin/system/import/page.tsx Removes unused imports.
src/app/(admin)/admin/system/import/[jobId]/page.tsx Removes an unused map index.
src/app/(admin)/admin/security/recovery/page.tsx Escapes JSX text.
src/app/(admin)/admin/security/passkeys/page.tsx Escapes JSX text.
src/app/(admin)/admin/security/page.tsx Removes unused ORM imports.
src/app/(admin)/admin/security/devices/page.tsx Escapes JSX text.
src/app/(admin)/admin/posts/actions.ts Removes unused imports and catch bindings.
src/app/(admin)/admin/posts/[id]/edit/page.tsx Escapes JSX text.
src/app/(admin)/admin/moderation/actions.ts Removes unused imports.
src/app/(admin)/admin/data-operations/RestoreSection.tsx Escapes JSX text.
src/app/(admin)/admin/data-operations/PurgeSection.tsx Escapes JSX text.
src/app/(admin)/admin/data-operations/AuditLogSection.tsx Makes placeholder state explicit.
scripts/sync-migrations.ts Removes unused database setup.
scripts/restore.ts Removes unused schema and ORM imports.
scripts/import-wxr.ts Removes dead variables and an unused directive.
scripts/backup.ts Removes an unused path import.
scripts/add-spdx.ts Removes unused helper logic.
README.md Updates versions and lint documentation.
pnpm-workspace.yaml Adds dependency overrides and peer constraints.
package.json Adds lint scripts and dependency upgrades. Critical finding: SimpleWebAuthn v14 callers still pass raw responses instead of { optionsJSON: options }; moderate finding: CI does not invoke pnpm lint or pnpm prechecks.
next.config.mjs Broadens Google avatar image hosts.
eslint.config.mjs Adds workspace-wide flat ESLint configuration.
docs/DEVELOPMENT.md Documents lint gates and deferred work.
docs/AGENTS.md Adds lint to standard commands.
CHANGELOG.md Documents release, dependency, and behavior changes.
Review details

Suppressed comments (5)

src/app/(public)/[slug]/page.tsx:62

  • After the duration values and Server-Timing header were removed, this measureAsync call—and the three similar calls below—only performs two performance.now() reads and discards the result; measureAsync has no logging or other side effect. Please either await these operations directly or retain a consumer for the timings so the instrumentation is not dead overhead.
  const { result: config } = await measureAsync(
    'config-load',
    async () => new ConfigServiceImpl({ db })
  );

src/components/Banner.tsx:33

  • This component has no call sites: the home page renders <Header /> (src/app/page.tsx:54), and Header already uses next/image. These lines therefore do not change the banner delivered to users, so the advertised banner performance upgrade is ineffective. Please update the live rendering path or remove this dead-component change.
    src/components/admin/posts/PostsManager.tsx:42
  • page is still required by PostsManagerProps and passed from the page wrapper, but it is no longer destructured or used; pagination is read from initialData.pagination instead. Remove this dead prop from the component contract and wrapper/caller so the API does not advertise an input that has no effect.
    src/components/admin/users/UsersManager.tsx:42
  • page is still required by UsersManagerProps and passed from the page wrapper, but it is no longer destructured or used; pagination is read from initialData.pagination instead. Remove this dead prop from the component contract and wrapper/caller so the API does not advertise an input that has no effect.
    src/components/comments/CommentThread.tsx:9
  • limitReplies is still a required prop in this component's type and is still passed by CommentsSection, but removing it from the destructuring leaves the component ignoring it. Remove the prop from the type and caller, or use it to enforce the limit; otherwise callers must provide a value that cannot affect rendering.
  • Files reviewed: 128/129 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread package.json
"@tiptap/pm": "^3.30.5",
"@tiptap/react": "^3.30.5",
"@tiptap/starter-kit": "^3.30.5",
"@simplewebauthn/browser": "^14.0.0",
Comment thread package.json
"prebuild": "node scripts/write-version.mjs",
"build": "next build",
"start": "next start",
"lint": "eslint . --max-warnings=0",
# Conflicts:
#	docs/DEVELOPMENT.md
#	eslint.config.mjs

Co-authored-by: sphildreth <193334+sphildreth@users.noreply.github.com>

Copilot AI commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved and merged main into this branch in bb06581.

@sphildreth
sphildreth merged commit 4d663f6 into main Sep 12, 2026
2 checks passed
@sphildreth
sphildreth deleted the perf/next-image-media branch September 12, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants