TechFlows is a bilingual public website for a youth tech creator community. The brand line is “让想做点什么的人,先找到彼此” / “For those who want to build something.” The site is an Astro 5 static project. Chinese pages live at the root. English pages live under /en/. Production canonical host is https://www.techflows.app. Deploy target is Vercel.
The site is not a product landing page and not a manifesto. It is an entry surface: explain who TechFlows is, show what is happening now, and send people to one concrete next step (an event, a project form, co-building, or a partnership). UNFINO is the continuation venue inside TechFlows. The first event is not yet set. It is a sibling of Events, not a product or a replacement for the community site.
This README is the working document for the repo. Use it when you change copy, layout, routes, or outbound links.
TechFlows focuses on young tech creators and early-stage work: AI practice, open source, hackathons, campus projects, and first startup attempts. The public pages should stay honest about early status. Do not invent metrics, dates, case studies, or partner logos to fill empty space. The Projects page is a collection call, not a fake portfolio.
Typical visitor jobs:
- Understand what TechFlows is in under a minute.
- Join as a member first; take responsibility later if you want.
- Find the next gathering or leave contact details for notices. Unpublished stays 待发布.
- Submit an unfinished project or idea.
- Leave partnership intent. There is no package table.
src/
pages/ Route files. Most of them only pick a locale and render a page component.
pages/en/ English routes. Same components, locale="en".
pages/404.astro Locale from the URL prefix. noindex.
pages/go/[slug].astro Outbound confirmation page (Chinese).
pages/en/go/ Outbound confirmation page (English).
components/ Page bodies, header, footer, buttons, lists.
layouts/BaseLayout.astro
layouts/RedirectLayout.astro
styles/global.css Tokens, atmosphere, chrome, and the shared list system.
data/ Copy, events, projects, partners, outbound targets.
utils/i18n.ts t(), pagePath(), locale helpers.
scripts/check-i18n.mjs Missing or unused translation keys fail the check.
public/assets/ Logo, header logo, OG images, atom-field.svg, fonts.
middleware.ts Locale cookie from `?lang=`. Chinese routes stay Chinese.
vercel.json Security headers, cache, HTML redirects.
.github/workflows/check.yml
Page components, not src/pages/*, own the real layout. A typical route file is:
---
import HomePage from "@components/HomePage.astro";
---
<HomePage locale="zh" />- Astro 5, TypeScript, static output
- pnpm, Node.js 20+
- Geist Mono via the
geistpackage - Smiley Sans / 得意黑, self-hosted at
public/assets/fonts/SmileySans-Oblique.ttf.woff2 - Vercel Routing Middleware (
@vercel/functions) - Vercel Analytics and Speed Insights, both off unless
PUBLIC_ENABLE_VERCEL_INSIGHTS=true
Path aliases live in tsconfig.json: @components/*, @layouts/*, @data/*, @utils/*, @styles/*.
pnpm install
pnpm devDev server: http://127.0.0.1:3456/
pnpm check # i18n key audit, then astro check
pnpm build # astro check, then write dist/
pnpm preview # serve the production build on 127.0.0.1:3456
pnpm generate:assets # regenerate OG images, header logo, and Apple Touch Iconpnpm check runs scripts/check-i18n.mjs first. A missing t() key or an unused dictionary entry fails the command.
Local requests keep Chinese at /. Open /en/ or use the header language control to see English. The language control writes ?lang=en or ?lang=zh. Only that explicit switch sets techflows_locale (HttpOnly, one year). There is no geographic 302 from Chinese routes to /en/.
The visual system is two materials on a quiet page. Do not turn it into a card dashboard, and do not turn it into a 1990s terminal.
- Page background is
#f4f7fb. BaseLayoutpaints a fixed atmosphere layer: a soft cyan/blue light field, a very faint pixel grid, andpublic/assets/atom-field.svg.- The atom field is atmosphere. Keep it low-contrast. Do not add scanlines, ASCII HUD labels, side rails, or denser tiles.
Glass (backdrop-filter, translucent fill, hairline highlight) is only for interactive chrome:
- floating header capsule
- homepage reading map
- mobile nav sheet
- secondary buttons
- footer
Do not glass body copy, principles, role essays, partner lists, or UNFINO format/builder rows. Do not nest glass inside glass. If a block feels weak, drop the frame and use type, spacing, and a hairline. Do not wrap it in another rounded panel.
- Measure is about 720px (
--measure/--container). - Shared page frame:
.page,.page-hero,.page-section,.page-cta,.page-kicker,.page-actions. Home and subpages use the same type scale and section whitespace. - One type scale: large page title (
--type-display), section title (--type-section), list titles (--type-row), muted body. - Kickers are small Geist Mono, blue, uppercase (
.page-kicker).
Structured content uses one list language:
Structured content uses one list language:
<div class="index-list">
<article class="index-row">
<p class="index-row__index">01</p>
<div class="index-row__main">
<p class="index-row__meta">Optional status</p>
<h3>Title</h3>
<p>One or two sentences.</p>
<a class="text-link" href="...">Continue</a>
</div>
</article>
</div>Variants:
index-row--lead: a featured story, allowed to hold one primary buttonindex-row--essay: several paragraphs (Join roles)index-row--status: label / state / action, used on the homepage status blockindex-list--plain: numbered<ul>(Projects collection prompts)
Pill buttons belong in the closing .page-cta of a page, or in one lead story. Mid-page paths use .text-link. A .page-actions cluster has one black primary; extra actions are secondary glass pills. Do not mix two primaries in the same cluster.
These already failed in review and should not come back:
- Glass list panels around rows, then glass tiles inside those panels
- 3-column and 4-column mini-cards competing with hairline lists
- Atom-circle bullets plus a second
01index on the same row - Pixel-tick ornaments on every card
- Full-viewport ASCII decoration
Shared CSS lives in src/styles/global.css. Page chrome for public routes is PageMain, PageHero, PageSection, and PageCta in src/components/. Page-specific exceptions should stay small. If a new block looks like a list, use .index-list.
Component: src/components/HomePage.astro
Desktop has a left reading map. Line-by-line reading is on by default on desktop (viewport above 1024px). It stays off on smaller screens and when prefers-reduced-motion is set. In this mode, wheel and arrow keys / j k step to the next line instead of scrolling the page. Map links jump to that section’s first line instead of a native hash scroll (which would land on hidden copy). The toggle still exits to show all; Escape does the same. A skip link to #main stays focusable on small screens.
| Section | Anchor | Job |
|---|---|---|
| Hero | #home |
Title, origin + work line, then one Join CTA. Doors are not on this path. |
| Now | #now |
Same table / write it clearly / continuation venue. The third row goes to UNFINO |
| Principles | #principles |
Unfinished can be seen; do not package what we cannot do; an event is not the end |
| Status | #status |
Honest tags: upcoming 待发布, project records not live, member work 待发布. No campus/hub list. Do not add a fake university list or works wall. |
| Join | #join |
Closing copy and the only homepage pill button: Join |
Now and principles are .index-list rows. Status rows are clickable index-row--status links. The three doors live on /join/#tracks as 「领走一件事」. Track row CTAs go through /go/job|school|city/ to the Feishu briefs. Do not put raw Feishu URLs on homepage buttons. The four who-can-come rows live only on /join/.
Copy keys live under hero.* and home.* in src/data/translations.ts.
Component: src/components/EventsPage.astro
Data: src/data/events.ts
Flow: opening → upcoming lead row with one primary button → what we hope happens on site (3 rows) → past records (EventCard list plus the WeChat album row) → how to take part (project / co-build / partner) → closing actions.
Upcoming content comes from upcomingEvent. Past items come from pastEvents. Only list the two gatherings already public on Feishu: Hefei first gathering, then Suzhou. Do not invent a third. A past row may omit cta if there is no recap link.
Component: src/components/UnfinoPage.astro
Copy keys: unfinoBrand.*
UNFINO is TechFlows' continuation venue. The first event is not yet set. Do not write a dated coming soon. A hackathon is not listed as a live event until a gathering is locked. Do not present UNFINO as a product or service.
Flow: wordmark and lead → two notes (format not set; rules announced on site) → closing actions to Join or the project form.
Keep the hero visible on first paint. Reveal animation is for list rows, not the whole page.
Component: src/components/ProjectsPage.astro
Data: src/data/projects.ts
This page is an empty-state collection call: why we will not fake case studies, four numbered prompts (why it began, how far it has gone, what is stuck, who is still needed), then submit / join actions.
When real projects exist, add them in src/data/projects.ts and extend the page. Do not paste placeholder companies.
Component: src/components/PartnersPage.astro
Data: src/data/partners.ts
List UI: src/components/RoleList.astro
Flow: short intent-only placeholder → partner form and partner@techflows.app. No package/benefits table, no sponsor deck, no invented event dates. The four partner-type rows in partners.ts are not shown on this page.
Component: src/components/JoinPage.astro
Flow: hero (come with what you have; this page is how to join, not a rank) → how to apply (one primary form button; members need not pick a responsibility door first) → four who-can-come rows (no project → /events/, already building → /go/project/, want to take responsibility → #tracks, have resources → /partners/) → what members get right away (meet, write it down on the projects page, find a buddy and take one small step, plus the red line) → 「领走一件事」 with the three Feishu briefs (job / school / city) → closing form + join@techflows.app.
Do not restore the six-role essays. Do not copy the four questions or the six functional jobs onto this page. Details for the three tracks live on Feishu.
Layout: src/layouts/RedirectLayout.astro
Component: src/components/RedirectPage.astro
Registry: src/data/forms.ts
Every external Feishu form, Feishu doc, or WeChat article goes through this page. The page is noindex, nofollow. Chinese and English route files only pass locale. Slugs:
| Slug | Kind | Use |
|---|---|---|
join |
form | Membership / contact |
project |
form | Submit a project or idea |
cobuild |
form | Help build the community |
partner |
form | Start a partnership |
activity-notice |
form | Same join form; button copy is “Join TechFlows”, not a separate list |
event-review |
article | A specific recap |
wechat-album |
album | All recaps |
job |
doc | 进来先选一个活 |
school |
doc | 阅读「一所学校」 |
city |
doc | 阅读「一座城」 |
activity-notice and join share the same Feishu URL. Do not add a new form just to make the notice button look like a newsletter.
Build links with goPath(locale, slug, source). The optional source query is a tracing tag such as home_entry_project or footer_cobuild. On confirm, the page copies it onto the outbound URL as tf_source. No source means the target URL is unchanged. Add a slug in forms.ts before you link it. Do not point page buttons straight at Feishu.
Almost all visitor-facing sentences live in data files, not in layout CSS.
| File | What to edit |
|---|---|
src/data/site.ts |
Site name, emails, canonical URL, lastUpdated, page titles, descriptions, paths, OG images, nav, footer |
src/data/translations.ts |
Shared UI and long-form copy. Keys are dotted, values are { zh, en } |
src/data/events.ts |
Upcoming event and past event records |
src/data/projects.ts |
Empty-state copy and, later, project entries |
src/data/partners.ts |
Partner type titles and descriptions |
src/data/forms.ts |
External URLs, labels, goPath() |
t(key, locale) is typed as keyof typeof translations. Dynamic keys (roles, formats, hope rows) use as const maps in the page component. Missing or unused keys fail pnpm check.
When you add a public page:
- Add it to
pagesandnavItemsinsrc/data/site.ts. - Add
src/pages/.../index.astroandsrc/pages/en/.../index.astro. - Middleware derives English twins from
pagesinsite.ts./go/still prefixes/enautomatically. - Add sitemap metadata in
src/pages/sitemap.xml.ts. - Add an OG image if the page should not reuse another page’s image.
- Use
.index-listfor structured blocks.
lastUpdated in src/data/site.ts feeds JSON-LD dateModified and llms.txt. Change it when public content changes.
src/utils/i18n.ts is the only place page code should ask for a path or a string. t() is typed to dictionary keys.
Middleware (middleware.ts) runs on Vercel:
- Cookie
techflows_localewins after an explicit?lang=switch. That cookie is HttpOnly. /and other Chinese routes stay Chinese. There is no geographic 302 to/en/.- If the cookie is
en, Chinese page paths 302 to their/en/...twins so the address bar matches the language. That 302 does not rewrite the cookie. /en/...is never redirected away.- Static files, fonts,
robots.txt,sitemap.xml, andllms.txtskip locale routing.
Missing cookies (local preview and first visit) keep Chinese.
HTML lang is zh-CN or en. Chinese pages preload and put Smiley Sans first; English pages put Geist Mono first. Both stacks are always available.
Header.astro is a floating glass capsule. It uses public/assets/techflows-logo-header.png (about 132px wide), not the full-width source logo. Desktop shows full nav. Below about 960px it becomes a hamburger and a sheet: opening moves focus into the panel, Tab cycles inside it, Escape or a link close it and return focus to the button. The language control points at the same page in the other locale with ?lang=.
Footer.astro repeats a short nav, contact email, and the brand line. “社区共建 / Co-build” is a /go/cobuild/ link (footer_cobuild), not the Join page.
Button.astro has primary (black pill) and secondary (glass pill). Set external only for mailto or true new-tab links. Form and article jumps still go through /go/.
English UI uses Geist Mono (--font-geist-mono). Chinese uses the full Smiley Sans file (about 943 KB) at:
public/assets/fonts/SmileySans-Oblique.ttf.woff2
CSS uses font-display: swap and unicode-range. Chinese pages preload the woff2 file. Vercel caches /assets/fonts/* for one year as immutable. Keep the full font so new Chinese characters on subpages do not go missing. Do not regenerate a site-character subset unless you have a new subsetting pipeline.
public/assets/techflows-logo.png— redirect page and OG cardspublic/assets/techflows-logo-header.png— header mark at display width ~132pxpublic/assets/atom-field.svg— repeating atmosphere tilepublic/assets/og-home.png,og-events.png,og-projects.png,og-partners.png,og-join.png,og-unfino.pngpublic/assets/apple-touch-icon.pngpublic/favicon.png,public/favicon.icoassets/techflows-logo.png— source logo backup, not served
pnpm generate:assets rebuilds OG images, the header logo, and the touch icon from scripts/generate-assets.mjs. After changing the logo, run that command and commit the outputs.
Cache policy in vercel.json:
/_astro/*and fonts: long immutable cache/assets/*: one week, stale-while-revalidate- HTML, OG, favicon, robots, sitemap, llms: shorter caches
src/pages/sitemap.xml.tsemits Chinese and English URLs withlastmod,changefreq,priority, and hreflang. Chinese iszh-CN, matchinghtml lang.public/robots.txtallows search and AI crawlers on public pages and points at the sitemap.src/pages/llms.txt.tsis a short map for assistants. It is generated frompagesinsite.ts, so new pages appear automatically.BaseLayout.astroemits canonical, alternate, Open Graph, Twitter Card, and JSON-LD (Organization,WebSite,WebPage).Organization.descriptionuses the homepage sentence, not the current page meta.- UNFINO uses
/assets/og-unfino.png. Do not reuse the events OG image for that page. src/pages/404.astroisnoindexand links home, events, and join. Locale follows/en/in the path.vercel.jsonpermanently redirects/projects.htmland/unfino.html(and the older.htmlaliases) to the trailing-slash routes.
/go/ and /en/go/ are noindex, nofollow via meta and X-Robots-Tag.
Keep astro.config.mjs, site.url, robots, sitemap, and llms on https://www.techflows.app. Do not mix the apex host and www.
Headers are in vercel.json:
- CSP:
default-src 'self'; scripts from'self','unsafe-inline', andhttps://va.vercel-scripts.com; images from self, data, and https; fonts from self;form-actionallowshttps://techflow.feishu.cn; frames denied X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policydisables unused device APIsCross-Origin-Opener-Policy: same-originX-Permitted-Cross-Domain-Policies: none
'unsafe-inline' exists because a few pages use inline scripts (header, homepage reading mode, UNFINO reveal, /go/ tf_source attach) and JSON-LD. Do not add third-party scripts without updating CSP. Outbound <a> tags that leave the site should keep rel="noopener noreferrer".
Non-GET/HEAD requests to HTML routes receive 405 from middleware.
The production path prefers a static HTML page with almost no client JS. Analytics stay off unless:
PUBLIC_ENABLE_VERCEL_INSIGHTS=true
Turning that on injects Vercel Analytics and Speed Insights, extra script work, and a looser CSP. Leave it off unless you are measuring a real incident.
Limit simultaneous backdrop-filter surfaces. The current set (header, mobile sheet, secondary buttons, footer) is already near the budget. Do not glass long lists.
DDoS protection and HTTP/3 are edge-network features. The static site cannot implement them.
- Enable Managed Rules / WAF.
- Rate-limit public HTML by IP. Prefer Challenge over Deny so crawlers are not blocked by mistake.
- Exclude
/_astro/*,/assets/*,/robots.txt,/sitemap.xml, and/llms.txt. /go/*and/en/go/*can use a tighter limit.- Do not blanket-block normal search or AI crawlers in WAF if
robots.txtallows them.
HTTP/3 availability depends on Vercel and the domain. This repo does not fake protocol headers. After deploy, check the Network Protocol column in DevTools.
Vercel project settings:
- Build command:
pnpm build - Output directory:
dist
Before a production deploy:
pnpm check
pnpm buildThen confirm /, /en/, /unfino/, /en/unfino/, and one /go/join/ page. After deploy from a non-CN network, confirm /events/ stays Chinese and does not set the language cookie. English remains at /en/events/.
GitHub Actions (.github/workflows/check.yml) runs pnpm install --frozen-lockfile, pnpm check, and pnpm build on main and pull requests.
- Run
pnpm checkandpnpm build. - New public page:
site.ts, both locale routes, sitemap. Middleware picks up Chinese paths frompages. - New outbound target: slug in
forms.ts, thengoPath(), never a raw Feishu URL in a page button. - Copy changes need both
zhanden. Unused translation keys failpnpm check. - Layout changes stay on
.index-list. Do not wrap body copy in glass cards. - Update
site.lastUpdatedwhen public content changes. - Do not commit
node_modules/,dist/,.astro/,.claude/,scripts/__pycache__/, or QA screenshots and planning drafts underdocs/.