···11+# AT Protocol Link Shortener Configuration
22+#
33+# Your AT Protocol DID (e.g., did:plc:abc123xyz or did:web:example.com)
44+# Find your DID at: https://pdsls.dev/
55+#
66+# This is the only required configuration!
77+# The service will automatically:
88+# - Resolve your PDS endpoint
99+# - Fetch your Linkat board data
1010+# - Create short links from your link titles
1111+#
1212+# Test your configuration with: npm run test:config
1313+1414+ATPROTO_DID=
···11+# Code Architecture
22+33+This document describes the modular architecture of the AT Protocol Link Shortener.
44+55+## Directory Structure
66+77+```
88+src/
99+├── lib/
1010+│ ├── constants.ts # Application-wide constants
1111+│ ├── index.ts # Main library exports
1212+│ ├── services/
1313+│ │ ├── atproto/ # AT Protocol services
1414+│ │ │ ├── agent-factory.ts # Agent creation utilities
1515+│ │ │ ├── agent-manager.ts # Agent caching and fallback
1616+│ │ │ ├── identity-resolver.ts # Slingshot DID resolution
1717+│ │ │ └── index.ts # Module exports
1818+│ │ ├── cache/ # Caching utilities
1919+│ │ │ └── index.ts # Generic TTL cache
2020+│ │ ├── linkat/ # Linkat service
2121+│ │ │ ├── fetcher.ts # Raw board fetching
2222+│ │ │ ├── generator.ts # Shortcode generation
2323+│ │ │ └── index.ts # Main service with caching
2424+│ │ ├── agent.ts # Backwards compatibility
2525+│ │ ├── linkat.ts # Backwards compatibility
2626+│ │ └── types.ts # Shared type definitions
2727+│ └── utils/
2828+│ └── encoding.ts # URL encoding utilities
2929+└── routes/
3030+ ├── +layout.svelte # Global layout with dark mode
3131+ ├── +page.server.ts # Homepage server logic
3232+ ├── +page.svelte # Homepage UI
3333+ ├── [shortcode]/
3434+ │ └── +server.ts # Redirect handler
3535+ ├── api/
3636+ │ └── links/
3737+ │ └── +server.ts # Links API endpoint
3838+ └── favicon/
3939+ └── favicon.ico/
4040+ └── +server.ts # Favicon handler
4141+```
4242+4343+## Module Responsibilities
4444+4545+### Constants (`lib/constants.ts`)
4646+4747+Central location for all configuration values:
4848+4949+- Cache settings (TTL, prefixes)
5050+- Shortcode configuration (length, base62 chars)
5151+- AT Protocol endpoints (Slingshot, public API)
5252+- HTTP status codes
5353+5454+**Benefits:**
5555+5656+- Single source of truth
5757+- Easy to modify configuration
5858+- Type-safe constants
5959+6060+### AT Protocol Services (`lib/services/atproto/`)
6161+6262+#### `agent-factory.ts`
6363+6464+- Creates AtpAgent instances
6565+- Handles fetch function injection for server-side contexts
6666+- Wraps fetch to ensure proper headers
6767+6868+#### `identity-resolver.ts`
6969+7070+- Resolves DIDs to PDS endpoints via Slingshot
7171+- Error handling and logging
7272+- Uses constants for endpoint configuration
7373+7474+#### `agent-manager.ts`
7575+7676+- Manages agent lifecycle and caching
7777+- Provides fallback logic (PDS → public API)
7878+- Exports helper functions for common operations
7979+8080+**Benefits:**
8181+8282+- Separation of concerns
8383+- Easy to test individual components
8484+- Reusable across different contexts
8585+8686+### Cache Service (`lib/services/cache/`)
8787+8888+Generic TTL-based cache implementation:
8989+9090+- Set/get/delete operations
9191+- Automatic expiration
9292+- Cache pruning utility
9393+- Type-safe generic interface
9494+9595+**Benefits:**
9696+9797+- Reusable for any cached data
9898+- Not tied to specific use case
9999+- Clean API with proper TypeScript support
100100+101101+### Linkat Service (`lib/services/linkat/`)
102102+103103+#### `fetcher.ts`
104104+105105+- Raw Linkat board data fetching
106106+- AT Protocol record retrieval
107107+- Data validation
108108+109109+#### `generator.ts`
110110+111111+- Shortcode generation from URLs
112112+- Collision handling
113113+- Link search functionality
114114+115115+#### `index.ts`
116116+117117+- Main service interface
118118+- Combines fetching + generation
119119+- Implements caching layer
120120+121121+**Benefits:**
122122+123123+- Pure functions in fetcher and generator (easy to test)
124124+- Side effects isolated to main service
125125+- Clear data flow
126126+127127+### Utilities (`lib/utils/`)
128128+129129+#### `encoding.ts`
130130+131131+- URL to shortcode encoding
132132+- Base62 conversion
133133+- Validation helpers
134134+135135+**Benefits:**
136136+137137+- Stateless utility functions
138138+- Reusable across application
139139+- Easy to unit test
140140+141141+## Backwards Compatibility
142142+143143+The old `agent.ts` and `linkat.ts` files remain as re-export wrappers:
144144+145145+- Existing imports continue to work
146146+- No breaking changes to route handlers
147147+- Smooth migration path
148148+149149+## Design Principles
150150+151151+### 1. Single Responsibility
152152+153153+Each module has one clear purpose:
154154+155155+- `agent-factory`: Create agents
156156+- `identity-resolver`: Resolve DIDs
157157+- `cache`: Cache data
158158+159159+### 2. Dependency Injection
160160+161161+- Fetch functions can be injected
162162+- Agents are created with custom configs
163163+- Makes testing easier
164164+165165+### 3. Separation of Concerns
166166+167167+- Pure logic in utilities
168168+- Side effects in services
169169+- UI in routes
170170+171171+### 4. Type Safety
172172+173173+- Explicit TypeScript types
174174+- Shared type definitions
175175+- Constants with `as const`
176176+177177+### 5. Testability
178178+179179+- Pure functions are easy to test
180180+- Services use dependency injection
181181+- Clear interfaces
182182+183183+## Import Patterns
184184+185185+### Using the main library export:
186186+187187+```typescript
188188+import { getShortLinks, encodeUrl, CACHE } from '$lib';
189189+```
190190+191191+### Using specific modules:
192192+193193+```typescript
194194+import { getPublicAgent } from '$lib/services/atproto';
195195+import { Cache } from '$lib/services/cache';
196196+```
197197+198198+### Using backwards-compatible imports:
199199+200200+```typescript
201201+import { createAgentForDID } from '$lib/services/agent';
202202+import { findShortLink } from '$lib/services/linkat';
203203+```
204204+205205+## Future Improvements
206206+207207+### Easy to Add:
208208+209209+1. **Database caching** - Replace in-memory cache
210210+2. **Custom shortcodes** - Extend generator
211211+3. **Analytics** - Add tracking module
212212+4. **Rate limiting** - Add middleware
213213+5. **Multiple DID support** - Extend agent manager
214214+215215+### Testing Strategy:
216216+217217+1. **Unit tests** for utils and pure functions
218218+2. **Integration tests** for services
219219+3. **E2E tests** for routes
220220+221221+## Configuration
222222+223223+All configuration is centralized in `constants.ts`:
224224+225225+- Change cache TTL in one place
226226+- Update API endpoints easily
227227+- Modify shortcode length globally
228228+229229+## Error Handling
230230+231231+Consistent error handling pattern:
232232+233233+1. Log errors with context
234234+2. Throw with descriptive messages
235235+3. Fallback to sensible defaults
236236+4. Surface errors to users when appropriate
+661
LICENCE
···11+ GNU AFFERO GENERAL PUBLIC LICENSE
22+ Version 3, 19 November 2007
33+44+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
55+ Everyone is permitted to copy and distribute verbatim copies
66+ of this license document, but changing it is not allowed.
77+88+ Preamble
99+1010+ The GNU Affero General Public License is a free, copyleft license for
1111+software and other kinds of works, specifically designed to ensure
1212+cooperation with the community in the case of network server software.
1313+1414+ The licenses for most software and other practical works are designed
1515+to take away your freedom to share and change the works. By contrast,
1616+our General Public Licenses are intended to guarantee your freedom to
1717+share and change all versions of a program--to make sure it remains free
1818+software for all its users.
1919+2020+ When we speak of free software, we are referring to freedom, not
2121+price. Our General Public Licenses are designed to make sure that you
2222+have the freedom to distribute copies of free software (and charge for
2323+them if you wish), that you receive source code or can get it if you
2424+want it, that you can change the software or use pieces of it in new
2525+free programs, and that you know you can do these things.
2626+2727+ Developers that use our General Public Licenses protect your rights
2828+with two steps: (1) assert copyright on the software, and (2) offer
2929+you this License which gives you legal permission to copy, distribute
3030+and/or modify the software.
3131+3232+ A secondary benefit of defending all users' freedom is that
3333+improvements made in alternate versions of the program, if they
3434+receive widespread use, become available for other developers to
3535+incorporate. Many developers of free software are heartened and
3636+encouraged by the resulting cooperation. However, in the case of
3737+software used on network servers, this result may fail to come about.
3838+The GNU General Public License permits making a modified version and
3939+letting the public access it on a server without ever releasing its
4040+source code to the public.
4141+4242+ The GNU Affero General Public License is designed specifically to
4343+ensure that, in such cases, the modified source code becomes available
4444+to the community. It requires the operator of a network server to
4545+provide the source code of the modified version running there to the
4646+users of that server. Therefore, public use of a modified version, on
4747+a publicly accessible server, gives the public access to the source
4848+code of the modified version.
4949+5050+ An older license, called the Affero General Public License and
5151+published by Affero, was designed to accomplish similar goals. This is
5252+a different license, not a version of the Affero GPL, but Affero has
5353+released a new version of the Affero GPL which permits relicensing under
5454+this license.
5555+5656+ The precise terms and conditions for copying, distribution and
5757+modification follow.
5858+5959+ TERMS AND CONDITIONS
6060+6161+ 0. Definitions.
6262+6363+ "This License" refers to version 3 of the GNU Affero General Public License.
6464+6565+ "Copyright" also means copyright-like laws that apply to other kinds of
6666+works, such as semiconductor masks.
6767+6868+ "The Program" refers to any copyrightable work licensed under this
6969+License. Each licensee is addressed as "you". "Licensees" and
7070+"recipients" may be individuals or organizations.
7171+7272+ To "modify" a work means to copy from or adapt all or part of the work
7373+in a fashion requiring copyright permission, other than the making of an
7474+exact copy. The resulting work is called a "modified version" of the
7575+earlier work or a work "based on" the earlier work.
7676+7777+ A "covered work" means either the unmodified Program or a work based
7878+on the Program.
7979+8080+ To "propagate" a work means to do anything with it that, without
8181+permission, would make you directly or secondarily liable for
8282+infringement under applicable copyright law, except executing it on a
8383+computer or modifying a private copy. Propagation includes copying,
8484+distribution (with or without modification), making available to the
8585+public, and in some countries other activities as well.
8686+8787+ To "convey" a work means any kind of propagation that enables other
8888+parties to make or receive copies. Mere interaction with a user through
8989+a computer network, with no transfer of a copy, is not conveying.
9090+9191+ An interactive user interface displays "Appropriate Legal Notices"
9292+to the extent that it includes a convenient and prominently visible
9393+feature that (1) displays an appropriate copyright notice, and (2)
9494+tells the user that there is no warranty for the work (except to the
9595+extent that warranties are provided), that licensees may convey the
9696+work under this License, and how to view a copy of this License. If
9797+the interface presents a list of user commands or options, such as a
9898+menu, a prominent item in the list meets this criterion.
9999+100100+ 1. Source Code.
101101+102102+ The "source code" for a work means the preferred form of the work
103103+for making modifications to it. "Object code" means any non-source
104104+form of a work.
105105+106106+ A "Standard Interface" means an interface that either is an official
107107+standard defined by a recognized standards body, or, in the case of
108108+interfaces specified for a particular programming language, one that
109109+is widely used among developers working in that language.
110110+111111+ The "System Libraries" of an executable work include anything, other
112112+than the work as a whole, that (a) is included in the normal form of
113113+packaging a Major Component, but which is not part of that Major
114114+Component, and (b) serves only to enable use of the work with that
115115+Major Component, or to implement a Standard Interface for which an
116116+implementation is available to the public in source code form. A
117117+"Major Component", in this context, means a major essential component
118118+(kernel, window system, and so on) of the specific operating system
119119+(if any) on which the executable work runs, or a compiler used to
120120+produce the work, or an object code interpreter used to run it.
121121+122122+ The "Corresponding Source" for a work in object code form means all
123123+the source code needed to generate, install, and (for an executable
124124+work) run the object code and to modify the work, including scripts to
125125+control those activities. However, it does not include the work's
126126+System Libraries, or general-purpose tools or generally available free
127127+programs which are used unmodified in performing those activities but
128128+which are not part of the work. For example, Corresponding Source
129129+includes interface definition files associated with source files for
130130+the work, and the source code for shared libraries and dynamically
131131+linked subprograms that the work is specifically designed to require,
132132+such as by intimate data communication or control flow between those
133133+subprograms and other parts of the work.
134134+135135+ The Corresponding Source need not include anything that users
136136+can regenerate automatically from other parts of the Corresponding
137137+Source.
138138+139139+ The Corresponding Source for a work in source code form is that
140140+same work.
141141+142142+ 2. Basic Permissions.
143143+144144+ All rights granted under this License are granted for the term of
145145+copyright on the Program, and are irrevocable provided the stated
146146+conditions are met. This License explicitly affirms your unlimited
147147+permission to run the unmodified Program. The output from running a
148148+covered work is covered by this License only if the output, given its
149149+content, constitutes a covered work. This License acknowledges your
150150+rights of fair use or other equivalent, as provided by copyright law.
151151+152152+ You may make, run and propagate covered works that you do not
153153+convey, without conditions so long as your license otherwise remains
154154+in force. You may convey covered works to others for the sole purpose
155155+of having them make modifications exclusively for you, or provide you
156156+with facilities for running those works, provided that you comply with
157157+the terms of this License in conveying all material for which you do
158158+not control copyright. Those thus making or running the covered works
159159+for you must do so exclusively on your behalf, under your direction
160160+and control, on terms that prohibit them from making any copies of
161161+your copyrighted material outside their relationship with you.
162162+163163+ Conveying under any other circumstances is permitted solely under
164164+the conditions stated below. Sublicensing is not allowed; section 10
165165+makes it unnecessary.
166166+167167+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168168+169169+ No covered work shall be deemed part of an effective technological
170170+measure under any applicable law fulfilling obligations under article
171171+11 of the WIPO copyright treaty adopted on 20 December 1996, or
172172+similar laws prohibiting or restricting circumvention of such
173173+measures.
174174+175175+ When you convey a covered work, you waive any legal power to forbid
176176+circumvention of technological measures to the extent such circumvention
177177+is effected by exercising rights under this License with respect to
178178+the covered work, and you disclaim any intention to limit operation or
179179+modification of the work as a means of enforcing, against the work's
180180+users, your or third parties' legal rights to forbid circumvention of
181181+technological measures.
182182+183183+ 4. Conveying Verbatim Copies.
184184+185185+ You may convey verbatim copies of the Program's source code as you
186186+receive it, in any medium, provided that you conspicuously and
187187+appropriately publish on each copy an appropriate copyright notice;
188188+keep intact all notices stating that this License and any
189189+non-permissive terms added in accord with section 7 apply to the code;
190190+keep intact all notices of the absence of any warranty; and give all
191191+recipients a copy of this License along with the Program.
192192+193193+ You may charge any price or no price for each copy that you convey,
194194+and you may offer support or warranty protection for a fee.
195195+196196+ 5. Conveying Modified Source Versions.
197197+198198+ You may convey a work based on the Program, or the modifications to
199199+produce it from the Program, in the form of source code under the
200200+terms of section 4, provided that you also meet all of these conditions:
201201+202202+ a) The work must carry prominent notices stating that you modified
203203+ it, and giving a relevant date.
204204+205205+ b) The work must carry prominent notices stating that it is
206206+ released under this License and any conditions added under section
207207+ 7. This requirement modifies the requirement in section 4 to
208208+ "keep intact all notices".
209209+210210+ c) You must license the entire work, as a whole, under this
211211+ License to anyone who comes into possession of a copy. This
212212+ License will therefore apply, along with any applicable section 7
213213+ additional terms, to the whole of the work, and all its parts,
214214+ regardless of how they are packaged. This License gives no
215215+ permission to license the work in any other way, but it does not
216216+ invalidate such permission if you have separately received it.
217217+218218+ d) If the work has interactive user interfaces, each must display
219219+ Appropriate Legal Notices; however, if the Program has interactive
220220+ interfaces that do not display Appropriate Legal Notices, your
221221+ work need not make them do so.
222222+223223+ A compilation of a covered work with other separate and independent
224224+works, which are not by their nature extensions of the covered work,
225225+and which are not combined with it such as to form a larger program,
226226+in or on a volume of a storage or distribution medium, is called an
227227+"aggregate" if the compilation and its resulting copyright are not
228228+used to limit the access or legal rights of the compilation's users
229229+beyond what the individual works permit. Inclusion of a covered work
230230+in an aggregate does not cause this License to apply to the other
231231+parts of the aggregate.
232232+233233+ 6. Conveying Non-Source Forms.
234234+235235+ You may convey a covered work in object code form under the terms
236236+of sections 4 and 5, provided that you also convey the
237237+machine-readable Corresponding Source under the terms of this License,
238238+in one of these ways:
239239+240240+ a) Convey the object code in, or embodied in, a physical product
241241+ (including a physical distribution medium), accompanied by the
242242+ Corresponding Source fixed on a durable physical medium
243243+ customarily used for software interchange.
244244+245245+ b) Convey the object code in, or embodied in, a physical product
246246+ (including a physical distribution medium), accompanied by a
247247+ written offer, valid for at least three years and valid for as
248248+ long as you offer spare parts or customer support for that product
249249+ model, to give anyone who possesses the object code either (1) a
250250+ copy of the Corresponding Source for all the software in the
251251+ product that is covered by this License, on a durable physical
252252+ medium customarily used for software interchange, for a price no
253253+ more than your reasonable cost of physically performing this
254254+ conveying of source, or (2) access to copy the
255255+ Corresponding Source from a network server at no charge.
256256+257257+ c) Convey individual copies of the object code with a copy of the
258258+ written offer to provide the Corresponding Source. This
259259+ alternative is allowed only occasionally and noncommercially, and
260260+ only if you received the object code with such an offer, in accord
261261+ with subsection 6b.
262262+263263+ d) Convey the object code by offering access from a designated
264264+ place (gratis or for a charge), and offer equivalent access to the
265265+ Corresponding Source in the same way through the same place at no
266266+ further charge. You need not require recipients to copy the
267267+ Corresponding Source along with the object code. If the place to
268268+ copy the object code is a network server, the Corresponding Source
269269+ may be on a different server (operated by you or a third party)
270270+ that supports equivalent copying facilities, provided you maintain
271271+ clear directions next to the object code saying where to find the
272272+ Corresponding Source. Regardless of what server hosts the
273273+ Corresponding Source, you remain obligated to ensure that it is
274274+ available for as long as needed to satisfy these requirements.
275275+276276+ e) Convey the object code using peer-to-peer transmission, provided
277277+ you inform other peers where the object code and Corresponding
278278+ Source of the work are being offered to the general public at no
279279+ charge under subsection 6d.
280280+281281+ A separable portion of the object code, whose source code is excluded
282282+from the Corresponding Source as a System Library, need not be
283283+included in conveying the object code work.
284284+285285+ A "User Product" is either (1) a "consumer product", which means any
286286+tangible personal property which is normally used for personal, family,
287287+or household purposes, or (2) anything designed or sold for incorporation
288288+into a dwelling. In determining whether a product is a consumer product,
289289+doubtful cases shall be resolved in favor of coverage. For a particular
290290+product received by a particular user, "normally used" refers to a
291291+typical or common use of that class of product, regardless of the status
292292+of the particular user or of the way in which the particular user
293293+actually uses, or expects or is expected to use, the product. A product
294294+is a consumer product regardless of whether the product has substantial
295295+commercial, industrial or non-consumer uses, unless such uses represent
296296+the only significant mode of use of the product.
297297+298298+ "Installation Information" for a User Product means any methods,
299299+procedures, authorization keys, or other information required to install
300300+and execute modified versions of a covered work in that User Product from
301301+a modified version of its Corresponding Source. The information must
302302+suffice to ensure that the continued functioning of the modified object
303303+code is in no case prevented or interfered with solely because
304304+modification has been made.
305305+306306+ If you convey an object code work under this section in, or with, or
307307+specifically for use in, a User Product, and the conveying occurs as
308308+part of a transaction in which the right of possession and use of the
309309+User Product is transferred to the recipient in perpetuity or for a
310310+fixed term (regardless of how the transaction is characterized), the
311311+Corresponding Source conveyed under this section must be accompanied
312312+by the Installation Information. But this requirement does not apply
313313+if neither you nor any third party retains the ability to install
314314+modified object code on the User Product (for example, the work has
315315+been installed in ROM).
316316+317317+ The requirement to provide Installation Information does not include a
318318+requirement to continue to provide support service, warranty, or updates
319319+for a work that has been modified or installed by the recipient, or for
320320+the User Product in which it has been modified or installed. Access to a
321321+network may be denied when the modification itself materially and
322322+adversely affects the operation of the network or violates the rules and
323323+protocols for communication across the network.
324324+325325+ Corresponding Source conveyed, and Installation Information provided,
326326+in accord with this section must be in a format that is publicly
327327+documented (and with an implementation available to the public in
328328+source code form), and must require no special password or key for
329329+unpacking, reading or copying.
330330+331331+ 7. Additional Terms.
332332+333333+ "Additional permissions" are terms that supplement the terms of this
334334+License by making exceptions from one or more of its conditions.
335335+Additional permissions that are applicable to the entire Program shall
336336+be treated as though they were included in this License, to the extent
337337+that they are valid under applicable law. If additional permissions
338338+apply only to part of the Program, that part may be used separately
339339+under those permissions, but the entire Program remains governed by
340340+this License without regard to the additional permissions.
341341+342342+ When you convey a copy of a covered work, you may at your option
343343+remove any additional permissions from that copy, or from any part of
344344+it. (Additional permissions may be written to require their own
345345+removal in certain cases when you modify the work.) You may place
346346+additional permissions on material, added by you to a covered work,
347347+for which you have or can give appropriate copyright permission.
348348+349349+ Notwithstanding any other provision of this License, for material you
350350+add to a covered work, you may (if authorized by the copyright holders of
351351+that material) supplement the terms of this License with terms:
352352+353353+ a) Disclaiming warranty or limiting liability differently from the
354354+ terms of sections 15 and 16 of this License; or
355355+356356+ b) Requiring preservation of specified reasonable legal notices or
357357+ author attributions in that material or in the Appropriate Legal
358358+ Notices displayed by works containing it; or
359359+360360+ c) Prohibiting misrepresentation of the origin of that material, or
361361+ requiring that modified versions of such material be marked in
362362+ reasonable ways as different from the original version; or
363363+364364+ d) Limiting the use for publicity purposes of names of licensors or
365365+ authors of the material; or
366366+367367+ e) Declining to grant rights under trademark law for use of some
368368+ trade names, trademarks, or service marks; or
369369+370370+ f) Requiring indemnification of licensors and authors of that
371371+ material by anyone who conveys the material (or modified versions of
372372+ it) with contractual assumptions of liability to the recipient, for
373373+ any liability that these contractual assumptions directly impose on
374374+ those licensors and authors.
375375+376376+ All other non-permissive additional terms are considered "further
377377+restrictions" within the meaning of section 10. If the Program as you
378378+received it, or any part of it, contains a notice stating that it is
379379+governed by this License along with a term that is a further
380380+restriction, you may remove that term. If a license document contains
381381+a further restriction but permits relicensing or conveying under this
382382+License, you may add to a covered work material governed by the terms
383383+of that license document, provided that the further restriction does
384384+not survive such relicensing or conveying.
385385+386386+ If you add terms to a covered work in accord with this section, you
387387+must place, in the relevant source files, a statement of the
388388+additional terms that apply to those files, or a notice indicating
389389+where to find the applicable terms.
390390+391391+ Additional terms, permissive or non-permissive, may be stated in the
392392+form of a separately written license, or stated as exceptions;
393393+the above requirements apply either way.
394394+395395+ 8. Termination.
396396+397397+ You may not propagate or modify a covered work except as expressly
398398+provided under this License. Any attempt otherwise to propagate or
399399+modify it is void, and will automatically terminate your rights under
400400+this License (including any patent licenses granted under the third
401401+paragraph of section 11).
402402+403403+ However, if you cease all violation of this License, then your
404404+license from a particular copyright holder is reinstated (a)
405405+provisionally, unless and until the copyright holder explicitly and
406406+finally terminates your license, and (b) permanently, if the copyright
407407+holder fails to notify you of the violation by some reasonable means
408408+prior to 60 days after the cessation.
409409+410410+ Moreover, your license from a particular copyright holder is
411411+reinstated permanently if the copyright holder notifies you of the
412412+violation by some reasonable means, this is the first time you have
413413+received notice of violation of this License (for any work) from that
414414+copyright holder, and you cure the violation prior to 30 days after
415415+your receipt of the notice.
416416+417417+ Termination of your rights under this section does not terminate the
418418+licenses of parties who have received copies or rights from you under
419419+this License. If your rights have been terminated and not permanently
420420+reinstated, you do not qualify to receive new licenses for the same
421421+material under section 10.
422422+423423+ 9. Acceptance Not Required for Having Copies.
424424+425425+ You are not required to accept this License in order to receive or
426426+run a copy of the Program. Ancillary propagation of a covered work
427427+occurring solely as a consequence of using peer-to-peer transmission
428428+to receive a copy likewise does not require acceptance. However,
429429+nothing other than this License grants you permission to propagate or
430430+modify any covered work. These actions infringe copyright if you do
431431+not accept this License. Therefore, by modifying or propagating a
432432+covered work, you indicate your acceptance of this License to do so.
433433+434434+ 10. Automatic Licensing of Downstream Recipients.
435435+436436+ Each time you convey a covered work, the recipient automatically
437437+receives a license from the original licensors, to run, modify and
438438+propagate that work, subject to this License. You are not responsible
439439+for enforcing compliance by third parties with this License.
440440+441441+ An "entity transaction" is a transaction transferring control of an
442442+organization, or substantially all assets of one, or subdividing an
443443+organization, or merging organizations. If propagation of a covered
444444+work results from an entity transaction, each party to that
445445+transaction who receives a copy of the work also receives whatever
446446+licenses to the work the party's predecessor in interest had or could
447447+give under the previous paragraph, plus a right to possession of the
448448+Corresponding Source of the work from the predecessor in interest, if
449449+the predecessor has it or can get it with reasonable efforts.
450450+451451+ You may not impose any further restrictions on the exercise of the
452452+rights granted or affirmed under this License. For example, you may
453453+not impose a license fee, royalty, or other charge for exercise of
454454+rights granted under this License, and you may not initiate litigation
455455+(including a cross-claim or counterclaim in a lawsuit) alleging that
456456+any patent claim is infringed by making, using, selling, offering for
457457+sale, or importing the Program or any portion of it.
458458+459459+ 11. Patents.
460460+461461+ A "contributor" is a copyright holder who authorizes use under this
462462+License of the Program or a work on which the Program is based. The
463463+work thus licensed is called the contributor's "contributor version".
464464+465465+ A contributor's "essential patent claims" are all patent claims
466466+owned or controlled by the contributor, whether already acquired or
467467+hereafter acquired, that would be infringed by some manner, permitted
468468+by this License, of making, using, or selling its contributor version,
469469+but do not include claims that would be infringed only as a
470470+consequence of further modification of the contributor version. For
471471+purposes of this definition, "control" includes the right to grant
472472+patent sublicenses in a manner consistent with the requirements of
473473+this License.
474474+475475+ Each contributor grants you a non-exclusive, worldwide, royalty-free
476476+patent license under the contributor's essential patent claims, to
477477+make, use, sell, offer for sale, import and otherwise run, modify and
478478+propagate the contents of its contributor version.
479479+480480+ In the following three paragraphs, a "patent license" is any express
481481+agreement or commitment, however denominated, not to enforce a patent
482482+(such as an express permission to practice a patent or covenant not to
483483+sue for patent infringement). To "grant" such a patent license to a
484484+party means to make such an agreement or commitment not to enforce a
485485+patent against the party.
486486+487487+ If you convey a covered work, knowingly relying on a patent license,
488488+and the Corresponding Source of the work is not available for anyone
489489+to copy, free of charge and under the terms of this License, through a
490490+publicly available network server or other readily accessible means,
491491+then you must either (1) cause the Corresponding Source to be so
492492+available, or (2) arrange to deprive yourself of the benefit of the
493493+patent license for this particular work, or (3) arrange, in a manner
494494+consistent with the requirements of this License, to extend the patent
495495+license to downstream recipients. "Knowingly relying" means you have
496496+actual knowledge that, but for the patent license, your conveying the
497497+covered work in a country, or your recipient's use of the covered work
498498+in a country, would infringe one or more identifiable patents in that
499499+country that you have reason to believe are valid.
500500+501501+ If, pursuant to or in connection with a single transaction or
502502+arrangement, you convey, or propagate by procuring conveyance of, a
503503+covered work, and grant a patent license to some of the parties
504504+receiving the covered work authorizing them to use, propagate, modify
505505+or convey a specific copy of the covered work, then the patent license
506506+you grant is automatically extended to all recipients of the covered
507507+work and works based on it.
508508+509509+ A patent license is "discriminatory" if it does not include within
510510+the scope of its coverage, prohibits the exercise of, or is
511511+conditioned on the non-exercise of one or more of the rights that are
512512+specifically granted under this License. You may not convey a covered
513513+work if you are a party to an arrangement with a third party that is
514514+in the business of distributing software, under which you make payment
515515+to the third party based on the extent of your activity of conveying
516516+the work, and under which the third party grants, to any of the
517517+parties who would receive the covered work from you, a discriminatory
518518+patent license (a) in connection with copies of the covered work
519519+conveyed by you (or copies made from those copies), or (b) primarily
520520+for and in connection with specific products or compilations that
521521+contain the covered work, unless you entered into that arrangement,
522522+or that patent license was granted, prior to 28 March 2007.
523523+524524+ Nothing in this License shall be construed as excluding or limiting
525525+any implied license or other defenses to infringement that may
526526+otherwise be available to you under applicable patent law.
527527+528528+ 12. No Surrender of Others' Freedom.
529529+530530+ If conditions are imposed on you (whether by court order, agreement or
531531+otherwise) that contradict the conditions of this License, they do not
532532+excuse you from the conditions of this License. If you cannot convey a
533533+covered work so as to satisfy simultaneously your obligations under this
534534+License and any other pertinent obligations, then as a consequence you may
535535+not convey it at all. For example, if you agree to terms that obligate you
536536+to collect a royalty for further conveying from those to whom you convey
537537+the Program, the only way you could satisfy both those terms and this
538538+License would be to refrain entirely from conveying the Program.
539539+540540+ 13. Remote Network Interaction; Use with the GNU General Public License.
541541+542542+ Notwithstanding any other provision of this License, if you modify the
543543+Program, your modified version must prominently offer all users
544544+interacting with it remotely through a computer network (if your version
545545+supports such interaction) an opportunity to receive the Corresponding
546546+Source of your version by providing access to the Corresponding Source
547547+from a network server at no charge, through some standard or customary
548548+means of facilitating copying of software. This Corresponding Source
549549+shall include the Corresponding Source for any work covered by version 3
550550+of the GNU General Public License that is incorporated pursuant to the
551551+following paragraph.
552552+553553+ Notwithstanding any other provision of this License, you have
554554+permission to link or combine any covered work with a work licensed
555555+under version 3 of the GNU General Public License into a single
556556+combined work, and to convey the resulting work. The terms of this
557557+License will continue to apply to the part which is the covered work,
558558+but the work with which it is combined will remain governed by version
559559+3 of the GNU General Public License.
560560+561561+ 14. Revised Versions of this License.
562562+563563+ The Free Software Foundation may publish revised and/or new versions of
564564+the GNU Affero General Public License from time to time. Such new versions
565565+will be similar in spirit to the present version, but may differ in detail to
566566+address new problems or concerns.
567567+568568+ Each version is given a distinguishing version number. If the
569569+Program specifies that a certain numbered version of the GNU Affero General
570570+Public License "or any later version" applies to it, you have the
571571+option of following the terms and conditions either of that numbered
572572+version or of any later version published by the Free Software
573573+Foundation. If the Program does not specify a version number of the
574574+GNU Affero General Public License, you may choose any version ever published
575575+by the Free Software Foundation.
576576+577577+ If the Program specifies that a proxy can decide which future
578578+versions of the GNU Affero General Public License can be used, that proxy's
579579+public statement of acceptance of a version permanently authorizes you
580580+to choose that version for the Program.
581581+582582+ Later license versions may give you additional or different
583583+permissions. However, no additional obligations are imposed on any
584584+author or copyright holder as a result of your choosing to follow a
585585+later version.
586586+587587+ 15. Disclaimer of Warranty.
588588+589589+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590590+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591591+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592592+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593593+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594594+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595595+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596596+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597597+598598+ 16. Limitation of Liability.
599599+600600+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601601+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602602+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603603+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604604+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605605+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606606+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607607+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608608+SUCH DAMAGES.
609609+610610+ 17. Interpretation of Sections 15 and 16.
611611+612612+ If the disclaimer of warranty and limitation of liability provided
613613+above cannot be given local legal effect according to their terms,
614614+reviewing courts shall apply local law that most closely approximates
615615+an absolute waiver of all civil liability in connection with the
616616+Program, unless a warranty or assumption of liability accompanies a
617617+copy of the Program in return for a fee.
618618+619619+ END OF TERMS AND CONDITIONS
620620+621621+ How to Apply These Terms to Your New Programs
622622+623623+ If you develop a new program, and you want it to be of the greatest
624624+possible use to the public, the best way to achieve this is to make it
625625+free software which everyone can redistribute and change under these terms.
626626+627627+ To do so, attach the following notices to the program. It is safest
628628+to attach them to the start of each source file to most effectively
629629+state the exclusion of warranty; and each file should have at least
630630+the "copyright" line and a pointer to where the full notice is found.
631631+632632+ <one line to give the program's name and a brief idea of what it does.>
633633+ Copyright (C) <year> <name of author>
634634+635635+ This program is free software: you can redistribute it and/or modify
636636+ it under the terms of the GNU Affero General Public License as published by
637637+ the Free Software Foundation, either version 3 of the License, or
638638+ (at your option) any later version.
639639+640640+ This program is distributed in the hope that it will be useful,
641641+ but WITHOUT ANY WARRANTY; without even the implied warranty of
642642+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643643+ GNU Affero General Public License for more details.
644644+645645+ You should have received a copy of the GNU Affero General Public License
646646+ along with this program. If not, see <https://www.gnu.org/licenses/>.
647647+648648+Also add information on how to contact you by electronic and paper mail.
649649+650650+ If your software can interact with users remotely through a computer
651651+network, you should also make sure that it provides a way for users to
652652+get its source. For example, if your program is a web application, its
653653+interface could display a "Source" link that leads users to an archive
654654+of the code. There are many ways you could offer source, and different
655655+solutions will be better for different programs; see section 13 for the
656656+specific requirements.
657657+658658+ You should also get your employer (if you work as a programmer) or school,
659659+if any, to sign a "copyright disclaimer" for the program, if necessary.
660660+For more information on this, and how to apply and follow the GNU AGPL, see
661661+<https://www.gnu.org/licenses/>.
+247
README.md
···11+# AT Protocol Link Shortener
22+33+A **server-side** link shortening service powered by your [Linkat](https://linkat.blue) board. No database required - all links are fetched directly from AT Protocol!
44+55+## ✨ Features
66+77+- **Zero Configuration Database**: Uses your existing Linkat board as the data source
88+- ⚡ **Hash-Based Shortcodes**: Automatic 6-character codes generated from URLs (e.g., `/a3k9zx`)
99+- 🚀 **Server-Side Only**: Pure API-based, no client UI needed
1010+- 🎯 **Smart Redirects**: Instant HTTP 301 redirects to your target URLs
1111+- 🔍 **Automatic PDS Discovery**: Resolves your PDS endpoint via Slingshot
1212+- ⚡ **Built-in Cache**: 5-minute cache for optimal performance
1313+1414+## 🚀 Quick Start
1515+1616+### 1. Clone and Install
1717+1818+```bash
1919+git clone git@github.com:ewanc26/atproto-shortlink # or git@tangled.sh:ewancroft.uk/atproto-shortlink
2020+cd atproto-shortlink
2121+npm install
2222+```
2323+2424+### 2. Configure Your DID
2525+2626+Create a `.env` file:
2727+2828+```bash
2929+cp .env.example .env
3030+```
3131+3232+Edit `.env` and add your AT Protocol DID:
3333+3434+```ini
3535+# Find your DID at https://pdsls.dev/ by entering your handle
3636+ATPROTO_DID=did:plc:your-did-here
3737+```
3838+3939+**How to find your DID:**
4040+4141+1. Visit [PDSls](https://pdsls.dev/)
4242+2. Enter your AT Protocol handle (e.g., `yourname.bsky.social`)
4343+3. Copy the `did:plc:...` identifier
4444+4545+### 3. Set Up Your Linkat Board
4646+4747+If you don't have a Linkat board yet:
4848+4949+1. Visit [https://linkat.blue](https://linkat.blue)
5050+2. Create a board with your links
5151+3. Add your links with titles and emojis
5252+5353+The shortener will automatically generate unique 6-character codes for each URL!
5454+5555+### 4. Test Your Configuration (Optional)
5656+5757+Run the configuration test to verify everything is set up correctly:
5858+5959+```bash
6060+npm run test:config
6161+```
6262+6363+This will:
6464+6565+- ✅ Check if `.env` exists and is configured
6666+- ✅ Validate your DID format
6767+- ✅ Test PDS connectivity
6868+- ✅ Verify your Linkat board is accessible
6969+- ✅ Show a preview of your first few links
7070+7171+### 5. Run the Server
7272+7373+```bash
7474+npm run dev
7575+```
7676+7777+Visit `http://localhost:5173` to see your service running!
7878+7979+## 📖 Usage
8080+8181+Once running, your short links work like this:
8282+8383+```bash
8484+# Redirect to your configured URLs
8585+http://localhost:5173/a3k9zx → Redirects to your GitHub
8686+http://localhost:5173/b7m2wp → Redirects to your blog
8787+http://localhost:5173/c4n8qz → Redirects to your portfolio
8888+8989+# View service info
9090+http://localhost:5173/ → Shows API information and available links
9191+9292+# Get JSON list of links
9393+http://localhost:5173/api/links → Returns all short links as JSON
9494+```
9595+9696+## 🔧 API Endpoints
9797+9898+| Endpoint | Method | Description | Response |
9999+| ------------- | ------ | ------------------------------- | ------------ |
100100+| `/` | GET | Service status and link listing | HTML |
101101+| `/:shortcode` | GET | Redirect to full URL | 301 Redirect |
102102+| `/api/links` | GET | List all available short links | JSON |
103103+104104+### Example API Response
105105+106106+```json
107107+{
108108+ "success": true,
109109+ "count": 3,
110110+ "links": [
111111+ {
112112+ "shortcode": "a3k9zx",
113113+ "url": "https://github.com/yourname",
114114+ "title": "My GitHub Profile",
115115+ "emoji": "💻",
116116+ "shortUrl": "/a3k9zx"
117117+ },
118118+ {
119119+ "shortcode": "b7m2wp",
120120+ "url": "https://yourblog.com",
121121+ "title": "Personal Blog",
122122+ "emoji": "📝",
123123+ "shortUrl": "/b7m2wp"
124124+ }
125125+ ]
126126+}
127127+```
128128+129129+## 📝 How Shortcodes Work
130130+131131+Shortcodes are automatically generated as 6-character base62 hashes from your URLs. Each URL will always produce the same shortcode, ensuring consistency.
132132+133133+- **Base62 encoding**: Uses 0-9, a-z, A-Z (62 characters)
134134+- **Collision-resistant**: 62^6 = ~56 billion possible combinations
135135+- **Deterministic**: Same URL = same shortcode every time
136136+- **URL-safe**: No special characters needed
137137+138138+## 🌐 Deployment
139139+140140+### Build for Production
141141+142142+```bash
143143+npm run build
144144+npm run preview # Test the production build locally
145145+```
146146+147147+### Deploy to Platforms
148148+149149+This project uses `@sveltejs/adapter-auto` which works with:
150150+151151+- **Vercel**: Push to GitHub and connect your repo
152152+- **Netlify**: Push to GitHub and connect your repo
153153+- **Cloudflare Pages**: Push to GitHub and connect your repo
154154+- **Node.js**: Use `adapter-node` for standalone Node servers
155155+156156+For specific platforms, see [SvelteKit adapters](https://kit.svelte.dev/docs/adapters).
157157+158158+### Environment Variables for Deployment
159159+160160+Make sure to set `ATPROTO_DID` in your deployment platform's environment variables!
161161+162162+## ⚙️ Configuration
163163+164164+| Variable | Required | Description | Example |
165165+| ------------- | -------- | -------------------- | ------------------- |
166166+| `ATPROTO_DID` | ✅ Yes | Your AT Protocol DID | `did:plc:abc123xyz` |
167167+168168+## 🏗️ How It Works
169169+170170+1. **You maintain your links** in [Linkat](https://linkat.blue) (stored in `blue.linkat.board` collection)
171171+2. **Service fetches on-demand** from your AT Protocol PDS via Slingshot resolution
172172+3. **URLs are shortened** using deterministic base62 hash encoding
173173+4. **Accessing a short link** (e.g., `/a3k9zx`) triggers an instant 301 redirect
174174+175175+```mermaid
176176+graph LR
177177+ A[User visits /a3k9zx] --> B[Service fetches Linkat data]
178178+ B --> C[Looks up shortcode in links]
179179+ C --> D[301 Redirect to target URL]
180180+```
181181+182182+## 🔒 Security
183183+184184+- ✅ All Linkat data is public by design
185185+- ✅ No authentication required
186186+- ✅ Read-only access to AT Protocol data
187187+- ✅ No data storage (fetches on-demand with cache)
188188+- ✅ 5-minute cache to prevent abuse
189189+190190+## 🛠️ Development
191191+192192+```bash
193193+# Install dependencies
194194+npm install
195195+196196+# Start dev server
197197+npm run dev
198198+199199+# Type check
200200+npm run check
201201+202202+# Format code
203203+npm run format
204204+205205+# Check formatting
206206+npm run lint
207207+```
208208+209209+## 📦 Tech Stack
210210+211211+- **Framework**: [SvelteKit 2](https://kit.svelte.dev/)
212212+- **Runtime**: Server-side only (no client JavaScript required)
213213+- **Data Source**: AT Protocol (`blue.linkat.board` collection)
214214+- **PDS Resolution**: [Slingshot](https://slingshot.microcosm.blue) by Microcosm
215215+- **Redirects**: HTTP 301 (permanent)
216216+- **Shortcode Format**: Base62 hash encoding
217217+218218+## 🔧 Troubleshooting
219219+220220+Having issues? Check the [Troubleshooting Guide](./TROUBLESHOOTING.md) for common problems and solutions.
221221+222222+Quick checks:
223223+224224+1. Run `npm run test:config` to verify your setup
225225+2. Make sure Node.js 18+ is installed: `node --version`
226226+3. Check your DID at [pdsls.dev](https://pdsls.dev/)
227227+4. Verify your Linkat board at [linkat.blue](https://linkat.blue)
228228+229229+## 🤝 Contributing
230230+231231+Contributions are welcome! Please feel free to submit a Pull Request.
232232+233233+## 📄 Licence
234234+235235+AGPLv3 Licence - See [LICENCE](./LICENCE) file for details
236236+237237+## Links
238238+239239+- [Linkat](https://linkat.blue) - The link board service
240240+- [AT Protocol](https://atproto.com) - The underlying protocol
241241+- [SvelteKit](https://kit.svelte.dev) - The web framework
242242+- [PDSls](https://pdsls.dev/) - Find your DID
243243+- [Slingshot](https://slingshot.microcosm.blue) - Identity resolver
244244+245245+---
246246+247247+Made with ❤️ using AT Protocol and Linkat
+226
TROUBLESHOOTING.md
···11+# Troubleshooting Guide
22+33+## Common Issues and Solutions
44+55+### 1. "ATPROTO_DID not configured" Error
66+77+**Symptoms:**
88+99+- Homepage shows a red error message
1010+- Service won't start or shows configuration error
1111+1212+**Solution:**
1313+1414+1. Create a `.env` file in your project root (copy from `.env.example`)
1515+2. Add your DID: `ATPROTO_DID=did:plc:your-did-here`
1616+3. Find your DID at [pdsls.dev](https://pdsls.dev/)
1717+4. Run `npm run test:config` to verify
1818+5. Restart the server with `npm run dev`
1919+2020+### 2. "Failed to fetch Linkat data" Error
2121+2222+**Symptoms:**
2323+2424+- Service starts but shows 0 links
2525+- Error in console about failed fetch
2626+2727+**Possible Causes & Solutions:**
2828+2929+**A. No Linkat Board**
3030+3131+- Visit [linkat.blue](https://linkat.blue)
3232+- Create a board
3333+- Add some links
3434+- Wait a few seconds for data to propagate
3535+3636+**B. PDS Connection Issues**
3737+3838+- Check your internet connection
3939+- Verify your DID is correct with `npm run test:config`
4040+- Try again in a few minutes (PDS might be temporarily down)
4141+4242+**C. Invalid DID Format**
4343+4444+- DID must start with `did:plc:` or `did:web:`
4545+- No spaces or special characters
4646+- Run `npm run test:config` to validate
4747+4848+### 3. Short Links Not Working (404)
4949+5050+**Symptoms:**
5151+5252+- Homepage shows links but accessing them gives 404
5353+- Shortcode appears but doesn't redirect
5454+5555+**Possible Causes:**
5656+5757+**A. Cache Issue**
5858+5959+- Links are cached for 5 minutes
6060+- If you just added/changed links, wait 5 minutes
6161+- Or restart the server to clear cache
6262+6363+**B. Shortcode Conflict**
6464+6565+- Two links can't have the same first word
6666+- Example: "blog My Blog" and "blog Tech Blog" will conflict
6767+- Make first words unique: "blog" and "tech"
6868+6969+**C. Special Characters**
7070+7171+- Shortcodes are lowercase only
7272+- Access `/github` not `/GitHub`
7373+- Spaces and special chars are removed
7474+7575+### 4. Slow Redirects
7676+7777+**Symptoms:**
7878+7979+- First redirect after server start is slow
8080+- Subsequent redirects are fast
8181+8282+**Explanation:**
8383+8484+- First request fetches from AT Protocol (takes ~1-2 seconds)
8585+- Data is then cached for 5 minutes
8686+- This is normal behavior
8787+8888+**To Improve:**
8989+9090+- Consider pre-warming cache on server start
9191+- Use a CDN or edge function for faster global access
9292+9393+### 5. Port Already in Use
9494+9595+**Symptoms:**
9696+9797+```
9898+Error: listen EADDRINUSE: address already in use :::5173
9999+```
100100+101101+**Solution:**
102102+103103+1. Find what's using port 5173: `lsof -i :5173` (Mac/Linux) or `netstat -ano | findstr :5173` (Windows)
104104+2. Kill that process
105105+3. Or change the port: `npm run dev -- --port 3000`
106106+107107+### 6. Module Not Found Errors
108108+109109+**Symptoms:**
110110+111111+```
112112+Cannot find module '@atproto/api'
113113+```
114114+115115+**Solution:**
116116+117117+```bash
118118+# Delete node_modules and reinstall
119119+rm -rf node_modules package-lock.json
120120+npm install
121121+```
122122+123123+### 7. TypeScript Errors
124124+125125+**Symptoms:**
126126+127127+- Red squiggly lines in VS Code
128128+- Type errors when running `npm run check`
129129+130130+**Solution:**
131131+132132+```bash
133133+# Sync SvelteKit types
134134+npx svelte-kit sync
135135+136136+# Or run the full check
137137+npm run check
138138+```
139139+140140+## Debugging Tips
141141+142142+### Enable Verbose Logging
143143+144144+The service logs important events. Check your terminal for:
145145+146146+- `[Linkat]` - Data fetching operations
147147+- `[Redirect]` - Redirect attempts
148148+- `[API]` - API endpoint calls
149149+150150+### Test Your Configuration
151151+152152+Always run this first when having issues:
153153+154154+```bash
155155+npm run test:config
156156+```
157157+158158+This will tell you exactly what's wrong!
159159+160160+### Check the API
161161+162162+Visit `http://localhost:5173/api/links` to see the JSON response:
163163+164164+- Check if links are being fetched
165165+- Verify shortcodes are correct
166166+- See what data is available
167167+168168+### Verify Your Linkat Board
169169+170170+1. Visit [linkat.blue](https://linkat.blue)
171171+2. Check your board has links
172172+3. Verify link titles are formatted correctly
173173+4. First word before space = shortcode
174174+175175+### Common Link Title Mistakes
176176+177177+❌ **Wrong:**
178178+179179+- `MyGitHub` (no space, shortcode will be "mygithub")
180180+- `"GitHub"` (will be "github" but might want just "gh")
181181+- `` (empty title)
182182+183183+✅ **Right:**
184184+185185+- `"gh GitHub Profile"` (shortcode: "gh")
186186+- `"blog My Blog"` (shortcode: "blog")
187187+- `"cv Resume"` (shortcode: "cv")
188188+189189+## Still Having Issues?
190190+191191+1. **Check the README**: Most setup instructions are there
192192+2. **Run test:config**: `npm run test:config`
193193+3. **Check Console Logs**: Look for error messages
194194+4. **Verify Linkat Board**: Visit linkat.blue and check your board
195195+5. **Try the API Directly**: `curl http://localhost:5173/api/links`
196196+197197+## Getting Help
198198+199199+If you're still stuck:
200200+201201+1. Make sure you're using Node.js 18 or higher: `node --version`
202202+2. Try the test config: `npm run test:config`
203203+3. Check if your DID works at [pdsls.dev](https://pdsls.dev/)
204204+4. Verify your Linkat board at [linkat.blue](https://linkat.blue)
205205+206206+## Quick Reference
207207+208208+```bash
209209+# Test configuration
210210+npm run test:config
211211+212212+# Start dev server
213213+npm run dev
214214+215215+# Build for production
216216+npm run build
217217+218218+# Preview production build
219219+npm run preview
220220+221221+# Check types
222222+npm run check
223223+224224+# Format code
225225+npm run format
226226+```
···11+/**
22+ * Application-wide constants and configuration
33+ */
44+55+/**
66+ * Cache configuration
77+ */
88+export const CACHE = {
99+ /** Default TTL for cached data (5 minutes) */
1010+ DEFAULT_TTL: 300000,
1111+1212+ /** Cache key prefix for Linkat data */
1313+ LINKAT_PREFIX: 'linkat:'
1414+} as const;
1515+1616+/**
1717+ * Shortcode configuration
1818+ */
1919+export const SHORTCODE = {
2020+ /** Default length for generated shortcodes */
2121+ DEFAULT_LENGTH: 6,
2222+2323+ /** Maximum collision resolution attempts */
2424+ MAX_COLLISION_ATTEMPTS: 20,
2525+2626+ /** Base70 character set (includes special characters) */
2727+ BASE62_CHARS: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+=_-?<>'
2828+} as const;
2929+3030+/**
3131+ * AT Protocol configuration
3232+ */
3333+export const ATPROTO = {
3434+ /** Slingshot identity resolver endpoint */
3535+ SLINGSHOT_ENDPOINT: 'https://slingshot.microcosm.blue',
3636+3737+ /** Default public Bluesky API endpoint */
3838+ PUBLIC_API: 'https://public.api.bsky.app',
3939+4040+ /** Linkat collection identifier */
4141+ LINKAT_COLLECTION: 'blue.linkat.board',
4242+4343+ /** Linkat record key */
4444+ LINKAT_RKEY: 'self'
4545+} as const;
4646+4747+/**
4848+ * HTTP configuration
4949+ */
5050+export const HTTP = {
5151+ /** Status code for permanent redirect */
5252+ REDIRECT_PERMANENT: 301,
5353+5454+ /** Status code for not found */
5555+ NOT_FOUND: 404,
5656+5757+ /** Status code for no content */
5858+ NO_CONTENT: 204
5959+} as const;
+22
src/lib/index.ts
···11+/**
22+ * Main library exports
33+ *
44+ * This file provides a clean public API for the application's core functionality.
55+ */
66+77+// Services
88+export { getShortLinks, findShortLink, clearCache as clearLinkatCache } from './services/linkat';
99+export { createAgent, getPublicAgent, getPDSAgent, resetAgents } from './services/atproto';
1010+1111+// Utilities
1212+export { encodeUrl, isValidShortcode, getMaxCombinations } from './utils/encoding';
1313+1414+// Types
1515+export type { LinkCard, LinkData, ShortLink } from './services/types';
1616+export type { ResolvedIdentity } from './services/atproto';
1717+1818+// Cache
1919+export { Cache } from './services/cache';
2020+2121+// Constants
2222+export { CACHE, SHORTCODE, ATPROTO, HTTP } from './constants';
+52
src/lib/services/agent.ts
···11+/**
22+ * AT Protocol Agent Service
33+ *
44+ * This file provides backwards compatibility.
55+ * The actual implementation has been modularized into:
66+ * - agent-factory.ts: Agent creation
77+ * - identity-resolver.ts: DID resolution via Slingshot
88+ * - agent-manager.ts: Agent caching and fallback logic
99+ */
1010+1111+import { ATPROTO_DID } from '$env/static/private';
1212+import {
1313+ createAgent,
1414+ resolveIdentity,
1515+ defaultAgent,
1616+ getPublicAgent,
1717+ getPDSAgent,
1818+ withFallback,
1919+ resetAgents,
2020+ type ResolvedIdentity
2121+} from './atproto';
2222+2323+// Re-export everything for backwards compatibility
2424+export { createAgent, resolveIdentity, defaultAgent, withFallback, resetAgents };
2525+export type { ResolvedIdentity };
2626+2727+/**
2828+ * Creates an AT Protocol agent for the configured DID
2929+ */
3030+export async function createAgentForDID(): Promise<import('@atproto/api').AtpAgent> {
3131+ return await getPublicAgent(ATPROTO_DID);
3232+}
3333+3434+/**
3535+ * Creates an AT Protocol agent with fallback to public Bluesky API
3636+ */
3737+export async function createAgentWithFallback(): Promise<{
3838+ agent: import('@atproto/api').AtpAgent;
3939+ isPDS: boolean;
4040+}> {
4141+ try {
4242+ const agent = await getPublicAgent(ATPROTO_DID);
4343+ return { agent, isPDS: true };
4444+ } catch (error) {
4545+ console.warn('Failed to resolve PDS, falling back to Bluesky public API:', error);
4646+ const agent = defaultAgent;
4747+ return { agent, isPDS: false };
4848+ }
4949+}
5050+5151+// Also export the manager functions for direct use
5252+export { getPublicAgent, getPDSAgent };
+38
src/lib/services/atproto/agent-factory.ts
···11+import { AtpAgent } from '@atproto/api';
22+33+/**
44+ * Creates an AtpAgent with optional fetch function injection
55+ *
66+ * @param service - Service URL for the agent
77+ * @param fetchFn - Optional custom fetch function (useful for server-side contexts)
88+ * @returns Configured AtpAgent instance
99+ */
1010+export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent {
1111+ // If we have an injected fetch, wrap it to ensure we handle headers correctly
1212+ const wrappedFetch = fetchFn
1313+ ? async (url: URL | RequestInfo, init?: RequestInit) => {
1414+ // Convert URL to string if needed
1515+ const urlStr = url instanceof URL ? url.toString() : url;
1616+1717+ // Make the request with the injected fetch
1818+ const response = await fetchFn(urlStr, init);
1919+2020+ // Create a new response with the same body but add content-type if missing
2121+ const headers = new Headers(response.headers);
2222+ if (!headers.has('content-type')) {
2323+ headers.set('content-type', 'application/json');
2424+ }
2525+2626+ return new Response(response.body, {
2727+ status: response.status,
2828+ statusText: response.statusText,
2929+ headers
3030+ });
3131+ }
3232+ : undefined;
3333+3434+ return new AtpAgent({
3535+ service,
3636+ ...(wrappedFetch && { fetch: wrappedFetch })
3737+ });
3838+}
+111
src/lib/services/atproto/agent-manager.ts
···11+import { ATPROTO } from '$lib/constants';
22+import type { AtpAgent } from '@atproto/api';
33+import { createAgent } from './agent-factory';
44+import { resolveIdentity } from './identity-resolver';
55+66+/**
77+ * Default fallback agent for public Bluesky API calls
88+ */
99+export const defaultAgent = createAgent(ATPROTO.PUBLIC_API);
1010+1111+/**
1212+ * Cached agents
1313+ */
1414+let resolvedAgent: AtpAgent | null = null;
1515+let pdsAgent: AtpAgent | null = null;
1616+1717+/**
1818+ * Gets or creates an agent using Slingshot resolution with fallback
1919+ *
2020+ * @param did - The DID to resolve
2121+ * @param fetchFn - Optional custom fetch function
2222+ * @returns Configured AtpAgent
2323+ */
2424+export async function getPublicAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> {
2525+ console.info(`[Agent] Getting public agent for DID: ${did}`);
2626+2727+ if (resolvedAgent) {
2828+ console.debug('[Agent] Using cached agent');
2929+ return resolvedAgent;
3030+ }
3131+3232+ try {
3333+ // Use Slingshot for PDS resolution
3434+ console.info('[Agent] Attempting Slingshot resolution');
3535+ const resolved = await resolveIdentity(did, fetchFn);
3636+ console.info(`[Agent] Resolved PDS endpoint: ${resolved.pds}`);
3737+ resolvedAgent = createAgent(resolved.pds, fetchFn);
3838+ return resolvedAgent;
3939+ } catch (err) {
4040+ console.error('[Agent] Slingshot resolution failed, falling back to Bluesky:', err);
4141+ resolvedAgent = defaultAgent;
4242+ return resolvedAgent;
4343+ }
4444+}
4545+4646+/**
4747+ * Gets or creates a PDS-specific agent
4848+ *
4949+ * @param did - The DID to resolve
5050+ * @param fetchFn - Optional custom fetch function
5151+ * @returns Configured AtpAgent for the PDS
5252+ * @throws Error if resolution fails
5353+ */
5454+export async function getPDSAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> {
5555+ if (pdsAgent) return pdsAgent;
5656+5757+ try {
5858+ const resolved = await resolveIdentity(did, fetchFn);
5959+ pdsAgent = createAgent(resolved.pds, fetchFn);
6060+ return pdsAgent;
6161+ } catch (err) {
6262+ console.error('Failed to resolve PDS for DID:', err);
6363+ throw err;
6464+ }
6565+}
6666+6767+/**
6868+ * Executes a function with automatic fallback between agents
6969+ *
7070+ * @param did - The DID to resolve
7171+ * @param operation - The operation to execute with the agent
7272+ * @param usePDSFirst - If true, tries PDS first before public API
7373+ * @param fetchFn - Optional custom fetch function
7474+ * @returns Result of the operation
7575+ * @throws Error if all attempts fail
7676+ */
7777+export async function withFallback<T>(
7878+ did: string,
7979+ operation: (agent: AtpAgent) => Promise<T>,
8080+ usePDSFirst = false,
8181+ fetchFn?: typeof fetch
8282+): Promise<T> {
8383+ const defaultAgentFn = () =>
8484+ fetchFn ? createAgent(ATPROTO.PUBLIC_API, fetchFn) : Promise.resolve(defaultAgent);
8585+8686+ const agents = usePDSFirst
8787+ ? [() => getPDSAgent(did, fetchFn), defaultAgentFn]
8888+ : [defaultAgentFn, () => getPDSAgent(did, fetchFn)];
8989+9090+ let lastError: any;
9191+9292+ for (const getAgent of agents) {
9393+ try {
9494+ const agent = await getAgent();
9595+ return await operation(agent);
9696+ } catch (error) {
9797+ console.warn('Operation failed, trying next agent:', error);
9898+ lastError = error;
9999+ }
100100+ }
101101+102102+ throw lastError;
103103+}
104104+105105+/**
106106+ * Resets cached agents (useful for testing or when identity changes)
107107+ */
108108+export function resetAgents(): void {
109109+ resolvedAgent = null;
110110+ pdsAgent = null;
111111+}
+55
src/lib/services/atproto/identity-resolver.ts
···11+import { ATPROTO } from '$lib/constants';
22+33+/**
44+ * Resolved identity from Slingshot
55+ */
66+export interface ResolvedIdentity {
77+ did: string;
88+ pds: string;
99+}
1010+1111+/**
1212+ * Resolves a DID to find its PDS endpoint using Slingshot
1313+ *
1414+ * @param did - The DID to resolve
1515+ * @param fetchFn - Optional custom fetch function
1616+ * @returns Resolved identity with DID and PDS endpoint
1717+ * @throws Error if resolution fails
1818+ */
1919+export async function resolveIdentity(
2020+ did: string,
2121+ fetchFn?: typeof fetch
2222+): Promise<ResolvedIdentity> {
2323+ console.info(`[Identity] Resolving DID: ${did}`);
2424+2525+ // Prefer an injected fetch (from SvelteKit load), fall back to global fetch
2626+ const _fetch = fetchFn ?? globalThis.fetch;
2727+2828+ const url = `${ATPROTO.SLINGSHOT_ENDPOINT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`;
2929+3030+ const response = await _fetch(url);
3131+3232+ if (!response.ok) {
3333+ console.error(`[Identity] Resolution failed: ${response.status} ${response.statusText}`);
3434+ throw new Error(
3535+ `Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}`
3636+ );
3737+ }
3838+3939+ const rawText = await response.text();
4040+ console.debug(`[Identity] Raw response:`, rawText);
4141+4242+ let data: any;
4343+ try {
4444+ data = JSON.parse(rawText);
4545+ } catch (err) {
4646+ console.error('[Identity] Failed to parse identity resolver response as JSON', err);
4747+ throw err;
4848+ }
4949+5050+ if (!data.did || !data.pds) {
5151+ throw new Error('Invalid response from identity resolver');
5252+ }
5353+5454+ return data;
5555+}
+16
src/lib/services/atproto/index.ts
···11+/**
22+ * AT Protocol service modules
33+ *
44+ * This module provides a clean interface for working with AT Protocol agents,
55+ * identity resolution, and PDS discovery.
66+ */
77+88+export { createAgent } from './agent-factory';
99+export { resolveIdentity, type ResolvedIdentity } from './identity-resolver';
1010+export {
1111+ defaultAgent,
1212+ getPublicAgent,
1313+ getPDSAgent,
1414+ withFallback,
1515+ resetAgents
1616+} from './agent-manager';
+94
src/lib/services/cache/index.ts
···11+/**
22+ * Generic in-memory cache with TTL (Time To Live) support
33+ */
44+export class Cache<T> {
55+ private data = new Map<string, { value: T; expires: number }>();
66+77+ /**
88+ * Set a value in the cache with optional TTL
99+ * @param key - Cache key
1010+ * @param value - Value to cache
1111+ * @param ttlMs - Time to live in milliseconds (default: 5 minutes)
1212+ */
1313+ set(key: string, value: T, ttlMs: number = 300000): void {
1414+ this.data.set(key, {
1515+ value,
1616+ expires: Date.now() + ttlMs
1717+ });
1818+ }
1919+2020+ /**
2121+ * Get a value from the cache
2222+ * @param key - Cache key
2323+ * @returns The cached value or null if expired/not found
2424+ */
2525+ get(key: string): T | null {
2626+ const entry = this.data.get(key);
2727+ if (!entry) return null;
2828+2929+ if (Date.now() > entry.expires) {
3030+ this.data.delete(key);
3131+ return null;
3232+ }
3333+3434+ return entry.value;
3535+ }
3636+3737+ /**
3838+ * Check if a key exists in the cache and is not expired
3939+ * @param key - Cache key
4040+ * @returns True if the key exists and is valid
4141+ */
4242+ has(key: string): boolean {
4343+ const entry = this.data.get(key);
4444+ if (!entry) return false;
4545+4646+ if (Date.now() > entry.expires) {
4747+ this.data.delete(key);
4848+ return false;
4949+ }
5050+5151+ return true;
5252+ }
5353+5454+ /**
5555+ * Delete a specific key from the cache
5656+ * @param key - Cache key
5757+ * @returns True if the key existed
5858+ */
5959+ delete(key: string): boolean {
6060+ return this.data.delete(key);
6161+ }
6262+6363+ /**
6464+ * Clear all cached data
6565+ */
6666+ clear(): void {
6767+ this.data.clear();
6868+ }
6969+7070+ /**
7171+ * Get the number of items in the cache (including expired)
7272+ */
7373+ size(): number {
7474+ return this.data.size;
7575+ }
7676+7777+ /**
7878+ * Remove all expired entries from the cache
7979+ * @returns Number of entries removed
8080+ */
8181+ prune(): number {
8282+ const now = Date.now();
8383+ let removed = 0;
8484+8585+ for (const [key, entry] of this.data.entries()) {
8686+ if (now > entry.expires) {
8787+ this.data.delete(key);
8888+ removed++;
8989+ }
9090+ }
9191+9292+ return removed;
9393+ }
9494+}
+11
src/lib/services/linkat.ts
···11+/**
22+ * Linkat Service
33+ *
44+ * This file provides backwards compatibility.
55+ * The actual implementation has been modularized into:
66+ * - fetcher.ts: Raw Linkat board fetching
77+ * - generator.ts: Shortcode generation and link finding
88+ * - index.ts: Main service with caching
99+ */
1010+1111+export { fetchLinkatData, getShortLinks, findShortLink, clearCache } from './linkat/index';
+34
src/lib/services/linkat/fetcher.ts
···11+import type { AtpAgent } from '@atproto/api';
22+import { ATPROTO } from '$lib/constants';
33+import type { LinkData } from '../types';
44+55+/**
66+ * Fetches Linkat board data from AT Protocol
77+ *
88+ * @param agent - AT Protocol agent to use for the request
99+ * @param did - DID of the user whose Linkat board to fetch
1010+ * @returns Linkat board data or null if not found/invalid
1111+ */
1212+export async function fetchLinkatBoard(agent: AtpAgent, did: string): Promise<LinkData | null> {
1313+ try {
1414+ const response = await agent.com.atproto.repo.getRecord({
1515+ repo: did,
1616+ collection: ATPROTO.LINKAT_COLLECTION,
1717+ rkey: ATPROTO.LINKAT_RKEY
1818+ });
1919+2020+ const value = response.data.value;
2121+2222+ if (!value || !Array.isArray((value as any).cards)) {
2323+ console.warn('[Linkat] Invalid data structure');
2424+ return null;
2525+ }
2626+2727+ return {
2828+ cards: (value as any).cards
2929+ };
3030+ } catch (error) {
3131+ console.error('[Linkat] Failed to fetch board data:', error);
3232+ return null;
3333+ }
3434+}
+52
src/lib/services/linkat/generator.ts
···11+import { SHORTCODE } from '$lib/constants';
22+import type { LinkData, ShortLink } from '../types';
33+import { encodeUrl } from '$lib/utils/encoding';
44+55+/**
66+ * Converts Linkat card data to short links with generated shortcodes
77+ *
88+ * @param linkatData - Raw Linkat board data
99+ * @returns Array of short links with generated codes
1010+ */
1111+export function generateShortLinks(linkatData: LinkData): ShortLink[] {
1212+ if (!linkatData || !linkatData.cards.length) {
1313+ return [];
1414+ }
1515+1616+ const shortLinks: ShortLink[] = [];
1717+ const usedShortcodes = new Set<string>();
1818+1919+ for (const card of linkatData.cards) {
2020+ // Generate encoded shortcode from URL
2121+ let shortcode = encodeUrl(card.url);
2222+2323+ // Ensure uniqueness (very unlikely to collide, but just in case)
2424+ let attempt = 0;
2525+ while (usedShortcodes.has(shortcode) && attempt < SHORTCODE.MAX_COLLISION_ATTEMPTS) {
2626+ shortcode = encodeUrl(card.url + attempt);
2727+ attempt++;
2828+ }
2929+3030+ usedShortcodes.add(shortcode);
3131+3232+ shortLinks.push({
3333+ shortcode,
3434+ url: card.url,
3535+ title: card.text,
3636+ emoji: card.emoji
3737+ });
3838+ }
3939+4040+ return shortLinks;
4141+}
4242+4343+/**
4444+ * Finds a short link by its shortcode
4545+ *
4646+ * @param links - Array of short links to search
4747+ * @param shortcode - The shortcode to find
4848+ * @returns The matching short link or null if not found
4949+ */
5050+export function findLinkByShortcode(links: ShortLink[], shortcode: string): ShortLink | null {
5151+ return links.find((link) => link.shortcode === shortcode) || null;
5252+}
+92
src/lib/services/linkat/index.ts
···11+import { ATPROTO_DID } from '$env/static/private';
22+import { CACHE } from '$lib/constants';
33+import { Cache } from '../cache';
44+import { createAgentForDID, createAgentWithFallback } from '../agent';
55+import { fetchLinkatBoard } from './fetcher';
66+import { generateShortLinks, findLinkByShortcode } from './generator';
77+import type { LinkData, ShortLink } from '../types';
88+99+/**
1010+ * Cache instance for Linkat data
1111+ */
1212+const cache = new Cache<LinkData>();
1313+1414+/**
1515+ * Fetches Linkat board data with caching
1616+ *
1717+ * @returns Linkat board data or null if fetch fails
1818+ */
1919+export async function fetchLinkatData(): Promise<LinkData | null> {
2020+ const cacheKey = `${CACHE.LINKAT_PREFIX}${ATPROTO_DID}`;
2121+ const cached = cache.get(cacheKey);
2222+2323+ if (cached) {
2424+ console.log('[Linkat] Returning cached data');
2525+ return cached;
2626+ }
2727+2828+ console.log('[Linkat] Fetching from AT Protocol...');
2929+3030+ try {
3131+ // Try PDS first, fallback to public API
3232+ let agent;
3333+ let usedPDS = false;
3434+3535+ try {
3636+ agent = await createAgentForDID();
3737+ usedPDS = true;
3838+ console.log('[Linkat] Using PDS agent');
3939+ } catch (error) {
4040+ console.warn('[Linkat] PDS unavailable, using fallback');
4141+ const result = await createAgentWithFallback();
4242+ agent = result.agent;
4343+ usedPDS = result.isPDS;
4444+ }
4545+4646+ const data = await fetchLinkatBoard(agent, ATPROTO_DID);
4747+4848+ if (!data) {
4949+ return null;
5050+ }
5151+5252+ console.log(`[Linkat] Successfully fetched ${data.cards.length} links`);
5353+ cache.set(cacheKey, data, CACHE.DEFAULT_TTL);
5454+ return data;
5555+ } catch (error) {
5656+ console.error('[Linkat] Failed to fetch data:', error);
5757+ return null;
5858+ }
5959+}
6060+6161+/**
6262+ * Gets all short links from the Linkat board
6363+ *
6464+ * @returns Array of short links with generated codes
6565+ */
6666+export async function getShortLinks(): Promise<ShortLink[]> {
6767+ const linkatData = await fetchLinkatData();
6868+6969+ if (!linkatData) {
7070+ return [];
7171+ }
7272+7373+ return generateShortLinks(linkatData);
7474+}
7575+7676+/**
7777+ * Finds a short link by its shortcode
7878+ *
7979+ * @param shortcode - The shortcode to find
8080+ * @returns The matching short link or null if not found
8181+ */
8282+export async function findShortLink(shortcode: string): Promise<ShortLink | null> {
8383+ const links = await getShortLinks();
8484+ return findLinkByShortcode(links, shortcode);
8585+}
8686+8787+/**
8888+ * Clears the Linkat cache
8989+ */
9090+export function clearCache(): void {
9191+ cache.clear();
9292+}
···11+import type { RequestHandler } from './$types';
22+import { HTTP } from '$lib/constants';
33+44+export const GET: RequestHandler = async () => {
55+ // Return 204 No Content to indicate no favicon is available
66+ return new Response(null, {
77+ status: HTTP.NO_CONTENT
88+ });
99+};
+3
static/robots.txt
···11+# allow crawling everything by default
22+User-agent: *
33+Disallow:
+17
svelte.config.js
···11+import adapter from '@sveltejs/adapter-auto';
22+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
33+44+/** @type {import('@sveltejs/kit').Config} */
55+const config = {
66+ // Consult https://svelte.dev/docs/kit/integrations
77+ // for more information about preprocessors
88+ preprocess: vitePreprocess(),
99+ kit: {
1010+ // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
1111+ // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
1212+ // See https://svelte.dev/docs/kit/adapters for more information about adapters.
1313+ adapter: adapter()
1414+ }
1515+};
1616+1717+export default config;
+20
tsconfig.json
···11+{
22+ "extends": "./.svelte-kit/tsconfig.json",
33+ "compilerOptions": {
44+ "rewriteRelativeImportExtensions": true,
55+ "allowJs": true,
66+ "checkJs": true,
77+ "esModuleInterop": true,
88+ "forceConsistentCasingInFileNames": true,
99+ "resolveJsonModule": true,
1010+ "skipLibCheck": true,
1111+ "sourceMap": true,
1212+ "strict": true,
1313+ "moduleResolution": "bundler"
1414+ }
1515+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
1616+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
1717+ //
1818+ // To make changes to top-level options such as include and exclude, we recommend extending
1919+ // the generated config; see https://svelte.dev/docs/kit/configuration#typescript
2020+}
+6
vite.config.ts
···11+import { sveltekit } from '@sveltejs/kit/vite';
22+import { defineConfig } from 'vite';
33+44+export default defineConfig({
55+ plugins: [sveltekit()]
66+});