Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions app/early-stage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import matter from 'gray-matter'
import { marked } from 'marked'
import Link from 'next/link'
import Image from 'next/image'
import Script from 'next/script'
import { ArrowLeft, Github, ExternalLink, ChevronRight } from 'lucide-react'

export const runtime = 'nodejs'
Expand Down Expand Up @@ -59,7 +60,6 @@ const containsAny = (hay: string, items: string[]) => items.some((p) => contains
function classify(meta: ResourceMeta): 'tooling' | 'courses' | 'guides' | 'code' | 'other' {
const hay = haystack(meta)

// Tooling Docs (official docs, references, install guides, APIs, CLI)
const toolingSignals = [
'docs','documentation','reference','api reference','api docs',
'handbook','manual','spec','specification','readme','guide (cli)',
Expand All @@ -68,23 +68,17 @@ function classify(meta: ResourceMeta): 'tooling' | 'courses' | 'guides' | 'code'
'hardhat','foundry','anvil','remix','metamask','phantom',
'solana cli','anchor','etherscan','solscan','node.js','npm','nvm',
]

// Courses (programs, bootcamps, MOOCs)
const courseSignals = [
'course','courses','bootcamp','curriculum','syllabus','academy',
'track','learning path','program','cohort','mooc','udemy','coursera','edx',
'university','certificate'
]

// Code Samples (templates, starters, examples, scaffolds)
const codeSignals = [
'example','examples','sample','samples','snippet','snippets',
'template','templates','boilerplate','boilerplates','starter','starter kit',
'scaffold','scaffolding','reference implementation','repo','github repository',
'playground'
]

// Guides (how-tos, tutorials, deep dives, best practices, articles)
const guideSignals = [
'guide','how to','how-to','tutorial','walkthrough','step by step','step-by-step',
'deep dive','best practices','explained','overview','article','blog','cookbook',
Expand Down Expand Up @@ -143,7 +137,6 @@ async function getEarlyResources(): Promise<(ResourceMeta & { key: string; secti
})
}

// newest first
list.sort((a, b) => (new Date(b.dateAdded || 0).getTime()) - (new Date(a.dateAdded || 0).getTime()))
return list
}
Expand Down Expand Up @@ -201,6 +194,8 @@ export default async function EarlyStagePage() {
</section>
) : null

const listUrl = 'https://twitter.com/i/lists/1959973436228288972'

return (
<div className="min-h-screen bg-black">
{/* Contained Banner */}
Expand Down Expand Up @@ -250,6 +245,66 @@ export default async function EarlyStagePage() {
<Section title="Code Samples" items={codeSamples} />
{other.length ? <Section title="Other" items={other} /> : null}

<section className="mb-12">
<h2 className="text-lg md:text-xl font-display font-semibold text-white mb-4">
Selected voices to boost your dev learning on X
</h2>

<div className="rounded-lg border border-white/10 bg-[#1a1a1a] p-4">
<div className="mb-3 text-sm text-white/70">
Curated builders & educators worth following{' '}
<a
className="text-[#73FDEA] hover:text-[#FF00AA]"
href="https://twitter.com/i/lists/1959973436228288972"
target="_blank"
rel="noopener noreferrer"
>
(open on X)
</a>.
</div>

{/* Programmatic timeline container */}
<div id="x-list" />

{/* Hidden fallback hint */}
<div id="x-fallback" className="hidden mt-3 text-xs text-white/60">
If you see “Nothing to see here”, enable third-party cookies or disable tracker blockers
for this site, then refresh. You can also open the list directly on X.
</div>
</div>

{/* Load widgets.js */}
<Script src="https://platform.twitter.com/widgets.js" strategy="afterInteractive" />

{/* Create the timeline and reveal fallback if the iframe never mounts */}
<Script id="x-init" strategy="afterInteractive">
{`
(function initList(){
function mount() {
var el = document.getElementById('x-list');
if (!window.twttr || !window.twttr.widgets || !el) return setTimeout(mount, 80);
if (el.dataset.mounted) return;
el.dataset.mounted = '1';
window.twttr.widgets.createTimeline(
{ sourceType: 'list', id: '1959973436228288972' },
el,
{ theme: 'dark', height: 620, dnt: true }
);
// If no iframe appears, show a helpful hint
setTimeout(function(){
if (!el.querySelector('iframe')) {
var fb = document.getElementById('x-fallback');
if (fb) fb.classList.remove('hidden');
}
}, 3000);
}
mount();
})();
`}
</Script>
</section>


{/* CTA */}
<div className="mt-12 bg-gradient-to-r from-[#8E1CF1] to-[#FF00AA] rounded-lg p-8 text-center">
<h3 className="text-2xl font-display font-bold text-white mb-4">Share Your Knowledge</h3>
Expand Down
35 changes: 32 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,39 @@ const nextConfig = {
experimental: {
appDir: true,
},

// Add CSP headers so the X/Twitter list embed can load
async headers() {
const csp = [
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://platform.twitter.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://cdn.syndication.twimg.com https://abs.twimg.com https://pbs.twimg.com",
"frame-src https://platform.twitter.com https://twitter.com https://syndication.twitter.com",
"connect-src 'self' https://cdn.syndication.twimg.com https://api.twitter.com",
"font-src 'self' data: https://abs.twimg.com",
].join('; ');

return [
{
source: "/(.*)",
headers: [{ key: "Content-Security-Policy", value: csp }],
},
];
},


// Keep your existing image settings and add Twitter domains
images: {
domains: ['images.unsplash.com', 'github.com'],
domains: [
'images.unsplash.com',
'github.com',
// add Twitter image/CDN domains for any <Image> usage inside your UI
'cdn.syndication.twimg.com',
'abs.twimg.com',
'pbs.twimg.com',
],
unoptimized: true,
},
}
};

module.exports = nextConfig
module.exports = nextConfig;
Loading