diff --git a/app/assets/logos/sponsors/bluesky-light.svg b/app/assets/logos/sponsors/bluesky-light.svg
new file mode 100644
index 000000000..5fa1b8009
--- /dev/null
+++ b/app/assets/logos/sponsors/bluesky-light.svg
@@ -0,0 +1,17 @@
+
diff --git a/app/assets/logos/sponsors/bluesky.svg b/app/assets/logos/sponsors/bluesky.svg
new file mode 100644
index 000000000..9d1b84422
--- /dev/null
+++ b/app/assets/logos/sponsors/bluesky.svg
@@ -0,0 +1,17 @@
+
diff --git a/app/assets/logos/sponsors/index.ts b/app/assets/logos/sponsors/index.ts
index acec2f9af..3ab7d951a 100644
--- a/app/assets/logos/sponsors/index.ts
+++ b/app/assets/logos/sponsors/index.ts
@@ -6,6 +6,8 @@ import LogoVlt from './vlt.svg'
import LogoVltLight from './vlt-light.svg'
import LogoNetlify from './netlify.svg'
import LogoNetlifyLight from './netlify-light.svg'
+import LogoBluesky from './bluesky.svg'
+import LogoBlueskyLight from './bluesky-light.svg'
// The list is used on the about page. To add, simply upload the logos nearby and add an entry here. Prefer SVGs.
// For logo src, specify a string or object with the light and dark theme variants.
@@ -51,4 +53,13 @@ export const SPONSORS = [
normalisingIndent: '0.125rem',
url: 'https://netlify.com/',
},
+ {
+ name: 'Bluesky',
+ logo: {
+ dark: LogoBluesky,
+ light: LogoBlueskyLight,
+ },
+ normalisingIndent: '0.625rem',
+ url: 'https://bsky.app/',
+ },
]
diff --git a/app/components/BlogPostListCard.vue b/app/components/BlogPostListCard.vue
index f620cb9b7..5fd17dc74 100644
--- a/app/components/BlogPostListCard.vue
+++ b/app/components/BlogPostListCard.vue
@@ -33,7 +33,9 @@ defineProps<{
- {{ published }}
+
+
+
new Date(buildInfo.value.time))
·
v{{ buildInfo.version }}
diff --git a/app/components/Header/ConnectorModal.vue b/app/components/Header/ConnectorModal.vue
index 445161dba..b22414df4 100644
--- a/app/components/Header/ConnectorModal.vue
+++ b/app/components/Header/ConnectorModal.vue
@@ -116,10 +116,10 @@ function handleDisconnect() {
dir="ltr"
>
$
- pnpm npmx-connector
+ npx npmx-connector
diff --git a/app/pages/blog/alpha-release.md b/app/pages/blog/alpha-release.md
index 24780f367..56e3e3aca 100644
--- a/app/pages/blog/alpha-release.md
+++ b/app/pages/blog/alpha-release.md
@@ -90,7 +90,7 @@ headline="Read more from the community"
description: 'Getting involved in open source doesn\'t have to be scary! Understand how to find a great project and make your first contribution in this guide from Salma.'
},
{
- url: 'https://roe.dev/blog/a-virtuous-cycle',
+ url: 'https://roe.dev/blog/virtuous-circle',
title: 'A Virtuous Circle',
authorHandle: 'danielroe.dev',
description: 'There\'s a reason why building npmx has been such a blast so far, and it\'s one of the most powerful patterns in open source software development. It\'s also why \'the 10x developer\' is an incredibly dangerous myth.'
diff --git a/config/env.ts b/config/env.ts
index 85a5fa601..cbd852d40 100644
--- a/config/env.ts
+++ b/config/env.ts
@@ -3,7 +3,9 @@
import Git from 'simple-git'
import * as process from 'node:process'
-export { version } from '../package.json'
+import { version as packageVersion } from '../package.json'
+
+export { packageVersion as version }
/**
* Environment variable `PULL_REQUEST` provided by Netlify.
@@ -41,14 +43,16 @@ export const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF
/**
* Whether this is the canary environment (main.npmx.dev).
*
- * Detected via the custom Vercel environment (`VERCEL_ENV === 'canary'`),
- * or as a fallback, a production deploy from the `main` branch.
+ * Detected as any non-PR Vercel deploy from the `main` branch
+ * (which may receive `VERCEL_ENV === 'production'` or `'preview'`
+ * depending on the project's production branch configuration).
*
* @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV}
*/
export const isCanary =
- process.env.VERCEL_ENV === 'canary' ||
- (process.env.VERCEL_ENV === 'production' && gitBranch === 'main')
+ (process.env.VERCEL_ENV === 'production' || process.env.VERCEL_ENV === 'preview') &&
+ gitBranch === 'main' &&
+ !isPR
/**
* Environment variable `CONTEXT` provided by Netlify.
@@ -56,7 +60,7 @@ export const isCanary =
* @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#build-metadata}
*
* Environment variable `VERCEL_ENV` provided by Vercel.
- * `production`, `preview`, `development`, or a custom environment name (e.g. `canary`).
+ * `production`, `preview`, or `development`.
* @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV}
*
* Whether this is some sort of preview environment.
@@ -152,12 +156,28 @@ export async function getFileLastUpdated(path: string) {
}
}
+/**
+ * Resolves the current version from git tags, falling back to `package.json`.
+ *
+ * Uses `git describe --tags --abbrev=0 --match 'v*'` to find the most recent
+ * reachable release tag (e.g. `v0.1.0` -> `0.1.0`).
+ */
+export async function getVersion() {
+ try {
+ const tag = (await git.raw(['describe', '--tags', '--abbrev=0', '--match', 'v*'])).trim()
+ return tag.replace(/^v/, '')
+ } catch {
+ return packageVersion
+ }
+}
+
export async function getEnv(isDevelopment: boolean) {
- const { commit, shortCommit, branch } = await getGitInfo()
+ const [{ commit, shortCommit, branch }, version] = await Promise.all([getGitInfo(), getVersion()])
const env = isDevelopment ? 'dev' : isCanary ? 'canary' : isPreview ? 'preview' : 'release'
const previewUrl = getPreviewUrl()
const productionUrl = getProductionUrl()
return {
+ version,
commit,
shortCommit,
branch,
diff --git a/modules/build-env.ts b/modules/build-env.ts
index 05c35bb81..cc21d90f1 100644
--- a/modules/build-env.ts
+++ b/modules/build-env.ts
@@ -1,7 +1,7 @@
import type { BuildInfo, EnvType } from '../shared/types'
import { createResolver, defineNuxtModule } from 'nuxt/kit'
import { isCI } from 'std-env'
-import { getEnv, getFileLastUpdated, version } from '../config/env'
+import { getEnv, getFileLastUpdated } from '../config/env'
const { resolve } = createResolver(import.meta.url)
@@ -26,7 +26,7 @@ export default defineNuxtModule({
prNumber: null,
} satisfies BuildInfo
} else {
- const [{ env: useEnv, commit, shortCommit, branch, prNumber }, privacyPolicyDate] =
+ const [{ env: useEnv, version, commit, shortCommit, branch, prNumber }, privacyPolicyDate] =
await Promise.all([getEnv(nuxt.options.dev), getFileLastUpdated('app/pages/privacy.vue')])
env = useEnv
nuxt.options.appConfig.env = useEnv
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 4031a9828..2687d0ea4 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -55,7 +55,7 @@ export default defineNuxtConfig({
},
},
- devtools: { enabled: !true },
+ devtools: { enabled: true },
devServer: {
// Used with atproto oauth
@@ -136,6 +136,14 @@ export default defineNuxtConfig({
// never cache
'/api/auth/**': { isr: false, cache: false },
'/api/social/**': { isr: false, cache: false },
+ '/api/atproto/bluesky-comments': {
+ isr: {
+ expiration: 60 * 60 /* one hour */,
+ passQuery: true,
+ allowQuery: ['uri'],
+ },
+ cache: { maxAge: 3600 },
+ },
'/api/atproto/bluesky-author-profiles': {
isr: {
expiration: 60 * 60 /* one hour */,
diff --git a/public/robots.txt b/public/robots.txt
index 3d6069d1c..bdd4add70 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,13 +1,5 @@
-# todo remove
User-agent: *
-Allow: /$
-Allow: /about$
-Allow: /privacy$
-Allow: /__og-image__/*
-Disallow: /
-
-# User-agent: *
-# Allow: /
+Allow: /
# Search pages: infinite query-param combinations
Disallow: /search
diff --git a/test/nuxt/components/HeaderConnectorModal.spec.ts b/test/nuxt/components/HeaderConnectorModal.spec.ts
index bf1450998..b2d0ce597 100644
--- a/test/nuxt/components/HeaderConnectorModal.spec.ts
+++ b/test/nuxt/components/HeaderConnectorModal.spec.ts
@@ -371,7 +371,6 @@ describe('HeaderConnectorModal', () => {
it('shows the CLI command to run', async () => {
const dialog = await mountAndOpen()
- // The command is now "pnpm npmx-connector"
expect(dialog?.textContent).toContain('npmx-connector')
})
diff --git a/test/unit/config/env.spec.ts b/test/unit/config/env.spec.ts
index 1aa4f97cf..48be8159b 100644
--- a/test/unit/config/env.spec.ts
+++ b/test/unit/config/env.spec.ts
@@ -27,24 +27,34 @@ describe('isCanary', () => {
vi.unstubAllEnvs()
})
- it('returns true when VERCEL_ENV is "canary"', async () => {
- vi.stubEnv('VERCEL_ENV', 'canary')
+ it('returns true when VERCEL_ENV is "production" and branch is "main"', async () => {
+ vi.stubEnv('VERCEL_ENV', 'production')
+ vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
const { isCanary } = await import('../../../config/env')
expect(isCanary).toBe(true)
})
- it('returns true when VERCEL_ENV is "production" and branch is "main"', async () => {
- vi.stubEnv('VERCEL_ENV', 'production')
+ it('returns true when VERCEL_ENV is "preview" and branch is "main" (non-PR)', async () => {
+ vi.stubEnv('VERCEL_ENV', 'preview')
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
const { isCanary } = await import('../../../config/env')
expect(isCanary).toBe(true)
})
+ it('returns false when VERCEL_ENV is "preview", branch is "main", but is a PR', async () => {
+ vi.stubEnv('VERCEL_ENV', 'preview')
+ vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
+ vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '123')
+ const { isCanary } = await import('../../../config/env')
+
+ expect(isCanary).toBe(false)
+ })
+
it.each([
['production (non-main branch)', 'production', 'v1.0.0'],
- ['preview', 'preview', undefined],
+ ['preview (non-main branch)', 'preview', 'feat/foo'],
['development', 'development', undefined],
['unset', undefined, undefined],
])('returns false when VERCEL_ENV is %s', async (_label, value, branch) => {
@@ -78,8 +88,8 @@ describe('getEnv', () => {
expect(result.env).toBe('dev')
})
- it('returns "canary" when VERCEL_ENV is "canary"', async () => {
- vi.stubEnv('VERCEL_ENV', 'canary')
+ it('returns "canary" for Vercel preview deploys from main branch (non-PR)', async () => {
+ vi.stubEnv('VERCEL_ENV', 'preview')
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
const { getEnv } = await import('../../../config/env')
const result = await getEnv(false)
@@ -87,10 +97,10 @@ describe('getEnv', () => {
expect(result.env).toBe('canary')
})
- it('returns "preview" for Vercel preview deploys', async () => {
+ it('returns "preview" for Vercel preview PR deploys', async () => {
vi.stubEnv('VERCEL_ENV', 'preview')
vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '123')
- vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
+ vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'feat/foo')
const { getEnv } = await import('../../../config/env')
const result = await getEnv(false)
@@ -125,18 +135,9 @@ describe('getEnv', () => {
expect(result.env).toBe('release')
})
- it('prioritises "canary" over "preview" when VERCEL_ENV is "canary" and PR is open', async () => {
- vi.stubEnv('VERCEL_ENV', 'canary')
- vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '789')
- vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
- const { getEnv } = await import('../../../config/env')
- const result = await getEnv(false)
-
- expect(result.env).toBe('canary')
- })
-
it('prioritises "dev" over "canary" in development mode', async () => {
- vi.stubEnv('VERCEL_ENV', 'canary')
+ vi.stubEnv('VERCEL_ENV', 'preview')
+ vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
const { getEnv } = await import('../../../config/env')
const result = await getEnv(true)
@@ -169,7 +170,6 @@ describe('getPreviewUrl', () => {
it.each([
['Netlify production', { CONTEXT: 'production', URL: 'https://prod.example.com' }],
['Vercel production', { VERCEL_ENV: 'production', NUXT_ENV_VERCEL_URL: 'prod.example.com' }],
- ['Vercel canary', { VERCEL_ENV: 'canary', NUXT_ENV_VERCEL_URL: 'main.example.com' }],
])('%s environment returns `undefined`', async (_name, envVars) => {
for (const [key, value] of Object.entries(envVars)) {
vi.stubEnv(key, value)
@@ -308,3 +308,20 @@ describe('getProductionUrl', () => {
expect(getProductionUrl()).toBe(expectedUrl)
})
})
+
+describe('getVersion', () => {
+ it('returns package.json version when no git tags are reachable', async () => {
+ const { getVersion, version } = await import('../../../config/env')
+ const result = await getVersion()
+
+ // In test environments without reachable tags, falls back to package.json
+ expect(result).toBe(version)
+ })
+
+ it('strips the leading "v" prefix from the tag', async () => {
+ const { getVersion } = await import('../../../config/env')
+ const result = await getVersion()
+
+ expect(result).not.toMatch(/^v/)
+ })
+})