A website of free tools and blogs.
Personal portfolio + blog built with Next.js 15 (App Router, SSR, standalone output), React 19, TypeScript, Tailwind CSS, and Framer Motion. Releases ship as a self-installing tarball with a systemd service and a GitHub-poll auto-updater.
npm ci
npm run dev # http://localhost:3000Production build:
npm run build && npm start| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router, SSR, output: 'standalone') |
| UI | React 19, Tailwind CSS, Framer Motion |
| Language | TypeScript 5 |
| Hosting | Self-hosted SSR behind Cloudflare (app assets served from origin); project showcase content on S3 + CloudFront at assets.devshram.com/projects/* |
| Releases | semantic-release on main |
| Distribution | Tarball with systemd installer |
- React components:
PascalCase.tsx(UserProfile.tsx) - Pages:
page.tsx(App Router) - Utilities:
camelCase.ts - Type-only files:
types.tsorComponentName.types.ts
interface ComponentProps {
// typed props, no `any`
}
export default function Component({ prop }: ComponentProps) {
return <div>...</div>;
}- React
- Next.js
- Third-party
- Local (
@/components,@/utils, ...)
- Define
interfacefor object shapes;typefor unions/primitives. - Strict null checks; no
any. - Export types that callers may reuse.
- Tailwind first; custom CSS only when Tailwind can't express it.
- Mobile-first responsive (
sm:,md:,lg:). - Never pass
z-indexas a prop — express layering via component structure and Tailwindz-*classes.
- Semantic HTML, ARIA labels where needed.
- Keyboard reachable.
- Color contrast: WCAG AA minimum.
- React hooks for local state.
- Shared state lives in
src/state/; follow existing patterns.
- Use
next/imagefor images. - Code-split heavy components; the
optimizePackageImportshint innext.config.tsalready tree-shakes Framer Motion per-import.
The portfolio uses Framer Motion. Reusable wrappers live in src/components/:
| Component | Purpose |
|---|---|
AnimatedSection |
Scroll-triggered enter animation. Props: direction (up/down/left/right), delay, duration, distance. |
AnimatedText |
Three modes via type: fade, typewriter, slide. |
AnimatedCard |
Entrance + hover lift/scale. |
AnimatedLoader |
Rotating spinner; size and color configurable. |
useInViewwithonce: trueso sections animate only on first scroll into view.- Prefer transforms (GPU-accelerated) over layout-changing properties.
- Stagger children with small
delayincrements to guide attention.
<AnimatedSection delay={0.2} direction="up">
<YourContent />
</AnimatedSection>
<AnimatedText text="Hello World" type="typewriter" delay={0.5} />
<AnimatedCard delay={0.3} hover>
<CardContent />
</AnimatedCard>semantic-release on push to main analyzes commit messages, bumps version, updates CHANGELOG.md, creates a git tag, and creates a GitHub release with the SSR tarball asset attached.
Configured in .releaserc.json. The release flow lives in .github/workflows/release.yml.
The version bump is determined by commit type:
| Type | Bump | Example |
|---|---|---|
fix: |
patch (0.1.0 → 0.1.1) | fix(auth): resolve login timeout |
feat: |
minor (0.1.0 → 0.2.0) | feat: add dark mode toggle |
feat!: / BREAKING CHANGE: |
major | feat(api)!: change response format |
chore:, docs:, style:, refactor:, test:, ci: |
none | chore: bump deps |
Commits are validated by commitlint via Husky. Use npm run commit for an interactive prompt:
git add .
npm run commit- GitHub release:
devshram-ssr-v<X.Y.Z>.tar.gzasset (self-installing — see Deployment). - Bundled static assets: the tarball ships
.next/static/andpublic/alongside the standalone server, which serves them at the origin. Cloudflare proxies the origin and caches at the edge — no separate CDN host, no upload step. Hashed.next/static/*filenames mean no cache invalidation is needed across versions.
Each release publishes a self-installing tarball as a GitHub release asset. Install on any systemd Linux (Debian, Ubuntu, Fedora, RHEL, Arch, openSUSE…) with one script. The installer registers a systemd service plus a self-update timer that polls GitHub releases every 5 minutes and applies new versions automatically.
- Linux with systemd
- Node.js 20+ in
$PATH. Debian 12 ships Node 18 — install from NodeSource first. curl,tar,flock(standard on every supported distro)- 512 MB RAM minimum (1 GB recommended)
One-liner — installs the latest release:
curl -fsSL https://raw.githubusercontent.com/avi892nash/devshram/main/packaging/install-latest.sh | sudo bashThe bootstrap script queries https://api.github.com/repos/avi892nash/devshram/releases/latest, downloads the tarball, extracts it, and runs the embedded install.sh. Source: packaging/install-latest.sh.
You only run this once. After the first install, the
devshram-update.timersystemd unit polls GitHub every 5 minutes and applies new releases automatically. Don't re-run this script to upgrade — see Auto-update below.
If you'd rather not pipe curl into a shell, do it manually — pick a version from the releases page and:
VERSION=0.0.18
curl -fsSLO "https://github.com/avi892nash/devshram/releases/download/v${VERSION}/devshram-ssr-v${VERSION}.tar.gz"
tar -xzf "devshram-ssr-v${VERSION}.tar.gz"
sudo bash devshram/install.shEither way, after install:
- Service:
devshram.service(logs viajournalctl -u devshram) - Auto-update timer:
devshram-update.timer(polls every 5 min; logs viajournalctl -u devshram-update) - Server: http://localhost:8888
| Path | Purpose |
|---|---|
/usr/lib/devshram/ |
Standalone Next.js server + minimal node_modules |
/etc/devshram/.env |
Editable config (PORT, NODE_ENV, HOSTNAME) |
/var/lib/devshram/installed-tag |
Currently-installed release tag (read by the updater) |
/etc/systemd/system/devshram*.service |
Service + updater oneshot |
/etc/systemd/system/devshram-update.timer |
5-minute poll timer |
/usr/local/bin/devshram-update |
Updater script |
Edit /etc/devshram/.env and restart the service:
sudo systemctl restart devshramDefaults:
NODE_ENV=production
PORT=8888
HOSTNAME=0.0.0.0
Enabled by default. The timer fires 2 min after boot and every 5 min after, queries /repos/avi892nash/devshram/releases/latest, and runs install.sh --upgrade if a newer tag exists.
# Disable
sudo systemctl disable --now devshram-update.timer
# Force a check now
sudo systemctl start devshram-update.service
sudo journalctl -u devshram-update -n 30The updater is unauthenticated — GitHub's 60 req/h IP limit covers a 5-minute timer on a single host.
sudo bash devshram/uninstall.sh # keeps /etc/devshram and /var/lib/devshram
sudo bash devshram/uninstall.sh --purge # removes config, state, and the system user# Service status & logs
sudo systemctl status devshram
sudo journalctl -u devshram -f
# Updater logs
sudo journalctl -u devshram-update -n 50
# Currently-installed tag
sudo cat /var/lib/devshram/installed-tag
# Next scheduled update poll
systemctl list-timers devshram-update.timerPort already in use — edit /etc/devshram/.env, change PORT=8888, sudo systemctl restart devshram.
Updater stuck on an old version — check journalctl -u devshram-update -n 100 for HTTP errors (rate limits, network). Force: sudo systemctl start devshram-update.service.
| File | Trigger | What it does |
|---|---|---|
| .github/workflows/release.yml | Push to main | Compile check → semantic-release (tag, CHANGELOG, GitHub release) → production build → tarball → upload tarball asset to the release |
| .github/workflows/smoke-test.yml | PR to main + after release.yml on main |
Matrix install of the tarball (Node 20, 22, 24) — sudo bash install.sh, HTTP probe, re-run installer for idempotency, --purge |
| .github/workflows/pr-test.yml | PR to main | Build, lint, type-check, dev-server route smoke (/, /about-me/, /projects/, …) |
smoke-test.yml actually installs the tarball on the runner via sudo bash install.sh, hits the service over HTTP, re-runs the installer to verify idempotency, then --purges. On PRs it gates the merge; after a push to main it gates whether the just-cut release is healthy.
The packaging scripts that get tested live in packaging/:
install.sh/uninstall.sh— root-elevating, idempotent, both runnable standalone (shipped inside the tarball).install-latest.sh— bootstrap for first-time installs. Curl-pipeable; queries the GitHub API, fetches the latest tarball, runsinstall.sh.build-tarball.sh— producesdevshram-ssr-v<X>.tar.gz. HonorsSKIP_BUILD=1so CI can reuse a build it already ran.bin/devshram-update— the bash GitHub poller dropped at/usr/local/bin/devshram-update.systemd/*.{service,timer}— installed at/etc/systemd/system/.
The portfolio takes structured project metadata (title, description, tech, category, theme, links, image). To extract that from a free-form description, paste the prompt below into any AI assistant along with your project info.
{
"title": "Project name (concise, clear)",
"description": "One-sentence description (max 100 chars)",
"technologies": ["Array", "of", "technologies"],
"category": "complete-app | small-project | blog | tool",
"theme": "purple | green | brown | dark",
"featured": true,
"liveLink": "https://assets.devshram.com/projects/{slug}/index.html",
"githubLink": "https://github.com/...",
"figmaLink": "(optional)",
"cachedLink": "(optional)",
"image": "https://assets.devshram.com/projects/{slug}/main.png"
}- category —
complete-appfor full apps,small-projectfor prototypes,blogfor articles,toolfor utilities/CLIs. - theme —
purple(AI/ML),green(productivity, devtools),brown(general web),dark(portfolio/meta). - featured —
trueonly for standout work. - liveLink / image —
{slug}must match the GitHub workflow YAML filename for that project. - Omit
cachedLink,githubLink,figmaLinkwhen not applicable — don't pass empty strings.
I need to add a project to my portfolio. Analyze the following and produce
JSON matching the schema below. Respond with ONLY the JSON.
[PASTE PROJECT INFO — repo URL, README, description, or any combination]
Schema:
{
"title": "...",
"description": "max 100 chars",
"technologies": ["..."],
"category": "complete-app | small-project | blog | tool",
"theme": "purple | green | brown | dark",
"featured": true | false,
"liveLink": "https://assets.devshram.com/projects/{slug}/index.html",
"githubLink": "(optional)",
"figmaLink": "(optional)",
"cachedLink": "(optional)",
"image": "https://assets.devshram.com/projects/{slug}/main.png"
}