Return

All tests / src/helpers postPath.ts

94.11% Statements 16/17
100% Branches 13/13
100% Functions 3/3
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62                        19x 19x 19x                               19x 17x 17x                   19x       5x                 216x 216x 216x 216x 216x    
/**
 * tag-facets-as-query-param. A post used to be linked at /blog/<tag>/<slug> for every tag it
 * carries, so each post had one URL per tag and Google sometimes picked a tag URL over the declared
 * canonical. Every internal link now points at the post's own category path, and the segment being
 * browsed rides along as `?tag=` when it is not that category. `src/middleware.ts` serves that URL
 * from the tag render (`tagRenderPath`), so the listing and the highlighted tag look exactly as before.
 */
 
/**
 * The shape of a `?tag=` value that the middleware rewrites to the tag render. Shared so the client
 * honours exactly the values the server rewrote; anything else renders the plain post on both sides.
 */
export const TAG_VALUE_PATTERN = '[a-z0-9-]+';
const TAG_VALUE = new RegExp(`^${TAG_VALUE_PATTERN}$`);
const POST_PATHNAME = /^\/blog\/[^/]+\/([^/]+)$/;
 
/**
 * @description The render that serves a post browsed from a tag: /blog/<category>/<slug>?tag=<tag>
 * is served from /blog/<tag>/<slug>, the page whose listing holds the tag's posts.
 *
 * This runs in the middleware, not as a `has` query rewrite in next.config.ts. That rewrite served
 * full page loads, but on Vercel it was not applied to the Pages Router data request
 * (`/_next/data/<build>/<locale>/blog/<category>/<slug>.json?tag=`). Client-side navigation therefore
 * got the category's listing under a highlighted tag. Measured in production on 2026-09-19. The
 * middleware runs for those data requests too, because Next prefixes every matcher with an optional
 * `/_next/data/<build>`.
 * @param pathname - The request path, locale already stripped by Next.
 * @param tag - The raw `?tag=` value, if any.
 * @returns `/blog/<tag>/<slug>`, or undefined when the path is not a post or the tag is not valid.
 */
export const tagRenderPath = (pathname: string, tag: string | null): string | undefined => {
    const slug = POST_PATHNAME.exec(pathname)?.[1];
    return slug && tag && TAG_VALUE.test(tag) ? `/blog/${tag}/${slug}` : undefined;
};
 
/**
 * @description The taxonomy segment the reader is browsing on a post page. After hydration Next
 * re-reads `router.query` from the visible URL, so on `/blog/<category>/<slug>?tag=node` the route's
 * `category` is the post's own category again; the browsed tag is in `tag`.
 * @param query - `router.query` of the post page.
 * @returns The browsed tag when `?tag=` holds a valid one, otherwise the path segment.
 */
export const browsedSegment = (query: {
    category?: string | string[];
    tag?: string | string[];
}): string | string[] | undefined =>
    typeof query.tag === 'string' && TAG_VALUE.test(query.tag) ? query.tag : query.category;
 
/**
 * @description The path of a post as linked from a taxonomy listing.
 * @param post - The post's `category` and `slug` from its frontmatter.
 * @param browsedSegment - The category or tag the reader is browsing, if any.
 * @returns `/blog/<category>/<slug>`, plus `?tag=<segment>` when browsing something other than the
 * post's own category.
 */
export const postPath = (post: { category: string; slug: string }, browsedSegment?: string): string => {
    const category = post.category.toLowerCase();
    const path = `/blog/${category}/${post.slug}`;
    const segment = browsedSegment?.toLowerCase();
    return segment && segment !== category ? `${path}?tag=${encodeURIComponent(segment)}` : path;
};