Skip to content

Commit bb719ee

Browse files
Merge pull request #225 from nemarOrg/staging
Release v0.2.2: drop the Community placeholder, mark outbound links
2 parents 97fffd3 + 5c58947 commit bb719ee

7 files changed

Lines changed: 150 additions & 67 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "nemar-website",
33
"type": "module",
4-
"version": "0.2.1",
4+
"version": "0.2.2",
55
"private": true,
66
"license": "CC-BY-NC-ND-4.0",
77
"description": "nemar.org redesign on Astro + Cloudflare Pages",

src/components/ExternalLink.astro

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
/**
3+
* A link that leaves nemar.org, marked as such and opened in a new tab.
4+
*
5+
* The NEMAR surfaces a visitor can reach from the nav and footer are not all
6+
* one site: the citation dashboard (`nemarOrg/nemar-citations`) and the
7+
* documentation (`nemarOrg/docs`) are separately built and deployed, as are
8+
* the data/api hosts. Navigating to one in place drops the visitor out of the
9+
* site they were reading with only the back button to return, and the old
10+
* markup gave no warning that was about to happen.
11+
*
12+
* Deliberately NOT applied to every `rel="external"` link in the codebase.
13+
* Download anchors (`BidsTree.astro`'s per-file links, the archive action)
14+
* also carry `rel="external"`, and neither the arrow nor `target="_blank"`
15+
* belongs on those: they trigger a download rather than a navigation, so the
16+
* arrow would promise a page that never appears. This component is for
17+
* links that genuinely navigate somewhere else.
18+
*
19+
* `rel="noopener"` is what actually matters with `target="_blank"` — without
20+
* it the opened page gets a live `window.opener` handle back to this one.
21+
* `noreferrer` rides along; these are all first-party NEMAR destinations, but
22+
* there is nothing here worth leaking a referrer for either.
23+
*/
24+
interface Props {
25+
href: string;
26+
/** Extra classes for the anchor, so callers keep their own layout styles. */
27+
class?: string;
28+
/** Set for links whose visible text is already a URL/host, where "opens in
29+
* a new tab" is the only thing the announcement needs to add. */
30+
label?: string;
31+
}
32+
33+
const { href, class: className, label } = Astro.props;
34+
---
35+
36+
<a
37+
href={href}
38+
class:list={["external-link", className]}
39+
target="_blank"
40+
rel="noopener noreferrer external"
41+
>
42+
<slot />
43+
{label && <span class="visually-hidden">{label}</span>}
44+
{/* Announced as "(opens in a new tab)" rather than left to the icon alone —
45+
a decorative arrow tells a sighted user what is about to happen and a
46+
screen-reader user nothing at all. */}
47+
<span class="visually-hidden">(opens in a new tab)</span>
48+
<svg
49+
class="external-link__icon"
50+
viewBox="0 0 24 24"
51+
fill="none"
52+
stroke="currentColor"
53+
stroke-width="2"
54+
stroke-linecap="round"
55+
stroke-linejoin="round"
56+
aria-hidden="true"
57+
>
58+
<path d="M7 17 17 7M9 7h8v8" />
59+
</svg>
60+
</a>
61+
62+
<style>
63+
.external-link {
64+
/* inline-flex, not inline: keeps the arrow on the same line as the last
65+
word and vertically centred against it, without the baseline drift a
66+
plain inline SVG gets. */
67+
display: inline-flex;
68+
align-items: center;
69+
gap: 0.25em;
70+
}
71+
/* Sized in em so it tracks whatever type scale the caller is using — the
72+
nav, the footer columns, and the footer's smaller build line all render
73+
this at their own size without passing anything in. */
74+
.external-link__icon {
75+
inline-size: 0.75em;
76+
block-size: 0.75em;
77+
flex: none;
78+
/* The arrow is supporting information, not part of the label. */
79+
opacity: 0.7;
80+
}
81+
.external-link:hover .external-link__icon,
82+
.external-link:focus-visible .external-link__icon {
83+
opacity: 1;
84+
}
85+
</style>

src/components/Footer.astro

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { apiBase } from "../lib/api-base";
33
import { APP_VERSION, BUILD_REF } from "../lib/build-info";
44
import { resolveDataBase } from "../lib/data-base";
55
import { resolveDocsBase } from "../lib/docs-base";
6+
import ExternalLink from "./ExternalLink.astro";
67
78
const year = new Date().getFullYear();
89
@@ -48,24 +49,28 @@ const versionUrl = isRelease ? `${REPO_URL}/releases/tag/v${APP_VERSION}` : `${R
4849
<h3>Explore</h3>
4950
<ul role="list">
5051
<li><a href="/discover">Discover</a></li>
51-
<li><a href="https://dashboard.nemar.org/citations">Citations</a></li>
52-
<li><a href="https://docs.nemar.org">Documentation</a></li>
52+
<li><ExternalLink href="https://dashboard.nemar.org/citations/">Citations</ExternalLink></li>
53+
<li><ExternalLink href="https://docs.nemar.org">Documentation</ExternalLink></li>
5354
</ul>
5455
</div>
5556
<div>
56-
<h3>Community</h3>
57+
{/* Was "Community", with a "Forum" link to a placeholder page
58+
promising a forum that does not exist (website#222). Both are gone.
59+
The heading is now "Project", which describes what is actually
60+
under it — About and Support — rather than a community surface
61+
NEMAR does not run. */}
62+
<h3>Project</h3>
5763
<ul role="list">
58-
<li><a href="/community">Forum</a></li>
5964
<li><a href="/about">About</a></li>
6065
<li><a href="/support">Support</a></li>
6166
</ul>
6267
</div>
6368
<div>
6469
<h3>Data</h3>
6570
<ul role="list">
66-
<li><a href={`https://${dataHost}/`}>{dataHost}</a></li>
67-
<li><a href={`https://${apiHost}/`}>{apiHost}</a></li>
68-
<li><a href={`https://${docsHost}/`}>{docsHost}</a></li>
71+
<li><ExternalLink href={`https://${dataHost}/`}>{dataHost}</ExternalLink></li>
72+
<li><ExternalLink href={`https://${apiHost}/`}>{apiHost}</ExternalLink></li>
73+
<li><ExternalLink href={`https://${docsHost}/`}>{docsHost}</ExternalLink></li>
6974
</ul>
7075
</div>
7176
<div>
@@ -80,8 +85,8 @@ const versionUrl = isRelease ? `${REPO_URL}/releases/tag/v${APP_VERSION}` : `${R
8085
the code, nemarDatasets holds one repository per dataset. Both
8186
are public and worth surfacing — people looking for the data
8287
repos never find them under nemarOrg. */}
83-
<li><a href="https://github.com/nemarOrg" rel="external">nemarOrg</a></li>
84-
<li><a href="https://github.com/nemarDatasets" rel="external">nemarDatasets</a></li>
88+
<li><ExternalLink href="https://github.com/nemarOrg">nemarOrg</ExternalLink></li>
89+
<li><ExternalLink href="https://github.com/nemarDatasets">nemarDatasets</ExternalLink></li>
8590
</ul>
8691
</div>
8792
</nav>
@@ -166,8 +171,11 @@ const versionUrl = isRelease ? `${REPO_URL}/releases/tag/v${APP_VERSION}` : `${R
166171
</p>
167172
<span class="site-footer__sep" aria-hidden="true">&middot;</span>
168173
<p>
169-
Built with <a href="https://astro.build">Astro</a> on
170-
<a href="https://pages.cloudflare.com">Cloudflare Pages</a>
174+
{/* Marked like every other outbound link. Two arrows in one short line
175+
is busier than leaving them bare, but a marker applied only to the
176+
links someone judged important stops being a reliable signal. */}
177+
Built with <ExternalLink href="https://astro.build">Astro</ExternalLink> on
178+
<ExternalLink href="https://pages.cloudflare.com">Cloudflare Pages</ExternalLink>
171179
</p>
172180
</div>
173181
</footer>
@@ -222,11 +230,15 @@ const versionUrl = isRelease ? `${REPO_URL}/releases/tag/v${APP_VERSION}` : `${R
222230
flex-direction: column;
223231
gap: var(--space-2);
224232
}
225-
.site-footer__nav a {
233+
/* `:global(a)` for the same reason as Nav.astro: the outbound entries render
234+
their anchor inside ExternalLink.astro, which carries that component's
235+
scope id, so a scoped descendant selector misses them and they fall back
236+
to the default link colour and size. Scoping stays on the container. */
237+
.site-footer__nav :global(a) {
226238
color: var(--color-fg-muted);
227239
font-size: var(--fs-sm);
228240
}
229-
.site-footer__nav a:hover {
241+
.site-footer__nav :global(a:hover) {
230242
color: var(--color-fg);
231243
}
232244
/* Credits band: SCCN + SDSC as the makers, then sponsors and partners. Logos

src/components/Nav.astro

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { getSession } from "../lib/auth";
33
import { appUrl, hostMode, marketingUrl } from "../lib/host";
44
import Brand from "./Brand.astro";
5+
import ExternalLink from "./ExternalLink.astro";
56
import ThemeToggle from "./ThemeToggle.astro";
67
import UserMenu from "./UserMenu.astro";
78
@@ -10,15 +11,26 @@ const pathname = url.pathname;
1011
const mode = hostMode(url.hostname);
1112
const session = getSession(Astro.locals);
1213
14+
// "Community" is gone (website#222). It pointed at a placeholder page
15+
// promising a forum that does not exist and was not being built; a nav entry
16+
// for a surface with nothing behind it is worse than no entry. Per-dataset
17+
// discussion is the real need it was standing in for, and that is tracked
18+
// separately against the dataset repositories' GitHub issues.
19+
//
20+
// Citation Dashboard and Documentation are separate deployments
21+
// (`nemarOrg/nemar-citations`, `nemarOrg/docs`), not routes on this site —
22+
// hence the absolute URLs, and hence `isExternal` below.
1323
const marketingLinks = [
1424
{ href: "/about", label: "About" },
1525
{ href: "/discover", label: "Discover" },
16-
{ href: "https://dashboard.nemar.org/citations", label: "Citation Dashboard" },
17-
{ href: "/community", label: "Community" },
26+
{ href: "https://dashboard.nemar.org/citations/", label: "Citation Dashboard" },
1827
{ href: "https://docs.nemar.org", label: "Documentation" },
1928
{ href: "/support", label: "Support" },
2029
];
2130
31+
/** A nav target that leaves this site, and so opens in a new tab. */
32+
const isExternal = (href: string) => href.startsWith("http");
33+
2234
// On the app host the nav depends on whether we can keep the user here.
2335
//
2436
// Signed in: relative links, so the marketing routes render on the app host
@@ -55,16 +67,16 @@ const signInHref = appUrl("/login", url.hostname);
5567
{
5668
links.map((link) => (
5769
<li>
58-
<a
59-
href={link.href}
60-
aria-current={
61-
link.href.startsWith("/") && pathname.startsWith(link.href)
62-
? "page"
63-
: undefined
64-
}
65-
>
66-
{link.label}
67-
</a>
70+
{isExternal(link.href) ? (
71+
<ExternalLink href={link.href}>{link.label}</ExternalLink>
72+
) : (
73+
<a
74+
href={link.href}
75+
aria-current={pathname.startsWith(link.href) ? "page" : undefined}
76+
>
77+
{link.label}
78+
</a>
79+
)}
6880
</li>
6981
))
7082
}
@@ -125,7 +137,13 @@ const signInHref = appUrl("/login", url.hostname);
125137
justify-content: center;
126138
align-items: center;
127139
}
128-
.primary-nav__list a {
140+
/* `:global(a)`, not a bare `a`: the external entries render their anchor
141+
inside ExternalLink.astro, which carries that component's scope id rather
142+
than this one's, so a scoped descendant selector would not reach them —
143+
Citation Dashboard and Documentation fell back to the default link style
144+
and sat in the nav at the wrong colour and size. The container class keeps
145+
its own scope, so this stays confined to this nav. */
146+
.primary-nav__list :global(a) {
129147
display: inline-flex;
130148
align-items: center;
131149
min-block-size: 44px;
@@ -137,10 +155,10 @@ const signInHref = appUrl("/login", url.hostname);
137155
text-decoration: none;
138156
transition: color 0.15s ease;
139157
}
140-
.primary-nav__list a:hover {
158+
.primary-nav__list :global(a:hover) {
141159
color: var(--color-fg);
142160
}
143-
.primary-nav__list a[aria-current="page"] {
161+
.primary-nav__list :global(a[aria-current="page"]) {
144162
color: var(--color-fg);
145163
box-shadow: inset 0 -2px 0 var(--brand-teal);
146164
}

src/lib/host.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ describe("isAppRoute", () => {
7373
"/discover",
7474
"/about",
7575
"/support",
76-
"/community",
7776
"/citation-dashboard",
7877
"/docs",
7978
"/docs/getting-started",
@@ -193,7 +192,7 @@ describe("getRetiredRedirect", () => {
193192

194193
it("redirects the retired citation dashboard to dashboard.nemar.org", () => {
195194
expect(getRetiredRedirect(u("/citation-dashboard"))).toBe(
196-
"https://dashboard.nemar.org/citations",
195+
"https://dashboard.nemar.org/citations/",
197196
);
198197
});
199198

@@ -417,7 +416,7 @@ describe("getCrossHostRedirect with a session", () => {
417416
const url = (host: string, pathAndSearch: string) => new URL(`https://${host}${pathAndSearch}`);
418417

419418
it("keeps marketing routes on the app host for a signed-in user", () => {
420-
for (const path of ["/discover", "/about", "/support", "/community", "/"]) {
419+
for (const path of ["/discover", "/about", "/support", "/"]) {
421420
expect(getCrossHostRedirect(url(APP_HOST, path), { hasSession: true })).toBeNull();
422421
}
423422
});

src/lib/host.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,10 @@ export function getRetiredRedirect(url: URL): string | null {
357357
if (mapped) return mapped;
358358
if (path === "/docs" || path.startsWith("/docs/")) return "https://docs.nemar.org/";
359359
if (path === "/citation-dashboard" || path.startsWith("/citation-dashboard/")) {
360-
return "https://dashboard.nemar.org/citations";
360+
// Trailing slash is the canonical form — `/citations` 308s to `/citations/`,
361+
// so omitting it costs every visitor an extra round trip on top of the
362+
// redirect that got them here.
363+
return "https://dashboard.nemar.org/citations/";
361364
}
362365
return null;
363366
}

src/pages/community.astro

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)