Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed website/public/example.gif
Binary file not shown.
Binary file added website/public/example.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/public/widget-default.gif
Binary file not shown.
Binary file added website/public/widget-default.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/public/widget-icon.gif
Binary file not shown.
Binary file added website/public/widget-icon.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed website/public/widget-small.gif
Binary file not shown.
Binary file added website/public/widget-small.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions website/src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const shouldRenderSearch =
</div>
<nav class="header-nav">
<a href="/getting-started/introduction/" class="header-link">Docs</a>
<a href="/contributors/" class="header-link">Contributors</a>
<a href="https://check.aeojs.org" target="_blank" rel="noopener" class="header-link">Checker</a>
</nav>
</div>
Expand Down
126 changes: 126 additions & 0 deletions website/src/components/HomepageContributors.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
type GhContributor = {
login: string;
avatar_url: string;
html_url: string;
contributions: number;
type: string;
};

let contributors: GhContributor[] = [];

try {
const res = await fetch(
'https://api.github.com/repos/multivmlabs/aeo.js/contributors?per_page=100',
{ headers: { 'User-Agent': 'aeojs-docs-build', Accept: 'application/vnd.github+json' } }
);
if (res.ok) {
const data = (await res.json()) as GhContributor[];
contributors = data
.filter((c) => c.type === 'User')
.sort((a, b) => b.contributions - a.contributions);
}
} catch {
// Silently skip the strip on fetch failure — homepage stays clean.
}

const top = contributors.slice(0, 8);
const remaining = Math.max(0, contributors.length - top.length);
---

{contributors.length > 0 && (
<section class="contrib-strip">
<h2 class="contrib-strip-title">Built by</h2>
<div class="contrib-strip-row">
{top.map((c) => (
<a
href={c.html_url}
target="_blank"
rel="noopener"
class="contrib-strip-avatar"
title={`${c.login} — ${c.contributions} commit${c.contributions === 1 ? '' : 's'}`}
>
<img
src={`${c.avatar_url}&s=80`}
alt={c.login}
width="40"
height="40"
loading="lazy"
decoding="async"
/>
</a>
))}
{remaining > 0 && (
<a href="/contributors/" class="contrib-strip-more" aria-label={`${remaining} more contributors`}>
+{remaining}
</a>
)}
</div>
<a href="/contributors/" class="contrib-strip-link">
View all {contributors.length} contributor{contributors.length === 1 ? '' : 's'} →
</a>
</section>
)}

<style>
.contrib-strip {
margin: 4rem auto 2rem;
text-align: center;
max-width: 60rem;
}
.contrib-strip-title {
font-size: 0.85rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 1rem;
}
.contrib-strip-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.contrib-strip-avatar,
.contrib-strip-more {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: border-color 0.15s ease, transform 0.15s ease;
text-decoration: none;
}
.contrib-strip-avatar:hover,
.contrib-strip-more:hover {
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.contrib-strip-avatar img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.contrib-strip-more {
font-size: 0.75rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
}
.contrib-strip-link {
display: inline-block;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
transition: color 0.15s ease;
}
.contrib-strip-link:hover {
color: #fff;
}
</style>
2 changes: 1 addition & 1 deletion website/src/content/docs/features/widget.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ widget: {

| Default | Small | Icon |
|---------|-------|------|
| ![Default widget](/widget-default.gif) | ![Small widget](/widget-small.gif) | ![Icon widget](/widget-icon.gif) |
| ![Default widget](/widget-default.webp) | ![Small widget](/widget-small.webp) | ![Icon widget](/widget-icon.webp) |
| `size: 'default'` | `size: 'small'` | `size: 'icon-only'` |
| Full labels with icons | Compact — ~30% smaller | Just icons, no labels |

Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/getting-started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Introduction
description: What is aeo.js and why you need Answer Engine Optimization.
---

![aeo.js in action](/example.gif)
![aeo.js in action](/example.webp)

## What is Answer Engine Optimization?

Expand Down
3 changes: 3 additions & 0 deletions website/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ hero:

import { Tabs, TabItem } from '@astrojs/starlight/components';
import StickyNav from '../../components/StickyNav.astro';
import HomepageContributors from '../../components/HomepageContributors.astro';

<StickyNav />

Expand Down Expand Up @@ -304,3 +305,5 @@ public/
</form>
<p style={{textAlign: 'center', fontSize: '0.8rem', color: 'rgba(255,255,255,0.3)', marginTop: '0.75rem'}}>Free — no signup required. Powered by <a href="https://check.aeojs.org" style={{color: 'rgba(255,255,255,0.5)', textDecoration: 'underline', textUnderlineOffset: '2px'}}>AEO Checker</a></p>
</div>

<HomepageContributors />
149 changes: 149 additions & 0 deletions website/src/pages/contributors.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';

type GhContributor = {
login: string;
avatar_url: string;
html_url: string;
contributions: number;
type: string;
};

let contributors: GhContributor[] = [];
let fetchError = false;

try {
const res = await fetch(
'https://api.github.com/repos/multivmlabs/aeo.js/contributors?per_page=100',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Paginate GitHub contributors API results

The contributors page fetches only a single page (per_page=100) and never follows pagination links, so once the repository exceeds 100 human contributors this page will silently omit everyone after the first page and present an incomplete list. Because this route is meant to represent project contributors, the data becomes inaccurate in normal growth scenarios unless additional pages are fetched.

Useful? React with 👍 / 👎.

{ headers: { 'User-Agent': 'aeojs-docs-build', Accept: 'application/vnd.github+json' } }
);
if (res.ok) {
const data = (await res.json()) as GhContributor[];
contributors = data
.filter((c) => c.type === 'User')
.sort((a, b) => b.contributions - a.contributions);
} else {
fetchError = true;
}
} catch {
fetchError = true;
}
---

<StarlightPage
frontmatter={{
title: 'Contributors',
description: 'The people who make aeo.js possible.',
tableOfContents: false,
}}
>
{fetchError ? (
<p class="contrib-fallback">
Couldn't load contributors right now. View the full list on
<a href="https://github.com/multivmlabs/aeo.js/graphs/contributors" target="_blank" rel="noopener">
GitHub
</a>
.
</p>
) : (
<>
<p class="contrib-intro">
{contributors.length} contributor{contributors.length === 1 ? '' : 's'} have shaped aeo.js. Thank you. 🙏
</p>
Comment on lines +50 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Two edge-case rendering issues in the success branch's intro text at website/src/pages/contributors.astro lines 50–52: (1) subject-verb disagreementcontributor{...=== 1 ? '' : 's'} pluralizes the noun but "have" is hardcoded, so a single contributor renders "1 contributor have shaped aeo.js." (should be "has shaped"). (2) Empty-success state — when res.ok is true but the contributor list is empty (e.g. GitHub's /contributors endpoint returns 202 with [] while stats are recomputing, or the type === 'User' filter removes everyone), fetchError stays false and the page renders "0 contributors have shaped aeo.js. Thank you. 🙏" plus an empty grid and a "Want to join them?" CTA instead of the graceful GitHub-link fallback. Trivial fix: branch the verb the same way the noun is, and treat !fetchError && contributors.length === 0 like fetchError.

Extended reasoning...

1. Subject-verb disagreement (line 51)

The success branch renders:

{contributors.length} contributor{contributors.length === 1 ? '' : 's'} have shaped aeo.js. Thank you. 🙏

The noun is conditionally pluralized via the ternary, but the verb have is hardcoded. Step-by-step proof for contributors.length === 1:

  1. {contributors.length}1
  2. contributor literal → contributor
  3. {contributors.length === 1 ? '' : 's'}'' (empty)
  4. have shaped aeo.js. Thank you. 🙏 literal → unchanged

Concatenated output: "1 contributor have shaped aeo.js. Thank you. 🙏" — a real subject-verb agreement error in user-facing copy. Counts of 0 ("0 contributors have") and 2+ render correctly. The fix mirrors the existing pluralization:

{contributors.length} contributor{contributors.length === 1 ? '' : 's'} {contributors.length === 1 ? 'has' : 'have'} shaped aeo.js.

For multivmlabs/aeo.js this only triggers if the User-filtered count drops to exactly 1 (currently 4), so the blast radius is small — hence nit.

2. Empty-success state (lines 17–28, 50–82)

fetchError is only set when !res.ok or the try throws. Step-by-step proof for the 202-empty-array case:

  1. fetch(...) resolves with res.status === 202. The Response ok getter is true for any status in 200–299, so res.ok === true and fetchError stays false.
  2. await res.json() parses an empty array body [] without throwing (this is the documented behavior of GET /repos/{owner}/{repo}/contributors while GitHub recomputes stats — see GitHub REST API docs).
  3. data.filter(c => c.type === 'User').sort(...) produces [].
  4. contributors.length === 0 and fetchError === false, so the success branch renders.
  5. Output: "0 contributors have shaped aeo.js. Thank you. 🙏", an empty <ul class="contrib-grid">, and "Want to join them? Open a PR." — visually broken even though no error occurred.

The same path is reachable if every entry is filtered out by c.type === 'User' (e.g. only bots committed since the last cache).

Addressing the refutation: the refuting verifier correctly notes the build doesn't crash, the trigger is brief/rare for an established repo, and the next deploy fixes it. That's why this is filed as nit, not normal — the page still renders and the fetchError fallback covers the meaningful 4xx/5xx cases. But the fix is one line and the file is being added in this PR, so it's worth catching now rather than after a confusing user report. The fix is to widen the fallback condition:

{fetchError || contributors.length === 0 ? (
  <p class="contrib-fallback">…GitHub fallback…</p>
) : (
  …existing success markup…
)}

Severity: nit — both issues are low-impact copy/UX polish on a docs page, not functional bugs. They co-locate in the same intro line, so a single edit addresses both.

<ul class="contrib-grid">
{contributors.map((c) => (
<li class="contrib-card">
Comment on lines +52 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 rel="noopener" should also include noreferrer

All target="_blank" anchors use only rel="noopener". Adding noreferrer is the recommended practice — it also suppresses the Referer header and independently implies noopener in all browsers, so it's strictly safer without any downside. The same applies to the contributor card links (href={c.html_url}) and the CTA link.

Suggested change
</p>
<ul class="contrib-grid">
{contributors.map((c) => (
<li class="contrib-card">
<a href="https://github.com/multivmlabs/aeo.js/graphs/contributors" target="_blank" rel="noopener noreferrer">
Prompt To Fix With AI
This is a comment left during a code review.
Path: website/src/pages/contributors.astro
Line: 52-55

Comment:
**`rel="noopener"` should also include `noreferrer`**

All `target="_blank"` anchors use only `rel="noopener"`. Adding `noreferrer` is the recommended practice — it also suppresses the `Referer` header and independently implies `noopener` in all browsers, so it's strictly safer without any downside. The same applies to the contributor card links (`href={c.html_url}`) and the CTA link.

```suggestion
      <a href="https://github.com/multivmlabs/aeo.js/graphs/contributors" target="_blank" rel="noopener noreferrer">
```

How can I resolve this? If you propose a fix, please make it concise.

<a href={c.html_url} target="_blank" rel="noopener" class="contrib-link">
<img
src={`${c.avatar_url}&s=120`}
alt={`${c.login} avatar`}
width="60"
height="60"
loading="lazy"
decoding="async"
class="contrib-avatar"
/>
<span class="contrib-name">{c.login}</span>
<span class="contrib-count">
{c.contributions} commit{c.contributions === 1 ? '' : 's'}
</span>
</a>
</li>
))}
</ul>
<p class="contrib-cta">
Want to join them?
<a href="https://github.com/multivmlabs/aeo.js" target="_blank" rel="noopener">
Open a PR
</a>
.
</p>
</>
)}
</StarlightPage>

<style>
.contrib-intro,
.contrib-cta,
.contrib-fallback {
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 0.95rem;
margin: 1.5rem 0;
}
.contrib-intro a,
.contrib-cta a,
.contrib-fallback a {
color: #fff;
text-decoration: underline;
text-underline-offset: 0.2em;
}
.contrib-grid {
list-style: none;
padding: 0;
margin: 2rem 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.75rem;
}
.contrib-card {
margin: 0;
}
.contrib-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem 0.75rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: inherit;
text-decoration: none;
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.contrib-link:hover {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.06);
transform: translateY(-1px);
}
.contrib-avatar {
Comment on lines +98 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hardcoded dark-mode colors break light theme

All color values are hardcoded in dark-mode terms (#fff, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.03/0.08/0.2/0.06)). Starlight ships with a built-in light/dark theme toggle; on the light theme, white text on an off-white card background would be nearly invisible. Prefer CSS custom properties from Starlight's design tokens (e.g. var(--sl-color-text), var(--sl-color-bg-sidebar)) so the cards adapt automatically.

Prompt To Fix With AI
This is a comment left during a code review.
Path: website/src/pages/contributors.astro
Line: 98-130

Comment:
**Hardcoded dark-mode colors break light theme**

All color values are hardcoded in dark-mode terms (`#fff`, `rgba(255, 255, 255, 0.6)`, `rgba(255, 255, 255, 0.03/0.08/0.2/0.06)`). Starlight ships with a built-in light/dark theme toggle; on the light theme, white text on an off-white card background would be nearly invisible. Prefer CSS custom properties from Starlight's design tokens (e.g. `var(--sl-color-text)`, `var(--sl-color-bg-sidebar)`) so the cards adapt automatically.

How can I resolve this? If you propose a fix, please make it concise.

width: 60px;
height: 60px;
border-radius: 50%;
display: block;
}
.contrib-name {
font-size: 0.85rem;
font-weight: 500;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.contrib-count {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
</style>
Loading