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/) + }) +})