diff --git a/scripts/check-library-content.test.ts b/scripts/check-library-content.test.ts
index 65db6936e0d..ce8d01c9649 100644
--- a/scripts/check-library-content.test.ts
+++ b/scripts/check-library-content.test.ts
@@ -77,6 +77,7 @@ describe('check-library-content', () => {
[
'## Overview',
'See [the guide](/library/kept-guide) and https://www.sim.ai/library/kept-guide#intro.',
+ 'Read https://www.sim.ai/library/kept-guide. Then https://www.sim.ai/library/moved-post, too.',
' and [tags](/library/tags) are not posts.',
'An apex https://sim.ai/library/missing link belongs to check:site-urls.',
'| a | b |',
@@ -177,6 +178,8 @@ describe('check-library-content', () => {
'[b](https://www.sim.ai/library/old-guide)',
'c',
'[d](/blog/kept-guide)',
+ 'Also see https://www.sim.ai/library/gone.',
+ '[e](/library/kept-guide.) and f',
].join('\n')
)
expect(await findingsFor('library', 'post')).toEqual([
@@ -200,6 +203,23 @@ describe('check-library-content', () => {
rule: 'internal-link',
message: '/blog/kept-guide does not exist (no apps/sim/content/blog/kept-guide/index.mdx).',
},
+ {
+ line: 14,
+ rule: 'internal-link',
+ message: '/library/gone does not exist (no apps/sim/content/library/gone/index.mdx).',
+ },
+ {
+ line: 15,
+ rule: 'internal-link',
+ message:
+ '/library/kept-guide. does not exist (no apps/sim/content/library/kept-guide./index.mdx).',
+ },
+ {
+ line: 15,
+ rule: 'internal-link',
+ message:
+ '/library/kept-guide. does not exist (no apps/sim/content/library/kept-guide./index.mdx).',
+ },
])
})
diff --git a/scripts/check-library-content.ts b/scripts/check-library-content.ts
index 796ebb36cdd..e59d8432d76 100644
--- a/scripts/check-library-content.ts
+++ b/scripts/check-library-content.ts
@@ -76,6 +76,10 @@ export interface PostRef {
*/
const INTERNAL_LINK =
/(?:https?:\/\/www\.sim\.ai|(?<=\]\(\s*|href=\{?["'`]))\/(library|blog|customers)\/([^\s)"'`#?/<>\]]+)(\/[^\s)"'`#?<>\]]*)?/g
+/** Sentence punctuation that ends a bare URL in prose (`…see https://www.sim.ai/library/x.`). */
+const TRAILING_PUNCTUATION = /[.,;:!]+$/
+/** Text just before a Markdown link target or `href` value, whose URL ends at its delimiter. */
+const LINK_TARGET_OPENER = /(?:\]\(\s*|href=\{?["'`])$/
const MARKDOWN_LINK = /\[[^\]\n]*\]\([^)\n]*\)/
const FAQ_HEADING = /^#{1,6}\s+FAQs?\s*:?\s*$/i
const CODE_FENCE = /^\s*(```|~~~)/
@@ -330,7 +334,8 @@ export async function checkPost(
}
for (const match of text.matchAll(INTERNAL_LINK)) {
const linkSection = match[1] as Section
- const target = match[2]
+ const isBareUrl = !LINK_TARGET_OPENER.test(text.slice(0, match.index))
+ const target = isBareUrl ? match[2].replace(TRAILING_PUNCTUATION, '') : match[2]
const rest = match[3] ?? ''
// A deeper path is a public asset (`/library//cover.jpg`) or a static sub-route.
if (rest !== '' && rest !== '/') continue