Skip to content

Commit fdfa7c5

Browse files
committed
feat: add territory SEO
1 parent 0394a5b commit fdfa7c5

File tree

4 files changed

+111
-7
lines changed

4 files changed

+111
-7
lines changed

components/layout.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import PullToRefresh from './pull-to-refresh'
1010

1111
export default function Layout ({
1212
sub, contain = true, footer = true, footerLinks = true,
13-
containClassName = '', seo = true, item, user, children
13+
containClassName = '', seo = true, item, user, territory, children
1414
}) {
1515
return (
1616
<>
17-
{seo && <Seo sub={sub} item={item} user={user} />}
17+
{seo && <Seo sub={sub} item={item} user={user} territory={territory} />}
1818
<Navigation sub={sub} />
1919
{contain
2020
? (

components/seo.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextSeo } from 'next-seo'
22
import { useRouter } from 'next/router'
33
import removeMd from 'remove-markdown'
44
import { numWithUnits } from '@/lib/format'
5+
import { processTerritoryDescription } from '@/lib/territory'
56

67
export function SeoSearch ({ sub }) {
78
const router = useRouter()
@@ -36,14 +37,17 @@ export function SeoSearch ({ sub }) {
3637
// index page seo
3738
// recent page seo
3839

39-
export default function Seo ({ sub, item, user }) {
40+
export default function Seo ({ sub, item, user, territory }) {
4041
const router = useRouter()
4142
const pathNoQuery = router.asPath.split('?')[0]
4243
const defaultTitle = pathNoQuery.slice(1)
4344
const snStr = `stacker news${sub ? ` ~${sub}` : ''}`
4445
let fullTitle = `${defaultTitle && `${defaultTitle} \\ `}stacker news`
4546
let desc = "It's like Hacker News but we pay you Bitcoin."
46-
if (item) {
47+
if (territory) {
48+
fullTitle = `${territory.name} \\ ${snStr}`
49+
desc = processTerritoryDescription(territory)
50+
} else if (item) {
4751
if (item.title) {
4852
fullTitle = `${item.title} \\ ${snStr}`
4953
} else if (item.root) {
@@ -67,8 +71,7 @@ export default function Seo ({ sub, item, user }) {
6771
} else if (item.boost) {
6872
desc += ` [${item.boost} boost]`
6973
}
70-
}
71-
if (user) {
74+
} else if (user) {
7275
desc = `@${user.name} has [${user.optional.stacked ? `${user.optional.stacked} stacked,` : ''}${numWithUnits(user.nitems, { unitSingular: 'item', unitPlural: 'items' })}]`
7376
}
7477

lib/territory.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TERRITORY_GRACE_DAYS, TERRITORY_PERIOD_COST } from './constants'
22
import { datePivot, diffDays } from './time'
3+
import removeMd from 'remove-markdown'
34

45
export function nextBilling (relativeTo, billingType) {
56
if (!relativeTo || billingType === 'ONCE') return null
@@ -28,3 +29,103 @@ export function nextBillingWithGrace (sub) {
2829
if (!sub) return null
2930
return datePivot(new Date(sub.billPaidUntil), { days: TERRITORY_GRACE_DAYS })
3031
}
32+
33+
function cleanMarkdownText (text) {
34+
if (!text) return ''
35+
36+
let cleaned = removeMd(text)
37+
cleaned = cleaned
38+
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
39+
.replace(/\[\[([^\]]+)\]\]\([^)]+\)/g, '$1') // Remove nested markdown links, keep inner text
40+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove regular markdown links, keep text
41+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // Remove images completely
42+
.replace(/`([^`]+)`/g, '$1') // Remove code formatting
43+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold formatting
44+
.replace(/\*([^*]+)\*/g, '$1') // Remove italic formatting
45+
.trim()
46+
47+
return cleaned
48+
}
49+
50+
function truncateAtSentenceBoundary (text, maxLength = 160) {
51+
if (!text || text.length <= maxLength) return text
52+
const sentences = text
53+
.split(/[.!?]+/)
54+
.map(s => s.trim())
55+
.filter(Boolean)
56+
57+
let result = ''
58+
let currentLength = 0
59+
60+
for (const sentence of sentences) {
61+
const separator = result ? '. ' : ''
62+
const newLength = currentLength + separator.length + sentence.length
63+
64+
if (newLength <= maxLength - 3) {
65+
result += separator + sentence
66+
currentLength = newLength
67+
} else {
68+
break
69+
}
70+
}
71+
if (result) {
72+
return result + '...'
73+
}
74+
// Fallback to simple truncation
75+
return text.substring(0, maxLength - 3) + '...'
76+
}
77+
78+
export function processTerritoryDescription (territory) {
79+
if (!territory) return null
80+
81+
if (territory.desc) {
82+
const processedDesc = cleanMarkdownText(territory.desc)
83+
84+
if (processedDesc.length >= 30 && processedDesc.length <= 160) {
85+
return processedDesc
86+
}
87+
if (processedDesc.length > 160) {
88+
return truncateAtSentenceBoundary(processedDesc, 160)
89+
}
90+
}
91+
92+
// Generate a contextual description if the original is too short or unsuitable
93+
return generateTerritoryDescription(territory)
94+
}
95+
96+
export function generateTerritoryDescription (territory) {
97+
const parts = []
98+
if (territory.desc) {
99+
const processedDesc = cleanMarkdownText(territory.desc)
100+
if (processedDesc) {
101+
parts.push(processedDesc)
102+
}
103+
}
104+
105+
if (territory.postTypes && territory.postTypes.length > 0) {
106+
const postTypeLabels = territory.postTypes.map(type => {
107+
switch (type) {
108+
case 'LINK': return 'links'
109+
case 'DISCUSSION': return 'discussions'
110+
case 'BOUNTY': return 'bounties'
111+
case 'POLL': return 'polls'
112+
default: return type.toLowerCase()
113+
}
114+
})
115+
parts.push(postTypeLabels.join(', '))
116+
}
117+
118+
if (territory.user?.name) {
119+
parts.push(`created by @${territory.user.name}`)
120+
}
121+
122+
let desc = parts.length > 1
123+
? `Stacker.news community: ${parts.join('. ')}`
124+
: `Stacker.news community for ${parts.join(', ')}`
125+
126+
if (desc.length > 160) {
127+
desc = truncateAtSentenceBoundary(desc, 160)
128+
}
129+
130+
return desc
131+
}

pages/~/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function Sub ({ ssrData }) {
2222
const { sub } = data || ssrData
2323

2424
return (
25-
<Layout sub={sub?.name}>
25+
<Layout sub={sub?.name} territory={sub}>
2626
{sub
2727
? <TerritoryHeader sub={sub} />
2828
: (

0 commit comments

Comments
 (0)