Risograph-editorial redesign + security/a11y/perf/SEO hardening#19
Risograph-editorial redesign + security/a11y/perf/SEO hardening#19rahmanef63 wants to merge 1 commit into
Conversation
Full breakdown in the PR description. Highlights: - security: blog stored-XSS closed via a shared sanitize-html allowlist (src/lib/content.ts renderArticle, reused for build-time README render); CSP with hashed JSON-LD and no unsafe-inline; rel=noopener on sanitized links - content: draft gating (isPublished) across pages/featured/sitemap; author from frontmatter authors[]; hardened parseFrontmatter; collision-safe projectSlug; default-branch resolution; honest sync-failure star totals - seo/a11y: per-page og:image + JSON-LD (WebSite/BlogPosting/SoftwareSourceCode); /sitemap.xml with lastmod; trailingSlash always; skip-link, focus-visible, single h1, WCAG-AA contrast verified, prefers-reduced-motion - perf: astro:assets <Picture> AVIF/WebP (hero 60KB->~8.5KB, logo 18KB->737B), width/height on all <img> (CLS ~0), hero LCP preload - design: risograph-editorial system — self-hosted variable fonts (Bricolage / Newsreader / Spline Sans Mono), disciplined riso spot palette, sticker/stamp/ halftone/grain/hard-shadow utilities, scroll-aware IntersectionObserver reveal (bundled module, CSP-safe), duotone riso hero keeping the optimized <Picture> - tooling: 16 node --test unit tests (slug/isPublished/sanitizer-XSS/parser); .env.example Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
indopensource.org/src/lib/projects.ts
Line 89 in 6044270
Because this helper feeds both getStaticPaths and every project-card/detail URL, appending the hash to every project changes all existing public project URLs, e.g. /projects/opensid--opensid/ becomes /projects/opensid--opensid-db3yyo/. Without redirects, bookmarked and search-indexed detail pages for all non-colliding repos will 404 after deploy; reserve the suffix for actual collisions or add redirects from the previous slug shape.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const resolve = (value, base) => { | ||
| if (!value || value.startsWith('#') || /^(https?:|mailto:)/i.test(value)) return value; | ||
| try { | ||
| const url = new URL(value, base); |
There was a problem hiding this comment.
Resolve README root-relative paths within the repo
For README links or images that start with /, this now resolves against the host root, so /docs/setup.md becomes https://github.com/docs/setup.md (or https://raw.githubusercontent.com/docs/setup.md) instead of staying under the synced repository. The previous client-side sanitizer had repo-root handling for this case; after pre-rendering, any project README that uses root-relative docs/assets links will render broken links/images on the detail page.
Useful? React with 👍 / 👎.
| with a JSON string is safe here: it is `JSON.stringify` output, not | ||
| user-controlled markup. | ||
| --> | ||
| <script type="application/ld+json" is:inline set:html={jsonLdString} /> |
There was a problem hiding this comment.
Escape JSON-LD before injecting it
When synced data used in jsonLd (such as a GitHub repo description or blog frontmatter title/description) contains </script>, JSON.stringify does not make it safe for a raw script context, so the browser can terminate this JSON-LD block and parse attacker-controlled markup in the page head. CSP should block a follow-on inline script, but non-script markup such as a meta refresh can still be injected and the structured data breaks; escape < (for example to \u003c) before set:html.
Useful? React with 👍 / 👎.
|
@coderabbitai review |
✅ Action performedReview finished.
|
📝 WalkthroughWalkthroughThis PR introduces a "riso/zine" visual design system across all pages and components, rewrites content sync scripts to resolve default branches and pre-render sanitized README HTML at build time, adds a typed content library ( ChangesRiso design system + content pipeline overhaul
Sequence Diagram(s)sequenceDiagram
participant build as Astro Build
participant sync as sync-projects.mjs
participant github as GitHub API
participant json as projects.json
participant slugpage as projects/[slug].astro
participant browser as Browser
build->>sync: npm run sync:projects
sync->>github: GET /repos/{awesome} → default_branch
sync->>github: GET repos.json (raw via default_branch URL)
loop per repo
sync->>github: GET /repos/{slug} metadata
sync->>github: GET README.md raw
github-->>sync: markdown string
sync->>sync: renderArticle(markdown) → sanitized readmeHtml
end
sync-->>json: write projects[] with readmeHtml + metaDescription + syncFailed
build->>slugpage: getStaticPaths(projects as Project[])
slugpage->>slugpage: set:html={project.readmeHtml}
slugpage-->>browser: pre-rendered HTML (no client fetch needed)
sequenceDiagram
participant page as Astro Page
participant BaseLayout as BaseLayout.astro
participant crypto as crypto.subtle
participant head as HTML head output
participant observer as IntersectionObserver (client)
page->>BaseLayout: Props{jsonLd, heroPreloadHref, title, ...}
BaseLayout->>crypto: digest("SHA-256", JSON.stringify(jsonLd))
crypto-->>BaseLayout: base64 hash
BaseLayout->>head: CSP meta (script-src sha256-{hash})
BaseLayout->>head: preconnect / dns-prefetch hints
BaseLayout->>head: <link rel="preload" as="image" href={heroPreloadHref}>
BaseLayout->>head: <script type="application/ld+json"> (set:html)
BaseLayout->>observer: IntersectionObserver toggles .in-view on .reveal elements
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
package.json (1)
6-14:⚠️ Potential issue | 🟡 MinorDeclare the Node runtime contract in
package.json.The PR adds
.mjsfiles that import.tsmodules directly (e.g.,scripts/sync-projects.mjsimports../src/lib/content.ts, andtest/lib.test.mjsimports multiple.tsfiles). While CI workflows already pinnode-version: 22,package.jsonlacks anengines.nodedeclaration. This leaves local developers without explicit version guidance and breaks the standard contract that tools and IDEs expect.Add
"engines": { "node": ">=22" }topackage.jsonto match the enforced CI version.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@package.json` around lines 6 - 14, The package.json file is missing an engines declaration to specify the Node.js version requirement. Add an "engines" field to the root level of package.json (at the same level as "scripts") with the value "node": ">=22" to explicitly document that the project requires Node.js version 22 or higher, matching the CI workflow requirements and providing clear guidance to local developers.src/pages/projects.astro (1)
66-66:⚠️ Potential issue | 🟡 MinorFilter
ProjectsDirectorytorefreshedProjectsor visually marksyncFailedentries.The directory currently displays all projects including
syncFailedentries, which show placeholder zero stars/forks without any visual indicator of failure. This contradicts the stated goal (CC-10) of showing "honest" totals—users will see projects with 0 stars and no explanation. The unavailability message (line 45) only explains the aggregate exclusion, not why individual cards display zeros. Either passrefreshedProjectstoProjectsDirectoryon line 66, or add a "Sync Failed" status badge toProjectCard(line 11 in ProjectCard.astro) to surface the issue explicitly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/projects.astro` at line 66, The ProjectsDirectory component is currently displaying all projects including those with syncFailed status, which show placeholder zero stars/forks without any visual indicator of failure, contradicting the goal of showing honest totals. To fix this, either pass refreshedProjects instead of projects to the ProjectsDirectory component to exclude failed syncs entirely, or add a "Sync Failed" status badge to the ProjectCard component to visually mark entries that failed to sync, making it clear to users why those cards display zero metrics.src/components/ProjectsDirectory.astro (1)
96-182:⚠️ Potential issue | 🔴 CriticalMove script to external bundled file or add nonce/hash support to CSP policy.
This inline
<script>will be blocked by the BaseLayout CSP policy (script-src 'self'). Astro 6.x inlines plain<script>tags in the HTML output by default, which violates the strict script-source constraint. The filtering, sorting, and pagination will fail at runtime.Options:
- Move the script to an external module and import it, or
- Update the CSP policy to include
script-srcnonce/hash directives if inline scripts are required.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ProjectsDirectory.astro` around lines 96 - 182, The inline script tag in ProjectsDirectory.astro containing the project filtering, sorting, and pagination logic is blocked by the BaseLayout's CSP policy which restricts scripts to 'self'. Extract the JavaScript code from the inline script block into a separate external file (e.g., projectsDirectory.js or similar), then import and execute it from the component instead of using an inline script tag. This will allow the bundler to handle it properly and comply with the strict CSP policy. Alternatively, if inline scripts must be kept, update the CSP policy in BaseLayout to support nonce or hash directives for script-src.
🧹 Nitpick comments (3)
astro.config.mjs (1)
16-23: 💤 Low valueMisleading comment:
inlineStylesheetsdoes not control script inlining.The comment mentions preventing inline
<script>for CSP compliance, butbuild.inlineStylesheetsonly controls stylesheet emission, not scripts. Astro's script bundling is controlled separately—module scripts (like the scroll-reveal script) are already emitted as external/_astro/*.jsfiles by default in Astro v5+.The setting is still correct for preventing inline
<style>blocks that would violatestyle-src 'self', but the comment should be updated to reflect this.📝 Suggested comment fix
build: { - // Never inline the scroll-reveal `<script>` (or any other) into the HTML. - // A strict `script-src 'self'` CSP (no nonce, no 'unsafe-inline') would block - // an inline code-bearing <script>; forcing Astro to emit it as an external - // /_astro/*.js entry referenced via `<script src>` keeps it covered by 'self'. - // (SEC-02 / MOTION) + // Never inline stylesheets into the HTML. A strict `style-src 'self'` CSP + // (no 'unsafe-inline') would block inline <style> blocks; forcing Astro to + // emit all CSS as external /_astro/*.css files keeps them covered by 'self'. + // (SEC-02) inlineStylesheets: 'never' },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@astro.config.mjs` around lines 16 - 23, The comment in the build configuration block for astro.config.mjs is misleading because it describes inlineStylesheets as controlling script inlining for CSP compliance, when this setting actually only controls stylesheet emission (preventing inline <style> blocks). Update the comment to accurately reflect that inlineStylesheets: 'never' prevents inline stylesheets that would violate style-src 'self', and clarify that script bundling is handled separately by Astro and already emits module scripts as external files by default in Astro v5+.src/layouts/BaseLayout.astro (1)
118-121: 💤 Low valueHardcoded
type="image/avif"assumes all preloaded heroes are AVIF.If
heroPreloadHrefever points to a WebP, JPEG, or other format, the browser may ignore or mishandle the preload due to the mismatched MIME type. Currently the homepage generates AVIF viagetImage, so this works, but the coupling is implicit.Consider accepting the image type as an additional prop or omitting the
typeattribute entirely (browsers will sniff the format).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/layouts/BaseLayout.astro` around lines 118 - 121, The preload link element in BaseLayout.astro hardcodes type="image/avif" regardless of the actual format of heroPreloadHref, which will cause browsers to ignore or mishandle preloads for non-AVIF images. Fix this by either accepting a heroPreloadType prop and using it dynamically in the type attribute of the link element, or removing the type attribute entirely from the preload link tag and allowing the browser to sniff the image format automatically. The simpler approach is to omit the type attribute since browsers can detect the format from the file extension or content.src/pages/blog/[slug].astro (1)
35-41: ⚡ Quick winUse an absolute URL for
BlogPosting.imagein JSON-LD.
thumbnailUrlcan be relative, which weakens structured-data portability across crawlers and validators. Prefer an absolute URL in the JSON-LD payload.♻️ Proposed refinement
const thumbnail = post.thumbnail || '/brand/indopensource-hero.jpg'; const thumbnailUrl = /^https?:\/\//.test(thumbnail) ? thumbnail : withBase(thumbnail); +const siteOrigin = Astro.site ?? new URL('https://indopensource.org'); +const thumbnailAbsoluteUrl = new URL(thumbnailUrl, siteOrigin).toString(); @@ - image: thumbnailUrl, + image: thumbnailAbsoluteUrl,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/blog/`[slug].astro around lines 35 - 41, The jsonLd object in the BlogPosting schema uses thumbnailUrl which may be a relative URL, but structured data requires absolute URLs for the image field to ensure proper portability across crawlers and validators. Modify the jsonLd object to ensure the image property receives an absolute URL by converting thumbnailUrl to a fully qualified URL (including the domain) before assigning it to the image field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/sync-blog-posts.mjs`:
- Around line 87-97: The YAML parsing logic in the array item handling is
incorrectly coercing empty scalar keys into arrays, which corrupts post fields.
When an array item contains a key-value pair (like `- key: value`), if the value
portion is empty (after the colon), the code should not treat it as a scalar
field but rather handle it appropriately to prevent field type corruption.
Modify the condition around the key-value pair parsing to check whether
`rest.join(':')` produces an actual value after trimming; if it results in an
empty string, either skip that item, treat it as a null scalar, or handle it in
a way that prevents initializing array fields that should remain scalars. This
same pattern correction should be applied at line 111-114 as well.
- Around line 250-263: The regex pattern in the date validation check accepts
timezone-less datetimes (e.g., "2023-06-14T14:30:00") which parse using local
machine timezone, breaking deterministic sorting. The timezone indicator at the
end of the regex pattern is currently optional due to the trailing question
mark. Modify the regex to require the timezone indicator (Z or UTC offset)
whenever a time component is present, or alternatively, restrict the validator
to only accept plain dates in YYYY-MM-DD format without time components. This
ensures Date.parse always interprets dates consistently across all machines
regardless of local timezone settings.
In `@src/components/SiteHeader.astro`:
- Around line 63-65: The isCurrent check at lines 63-65 in SiteHeader.astro uses
strict equality which fails to mark parent nav items as current when viewing
detail pages like /blog/my-post/ since they don't exactly match /blog/.
Additionally, the same issue exists at lines 98-100. Replace the strict equality
comparison with section-aware matching that checks if the current path starts
with the href value, ensuring parent navigation items are correctly marked as
current for both overview pages and their dynamic route detail pages. Apply this
fix to both locations where isCurrent is evaluated.
In `@src/pages/blog/`[slug].astro:
- Around line 6-7: The post.author.url is being inserted directly into anchor
href attributes without validation, which creates a security vulnerability where
malicious JavaScript URLs could be executed. Validate the post.author.url before
rendering it into any href attribute by implementing a URL validation function
that only allows safe protocols (http, https, mailto) and rejects potentially
dangerous ones like javascript. Apply this validation wherever post.author.url
is used in href attributes throughout the file, ensuring that unsafe URLs are
either sanitized or replaced with empty strings or fallback values.
In `@src/pages/index.astro`:
- Around line 14-15: The topProjects variable is being created by slicing
directly from the full projects array, which includes entries with syncFailed
status. These failed sync entries should be excluded from the featured rail on
the homepage. Filter the projects array to exclude any entries where syncFailed
is true before slicing to create topProjects, ensuring that only successfully
synced repositories appear in the featured cards.
In `@src/styles/global.css`:
- Around line 178-186: In the `.sr-only` class definition, the deprecated `clip`
property needs to be replaced. Remove or replace the line containing `clip:
rect(0, 0, 0, 0);` with the modern `clip-path: inset(50%);` property. If legacy
browser support is required, you may keep the deprecated `clip` property as a
fallback on a separate line before the `clip-path` property, but the modern
property should be the primary rule.
- Line 16: The stylelint configuration does not recognize Tailwind v4's `@theme`
directive, causing CI failures for valid CSS. Update your stylelint
configuration file (typically `.stylelintrc.js`, `.stylelintrc.json`, or
similar) to allow Tailwind's custom at-rules by either installing and extending
a Tailwind-specific stylelint config package like
`@dreamsicle.io/stylelint-config-tailwindcss`, or manually modify the
`at-rule-no-unknown` rule in your existing stylelint config to add an exceptions
list that includes Tailwind's custom at-rules (such as `theme`, `layer`,
`apply`, etc.).
---
Outside diff comments:
In `@package.json`:
- Around line 6-14: The package.json file is missing an engines declaration to
specify the Node.js version requirement. Add an "engines" field to the root
level of package.json (at the same level as "scripts") with the value "node":
">=22" to explicitly document that the project requires Node.js version 22 or
higher, matching the CI workflow requirements and providing clear guidance to
local developers.
In `@src/components/ProjectsDirectory.astro`:
- Around line 96-182: The inline script tag in ProjectsDirectory.astro
containing the project filtering, sorting, and pagination logic is blocked by
the BaseLayout's CSP policy which restricts scripts to 'self'. Extract the
JavaScript code from the inline script block into a separate external file
(e.g., projectsDirectory.js or similar), then import and execute it from the
component instead of using an inline script tag. This will allow the bundler to
handle it properly and comply with the strict CSP policy. Alternatively, if
inline scripts must be kept, update the CSP policy in BaseLayout to support
nonce or hash directives for script-src.
In `@src/pages/projects.astro`:
- Line 66: The ProjectsDirectory component is currently displaying all projects
including those with syncFailed status, which show placeholder zero stars/forks
without any visual indicator of failure, contradicting the goal of showing
honest totals. To fix this, either pass refreshedProjects instead of projects to
the ProjectsDirectory component to exclude failed syncs entirely, or add a "Sync
Failed" status badge to the ProjectCard component to visually mark entries that
failed to sync, making it clear to users why those cards display zero metrics.
---
Nitpick comments:
In `@astro.config.mjs`:
- Around line 16-23: The comment in the build configuration block for
astro.config.mjs is misleading because it describes inlineStylesheets as
controlling script inlining for CSP compliance, when this setting actually only
controls stylesheet emission (preventing inline <style> blocks). Update the
comment to accurately reflect that inlineStylesheets: 'never' prevents inline
stylesheets that would violate style-src 'self', and clarify that script
bundling is handled separately by Astro and already emits module scripts as
external files by default in Astro v5+.
In `@src/layouts/BaseLayout.astro`:
- Around line 118-121: The preload link element in BaseLayout.astro hardcodes
type="image/avif" regardless of the actual format of heroPreloadHref, which will
cause browsers to ignore or mishandle preloads for non-AVIF images. Fix this by
either accepting a heroPreloadType prop and using it dynamically in the type
attribute of the link element, or removing the type attribute entirely from the
preload link tag and allowing the browser to sniff the image format
automatically. The simpler approach is to omit the type attribute since browsers
can detect the format from the file extension or content.
In `@src/pages/blog/`[slug].astro:
- Around line 35-41: The jsonLd object in the BlogPosting schema uses
thumbnailUrl which may be a relative URL, but structured data requires absolute
URLs for the image field to ensure proper portability across crawlers and
validators. Modify the jsonLd object to ensure the image property receives an
absolute URL by converting thumbnailUrl to a fully qualified URL (including the
domain) before assigning it to the image field.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 875fa46f-6a17-4989-9b18-2d425c655e61
⛔ Files ignored due to path filters (9)
docs/redesign/blog.pngis excluded by!**/*.pngdocs/redesign/contact.pngis excluded by!**/*.pngdocs/redesign/falsafah.pngis excluded by!**/*.pngdocs/redesign/forum.pngis excluded by!**/*.pngdocs/redesign/home.pngis excluded by!**/*.pngdocs/redesign/projects.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.jsonsrc/assets/brand/icon-192.pngis excluded by!**/*.pngsrc/assets/brand/indopensource-hero.jpgis excluded by!**/*.jpg
📒 Files selected for processing (33)
.env.exampleastro.config.mjspackage.jsonscripts/sync-blog-posts.mjsscripts/sync-projects.mjssrc/components/BaseButton.astrosrc/components/HomeHero.astrosrc/components/InfoCard.astrosrc/components/LightSvgMotion.astrosrc/components/PageHeader.astrosrc/components/ProjectCard.astrosrc/components/ProjectsDirectory.astrosrc/components/RoadmapTimeline.astrosrc/components/SectionHeader.astrosrc/components/SiteFooter.astrosrc/components/SiteHeader.astrosrc/layouts/BaseLayout.astrosrc/lib/content.tssrc/lib/hero.tssrc/lib/projects.tssrc/pages/blog.astrosrc/pages/blog/[slug].astrosrc/pages/contact.astrosrc/pages/falsafah.astrosrc/pages/forum.astrosrc/pages/index.astrosrc/pages/projects.astrosrc/pages/projects/[slug].astrosrc/pages/robots.txt.tssrc/pages/sitemap-index.xml.tssrc/pages/sitemap.xml.tssrc/styles/global.csstest/lib.test.mjs
💤 Files with no reviewable changes (1)
- src/pages/sitemap-index.xml.ts
| const arrayItem = line.match(/^\s+-\s+(.*)$/); | ||
| if (arrayItem && activeArray) { | ||
| const value = arrayItem[1].trim(); | ||
| if (value.includes(':')) { | ||
| const item = {}; | ||
| const [key, ...rest] = value.split(':'); | ||
| item[key.trim()] = rest.join(':').trim().replace(/^"|"$/g, ''); | ||
| item[key.trim()] = unquoteScalar(rest.join(':')); | ||
| data[activeArray].push(item); | ||
| } else { | ||
| data[activeArray].push(value.replace(/^"|"$/g, '')); | ||
| data[activeArray].push(unquoteScalar(value)); | ||
| } |
There was a problem hiding this comment.
Empty scalar keys are coerced into arrays, which can corrupt emitted post fields.
With the current logic, key: is initialized as [] before knowing whether list items follow. That can turn scalar fields into arrays (for example status:), breaking downstream contracts and publish gating behavior.
Proposed fix
- if (arrayItem && activeArray) {
+ if (arrayItem && activeArray) {
+ if (!Array.isArray(data[activeArray])) data[activeArray] = [];
const value = arrayItem[1].trim();
if (value.includes(':')) {
const item = {};
const [key, ...rest] = value.split(':');
item[key.trim()] = unquoteScalar(rest.join(':'));
data[activeArray].push(item);
} else {
data[activeArray].push(unquoteScalar(value));
}
continue;
}
if (!rawValue.trim()) {
- data[key] = [];
+ data[key] = '';
activeArray = key;
continue;
}Also applies to: 111-114
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/sync-blog-posts.mjs` around lines 87 - 97, The YAML parsing logic in
the array item handling is incorrectly coercing empty scalar keys into arrays,
which corrupts post fields. When an array item contains a key-value pair (like
`- key: value`), if the value portion is empty (after the colon), the code
should not treat it as a scalar field but rather handle it appropriately to
prevent field type corruption. Modify the condition around the key-value pair
parsing to check whether `rest.join(':')` produces an actual value after
trimming; if it results in an empty string, either skip that item, treat it as a
null scalar, or handle it in a way that prevents initializing array fields that
should remain scalars. This same pattern correction should be applied at line
111-114 as well.
| // Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so | ||
| // ordering is deterministic across machines/locales. `Date.parse` is lenient | ||
| // and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would | ||
| // reintroduce the nondeterministic ordering CC-4 is about. Anything that is | ||
| // not strict ISO is treated as "no usable date" and pushed to the end. | ||
| if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) { | ||
| if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`); | ||
| return null; | ||
| } | ||
| const time = Date.parse(trimmed); | ||
| if (Number.isNaN(time)) { | ||
| console.warn(`Invalid blog date "${value}" — sorting this post last.`); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Timezone-less datetimes still parse in local time, so ordering can differ across machines.
The validator allows datetime values without Z/offset. Date.parse() then uses local timezone semantics, which undermines the deterministic sort guarantee.
Proposed fix
if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) {
if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`);
return null;
}
+ const hasTime = /[T ]\d{2}:\d{2}/.test(trimmed);
+ const hasTimezone = /(Z|[+-]\d{2}:?\d{2})$/.test(trimmed);
+ if (hasTime && !hasTimezone) {
+ console.warn(`Timezone-less blog date "${value}" — sorting this post last (add Z or ±hh:mm).`);
+ return null;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so | |
| // ordering is deterministic across machines/locales. `Date.parse` is lenient | |
| // and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would | |
| // reintroduce the nondeterministic ordering CC-4 is about. Anything that is | |
| // not strict ISO is treated as "no usable date" and pushed to the end. | |
| if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) { | |
| if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`); | |
| return null; | |
| } | |
| const time = Date.parse(trimmed); | |
| if (Number.isNaN(time)) { | |
| console.warn(`Invalid blog date "${value}" — sorting this post last.`); | |
| return null; | |
| } | |
| // Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so | |
| // ordering is deterministic across machines/locales. `Date.parse` is lenient | |
| // and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would | |
| // reintroduce the nondeterministic ordering CC-4 is about. Anything that is | |
| // not strict ISO is treated as "no usable date" and pushed to the end. | |
| if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) { | |
| if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`); | |
| return null; | |
| } | |
| const hasTime = /[T ]\d{2}:\d{2}/.test(trimmed); | |
| const hasTimezone = /(Z|[+-]\d{2}:?\d{2})$/.test(trimmed); | |
| if (hasTime && !hasTimezone) { | |
| console.warn(`Timezone-less blog date "${value}" — sorting this post last (add Z or ±hh:mm).`); | |
| return null; | |
| } | |
| const time = Date.parse(trimmed); | |
| if (Number.isNaN(time)) { | |
| console.warn(`Invalid blog date "${value}" — sorting this post last.`); | |
| return null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/sync-blog-posts.mjs` around lines 250 - 263, The regex pattern in the
date validation check accepts timezone-less datetimes (e.g.,
"2023-06-14T14:30:00") which parse using local machine timezone, breaking
deterministic sorting. The timezone indicator at the end of the regex pattern is
currently optional due to the trailing question mark. Modify the regex to
require the timezone indicator (Z or UTC offset) whenever a time component is
present, or alternatively, restrict the validator to only accept plain dates in
YYYY-MM-DD format without time components. This ensures Date.parse always
interprets dates consistently across all machines regardless of local timezone
settings.
| navItems.map(([label, href]) => { | ||
| const isCurrent = currentPathWithoutBase === href; | ||
| return isCurrent ? ( |
There was a problem hiding this comment.
Use section-aware matching for active nav state.
At Line 64 and Line 99, strict equality means /blog/[slug]/ and /projects/[slug]/ won’t mark their parent nav item as current, so visual active state and aria-current are incorrect on detail pages.
Suggested patch
const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replace(/\/$/, '')}`), '') || '/';
+const isCurrentPath = (href: string) =>
+ href === '/' ? currentPathWithoutBase === '/' : currentPathWithoutBase.startsWith(href);
@@
- const isCurrent = currentPathWithoutBase === href;
+ const isCurrent = isCurrentPath(href);
@@
- const isCurrent = currentPathWithoutBase === href;
+ const isCurrent = isCurrentPath(href);Also applies to: 98-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/SiteHeader.astro` around lines 63 - 65, The isCurrent check at
lines 63-65 in SiteHeader.astro uses strict equality which fails to mark parent
nav items as current when viewing detail pages like /blog/my-post/ since they
don't exactly match /blog/. Additionally, the same issue exists at lines 98-100.
Replace the strict equality comparison with section-aware matching that checks
if the current path starts with the href value, ensuring parent navigation items
are correctly marked as current for both overview pages and their dynamic route
detail pages. Apply this fix to both locations where isCurrent is evaluated.
| import { isPublished, renderArticle, type BlogPost } from '../../lib/content'; | ||
|
|
There was a problem hiding this comment.
Validate author URLs before rendering them into href.
post.author.url is inserted directly into an anchor. If content contains a javascript: URL, it becomes an executable link.
🔒 Proposed fix
-import { withBase } from '../../lib/urls';
+import { normalizeHref, withBase } from '../../lib/urls';
@@
const thumbnail = post.thumbnail || '/brand/indopensource-hero.jpg';
const thumbnailUrl = /^https?:\/\//.test(thumbnail) ? thumbnail : withBase(thumbnail);
+const authorUrl = post.author?.url ? normalizeHref(post.author.url) : '';
@@
- {post.author?.url
- ? <a class="display-sm text-brand underline decoration-2 underline-offset-2 hover:text-accent" href={post.author.url}>{post.author?.name}</a>
+ {authorUrl
+ ? <a class="display-sm text-brand underline decoration-2 underline-offset-2 hover:text-accent" href={authorUrl}>{post.author?.name}</a>
: <span class="display-sm text-brand">{post.author?.name}</span>}Also applies to: 87-89
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/blog/`[slug].astro around lines 6 - 7, The post.author.url is being
inserted directly into anchor href attributes without validation, which creates
a security vulnerability where malicious JavaScript URLs could be executed.
Validate the post.author.url before rendering it into any href attribute by
implementing a URL validation function that only allows safe protocols (http,
https, mailto) and rejects potentially dangerous ones like javascript. Apply
this validation wherever post.author.url is used in href attributes throughout
the file, ensuring that unsafe URLs are either sanitized or replaced with empty
strings or fallback values.
| const totalProjects = projects.length; | ||
| const topProjects = projects.slice(0, 5); |
There was a problem hiding this comment.
Filter out failed sync entries before computing topProjects.
Line 15 slices from all projects, so homepage cards can still include syncFailed entries that are excluded elsewhere. This can surface partial/unavailable repos in the featured rail.
Suggested fix
const totalProjects = projects.length;
-const topProjects = projects.slice(0, 5);
+const refreshedProjects = projects.filter((project) => !project.syncFailed);
+const topProjects = refreshedProjects.slice(0, 5);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const totalProjects = projects.length; | |
| const topProjects = projects.slice(0, 5); | |
| const totalProjects = projects.length; | |
| const refreshedProjects = projects.filter((project) => !project.syncFailed); | |
| const topProjects = refreshedProjects.slice(0, 5); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/index.astro` around lines 14 - 15, The topProjects variable is
being created by slicing directly from the full projects array, which includes
entries with syncFailed status. These failed sync entries should be excluded
from the featured rail on the homepage. Filter the projects array to exclude any
entries where syncFailed is true before slicing to create topProjects, ensuring
that only successfully synced repositories appear in the featured cards.
|
|
||
| @import "tailwindcss"; | ||
|
|
||
| @theme { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether stylelint config still rejects Tailwind v4 directives.
fd -HI 'stylelint\.config|\.stylelintrc' | while read -r f; do
echo "===== $f ====="
sed -n '1,220p' "$f"
done
echo "===== stylelint rule references ====="
rg -n "at-rule-no-unknown|scss/at-rule-no-unknown|tailwind|value-keyword-case|declaration-empty-line-before|property-no-deprecated"Repository: IndopenSource/indopensource.org
Length of output: 7017
🏁 Script executed:
# Check the global.css file content around line 16
wc -l src/styles/global.css
echo "===== global.css content ====="
cat -n src/styles/global.css | head -30Repository: IndopenSource/indopensource.org
Length of output: 1807
🏁 Script executed:
# Check for at-rule-no-unknown in the stylelint config and related documentation
rg -A 5 -B 5 "at-rule-no-unknown" .stylelintrc.jsonRepository: IndopenSource/indopensource.org
Length of output: 57
🌐 Web query:
Tailwind CSS v4 @theme directive stylelint configuration
💡 Result:
To use Stylelint with Tailwind CSS v4 and the @theme directive, you must configure Stylelint to recognize Tailwind’s custom CSS at-rules and syntax, as standard CSS parsers will otherwise flag them as errors [1][2][3]. Since Tailwind CSS v4 utilizes the @theme directive to define design tokens [4], standard Stylelint configurations often require supplemental settings to prevent validation failures. The most effective approach is to use a dedicated Stylelint configuration package specifically built for Tailwind CSS v4+ [1][2][5]. Recommended Configuration Approach Several community-maintained packages, such as @dreamsicle.io/stylelint-config-tailwindcss, are designed to implement Tailwind's directives (including @theme) into Stylelint's languageOptions [1][5]. 1. Install the package: npm install --save-dev @dreamsicle.io/stylelint-config-tailwindcss 2. Add it to your stylelint.config.mjs (or equivalent configuration file): / * @type {import("stylelint").Config} */ export default { extends: [ "stylelint-config-standard", "@dreamsicle.io/stylelint-config-tailwindcss", ], }; Why this is necessary: Tailwind CSS v4 introduces custom syntax that extends beyond standard CSS specifications [4]. By using a specialized configuration, you ensure that Stylelint understands the @theme directive and associated custom properties as valid syntax rather than unknown at-rules [1][6]. These configurations typically inject Tailwind's custom at-rules, properties, and types into Stylelint’s syntax validation engine, allowing for proper linting without suppressing critical CSS errors [1][3]. If you are not using a pre-built configuration, you would otherwise need to manually configure Stylelint’s customSyntax or ignore the specific at-rule violations using the rules configuration (e.g., at-rule-no-unknown), which is generally discouraged as it sacrifices linting quality [1][2].
Citations:
- 1: https://github.com/dreamsicle-io/stylelint-config-tailwindcss
- 2: https://github.com/studiometa/stylelint-config-tailwindcss
- 3: https://github.com/t0byman/stylelint-config-tailwindcss
- 4: https://tailwindcss.com/docs/theme
- 5: https://registry.npmjs.org/@dreamsicle.io/stylelint-config-tailwindcss
- 6: https://github.com/dreamsicle-io/stylelint-config-tailwindcss/blob/main/README.md
Update stylelint config to accept Tailwind v4 @theme directive.
Line 16 uses a valid Tailwind v4 directive, but standard stylelint configurations will report it as an unknown at-rule. This will block CI merges despite the CSS being valid. Add a Tailwind-specific stylelint configuration (e.g., @dreamsicle.io/stylelint-config-tailwindcss) or manually configure the at-rule-no-unknown rule to allow Tailwind's custom at-rules.
🧰 Tools
🪛 Stylelint (17.12.0)
[error] 16-16: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/styles/global.css` at line 16, The stylelint configuration does not
recognize Tailwind v4's `@theme` directive, causing CI failures for valid CSS.
Update your stylelint configuration file (typically `.stylelintrc.js`,
`.stylelintrc.json`, or similar) to allow Tailwind's custom at-rules by either
installing and extending a Tailwind-specific stylelint config package like
`@dreamsicle.io/stylelint-config-tailwindcss`, or manually modify the
`at-rule-no-unknown` rule in your existing stylelint config to add an exceptions
list that includes Tailwind's custom at-rules (such as `theme`, `layer`,
`apply`, etc.).
Source: Linters/SAST tools
| .sr-only { | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; | ||
| overflow: hidden; | ||
| clip: rect(0, 0, 0, 0); | ||
| white-space: nowrap; |
There was a problem hiding this comment.
Replace deprecated .sr-only clipping rule.
Line 185 uses clip, which is deprecated and already flagged by stylelint. Prefer clip-path: inset(50%) (optionally keep clip as fallback only if legacy support is required).
Suggested patch
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
- clip: rect(0, 0, 0, 0);
+ clip-path: inset(50%);
+ clip: rect(0, 0, 0, 0); /* optional fallback */
white-space: nowrap;
border: 0;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip-path: inset(50%); | |
| clip: rect(0, 0, 0, 0); /* optional fallback */ | |
| white-space: nowrap; |
🧰 Tools
🪛 Stylelint (17.12.0)
[error] 185-185: Deprecated property "clip" (property-no-deprecated)
(property-no-deprecated)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/styles/global.css` around lines 178 - 186, In the `.sr-only` class
definition, the deprecated `clip` property needs to be replaced. Remove or
replace the line containing `clip: rect(0, 0, 0, 0);` with the modern
`clip-path: inset(50%);` property. If legacy browser support is required, you
may keep the deprecated `clip` property as a fallback on a separate line before
the `clip-path` property, but the modern property should be the primary rule.
Source: Linters/SAST tools
Ringkasan
PR ini membawa dua hal ke situs hub indopensource.org, di atas base
mainsaat ini:Semua perubahan menjaga
astro checkbersih, 16 unit test baru hijau, build 57 halaman, dan 0 inline style (CSPstyle-src 'self'tetap utuh). Dibuka untuk diskusi — silakan ambil sebagian atau seluruhnya.1. Pengerasan engineering
Keamanan
marked.parse(post.content)sebelumnya masukset:htmltanpa sanitizer (marked v18 tidak punya sanitizer bawaan). Kini lewat allowlistsanitize-htmlterpusat (src/lib/content.ts→renderArticle()), dipakai juga untuk README project yang sekarang dirender saat build (bukan fetch klien).<meta>:script-src 'self'+ hash JSON-LD (tanpaunsafe-inline),style-src 'self',frame-ancestors 'none',object-src 'none';rel="noopener noreferrer"dipaksa di semua link hasil sanitasi.Konten & kebenaran data
isPublished()dipakai digetStaticPaths, pemilihan featured, dan sitemap — poststatus: drafttidak menghasilkan halaman dan tidak masuk sitemap.authors[], bukan committer git.parseFrontmatterdiperkuat (CRLF, komentar inline, single/double quote, duplicate key, tanpa trailing newline), sort deterministik, resolusi default-branch repo via API (tidak hardcodemain), total stars jujur (entry yang gagal di-sync ditandai & dikecualikan dari agregat).projectSluganti-collision (suffix FNV-1a) + hapus fungsi inverse yang mati.SEO & aksesibilitas
og:image/twitter:image+og:image:altper-halaman; JSON-LD (WebSite/BlogPosting/SoftwareSourceCode); sitemap/sitemap.xmldenganlastmoddariAstro.site;trailingSlash: 'always'.:focus-visible, satu<h1>per halaman, kontras WCAG AA diverifikasi numerik,prefers-reduced-motion.Performa
astro:assets<Picture>(AVIF/WebP): hero 60KB → ~8.5KB, logo 18KB → 737B;width/heightdi semua<img>(CLS ~0); preload LCP hero. README dirender saat build → tidak ada lagi fetch GitHub API per kunjungan.Tooling
node --test) untuk slug,isPublished, sanitizer/XSS, dan parser frontmatter; tambah.env.example.2. Redesain "Risograph Editorial"
Arah visual: zine cetak risograph Indonesia — hangat, bertekstur, percaya diri. Bukan SaaS gradient.
@fontsource, tanpa CDN): Bricolage Grotesque (display), Newsreader (serif body), Spline Sans Mono (slug/stat/metadata).<Picture>AVIF teroptimasi.Tangkapan layar
Catatan scope
.github/workflows/*,vercel.json, dan tweakREADME.md/SECURITY.md.astro.config.mjssitetetaphttps://indopensource.org.docs/redesign/hanya ilustrasi untuk review — silakan dihapus sebelum merge bila lebih suka attachment.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements