lightnet 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # lightnet
2
2
 
3
+ ## 4.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Support protocol-less external URLs such as `example.com`
8
+
9
+ - [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Flip external link icon and share icon on rtl locales.
10
+
11
+ - [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Details page default to "Open" if they do not know a file extension
12
+
13
+ - [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Update dependencies
14
+
3
15
  ## 4.1.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "LightNet makes it easy to run your own digital media library.",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "4.1.0",
6
+ "version": "4.1.1",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@astrojs/react": "^5.0.4",
55
- "@iconify-json/lucide": "^1.2.105",
55
+ "@iconify-json/lucide": "^1.2.106",
56
56
  "@iconify-json/mdi": "^1.2.3",
57
57
  "@iconify/tailwind": "^1.2.0",
58
58
  "@tailwindcss/typography": "^0.5.19",
@@ -61,7 +61,7 @@
61
61
  "embla-carousel": "^8.6.0",
62
62
  "embla-carousel-wheel-gestures": "^8.1.0",
63
63
  "fuse.js": "^7.3.0",
64
- "i18next": "^26.0.8",
64
+ "i18next": "^26.0.10",
65
65
  "lucide-react": "^1.14.0",
66
66
  "marked": "^18.0.3",
67
67
  "postcss": "^8.5.14",
@@ -71,7 +71,7 @@
71
71
  "devDependencies": {
72
72
  "@playwright/test": "^1.59.1",
73
73
  "@types/react": "^19.2.14",
74
- "astro": "^6.2.2",
74
+ "astro": "^6.3.1",
75
75
  "typescript": "^6.0.3",
76
76
  "vitest": "^4.1.5",
77
77
  "@internal/e2e-test-utils": "^0.0.1"
@@ -4,6 +4,8 @@ import { getImage } from "astro:assets"
4
4
  import { Image } from "astro:assets"
5
5
  import { ExternalLinkIcon } from "lucide-react"
6
6
 
7
+ import { getLinkAttributes } from "../utils/link-attributes"
8
+
7
9
  interface Props {
8
10
  /**
9
11
  * Url of the video. This can be YouTube, vimeo, or a mp4 file.
@@ -116,9 +118,7 @@ async function parseUrl(
116
118
  ) : (
117
119
  <a
118
120
  class="group relative flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
119
- href={href ?? url}
120
- target="_blank"
121
- rel="noreferrer noopener"
121
+ {...getLinkAttributes(href ?? url)}
122
122
  >
123
123
  {imageMetadata ? (
124
124
  <Image
@@ -145,7 +145,7 @@ async function parseUrl(
145
145
  </span>
146
146
  </div>
147
147
  <div class="absolute bottom-0 end-0 start-0 z-20 flex items-center gap-2 rounded bg-black/70 px-4 py-4 text-sm font-medium text-white backdrop-blur md:text-sm">
148
- <ExternalLinkIcon className="shrink-0" />
148
+ <ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
149
149
  {t("ln.external-link")}
150
150
  </div>
151
151
  </a>
@@ -2,104 +2,106 @@
2
2
  # This is only "visible" to a screen-reader.
3
3
  #
4
4
  # English: Open main menu
5
- # Used on: https://sk8-ministries.pages.dev/en (not visible)
5
+ # Used on: https://kuuluu.org/en (not visible)
6
6
  ln.header.open-main-menu: Open main menu
7
7
 
8
8
  # Accessibility label for the language menu.
9
9
  # This is only "visible" to a screen-reader.
10
10
  #
11
11
  # English: Select language
12
- # Used on: https://sk8-ministries.pages.dev/en (not visible)
12
+ # Used on: https://kuuluu.org/en (not visible)
13
13
  ln.header.select-language: Select language
14
14
 
15
15
  # Display name for the home page.
16
16
  #
17
17
  # English: Home
18
- # Used on: https://sk8-ministries.pages.dev/en (first item in the main menu)
18
+ # Used on: https://kuuluu.org/en (first item in the main menu)
19
19
  ln.home.title: Home
20
20
 
21
21
  # Label for a single media item category.
22
22
  #
23
23
  # English: Category
24
- # Used on: https://sk8-ministries.pages.dev/en/media
24
+ # Used on: https://kuuluu.org/en/media
25
25
  ln.category: Category
26
26
 
27
27
  # Label for multiple media item categories.
28
28
  #
29
29
  # English: Categories
30
- # Used on: https://sk8-ministries.pages.dev/en/media/how-to-360-flip--en/
30
+ # Used on: https://kuuluu.org/en/media/how-to-360-flip--en/
31
31
  ln.categories: Categories
32
32
 
33
33
  # Label for a single language.
34
34
  #
35
35
  # English: Language
36
- # Used on: https://sk8-ministries.pages.dev/en/media
36
+ # Used on: https://kuuluu.org/en/media
37
37
  ln.language: Language
38
38
 
39
39
  # Label for multiple languages.
40
40
  #
41
41
  # English: Languages
42
- # Used on: https://sk8-ministries.pages.dev/en/media/how-to-kickflip--en/
42
+ # Used on: https://kuuluu.org/en/media/how-to-kickflip--en/
43
43
  ln.languages: Languages
44
44
 
45
45
  # Label for a single media type.
46
46
  #
47
47
  # English: Type
48
- # Used on: https://sk8-ministries.pages.dev/en/media
48
+ # Used on: https://kuuluu.org/en/media
49
49
  ln.type: Type
50
50
 
51
51
  # Accessibility label for buttons to show the previous items.
52
52
  # This is used on the carousel arrow button.
53
53
  #
54
54
  # English: Previous
55
+ # Used on: https://kuuluu.org/en (not visible)
55
56
  ln.previous: Previous
56
57
 
57
58
  # Accessibility label for buttons to show the next items.
58
59
  # This is used on the carousel arrow button.
59
60
  #
60
61
  # English: Next
62
+ # Used on: https://kuuluu.org/en (not visible)
61
63
  ln.next: Next
62
64
 
63
65
  # Hint for an external link.
64
66
  #
65
67
  # English: External link - opens in a new tab
66
- # Used on: https://sk8-ministries.pages.dev/en/media/ollie-with-integrity--en/
68
+ # Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
67
69
  ln.external-link: External link - opens in a new tab
68
70
 
69
71
  # Title for the search page.
70
72
  #
71
73
  # English: Search
72
- # Used on: https://sk8-ministries.pages.dev/en/media/ (not visible)
74
+ # Used on: https://kuuluu.org/en/media/ (not visible)
73
75
  ln.search.title: Search
74
76
 
75
77
  # Placeholder text for the search input to search for media items.
76
78
  # English: Search media
77
79
  #
78
- # Used on: https://sk8-ministries.pages.dev/en/media/
80
+ # Used on: https://kuuluu.org/en/media/
79
81
  ln.search.placeholder: Search media
80
82
 
81
83
  # Filter option to display content in all languages.
82
84
  #
83
85
  # English: All languages
84
- # Used on: https://sk8-ministries.pages.dev/en/media/
86
+ # Used on: https://kuuluu.org/en/media/
85
87
  ln.search.all-languages: All languages
86
88
 
87
89
  # Filter option to display all media types.
88
90
  #
89
91
  # English: All types
90
- # Used on: https://sk8-ministries.pages.dev/en/media/
92
+ # Used on: https://kuuluu.org/en/media/
91
93
  ln.search.all-types: All types
92
94
 
93
95
  # Filter option to display all media item categories.
94
96
  #
95
97
  # English: All categories
96
- # Used on: https://sk8-ministries.pages.dev/en/media/
98
+ # Used on: https://kuuluu.org/en/media/
97
99
  ln.search.all-categories: All categories
98
100
 
99
101
  # Message displayed when no search results are found.
100
102
  #
101
103
  # English: No results
102
- # Used on: https://sk8-ministries.pages.dev/en/media/?language=de&type=book
104
+ # Used on: https://kuuluu.org/en/media/?language=de&type=book
103
105
  ln.search.no-results: No results
104
106
 
105
107
  # Button label to open a media item's main content.
@@ -110,7 +112,7 @@ ln.details.open: Open
110
112
  # Button label to share a media item.
111
113
  #
112
114
  # English: Share
113
- # Used on: https://sk8-ministries.pages.dev/en/media/ollie-with-integrity--en/
115
+ # Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
114
116
  ln.details.share: Share
115
117
 
116
118
  # Button label to start editing an item
@@ -121,32 +123,33 @@ ln.details.edit: Edit
121
123
  # Label for indicating that the media item belongs to a collection.
122
124
  #
123
125
  # English: Part of collection
124
- # Used on: https://sk8-ministries.pages.dev/en/media/ollie-with-integrity--en/
126
+ # Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
125
127
  ln.details.part-of-collection: Part of collection
126
128
 
127
129
  # Button label for downloading a media item.
128
130
  #
129
131
  # English: Download
130
- # Used on: https://sk8-ministries.pages.dev/en/media/ride-the-ramp-of-faith--en/
132
+ # Used on: https://kuuluu.org/en/media/ride-the-ramp-of-faith--en/
131
133
  ln.details.download: Download
132
134
 
133
135
  # Notification message indicating that the link has been copied to the clipboard.
134
136
  #
135
137
  # English: Link copied to clipboard
136
- # Used on: https://sk8-ministries.pages.dev/en/media/ride-the-ramp-of-faith--en/ (Firefox)
138
+ # Used on: https://kuuluu.org/en/media/ride-the-ramp-of-faith--en/ (Firefox)
137
139
  ln.share.url-copied-to-clipboard: Link copied to clipboard
138
140
 
139
141
  # Title for the 404-error page. This page is displayed when a path does not exist.
140
142
  #
141
143
  # English: Page not found
142
- # Used on: https://sk8-ministries.pages.dev/unexisting-path
144
+ # Used on: https://kuuluu.org/unexisting-path
143
145
  ln.404.page-not-found: Page not found
144
146
 
145
147
  # Button label on the 404-error page for returning to the home page.
146
148
  #
147
149
  # English: Go to Home page
148
- # Used on: https://sk8-ministries.pages.dev/unexisting-path
150
+ # Used on: https://kuuluu.org/unexisting-path
149
151
  ln.404.go-to-the-home-page: Go to Home page
150
152
 
151
153
  # Footer text to give credits to LightNet
154
+ # Used on: https://kuuluu.org/en
152
155
  ln.footer.powered-by-lightnet: Powered by LightNet
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  import config from "virtual:lightnet/config"
3
3
 
4
+ import { getLinkAttributes } from "../../utils/link-attributes"
4
5
  import { localizePath } from "../../utils/paths"
5
- import { isExternalUrl } from "../../utils/urls"
6
6
  import LightNetLogo from "./LightNetLogo.svg"
7
7
 
8
8
  const { t, tConfigField, currentLocale } = Astro.locals.i18n
@@ -15,22 +15,16 @@ const footerText = config.footerText
15
15
  // Prepare footer links using the same locale rules as the main menu.
16
16
  const footerLinks = (config.footerLinks ?? []).map(
17
17
  ({ href, label, requiresLocale }) => {
18
- const isExternal = isExternalUrl(href)
19
-
20
18
  return {
21
- href:
22
- isExternal || !requiresLocale
23
- ? href
24
- : localizePath(currentLocale, href),
19
+ href: requiresLocale ? localizePath(currentLocale, href) : href,
25
20
  label: tConfigField(label, config),
26
- isExternal,
27
21
  }
28
22
  },
29
23
  )
30
24
 
31
25
  // Hide the footer entirely when there is nothing to show.
32
26
  const shouldRenderFooter =
33
- config.credits || Boolean(footerText) || footerLinks.length > 0
27
+ config.credits || !!footerText || footerLinks.length > 0
34
28
 
35
29
  if (!shouldRenderFooter) {
36
30
  return
@@ -51,9 +45,7 @@ if (!shouldRenderFooter) {
51
45
  <Fragment>
52
46
  {(footerText || index > 0) && <span class="text-gray-400">·</span>}
53
47
  <a
54
- href={link.href}
55
- target={link.isExternal ? "_blank" : undefined}
56
- rel={link.isExternal ? "noopener noreferrer" : undefined}
48
+ {...getLinkAttributes(link.href)}
57
49
  class="underline-offset-4 hover:underline"
58
50
  >
59
51
  {link.label}
@@ -68,8 +60,7 @@ if (!shouldRenderFooter) {
68
60
  <div class="flex items-center text-sm">
69
61
  <a
70
62
  class="flex items-center gap-2 text-gray-800 underline-offset-4 hover:underline"
71
- href="https://lightnet.community"
72
- target="_blank"
63
+ {...getLinkAttributes("https://lightnet.community")}
73
64
  >
74
65
  <img
75
66
  src={LightNetLogo.src}
@@ -1,18 +1,18 @@
1
1
  ---
2
+ import { getLinkAttributes } from "../../utils/link-attributes"
3
+
2
4
  interface Props {
3
5
  href: string
4
6
  active: boolean
5
7
  hreflang?: string
6
- target?: "_blank" | "_self"
7
8
  }
8
- const { href, hreflang, active, target } = Astro.props
9
+ const { href, hreflang, active } = Astro.props
9
10
  ---
10
11
 
11
12
  <li>
12
13
  <a
13
- href={href}
14
+ {...getLinkAttributes(href)}
14
15
  hreflang={hreflang}
15
- target={target}
16
16
  class="flex items-center gap-2 px-6 py-3 decoration-gray-800"
17
17
  class:list={[active ? "font-bold" : "hover:underline"]}
18
18
  >
@@ -2,8 +2,8 @@
2
2
  import { ExternalLinkIcon, MenuIcon, SearchIcon } from "lucide-react"
3
3
  import config from "virtual:lightnet/config"
4
4
 
5
+ import { isExternalUrl } from "../../utils/is-external-url"
5
6
  import { localizePath, searchPagePath } from "../../utils/paths"
6
- import { isExternalUrl } from "../../utils/urls"
7
7
  import LanguagePicker from "./LanguagePicker.astro"
8
8
  import Menu from "./Menu.astro"
9
9
  import MenuItem from "./MenuItem.astro"
@@ -13,18 +13,15 @@ const { t, tConfigField, currentLocale } = Astro.locals.i18n
13
13
 
14
14
  const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
15
15
  const isExternal = isExternalUrl(href)
16
- const path =
17
- isExternal || !requiresLocale ? href : localizePath(currentLocale, href)
18
16
  const isActive =
19
- !isExternal &&
20
- (currentPath === localizePath(currentLocale, href) ||
21
- currentPath === localizePath(currentLocale, `${href}/`) ||
22
- currentPath === href)
17
+ currentPath === localizePath(currentLocale, href) ||
18
+ currentPath === localizePath(currentLocale, `${href}/`) ||
19
+ currentPath === href
23
20
  return {
24
- path,
25
- isExternal,
21
+ href: requiresLocale ? localizePath(currentLocale, href) : href,
26
22
  labelText: tConfigField(label, config),
27
23
  isActive,
24
+ isExternal,
28
25
  }
29
26
  })
30
27
  ---
@@ -49,15 +46,14 @@ const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
49
46
  !!items.length && (
50
47
  <Menu label="ln.header.open-main-menu">
51
48
  <MenuIcon slot="icon" />
52
- {items.map(({ labelText, path, isActive, isExternal }) => (
53
- <MenuItem
54
- href={path}
55
- active={isActive}
56
- target={isExternal ? "_blank" : "_self"}
57
- >
49
+ {items.map(({ labelText, href, isActive, isExternal }) => (
50
+ <MenuItem href={href} active={isActive}>
58
51
  {labelText}
59
52
  {isExternal && (
60
- <ExternalLinkIcon size="1.2rem" className="shrink-0" />
53
+ <ExternalLinkIcon
54
+ size="1.2rem"
55
+ className="shrink-0 rtl:scale-x-[-1]"
56
+ />
61
57
  )}
62
58
  </MenuItem>
63
59
  ))}
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  import { ExternalLinkIcon } from "lucide-react"
3
3
 
4
+ import { getLinkAttributes } from "../../../utils/link-attributes"
5
+
4
6
  type Props = {
5
7
  src: string
6
8
  className?: string
@@ -20,9 +22,7 @@ const isMp3 = audioExtension.endsWith(".mp3")
20
22
  <a
21
23
  class="group flex w-full items-center gap-4 rounded-2xl border border-gray-200 bg-white px-4 py-4 shadow-sm transition hover:border-gray-300 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-900/70"
22
24
  class:list={[className]}
23
- href={src}
24
- target="_blank"
25
- rel="noreferrer noopener"
25
+ {...getLinkAttributes(src)}
26
26
  aria-label={t("ln.external-link")}
27
27
  >
28
28
  <span class="flex h-12 w-12 items-center justify-center rounded-full border border-gray-700 text-gray-700 shadow-sm transition group-hover:scale-105">
@@ -37,7 +37,7 @@ const isMp3 = audioExtension.endsWith(".mp3")
37
37
  </span>
38
38
  <div class="flex h-full grow flex-col gap-3">
39
39
  <span class="flex items-center gap-2 font-semibold text-gray-900">
40
- <ExternalLinkIcon className="shrink-0" />
40
+ <ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
41
41
  {t("ln.external-link")}
42
42
  </span>
43
43
  <div class="h-2 w-full rounded-full bg-gray-200">
@@ -13,6 +13,7 @@ import {
13
13
  } from "lucide-react"
14
14
 
15
15
  import { getMediaItem } from "../../../content/get-media-items"
16
+ import { getLinkAttributes } from "../../../utils/link-attributes"
16
17
  import {
17
18
  createContentMetadata,
18
19
  type UrlType,
@@ -54,47 +55,44 @@ const iconDirectionClass = direction === "rtl" ? "scale-x-[-1]" : ""
54
55
  class="mx-auto mt-16 max-w-screen-md overflow-hidden bg-gray-200 md:mt-20 md:rounded-xl"
55
56
  >
56
57
  {
57
- content.map(
58
- ({ extension, labelText, type, canBeOpened, url, target }, index) => {
59
- const TypeIcon = typeIcons[type]
58
+ content.map(({ extension, labelText, type, isDownload, url }, index) => {
59
+ const TypeIcon = typeIcons[type]
60
60
 
61
- return (
62
- <li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
63
- <a
64
- href={url}
65
- target={target}
66
- class="flex items-center justify-between py-8"
67
- >
68
- <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800 text-gray-200">
69
- <TypeIcon size="1.2rem" />
70
- </span>
61
+ return (
62
+ <li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
63
+ <a
64
+ {...getLinkAttributes(url)}
65
+ class="flex items-center justify-between py-8"
66
+ >
67
+ <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800 text-gray-200">
68
+ <TypeIcon size="1.2rem" />
69
+ </span>
71
70
 
72
- <div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
73
- {labelText}
74
- </div>
75
- <div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
76
- {extension}
77
- </div>
78
- {canBeOpened ? (
79
- <>
80
- <ChevronRightIcon
81
- className={`shrink-0 text-gray-600 group-hover:text-gray-800 ${iconDirectionClass}`}
82
- />
83
- <span class="sr-only">{t("ln.details.open")}</span>
84
- </>
85
- ) : (
86
- <>
87
- <DownloadIcon className="shrink-0 text-gray-600 group-hover:text-gray-800" />
88
- <span class="sr-only">{t("ln.details.download")}</span>
89
- </>
90
- )}
91
- </a>
92
- {index !== content.length - 1 && (
93
- <div class="h-px w-full bg-gray-300" />
71
+ <div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
72
+ {labelText}
73
+ </div>
74
+ <div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
75
+ {extension}
76
+ </div>
77
+ {isDownload ? (
78
+ <>
79
+ <DownloadIcon className="shrink-0 text-gray-600 group-hover:text-gray-800" />
80
+ <span class="sr-only">{t("ln.details.download")}</span>
81
+ </>
82
+ ) : (
83
+ <>
84
+ <ChevronRightIcon
85
+ className={`shrink-0 text-gray-600 group-hover:text-gray-800 ${iconDirectionClass}`}
86
+ />
87
+ <span class="sr-only">{t("ln.details.open")}</span>
88
+ </>
94
89
  )}
95
- </li>
96
- )
97
- },
98
- )
90
+ </a>
91
+ {index !== content.length - 1 && (
92
+ <div class="h-px w-full bg-gray-300" />
93
+ )}
94
+ </li>
95
+ )
96
+ })
99
97
  }
100
98
  </ol>
@@ -3,6 +3,7 @@ import { ExternalLinkIcon } from "lucide-react"
3
3
 
4
4
  import { getMediaItem } from "../../../../content/get-media-items"
5
5
  import { getMediaType } from "../../../../content/get-media-types"
6
+ import { getLinkAttributes } from "../../../../utils/link-attributes"
6
7
  import { createContentMetadata } from "../../utils/create-content-metadata"
7
8
 
8
9
  interface Props {
@@ -33,11 +34,14 @@ const getOpenActionLabel = () => {
33
34
 
34
35
  <a
35
36
  class="flex min-w-52 items-center justify-center gap-2 rounded-2xl bg-gray-800 px-6 py-3 font-bold text-gray-100 shadow-sm hover:bg-gray-950 hover:text-gray-300"
36
- href={content.url}
37
- target={content.target}
37
+ {...getLinkAttributes(content.url)}
38
38
  hreflang={item.data.language}
39
39
  class:list={[className]}
40
40
  >
41
- {content.isExternal && <ExternalLinkIcon className="shrink-0" />}
42
- {content.canBeOpened ? getOpenActionLabel() : t("ln.details.download")}
41
+ {
42
+ content.isExternal && (
43
+ <ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
44
+ )
45
+ }
46
+ {content.isDownload ? t("ln.details.download") : getOpenActionLabel()}
43
47
  </a>
@@ -14,7 +14,7 @@ const { t } = Astro.locals.i18n
14
14
  class="flex cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
15
15
  class:list={[Astro.props.className]}
16
16
  id="share-btn"
17
- ><ForwardIcon />
17
+ ><ForwardIcon className="rtl:scale-x-[-1]" />
18
18
  {t("ln.details.share")}</button
19
19
  >
20
20
  <Toast id="share-success" variant="success">
@@ -1,4 +1,4 @@
1
- import { isExternalUrl } from "../../../utils/urls"
1
+ import { isExternalUrl } from "../../../utils/is-external-url"
2
2
 
3
3
  export type UrlType =
4
4
  | "link"
@@ -11,36 +11,36 @@ export type UrlType =
11
11
 
12
12
  const KNOWN_EXTENSIONS: Record<
13
13
  string,
14
- { type: UrlType; canBeOpened?: boolean } | undefined
14
+ { type: UrlType; isDownload?: boolean } | undefined
15
15
  > = {
16
- htm: { type: "link", canBeOpened: true },
17
- html: { type: "link", canBeOpened: true },
18
- php: { type: "link", canBeOpened: true },
19
- json: { type: "source", canBeOpened: true },
20
- xml: { type: "source", canBeOpened: true },
21
- md: { type: "source", canBeOpened: true },
22
- svg: { type: "image", canBeOpened: true },
23
- jpg: { type: "image", canBeOpened: true },
24
- jpeg: { type: "image", canBeOpened: true },
25
- png: { type: "image", canBeOpened: true },
26
- gif: { type: "image", canBeOpened: true },
27
- ico: { type: "image", canBeOpened: true },
28
- webp: { type: "image", canBeOpened: true },
29
- mp3: { type: "audio", canBeOpened: true },
30
- wav: { type: "audio", canBeOpened: true },
31
- aac: { type: "audio", canBeOpened: true },
32
- ogg: { type: "audio", canBeOpened: true },
33
- mp4: { type: "video", canBeOpened: true },
34
- webm: { type: "video", canBeOpened: true },
35
- ogv: { type: "video", canBeOpened: true },
36
- pdf: { type: "text", canBeOpened: true },
37
- txt: { type: "text", canBeOpened: true },
38
- epub: { type: "text" },
39
- zip: { type: "package" },
40
- ppt: { type: "text" },
41
- pptx: { type: "text" },
42
- doc: { type: "text" },
43
- docx: { type: "text" },
16
+ htm: { type: "link" },
17
+ html: { type: "link" },
18
+ php: { type: "link" },
19
+ json: { type: "source" },
20
+ xml: { type: "source" },
21
+ md: { type: "source" },
22
+ svg: { type: "image" },
23
+ jpg: { type: "image" },
24
+ jpeg: { type: "image" },
25
+ png: { type: "image" },
26
+ gif: { type: "image" },
27
+ ico: { type: "image" },
28
+ webp: { type: "image" },
29
+ mp3: { type: "audio" },
30
+ wav: { type: "audio" },
31
+ aac: { type: "audio" },
32
+ ogg: { type: "audio" },
33
+ mp4: { type: "video" },
34
+ webm: { type: "video" },
35
+ ogv: { type: "video" },
36
+ pdf: { type: "text" },
37
+ txt: { type: "text" },
38
+ epub: { type: "text", isDownload: true },
39
+ zip: { type: "package", isDownload: true },
40
+ ppt: { type: "text", isDownload: true },
41
+ pptx: { type: "text", isDownload: true },
42
+ doc: { type: "text", isDownload: true },
43
+ docx: { type: "text", isDownload: true },
44
44
  } as const
45
45
 
46
46
  export function createContentMetadata({
@@ -66,16 +66,14 @@ export function createContentMetadata({
66
66
 
67
67
  const labelText = customLabel ?? fileName ?? linkName
68
68
  const type = KNOWN_EXTENSIONS[extension]?.type ?? "link"
69
- const canBeOpened =
70
- !hasExtension || !!KNOWN_EXTENSIONS[extension]?.canBeOpened
69
+ const isDownload = KNOWN_EXTENSIONS[extension]?.isDownload
71
70
 
72
71
  return {
73
72
  url,
74
73
  extension,
75
74
  isExternal,
76
75
  labelText,
77
- canBeOpened,
76
+ isDownload,
78
77
  type,
79
- target: isExternal ? "_blank" : "_self",
80
78
  } as const
81
79
  }
@@ -0,0 +1,34 @@
1
+ import config from "virtual:lightnet/config"
2
+
3
+ import { parseUrl } from "./urls"
4
+
5
+ /**
6
+ * Test if a given url is outside this site.
7
+ * Will return false if the url is relative or if it
8
+ * starts with the site config from astro config.
9
+ *
10
+ * @param url to test
11
+ * @returns is the url external?
12
+ */
13
+ export function isExternalUrl(url: string) {
14
+ const parsedUrl = parseUrl(url)
15
+ if (!parsedUrl) {
16
+ return false
17
+ }
18
+
19
+ if (config.internalDomains.includes(parsedUrl.hostname)) {
20
+ return false
21
+ }
22
+
23
+ const { SITE: site } = import.meta.env
24
+ if (!site) {
25
+ return true
26
+ }
27
+
28
+ const parsedSiteUrl = parseUrl(site)
29
+ if (!parsedSiteUrl) {
30
+ return true
31
+ }
32
+
33
+ return !parsedUrl.href.startsWith(parsedSiteUrl.href)
34
+ }
@@ -0,0 +1,10 @@
1
+ import { isExternalUrl } from "./is-external-url"
2
+
3
+ export function getLinkAttributes(href: string) {
4
+ const isExternal = isExternalUrl(href)
5
+ return {
6
+ href,
7
+ target: isExternal ? "_blank" : "_self",
8
+ rel: isExternal ? "noopener noreferrer" : undefined,
9
+ }
10
+ }
@@ -1,3 +1,5 @@
1
+ import { isAbsoluteUrl } from "./urls"
2
+
1
3
  /**
2
4
  * Prefix a site-internal path with Astro's configured base path.
3
5
  *
@@ -91,6 +93,10 @@ export function searchPagePath(
91
93
  * @returns resolved path. Eg. '/en/about' for input "en" and "/about"
92
94
  */
93
95
  export function localizePath(locale: string | undefined, path: string) {
96
+ if (isAbsoluteUrl(path)) {
97
+ return path
98
+ }
99
+
94
100
  return pathWithBase(
95
101
  `${locale ? `/${locale}` : ""}/${path.replace(/^\//, "")}`,
96
102
  )
package/src/utils/urls.ts CHANGED
@@ -1,28 +1,16 @@
1
- import config from "virtual:lightnet/config"
2
-
3
- /**
4
- * Test if a given url is outside this site.
5
- * Will return false if the url is relative or if it
6
- * starts with the site config from astro config.
7
- *
8
- * @param url to test
9
- * @returns is the url external?
10
- */
11
- export function isExternalUrl(url: string) {
12
- let parsedUrl
1
+ export function parseUrl(url: string) {
13
2
  try {
14
- // test if url is absolute
15
- parsedUrl = new URL(url)
3
+ return new URL(url)
16
4
  } catch {
17
- // url is relative
18
- return false
19
- }
20
- if (config.internalDomains.includes(parsedUrl.hostname)) {
21
- return false
22
- }
23
- const { SITE: site } = import.meta.env
24
- if (!site) {
25
- return true
5
+ // Support host-like values such as `example.com` without treating plain paths as external.
6
+ if (/^(localhost(?::\d+)?|[^/\s]+\.[^/\s]+)(?:[/?#]|$)/.test(url)) {
7
+ return new URL(`https://${url}`)
8
+ }
9
+
10
+ return null
26
11
  }
27
- return !url.startsWith(site)
12
+ }
13
+
14
+ export function isAbsoluteUrl(url: string) {
15
+ return !!parseUrl(url)
28
16
  }