Skip to content

Commit 20be5c7

Browse files
style: better landing
1 parent d717409 commit 20be5c7

33 files changed

Lines changed: 1515 additions & 176 deletions

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ nothing but the `(tests)` group). It audits the production build: `build:demo`
146146
`dist/`) then `next start --port 3012`. Port 3012, not the demo's own 3002, so a
147147
server left over from `pnpm run dev` cannot be silently accepted in place of it.
148148

149-
**Only the four public pages**`/`, `/react-router`, `/remix`, `/astro`. The app also
149+
**Only the four demo pages**`/`, `/react-router`, `/remix`, `/astro` (`/nextjs` and
150+
`/vs/nuqs` are prerendered prose pages and are audited by the a11y suite instead). The app also
150151
serves `/useUrlState`, `/test-ssr` and the rest of the `(tests)` group, but those
151152
are e2e fixtures that happen to be deployed: they exist to be asserted against,
152153
not read, and several render in ways no real page would.
@@ -177,6 +178,7 @@ answers agents in four overlapping ways — they disagree about which to try:
177178
| `/llms.txt` | `public/llms.txt` — the single source |
178179
| `/` under `Accept: text/markdown`, or a known agent user-agent — 307 to `/index.md` | `src/proxy.ts` |
179180
| `/index.md`, `/react-router.md`, `/remix.md`, `/astro.md` | `src/app/<route>.md/route.ts`, all four through `src/app/llmsTxtResponse.ts` |
181+
| `/nextjs.md`, `/vs/nuqs.md` | built from the page's own copy module and samples — those pages have content of their own |
180182
| `<link rel="alternate">` and a `Link:` response header | `src/app/seoStuff.ts` (`markdownAlternates`), `next.config.mjs` |
181183

182184
All three `.md` mirrors serve the same document, deliberately: this site is one

packages/example-nextjs16/next.config.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ const nextConfig = {
6565
// !! WARN !!
6666
ignoreBuildErrors: true,
6767
},
68+
async redirects() {
69+
// `www.` served the whole site as a second host; canonical tags pointed
70+
// at the apex but nothing sent a reader there. One 308 does.
71+
return [
72+
{
73+
source: '/:path*',
74+
has: [{ type: 'host', value: 'www.state-in-url.dev' }],
75+
destination: 'https://state-in-url.dev/:path*',
76+
permanent: true,
77+
},
78+
];
79+
},
6880
async headers() {
6981
// A `Link:` header survives a HEAD request and reaches clients that never
7082
// parse markup. Per documented page, not globbed, so it can't end up on
@@ -129,7 +141,7 @@ const nextConfig = {
129141
// and `<Link>` prefetch revalidates on a loop, measured at ~26 requests a
130142
// second from one idle tab. Its layout's `revalidate` caches it for a week.
131143
const demoPages = ['', '/react-router', '/remix', '/astro'];
132-
const pages = [...demoPages, '/vs/nuqs'];
144+
const pages = [...demoPages, '/nextjs', '/vs/nuqs'];
133145
const prefixes = ['', ...LOCALES.map((locale) => `/${locale.dir}`)];
134146

135147
return [

packages/example-nextjs16/public/llms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ Same module-scoped default-state-identity rule. Use only when URL sync is explic
301301

302302
## Documentation resources
303303

304+
- Next.js App Router guide (searchParams forwarding, layouts, history, FAQ): https://state-in-url.dev/nextjs — Markdown at https://state-in-url.dev/nextjs.md
305+
- Comparison with nuqs, TanStack Router, use-query-params: https://state-in-url.dev/vs/nuqs — Markdown at https://state-in-url.dev/vs/nuqs.md
304306
- Full README (humans): https://github.com/asmyshlyaev177/state-in-url/blob/master/README.md
305307
- URL size limits: https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md
306308
- Working examples per framework: `packages/example-nextjs{14,15,16}`, `packages/example-react-router{6,7}`, `packages/example-remix2`, `packages/example-astro`
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { copy } from '../../i18n/copy/en';
2+
import { breadcrumbListJsonLd, faqPageJsonLd, jsonLdScript } from '../../jsonLd';
3+
import { NextjsPage } from '../../pages/NextjsPage';
4+
import { pageMetadata } from '../../seoStuff';
5+
6+
export const metadata = pageMetadata({
7+
path: '/nextjs',
8+
title: copy.meta.nextjs.title,
9+
description: copy.meta.nextjs.description,
10+
});
11+
12+
export default async function Nextjs() {
13+
return (
14+
<>
15+
<script
16+
type="application/ld+json"
17+
dangerouslySetInnerHTML={{
18+
__html: jsonLdScript([
19+
faqPageJsonLd(copy.nextjs.faq.items),
20+
breadcrumbListJsonLd({
21+
homeName: copy.chrome.home,
22+
homeHref: '/',
23+
name: copy.nextjs.crumb,
24+
path: '/nextjs',
25+
}),
26+
]),
27+
}}
28+
/>
29+
<NextjsPage copy={copy} homeHref="/" vsHref="/vs/nuqs" />
30+
</>
31+
);
32+
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { CodeBlocks } from '../CodeBlocksNext';
22
import { copy } from '../i18n/copy/en';
3+
import { faqPageJsonLd, jsonLdScript } from '../jsonLd';
34
import { DemoPage } from '../pages/DemoPage';
45

56
export default async function Home({ searchParams }: { searchParams: Promise<object> }) {
67
return (
7-
<DemoPage
8-
searchParams={searchParams}
9-
copy={copy}
10-
codeBlocks={<CodeBlocks copy={copy.quickStart} />}
11-
aiSkills
12-
/>
8+
<>
9+
<script
10+
type="application/ld+json"
11+
dangerouslySetInnerHTML={{ __html: jsonLdScript([faqPageJsonLd(copy.faq.items)]) }}
12+
/>
13+
<DemoPage
14+
searchParams={searchParams}
15+
copy={copy}
16+
codeBlocks={<CodeBlocks copy={copy.quickStart} />}
17+
aiSkills
18+
faq
19+
/>
20+
</>
1321
);
1422
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { notFound } from 'next/navigation';
2+
3+
import { localeFromParam } from '../../i18n';
4+
import { copyFor } from '../../i18n/copy';
5+
import { breadcrumbListJsonLd, faqPageJsonLd, jsonLdScript } from '../../jsonLd';
6+
import { NextjsPage } from '../../pages/NextjsPage';
7+
import { localeMetadata } from '../../seoStuff';
8+
9+
export async function generateMetadata({
10+
params,
11+
}: {
12+
params: Promise<{ locale: string }>;
13+
}) {
14+
const { locale } = await params;
15+
return localeMetadata({ localeDir: locale, path: '/nextjs' });
16+
}
17+
18+
export default async function Nextjs({
19+
params,
20+
}: {
21+
params: Promise<{ locale: string }>;
22+
}) {
23+
const { locale: dir } = await params;
24+
const locale = localeFromParam(dir);
25+
if (!locale) notFound();
26+
27+
const copy = copyFor(locale.code);
28+
29+
return (
30+
<>
31+
<script
32+
type="application/ld+json"
33+
dangerouslySetInnerHTML={{
34+
__html: jsonLdScript([
35+
faqPageJsonLd(copy.nextjs.faq.items),
36+
breadcrumbListJsonLd({
37+
homeName: copy.chrome.home,
38+
homeHref: `/${dir}`,
39+
name: copy.nextjs.crumb,
40+
path: '/nextjs',
41+
}),
42+
]),
43+
}}
44+
/>
45+
<NextjsPage copy={copy} homeHref={`/${dir}`} vsHref={`/${dir}/vs/nuqs`} />
46+
</>
47+
);
48+
}

packages/example-nextjs16/src/app/[locale]/page.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
33
import { CodeBlocks } from '../CodeBlocksNext';
44
import { localeFromParam } from '../i18n';
55
import { copyFor } from '../i18n/copy';
6+
import { faqPageJsonLd, jsonLdScript } from '../jsonLd';
67
import { DemoPage } from '../pages/DemoPage';
78
import { localeMetadata } from '../seoStuff';
89

@@ -25,11 +26,20 @@ export default async function Home({
2526
const copy = copyFor(locale.code);
2627

2728
return (
28-
<DemoPage
29-
searchParams={searchParams}
30-
copy={copy} vsHref={`/${dir}/vs/nuqs`}
31-
codeBlocks={<CodeBlocks copy={copy.quickStart} />}
32-
aiSkills
33-
/>
29+
<>
30+
<script
31+
type="application/ld+json"
32+
dangerouslySetInnerHTML={{ __html: jsonLdScript([faqPageJsonLd(copy.faq.items)]) }}
33+
/>
34+
<DemoPage
35+
searchParams={searchParams}
36+
copy={copy}
37+
vsHref={`/${dir}/vs/nuqs`}
38+
nextjsHref={`/${dir}/nextjs`}
39+
codeBlocks={<CodeBlocks copy={copy.quickStart} />}
40+
aiSkills
41+
faq
42+
/>
43+
</>
3444
);
3545
}

packages/example-nextjs16/src/app/components/Breadcrumbs.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import type { SiteCopy } from '../i18n/copy/types';
1111
* subpages get their trail right under the top bar, above the hero.
1212
*
1313
* The matching BreadcrumbList JSON-LD is emitted by each subpage's route
14-
* (see `breadcrumbJsonLd` in VsNuqsPage) — keep the two in step.
14+
* (`breadcrumbListJsonLd` in jsonLd.ts) — keep the two in step.
1515
*/
1616
const CRUMB_OF: Record<string, (copy: SiteCopy) => string> = {
1717
'/vs/nuqs': (copy) => copy.comparison.title,
18+
'/nextjs': (copy) => copy.nextjs.crumb,
1819
};
1920

2021
export const Breadcrumbs = ({

packages/example-nextjs16/src/app/components/Description.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { Link } from "./Link"
12
import { Share } from "./Share"
23
import { Word } from "./Word"
34
import type { SiteCopy } from "../i18n/copy/types"
45

5-
export const Description = ({ copy }: { copy: SiteCopy }) => (
6+
export const Description = ({ copy, nextjsHref }: { copy: SiteCopy; nextjsHref: string }) => (
67
<section className='flex flex-col items-start w-full max-w-[640px] mt-4'>
78
<h2 className='text-3xl mt-12 mb-4 font-bold font-display text-ink'>
89
{copy.description.title}
@@ -16,6 +17,9 @@ export const Description = ({ copy }: { copy: SiteCopy }) => (
1617

1718
<h3 className='mt-8 mb-2 font-bold text-lg font-display text-ink'>{copy.description.suspenseTitle}</h3>
1819
<p className='text-base leading-relaxed text-ink2'>{copy.description.suspenseLead} <Word>useSearchParams</Word>{copy.description.suspenseAfterHook} <Word>Suspense</Word> {copy.description.suspenseAfterBoundary} <Word>cacheComponents</Word> {copy.description.suspenseAfterFlag} <Word>history.pushState</Word> {copy.description.suspenseTail}</p>
20+
<p className='mt-3 text-base leading-relaxed'>
21+
<Link href={nextjsHref} className="font-semibold text-[var(--accent-on-soft)] hover:underline">{copy.nextjs.crumb}</Link>
22+
</p>
1923

2024
<h3 className='mt-8 mb-2 font-bold text-lg font-display text-ink'>{copy.description.otherTitle}</h3>
2125
<p className='text-base leading-relaxed text-ink2'>{copy.description.helpersLead} <Word>encodeState</Word> / <Word>decodeState</Word> {copy.description.helpersTail}</p>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import type { FaqCopy } from '../i18n/copy/types';
4+
5+
/**
6+
* A FAQ block, rendered as a definition list so the question/answer pairing
7+
* survives without styling. The same items go out as FAQPage JSON-LD through
8+
* `faqPageJsonLd` in `jsonLd.ts`; routes emit that, this renders the DOM, and
9+
* both read one copy object so they cannot disagree.
10+
*/
11+
export const Faq = ({ copy, id }: { copy: FaqCopy; id: string }) => (
12+
<section
13+
className="flex w-full max-w-[640px] flex-col items-start"
14+
aria-labelledby={id}
15+
>
16+
<h2
17+
id={id}
18+
className="font-display text-ink mb-4 mt-12 text-3xl font-bold"
19+
>
20+
{copy.title}
21+
</h2>
22+
<dl className="w-full">
23+
{copy.items.map((item) => (
24+
<div
25+
key={item.q}
26+
className="border-b border-[var(--line)] py-4 last:border-b-0"
27+
>
28+
<dt className="text-ink font-semibold">{item.q}</dt>
29+
<dd className="text-ink2 mt-2 text-base leading-relaxed">{item.a}</dd>
30+
</div>
31+
))}
32+
</dl>
33+
</section>
34+
);

0 commit comments

Comments
 (0)